Pular para conteúdo

0042 — Tutoriais em vídeo, e onde encontrá-los

Status CONCLUÍDA
Branch spec/0042-tutoriais-em-video
Criada em 2026-08-20
Apps afetados api · web · e2e · skill
Revisão do problema Aprovada na rodada 4 (seção 6)
Revisão da experiência Aprovada na rodada 4 (seção 8)
Revisão do detalhamento 6 rodadas — sem achado em aberto, sem aprovação formal (seção 16)
Revisão da implementação conferida por execução e por captura; ver a seção 17

Status: RASCUNHO → APROVADA → EM IMPLEMENTAÇÃO → CONCLUÍDA

A spec tem três partes, escritas em ordem, e quatro portões — cada um revisado por um subagente de contexto limpo, em até 3 rodadas. A máquina de revisão é do plugin superpowers; a régua é a rubrica correspondente em .claude/rubrics/. Achados vêm classificados: qualquer Critical reprova, Important em aberto reprova salvo decisão registrada do usuário, e Minor vira pendência aceita.

  1. A Parte I diz qual é o problema e como se convive com ele hoje. Revisada na seção 6 (necessidade × jornada de hoje). É o portão mais barato de todos, e o único que pergunta contra que problema isto está sendo construído.
  2. A Parte II diz como se usa o que vai existir. Revisada na seção 8 (jornada × viewports).
  3. A Parte III diz como se constrói. Revisada na seção 16 (segurança × clareza técnica) — autorização, isolamento, e se sobrou decisão para quem vai codar.
  4. A implementação é medida contra as três, na seção 17 (aderência × qualidade × testes) — uma revisão por fase, e uma da branch inteira antes de fechar.

Cada parte só se escreve depois que a anterior passou. Descrever tela sobre problema mal enquadrado, ou detalhar endpoint sobre experiência vaga, é o jeito mais caro de descobrir que era outra coisa.


Parte I — O problema

Aqui não entra solução: nem tela, nem endpoint, nem botão. Só o que dói, para quem, e o que se faz hoje.

1. Dor

O Garantora não tem como ensinar ninguém a usá-lo. Não há material que descreva um caminho — como se leva uma análise do começo ao fim, em que ordem, e o que se faz em cada parada. Quando a primeira pessoa de fora precisar aprender, o único recurso disponível será ocupar alguém que já sabe, ao vivo, uma vez por pessoa.

Esta dor é projetada, não observada. A produção subiu em 2026-08-08 (spec 0028) e até hoje nenhuma chamada de treinamento aconteceu, com ninguém — nem parceiro, nem cliente, nem pessoa nova no time. O que se afirma aqui é o que o produto impõe a quem chegar, derivado do que existe no código; o que não se afirma é frequência, custo medido ou repertório de dúvidas reais. A seção 2 delimita o alcance nesses termos.

O que existe hoje é ajuda sobre campo, não sobre caminho. O HelpDialog (apps/web/src/components/help-dialog/) abre um texto explicativo por parâmetro — taxa de garantia, LMI, carência, dias para sinistro, dia de vencimento —, com definição e exemplo numérico. É bom, e é a linha de base honesta desta spec. Mas tem três limites que o mantêm longe do problema:

  • Vive numa tela só. O FieldHelperText é importado em um único lugar — apps/web/src/pages/settings/index.tsx. Nenhuma outra tela do produto tem ajuda explicativa; a de exigência documental tem apenas Tooltip em botão de ação, que nomeia o botão e não ensina nada.
  • Responde "o que é este campo?", não "o que eu faço agora?". Quem lê já está na tela certa, com o campo na frente. Quem não sabe por onde começar não é alcançado — a dúvida dele acontece antes de existir campo na tela.
  • Não alcança quem está fora da garantidora. O corretor da imobiliária não entra em Configurações; ele abre análise e anexa documento, e ali não há ajuda nenhuma.

O custo de não ter material de caminho recai em três lugares:

  • Hora de quem menos sobra. Quem conduziria o treinamento é quem entende do fluxo — a mesma pessoa que analisa crédito, atende imobiliária e implanta garantidora nova. Cada chamada é tempo dessa pessoa repetindo um roteiro que ela já repetiu.
  • O conhecimento não fica. Terminada a chamada, não sobra nada consultável. A dúvida volta depois, fora de hora, e é respondida de novo, à mão, por alguém.
  • Quem não pergunta, erra ou desiste. Sem material, ou se interrompe alguém, ou se faz do jeito que parece certo — e a análise chega com documento errado, no campo errado, ou não chega.

Há uma agravante de tempo, e ela atinge todo material, inclusive a ajuda que já existe: o produto está mudando rápido. Emissão e assinatura de garantia acabaram de existir (specs 0038 a 0040), o retorno da análise à imobiliária mudou na 0036 e as pendências mudaram de vocabulário na 0037 — PENDING_DOCS virou PENDING. Cada mudança dessas envelhece em silêncio o material escrito antes dela. Material que não acompanha o produto não fica neutro: ele passa a ensinar errado.

2. Quem sente

Em ordem de quem sente mais:

  • Usuário de imobiliária parceira (AGENCY_USER) — corretor. O mais difícil de alcançar: está fora da garantidora, usa de forma intermitente, muitas vezes do celular, e não tem a quem perguntar internamente.

Há duas rotas de origem, e elas convivem — não há padrão de operação definido, porque não há imobiliária em produção para tê-lo definido. A análise pode ser aberta pelo próprio corretor, ou registrada em nome da imobiliária por alguém de dentro da garantidora: o formulário interno tem seletor de imobiliária para quem não é AGENCY_USER, e partner_agency_id é nullable (apps/api/app/models/credit_analysis.py:125). O corretor vem primeiro nesta lista por ser a rota que escala — a que não consome hora da garantidora por análise — e porque é a única das duas em que quem opera não tem a quem perguntar. Se a operação se firmar na outra rota, a ordem desta seção muda. - Equipe interna da garantidora (analista, gestor, financeiro, estagiário). Sentirá ao entrar na equipe e a cada mudança de versão. O estagiário é o caso extremo: entra sem contexto de domínio e é quem mais consumiria tempo de quem sabe. - Garantidora nova, cliente do SaaS (ADMIN_TENANT). Aceita o convite por link, cai no onboarding (spec 0031) e precisa configurar taxas, LMI, exigência documental e usuários sem nunca ter visto o produto. É o momento de ativação: se travar aqui, trava tudo o que vem depois. É também o único público que a ajuda existente alcança — e só depois de ele já ter encontrado a tela de Configurações sozinho.

Não há volume medido, não há observação, e não há repertório de dúvidas reais. Nenhum dos três públicos passou por isso: não houve corretor treinado, pessoa nova onboardada pelo produto no ar, nem garantidora implantada de fora. Frequência de uso e rotatividade de corretor são expectativa de mercado, não medição. Consequência prática para quem vai desenhar: o conteúdo dos tutoriais terá de ser derivado do produto — das telas, das transições de status, das regras — e não de um roteiro humano existente ou de uma lista de perguntas frequentes, porque nenhum dos dois existe.

O alcance sustenta-se pelo desenho, não por número, e com precisão diferente para cada público:

  • Garantidora nova — caminho obrigatório. Toda garantidora passa pelo onboarding e sai dele sem nada parametrizado, pelas duas portas de entrada (convite em link e cadastro próprio, ambos previstos na 0031). Não existe rota alternativa.
  • Equipe interna — caminho obrigatório. Quem entra na equipe encontra o produto como ele está, com ajuda em uma tela.
  • Corretor — a rota que escala, não a única. Pelas duas rotas de origem acima. Se toda análise for registrada por dentro, o corretor deixa de ser alcançado — e o custo migra para a equipe interna, que passa a digitar por ele.

3. A jornada de hoje

As três jornadas abaixo são projetadas, não observadas — pelo motivo da seção 2. Cada passo é derivado do que o produto exige hoje (o convite da 0031, o vínculo por imobiliária da 0034, a ausência de ajuda fora de Configurações), não de um caso vivido.

Corretor de imobiliária parceira

  1. A garantidora fecha o contrato de parceria e cria o acesso do usuário da imobiliária.
  2. Alguém do time marca uma chamada com o corretor — e a agenda dos dois é o primeiro atrito: o corretor quer usar hoje, a chamada sai quando der.
  3. Na chamada, compartilha-se a tela e percorre-se o fluxo: abrir análise, cadastrar inquilino, anexar documento, acompanhar o retorno.
  4. O corretor usa uma vez, com ajuda. Passa um tempo. Na vez seguinte não lembra por onde começa, ou lembra de uma tela que mudou desde então.
  5. A dúvida vai para o contato da garantidora, pelo canal que houver, junto das outras mensagens do dia — e é respondida à mão, uma a uma.
  6. A resposta morre na conversa. O corretor seguinte, da mesma imobiliária, começa do passo 2.

Onde quebra: nos passos 2 e 5. O passo 2 põe a agenda de quem sabe entre o parceiro e o primeiro uso. O passo 5 gasta de novo, uma a uma, respostas que já foram dadas — e nenhuma delas fica. Em nenhum dos seis passos o corretor encontra ajuda dentro do produto: as telas por onde ele passa não têm nenhuma.

Pessoa nova na equipe interna

  1. Recebe acesso e senta com alguém do time, presencialmente ou em call.
  2. Percorre-se o sistema por cima; o que não couber vira "qualquer coisa me chama".
  3. Nos primeiros dias, interrompe colegas a cada dúvida — cada interrupção custa duas pessoas.
  4. As regras de domínio (Pacote Locatício, LMI, carência, sub-rogação) aparecem soltas, no meio da tela. As que são parâmetro têm o HelpDialog em Configurações; as que são sequência — quando uma análise pode virar garantia, o que acontece ao devolver à imobiliária — não têm nada a que voltar.

Onde quebra: no passo 3. Sem material consultável, a única fonte é outra pessoa, e a curva de quem entra é paga em hora de quem já produz.

Garantidora nova

  1. Chega ao onboarding — por convite em link ou por cadastro próprio, já que a 0031 deixou POST /auth/register aberto de propósito (RN10). As duas portas terminam no mesmo lugar.
  2. O onboarding é um passo só, e cadastral: razão social, CNPJ, telefones, endereço (apps/web/src/pages/onboarding/StepTenant.tsx). Nada de taxa, LMI ou exigência documental.
  3. Ao concluir, o sistema chama activate() e a joga na raiz do app (apps/web/src/pages/onboarding/index.tsx:38-40) — com a garantidora ativa e nada parametrizado. Nenhuma tela diz o que falta, e nada aponta para Configurações.
  4. Para a primeira análise rodar, ela precisa descobrir Configurações sozinha. Chegando lá, a ajuda existente funciona: o HelpDialog explica taxa, LMI, carência e vencimento, com exemplo.
  5. Na dúvida sobre o que vem antes ou depois disso, para e espera acompanhamento. A implantação fica pendurada em quem vai atender.

Onde quebra: entre os passos 3 e 4 — no vazio entre ativar e descobrir onde se parametriza. É o custo mais caro dos três, porque não é retrabalho, é ativação que não acontece. E é a evidência mais limpa do recorte da seção 1: a ajuda por campo está lá e não basta, porque o que falta acontece antes de ela ser alcançada.

O que já se tentou

Três coisas existem, e nenhuma cobre o problema. Duas delas são boas no que fazem — o diagnóstico não é ausência de esforço, é ausência de material de caminho, para quem está aprendendo, alcançável de dentro do produto.

1. Ajuda contextual por parâmetro (HelpDialog) — está no produto. Boa no que faz, e cobre uma tela. Os três limites estão na seção 1.

2. O skill /journeys (spec 0026) — a tentativa mais próxima, e a mais importante de entender. Ele percorre um processo ponta a ponta com Playwright, encena um caso verossímil, captura cada passo em desktop e 375px e monta uma página navegável em docs/journeys/index.html. O mapa de cada jornada é versionado em journeys/, e dois dos três públicos desta spec já têm mapa escrito: analise-credito-imobiliaria.json e analise-credito-analista.json — e há ainda o analise-credito-gestor.json, do mesmo público interno. Ou seja: já existe no repositório um caminho declarado, executável e regerável sem trabalho manual — a propriedade mais difícil, e ela está resolvida.

Onde ele não basta, e o motivo de não ser simplesmente "publicar as jornadas":

  • O público é o time, não quem aprende. A /journeys existe para revisar — pegar título truncado, campo estourando, rolagem horizontal em 375px. O mapa percorre o que a revisão precisa ver, sem recorte didático: não há o que explicar, nem por quê, nem em que ordem alguém deveria fazer aquilo pela primeira vez.
  • Mostra estado, não ação. São prints de tela assentada. O clique, a digitação e a ordem dos gestos — que é justamente o que se ensina — ficam de fora.
  • Não é alcançável por quem precisa. A saída é ignorada pelo git e servida no ambiente de desenvolvimento. O corretor e a garantidora nova não têm como chegar nela, e não há nada dentro do produto que aponte para lá.
  • É cara e sob demanda. Cria dado em ambiente compartilhado e leva minutos; roda por pedido explícito, nunca sozinha. Não é material publicado, é uma inspeção que se pede.

3. Chamada ao vivo, uma a uma — o recurso que sobra para tudo o que a ajuda por campo não cobre. Ainda não aconteceu nenhuma vez, pelo motivo da seção 2; é o caminho disponível, não um contorno em uso. Funciona e não escala.

Não há material gravado, roteiro didático escrito, PDF, nem índice de material voltado a quem aprende. Também não há material que tenha sido tentado e abandonado.

4. Resultado esperado

O que passa a ser possível:

  • A pessoa aprende sem ocupar ninguém. O caminho até o primeiro uso deixa de depender da agenda de quem sabe.
  • A resposta fica. Uma dúvida respondida uma vez continua respondida — para o próximo corretor da mesma imobiliária, e para quem entrar na equipe depois.
  • O material acompanha o produto sem custo humano por revisão. Mudou a tela, o material volta a corresponder sem que alguém precise refazê-lo à mão. É propriedade exigida, não escolha de produção: sem ela, a agravante da seção 1 se aplica à própria solução e o material vira dívida. A /journeys já demonstra que a propriedade é alcançável neste repositório.
  • Quem está dentro do sistema acha ajuda de dentro do sistema, sem sair para pedir link a alguém, e sem precisar já saber em que tela procurar.

Como se sabe que resolveu — verificável, sem instrumentação:

  1. Existe material de caminho publicado para os três públicos da seção 2, onde hoje há apenas ajuda por campo em uma tela.
  2. O material volta a corresponder à tela sem trabalho manual depois de uma mudança de produto — conferível comparando o material com a tela do ambiente, e conferível de novo depois da spec seguinte que mexer naquele fluxo.
  3. Quem chega ao produto alcança o material de dentro do app, e a partir de mais de uma tela — não só de Configurações.
  4. O material explica, não só mostra: cada passo diz por que se faz aquilo, não só onde clicar. Confere-se lendo o roteiro, sem depender de haver alguém não treinado para testar — o que não há (seção 2). É a diferença entre isto e o que a /journeys entrega, e o que separa "tutorial" de "sequência de telas narrada".

5. Fora de escopo

Cobertura desta spec: três tutoriais, um por público da seção 2 — a análise de crédito pelo lado da imobiliária (corretor), a análise pelo lado do analista (equipe interna) e a configuração inicial após o convite (garantidora nova). Três é o menor conjunto que cobre os três públicos e prova o material em fluxos de natureza diferente; menos que isso deixa um público sem nada no dia da entrega.

O que não entra:

  • Tutorial dos demais fluxos — emissão e assinatura da garantia, cobrança e repasse, sinistro. São os próximos candidatos, e ficam de fora para a spec não virar três: cada tutorial custa roteiro, revisão e produção próprios.
  • Substituir ou absorver a /journeys. Ela continua servindo à revisão do time, com o público e o propósito dela. Unificar as duas é discussão para depois de existirem as duas.
  • Substituir a ajuda por campo que já existe. O HelpDialog continua onde está, respondendo o que responde bem. Os dois materiais convivem.
  • Voz humana gravada. Pelo item 2 da seção 4: material que dependa de regravação humana a cada mudança de tela não é refeito, e envelhece calado.
  • Legenda ou narração em outro idioma. Todo o público é de língua portuguesa.
  • Música de fundo e marca d'água em movimento. Produção de vídeo, não ensino.

A vinheta saiu daqui e entrou no escopo, por decisão do usuário em 2026-08-21. O argumento que a excluía — "produção de vídeo, não ensino" — não se sustentou quando o vídeo passou a circular por link solto: sem abertura, quem recebe o arquivo não sabe de que produto ele é nem qual dos tutoriais está abrindo, e sem fechamento não sabe para onde ir depois. Isso é orientação, não enfeite. Ver RN24. - Ajuste manual do material depois de produzido. Quebra a propriedade do item 2 da seção 4: o que foi retocado à mão não sobrevive ao próximo refazimento. - Métrica de audiência (quem assistiu, até onde). Interessa um dia; hoje não há operação que sustente a leitura do número. - Ajuda embutida ao lado de cada botão ("como faço isto?" na própria tela). Cogitado e adiado: depende de existir onde pendurar esses trechos. - Automatizar a publicação. Ela é manual, por uma restrição da plataforma registrada na seção 15. Entra em spec própria quando deixar de existir. - Substituir a suíte e2e. Isto descreve e ensina; quem falha quando o comportamento quebra continua sendo o e2e.

6. Revisão do problema

Rubrica .claude/rubrics/problema.md, subagente de contexto limpo a cada rodada.

Rodada Critical Important Minor Veredicto
1 — 2 2 Reprovada
2 1 1 — Reprovada
3 — 2 2 Reprovada
4 — — 3 Aprovada

Rodada 1. Não reconhecia o HelpDialog, que já existe e explica os parâmetros de Configurações — a spec afirmava "o ponto de partida é o zero". Faltava também dizer quantos tutoriais entram e de quais fluxos. Reescrita com a linha de base declarada e a cobertura fixada em três tutoriais, um por público.

Rodada 2. Critical: ignorava o skill /journeys (spec 0026), que já percorre processo ponta a ponta, versiona o mapa e monta página navegável — com mapa pronto para dois dos três públicos. Dizer "nunca se tentou" era falso, e faria quem desenha reconstruir do zero um mecanismo que existe. Reescrita posicionando a /journeys como a tentativa mais próxima, com os quatro pontos em que ela não basta. Também desempatada, com o usuário, a contradição entre "a chamada acontece hoje" e "ninguém passou por isso": nenhuma chamada aconteceu, e a dor inteira passou a ser declarada como projetada.

Rodada 3. Dois Important. O primeiro — a jornada da garantidora nova descrevia um onboarding que decide taxas e LMI, quando o onboarding real é um passo cadastral (CNPJ, telefones) que chama activate() e cai na raiz do app, sem nada apontar para Configurações — foi corrigido contra o código: a quebra passou a ser o vazio entre ativar e descobrir onde se parametriza. Os dois Minor (a ajuda da exigência documental é Tooltip de botão, não ajuda explicativa; e o critério 2 descrevia mecanismo) também foram corrigidos. O segundo Important continua aberto e é decisão de negócio — ver abaixo.

Rodada 4 — confirmação. Rodada extra, porque nenhum revisor tinha lido as correções da rodada 3. Aprovada, com três Minor, todos corrigidos: o critério 4 prometia verificabilidade que dependia de gente não treinada — que a seção 2 declara inexistente —, e virou conferência do roteiro; o item de fora de escopo sobre publicação trazia mecânica de solução para dentro da Parte I; e havia um terceiro mapa de jornada (analise-credito-gestor.json) que o texto não citava.

Resultado: aprovada na rodada 4.

Achado em aberto, resolvido pelo usuário após o laço. A seção 2 sustentava o alcance do corretor dizendo que "toda análise entra por uma imobiliária". O código não impõe isso: partner_agency_id é nullable em apps/api/app/models/credit_analysis.py:125 ("quando aplicável"), e o formulário interno oferece seletor de imobiliária para quem não é AGENCY_USER (apps/web/src/pages/credit-analyses/CreditAnalysisForm.tsx) — ou seja, o time da garantidora pode registrar a análise em nome da imobiliária, sem o corretor entrar no sistema. Como a spec renuncia a número e sustenta o alcance pelo desenho, esse é o único apoio que ela tem. A resposta muda qual é o primeiro público e se o tutorial do corretor é caminho obrigatório ou um de dois caminhos possíveis.

Resposta do usuário: as duas rotas convivem, sem padrão definido — não há imobiliária em produção para tê-lo definido. A seção 2 foi reescrita: o corretor deixa de ser sustentado como caminho obrigatório e passa a ser a rota que escala, com o efeito registrado caso a operação se firme na outra. Garantidora nova e equipe interna continuam obrigatórios.

Pendências aceitas: —


Parte II — A experiência

7. Jornada pretendida

São duas experiências, com públicos e frequências opostas. A primeira é do produto e acontece muitas vezes; a segunda é de bastidor e acontece quando um fluxo muda.

A. Quem aprende — dentro do app

Ponto de entrada. Três, e cada um resolve uma quebra nomeada na seção 3:

  1. Item fixo no menu, "Ajuda", ancorado na base da sider, fora do contêiner rolável dos grupos — sem rótulo de seção, como o "Painel". A âncora não é detalhe: os grupos rolam, e o de ADMINISTRAÇÃO já tem oito itens, então um grupo colocado por último cairia abaixo da dobra justamente para o ADMIN_TENANT recém-ativado, que é quem o ponto de entrada 2 acabou de mandar procurar ajuda. Presente em toda tela do app da garantidora.

Com a sider recolhida (60px, só ícone), ele segue o tratamento dos itens dos grupos: ícone centralizado e rótulo no Tooltip. Por ficar fora do laço que desenha os grupos, isso não vem de graça — é comportamento a implementar, não a herdar.

No mobile, fica no fim da gaveta, depois de todos os grupos, separado por divisor. A gaveta também rola, mas aqui a base é o lugar certo e não o problema: ela se abre por gesto deliberado, e quem a abriu está procurando o que não está nas três abas. Visível para todo usuário autenticado, sem exigir permissão: o conteúdo é do produto, igual para todas as garantidoras, não expõe dado de tenant nenhum, e esconder ajuda de quem tem menos acesso é o avesso do problema. O recorte da 0035 continua valendo para todo o resto — este item é a exceção declarada, e ela é compatível com o podeVerItem, que já aceita item sem exigência declarada.

No painel do SaaS (ADMIN_GLOBAL) o item não aparece. Aquela casca tem árvore própria (buildAdminNavGroups/buildAdminNavTabs) e outro público: os três tutoriais ensinam a operar uma garantidora, não a administrar o SaaS. A rota /ajuda continua acessível se ele digitar a URL, e é isso mesmo — a lição da 0035 é que esconder item de menu não fecha rota, e aqui não há o que fechar: o conteúdo é material de produto, sem dado de garantidora nenhuma. Ausência no menu é recorte de relevância, não de acesso.

No mobile ele entra na gaveta ("Mais"), não na barra inferior — as três abas de lá são de operação diária.

  1. O destino do onboarding. Hoje, ao concluir, activate() joga a pessoa na raiz do app com a garantidora ativa e nada parametrizado. Passa a levá-la direto ao tutorial de configuração inicial (/ajuda/configuracao-inicial), não à lista — quem acabou de ativar não precisa escolher entre três, precisa do primeiro passo.

Se esse tutorial não estiver publicado, o destino continua sendo a raiz, como hoje. Mandar alguém recém-ativado para uma tela que diz "nenhum tutorial publicado ainda" é pior que o comportamento atual, e o desvio precisa existir desde o primeiro dia, porque há um intervalo entre a tela existir e o vídeo estar no ar.

  1. O link direto, de fora. Cada tutorial tem URL própria (/ajuda/<slug>), que se manda por WhatsApp ou e-mail. Quem não estiver autenticado cai no login e volta para o tutorial pedido.

E há uma segunda porta, que o YouTube dá de graça: o link não listado do vídeo abre para quem não tem conta. É o único caminho que alcança o corretor antes de o acesso dele existir — a seção 2 diz que ele é o público mais difícil, e este é o furo por onde ele passa. O que se perde nesse caminho é o roteiro em texto e a saída para a tela; o vídeo, com a legenda queimada, vai inteiro.

Jornada, caso feliz:

  1. A pessoa abre Ajuda e vê os tutoriais disponíveis, os do papel dela primeiro — ordenados, não filtrados: o corretor continua enxergando o tutorial do analista, porque entender o outro lado é metade de trabalhar com ele.

A regra da ordem: cada tutorial declara no catálogo a que público serve. Vêm primeiro os que servem a algum papel de quem está olhando; depois os demais, na ordem do catálogo. Quem acumula papéis casa com mais de um grupo e vê todos eles na frente — não há desempate entre papéis, porque com três tutoriais o desempate decidiria entre um e dois itens e custaria uma regra que ninguém consegue prever de cabeça. 2. Cada tutorial se apresenta por título, para quem é, duração e uma frase do que ensina. A duração é o dado que mais decide se alguém começa agora ou adia. 3. Ela escolhe um e o vídeo abre com legenda visível — sem depender de som, porque boa parte assiste no meio de outra coisa. 4. Ao lado (ou abaixo, no celular) está o roteiro em texto, dividido nos passos do fluxo, com a marca de tempo de cada um. Clicar num passo salta o vídeo para ele. 5. Terminado, ela vai fazer — pela própria tela do tutorial: a ação principal leva à tela que ele acabou de ensinar — os três destinos, já decididos: o de configuração inicial leva a Parâmetros, o do corretor a nova análise, e o do analista à fila de análises. Sem isso a jornada da garantidora nova morreria no vídeo, e o vazio que a seção 3 nomeia continuaria aberto: assistir não é o desfecho, fazer é.

Quando quem assiste não alcança aquela tela — o corretor abrindo o tutorial de configuração inicial, já que /settings exige ADMIN_TENANT —, a ação aparece desabilitada, com a explicação de que a tela é de outro perfil, nomeando qual. O vídeo e o roteiro continuam inteiros: a decisão da jornada é que ver o outro lado vale, e a tela diz de quem é aquele lado em vez de fingir que a porta abre.

Efeito colateral aceito, e registrado como tal: mostrar porta que não abre costuma gerar pedido de acesso. Preferiu-se isso a esconder a ação, porque o corretor que entende o processo inteiro pede menos ajuda em tudo o mais. 6. Ela volta ao passo que esquecer, sem reassistir do começo.

Custo de uso: zero digitação. Não há formulário, filtro obrigatório nem busca. O sistema já sabe o papel de quem está olhando e usa isso para ordenar; já sabe a duração e a quem cada tutorial serve, porque isso é do material, não do usuário.

B. Quem produz — o time, fora do app

Não há tela de administração de tutoriais, e é decisão consciente: com três tutoriais, uma tela de gestão custa mais que o problema que resolve. A produção acontece no terminal, por quem desenvolve, e o catálogo é arquivo versionado — a mesma escolha que a /journeys fez pelos mesmos motivos.

A jornada, do começo ao fim:

  1. Início. A pessoa roda /tutoriais <processo> no terminal, na raiz do repositório. Sem argumento, lista os tutoriais já mapeados e para — não grava nada por iniciativa própria.
  2. Roteiro. Se o processo ainda não tem roteiro, ele é escrito e mostrado no terminal antes de qualquer gravação: o texto narrado de cada passo, a duração estimada de cada um e o total. Nada é gravado sem aceite explícito. Se já existe, é lido e executado sem reanálise — refazer o roteiro em silêncio apagaria o ajuste manual de quem o revisou. O roteiro aceito é gravado em videos/<processo>.json e versionado: é ele, não o vídeo, a fonte da verdade.
  3. Produção. O navegador percorre o fluxo gravando, a narração é sintetizada e o vídeo é montado com a legenda queimada. Roda na máquina de quem chamou, leva minutos, e o terminal mostra em que passo está. O resultado é um .mp4 em docs/videos/, fora do git — é artefato, e se refaz.
  4. Conferência. A pessoa assiste ao arquivo antes de qualquer publicação. É o segundo portão humano, e o único que pega narração que ficou fora de sincronia com a tela.
  5. Publicação — dois atos, nesta ordem. Sobe-se o .mp4 ao YouTube como não listado, pela conta do time, e registra-se o ID do vídeo no catálogo versionado, junto de título, público-alvo, duração e o roteiro com marcas de tempo. É o segundo ato que faz o tutorial aparecer em /ajuda — enquanto o ID não estiver no catálogo, o tutorial não existe para o app.
  6. Atualização. Mudou a tela: repete-se do passo 1. O roteiro existente é reaproveitado, a gravação é refeita, e a publicação substitui o vídeo no mesmo registro do catálogo — a URL /ajuda/<slug> não muda, e quem tiver o link antigo continua caindo no lugar certo.

Estados desta jornada — todos no terminal, e todos param em vez de seguir:

Quando O que acontece
O ambiente alvo está desatualizado em relação ao código Avisa e não grava. Gravar contra uma versão velha produz um tutorial que ensina uma tela que já não existe — o pior defeito possível aqui, porque passa despercebido: o vídeo fica bonito do mesmo jeito.
Um passo do roteiro não encontra a tela ou o elemento (a interface mudou) Para no passo, diz qual é e o que esperava encontrar. Os passos seguintes dependem daquele estado; seguir produziria um vídeo de uma jornada que não aconteceu.
A produção falha no meio O que já foi gravado é descartado e nada é publicado. Um vídeo pela metade no catálogo é pior que tutorial nenhum.
O vídeo sai errado, e só se percebe assistindo Não se conserta o arquivo — corrige-se o roteiro e refaz-se. Retoque manual quebra a propriedade do item 2 da seção 4.
A jornada mexeu em configuração compartilhada do ambiente É desfeito ao final, mesmo se a produção falhar no meio. Configuração do tenant é dividida com o e2e, e já houve caso de script de captura derrubar teste alheio.

7.1 Desktop

Lista (/ajuda). PageHeader com ícone, título "Ajuda" e subtítulo "Vídeos curtos mostrando como fazer cada coisa" — lista leva subtítulo, pelo padrão. Abaixo, uma grade de cartões — três por linha em tela larga, dois em tela média. Cada cartão traz, nesta ordem de peso visual: miniatura do vídeo, título, uma frase, e um rodapé com para quem é e duração. Cartão inteiro clicável.

A miniatura é asset nosso, não do YouTube. Um quadro do próprio vídeo, extraído na produção e versionado junto do catálogo, servido pela nossa origem. A alternativa — apontar para a thumbnail do YouTube — cairia junto com o player na rede que 7.3 chama de caso comum, e a lista inteira viraria três retângulos vazios para o público que mais depende dela. Extrair o quadro custa nada: o arquivo já está na mão de quem produz.

Se a miniatura não carregar, o cartão mostra no lugar dela um bloco liso com o ícone do módulo, mantendo a mesma proporção — nunca um retângulo vazio nem um espaço que colapsa e faz a grade dançar.

Por que grade, e não tabela. As listas do app são tabela no desktop e cartão no mobile porque listam registros — linhas comparáveis campo a campo, que se filtram e ordenam. Isto é um catálogo de três itens cujo atributo mais informativo é uma imagem: miniatura não cabe em linha de tabela sem virar ícone, e não há o que comparar entre tutoriais. O cartão é peça nova (TutorialCard), não o RecordCard — este espera pares de rótulo e valor, e aqui não há campos, há uma capa e três informações curtas.

Com três tutoriais, tudo cabe acima da dobra em 1440×900 e não há paginação, filtro nem busca — acrescentá-los agora seria mobiliar sala vazia. A grade absorve crescimento sem mudar de desenho quando forem oito.

Tutorial (/ajuda/<slug>). Duas colunas:

  • Esquerda, ~2/3: o vídeo do YouTube embutido, em 16:9, com largura máxima de 880px para o texto da interface gravada continuar legível. Player do próprio YouTube — nada de player nosso.

O título do tutorial fica no PageHeader e a frase no subtítulo, sem repetir nenhum dos dois abaixo do vídeo. É detalhe, mas é o tipo de detalhe que, sem decisão escrita, vira título duplicado na tela. - Direita, ~1/3, rolável: o roteiro, em passos numerados, cada um com marca de tempo e o texto narrado. Clicar em qualquer passo salta o vídeo para ele.

A coluna do roteiro é o que diferencia isto de um vídeo solto: é legível sem assistir, serve de consulta rápida a quem só quer conferir a ordem, e é a rede de segurança quando o vídeo não carrega — que aqui não é hipótese remota (ver 7.3).

Duas ações no PageHeader: a principal leva à tela que o tutorial ensina (é o passo 5 da jornada, e o que fecha o vazio da garantidora nova); a secundária volta para a lista. Sem "compartilhar" — a URL na barra do navegador já é o link.

A principal é desabilitada, com explicação, quando quem assiste não alcança o destino. O catálogo declara a rota de destino de cada tutorial e o papel que ela exige; a tela compara com quem está olhando. Desabilitada, ela mantém o rótulo e ganha a glosa "esta tela é do perfil " — o alvo continua tendo tamanho e nome acessível, para leitor de tela anunciar a razão em vez de um botão mudo.

7.2 Mobile (375px)

Este é o viewport do corretor, e o mais provável para o material inteiro — o público que mais precisa dele é o que menos senta na frente de um computador.

  • A grade vira coluna única. Um cartão por linha, miniatura em 16:9 no topo (375px de largura dão ~211px de altura), título e frase abaixo, e o rodapé com público e duração na mesma linha. Três cartões: rola-se pouco, e não há LoadMore — não há o que paginar.
  • Não há MobileFilterBar nem FilterPill, porque não há filtro a colapsar. A ordenação por papel é automática e não é um controle.
  • Na página do tutorial, as duas colunas empilham: o vídeo no topo, em largura cheia, e o roteiro abaixo. O vídeo não fica fixo ao rolar — em 375px uma faixa fixa de 211px come um quarto da tela e deixa o roteiro numa fresta.
  • As ações ficam nas slots do PageHeader, como em toda tela do app — o componente é que decide a forma: botão com rótulo no desktop, botão preenchido de 44×44 no mobile (a secundária, contornada). Não há segunda cópia da ação abaixo do vídeo: a 0024 fixou que a mesma tela mostra a ação no mesmo lugar nos dois viewports, e duplicar aqui reabriria exatamente o que ela fechou. A secundária ("voltar para a lista") vira o ícone contornado ao lado, e o aria-label sai do rótulo, que some no mobile.
  • O roteiro vira lista rolável, com os mesmos passos numerados e marcas de tempo, cada passo com alvo de toque de 44px. Tocar salta o vídeo e leva a página de volta ao topo, para o salto ser visto acontecendo.
  • Nada sai da tela. Não há coluna sacrificada nem informação atrás de "ver mais": o conteúdo é um vídeo e uma lista de passos, e os dois cabem empilhados.
  • A legenda sustenta o mobile — é a tela onde mais se assiste sem som. Ela deixou de ser queimada em 2026-08-22 (ver a revisão da RN24a): quem a desenha passou a ser o player do YouTube, que a liga sozinho no celular. O que não muda é a reserva do rodapé na gravação: mudou quem escreve o texto, não onde ele aparece.
  • PWA: a página de Ajuda é uma tela como as outras e entra no app instalado. O vídeo vem do YouTube e não existe offline — sem rede, vale o estado de 7.3, que é onde o roteiro em texto se paga.

7.3 Estados

Estado O que a pessoa vê Qual é a saída
Carregando a lista Esqueleto dos cartões, no formato deles. Nenhuma ação; resolve sozinho.
Lista vazia "Nenhum tutorial publicado ainda." Uma frase, sem ilustração e sem cara de erro — é estado legítimo, não falha. Nada a fazer aqui. Acontece antes do primeiro tutorial publicado, e é por isso que o onboarding tem o desvio do ponto de entrada 2.
Erro ao listar "Não foi possível carregar os tutoriais." Tentar de novo, que refaz a busca sem recarregar a página.
Carregando o tutorial Esqueleto do player e das linhas do roteiro. Nenhuma ação.
O vídeo não carrega — rede corporativa que bloqueia YouTube, offline no PWA, vídeo removido No desktop, a coluna esquerda troca o player por um aviso curto e o link direto do YouTube; a coluna do roteiro fica como está — ela já é permanente, e duplicá-la seria mostrar o mesmo texto duas vezes na mesma tela. No mobile, onde as colunas empilham, o aviso e o link ocupam a faixa do vídeo e o roteiro segue logo abaixo, no lugar de sempre. Em nenhum dos dois o roteiro se move: muda só o que aparece onde o vídeo estava. Tentar de novo, e o roteiro segue consultável enquanto isso. É o estado mais importante desta seção, e não é exceção: rede de imobiliária que bloqueia YouTube é caso comum, e o público que mais depende do material é justamente o que está nessas redes. Quem cai aqui ainda consegue executar o fluxo lendo.
Tutorial inexistente — URL antiga, slug renomeado "Este tutorial não está mais disponível." Link para a lista de Ajuda.
Sem permissão para o conteúdo Não se aplica. Todo usuário autenticado vê todos os tutoriais; o conteúdo é do produto, não da garantidora. Quem não estiver autenticado é levado ao login e volta para a URL pedida — ou assiste pelo link não listado do YouTube, sem conta. —
Sem permissão para a tela ensinada O tutorial abre normalmente — vídeo e roteiro inteiros —, e a ação principal fica desabilitada, com a glosa do perfil a que aquela tela pertence. Acontece de verdade: /settings exige ADMIN_TENANT, e o tutorial de configuração inicial está visível para todos. Nenhuma, e é deliberado: a saída seria pedir acesso a quem administra, fora do produto. A tela não finge que a porta abre.

Os estados se comportam igual nos dois viewports; só muda a largura — com uma exceção descrita acima: quando o vídeo não carrega, o desktop troca só a coluna do player e o mobile troca só a faixa do topo, porque o roteiro ocupa lugares diferentes nos dois. É também o estado que no celular é caso comum e não exceção.

8. Revisão da experiência

Rubrica .claude/rubrics/experiencia.md, subagente de contexto limpo a cada rodada.

Rodada Critical Important Minor Veredicto
1 1 2 2 Reprovada
2 1 1 2 Reprovada
3 — 2 2 Reprovada
4 — — 4 Aprovada

Rodada 1. Critical: a jornada de quem produz era só promessa — não dizia por onde começa, onde o roteiro é revisado, onde o vídeo mora nem que ato o publica. Escrita em seis passos, com tabela de estados. Também corrigidos: o destino do onboarding, que era ambíguo e podia cair numa tela vazia; a jornada, que morria no vídeo em vez de levar de volta ao produto; o painel do SaaS, não tratado; e a grade de cartões, sem justificativa contra o padrão de tabela. Aqui o usuário decidiu a hospedagem: YouTube não listado, embutido.

Rodada 2. Critical: a ação principal levava a uma tela que quem assiste pode não alcançar — /settings exige ADMIN_TENANT, e o tutorial de configuração inicial fica visível para o corretor. Decisão do usuário: botão desabilitado com explicação do perfil, em vez de escondido, aceitando que isso gere pedido de acesso. Corrigidas também a contradição sobre onde vive a ação no mobile (as slots do PageHeader, pela 0024), a rota /ajuda para o ADMIN_GLOBAL e a regra de ordenação por papel.

Rodada 3. Dois Important, os dois de coerência interna, os dois corrigidos sem reabrir decisão de produto: o estado "o vídeo não carrega" mandava o roteiro para o lugar do player, o que no desktop não quer dizer nada porque o roteiro já está na tela — agora está descrito por viewport; e a miniatura, que sustenta a escolha de grade, não tinha origem declarada — vinda do YouTube, cairia junto com o player na mesma rede corporativa, então passou a ser quadro extraído na produção e versionado junto do catálogo, com bloco liso no lugar quando falhar. Os dois Minor (o destino do tutorial do analista, e a âncora do item Ajuda na base da sider para não cair abaixo da dobra) também foram fechados.

Rodada 4 — confirmação. Aprovada, com quatro Minor, todos corrigidos: a sider é recolhível e o item "Ajuda", por ficar fora do laço dos grupos, não herdava o tratamento de ícone com Tooltip; a posição dele na gaveta do mobile não estava dita; faltava subtítulo no PageHeader das duas telas, e o texto mandava repetir o título abaixo do vídeo; e o grupo ADMINISTRAÇÃO tem oito itens, não sete.

Resultado: aprovada na rodada 4.

Pendências aceitas: —

Riscos aceitos: mostrar ação desabilitada a quem não tem acesso à tela ensinada tende a gerar pedido de acesso a quem administra. Decidido pelo usuário, em troca de o corretor entender o processo inteiro.


Parte III — Detalhamento

Duas decisões organizam esta parte.

1. Não há API nova. O catálogo é arquivo versionado e o conteúdo é idêntico para todas as garantidoras, então a tela de Ajuda é frontend puro — nenhum endpoint, nenhuma migration, nenhuma tabela.

2. Produção e catálogo são arquivos diferentes, em lugares diferentes. O que a gravação usa (videos/, na raiz) nunca entra no bundle; o que a tela lê (apps/web/src/tutoriais/) é escrito pela produção no momento da publicação. A primeira versão desta spec tinha um arquivo só, importado da raiz por alias — e isso quebrava o build do container (que copia apenas apps/web/) e arrastaria para o bundle público o config.json da skill, que tem senha de ambiente em texto claro, como o da /journeys tem hoje. A separação é o que fecha os dois.

A seção 10 continua obrigatória e não vira "sem impacto": a exposição desta spec é de conteúdo gravado, não de API, e é risco de espécie diferente do usual.

9. Regras de negócio

Os dois arquivos

  • RN1. videos/<slug>.json, na raiz, é o roteiro de produção: os passos, as ações, o texto narrado, o setup de dados e o desfazer. Fica fora de apps/web/ e nunca é importado por código do app.
  • RN2. apps/web/src/tutoriais/<slug>.json é o catálogo, escrito pela produção, e é a única coisa que a tela lê. Todas as chaves são obrigatórias; duas delas são anuláveis, e a diferença importa porque é ela que a tipagem e o preflight espelham: slug, titulo, frase, publico (papéis a que serve), duracaoSegundos, miniatura, destino, roteiro (uma entrada por fala, com t, texto, passo e nome — ver a nota abaixo), mais youtubeId (string | null) e destinoExige (objeto ou null, quando a rota é aberta a qualquer autenticado).
  • RN2a. O roteiro do catálogo tem uma entrada por fala, não por passo: é a granularidade em que a legenda troca e a única em que saltar o vídeo faz sentido — um passo de trinta segundos com uma marca de tempo só não serve para navegar. Cada entrada carrega passo (o número) e nome (o título do passo), para a tela agrupar as falas sob o passo a que pertencem. Sem isso, um tutorial de oito passos vira vinte e quatro linhas soltas, e a CA16 não teria o que exercitar.

Ajustado durante a implementação: a primeira versão desta RN dizia "passos com t e texto", e o exemplo da seção 12 já mostrava duas falas do mesmo passo. As duas leituras discordavam, e quem escrevesse a tela construiria contra a discordância.

  • RN3. youtubeId: null significa "produzido, ainda não publicado". A tela ignora a entrada: ela não aparece na lista e sua URL responde como inexistente. É o que permite commitar o catálogo de um tutorial já gravado e conferido antes de existir onde hospedá-lo — situação real desta spec (seção 15) — sem que ele apareça para ninguém.
  • RN4. O make preflight falha em quatro situações, e cada uma tem método declarado, para o portão não virar promessa:

  • Catálogo divergente do roteiro — compara os seis campos copiados: slug, titulo, frase, publico, destino e destinoExige, entre apps/web/src/tutoriais/<slug>.json e videos/<slug>.json. Os dois últimos são os que decidem o botão, e deixá-los fora da conferência era permitir que o catálogo dissesse uma coisa e o roteiro outra justamente onde importa.

  • Catálogo sem roteiro — arquivo em tutoriais/ sem par em videos/.
  • Catálogo incompleto — valida o arquivo contra o esquema da RN2, chave por chave. Sem isto, catálogo sem roteiro passa no portão e quebra /ajuda para todo mundo em runtime.
  • Vazamento de videos/ para o app — duas buscas em apps/web/src: qualquer import cujo caminho suba até videos/, e qualquer arquivo de src/tutoriais/ que contenha as chaves passos ou desfazer, que são exclusivas do roteiro.

As cinco rodam sempre, não só sob --all. O preflight filtra por linha acrescentada pela branch, e estas verificações são por par de arquivos: uma branch que edite só videos/<slug>.json sem tocar o catálogo cairia fora do filtro, e a divergência passaria — que é exatamente o caso que elas existem para pegar.

A duplicação entre os dois arquivos é deliberada, e este é o preço dela: sem verificação, divergem no primeiro esquecimento e ninguém vê até alguém assistir. - RN5. duracaoSegundos é a duração medida do arquivo produzido, não estimativa — é o dado que decide se alguém começa agora, e errá-lo por meio minuto já o inutiliza. - RN6. A miniatura é asset local, em apps/web/public/tutoriais/<slug>.png. O catálogo guarda o caminho relativo (tutoriais/<slug>.png) e a tela o resolve concatenando com import.meta.env.BASE_URL — o app é servido sob /app/, então caminho começando com / apontaria para fora da base — nunca a thumbnail do YouTube, que cairia junto com o player na rede que bloqueia o domínio. Estar em public/ e não em src/ é o que a dispensa de resolução do bundler e a mantém previsível.

A tela

  • RN7. Todo usuário autenticado vê todos os tutoriais. Não há filtro por papel, e não se cria permissão para isto (ver 10.2).
  • RN8. A ordem: primeiro os tutoriais cujo publico contém algum papel do usuário, na ordem do catálogo; depois os demais, também na ordem do catálogo. Quem acumula papéis vê mais itens à frente. Não há desempate entre papéis.

Papel customizado não casa com nada, e isso é aceitável: o produto permite criar perfis próprios em /roles, e um usuário só com papel customizado não bate com nenhum publico. A lista degrada para a ordem do catálogo — todos visíveis, nenhum promovido. É perda de conveniência, não de acesso, e o contrário (inferir público a partir de permissão) criaria regra que ninguém consegue prever de cabeça. - RN9. destinoExige, quando não é null, é objeto, não string: { "tipo": "papel" | "permissao", "valor": "...", "glosa": "..." }. O tipo diz contra qual lista do /auth/me comparar (roles ou permissions), e a glosa é o texto exibido quando falta — escrito no catálogo, não derivado. Derivar frase de um identificador técnico produziria "esta tela é do perfil credit_analyses:create". null significa rota aberta a qualquer autenticado. - RN10. A ação principal leva a destino. Quando destinoExige não é satisfeito, ela aparece desabilitada, mantendo rótulo e nome acessível, com a glosa. Nunca escondida, nunca clicável para um 403. - RN10a. Para o ADMIN_GLOBAL a ação é sempre desabilitada, com a glosa "estas telas são do app da garantidora", antes de qualquer comparação da RN9. A exceção é por papel e precisa vir primeiro: ele satisfaz quase toda permissão do rbac_seed.py, então a checagem normal o aprovaria e o levaria a uma tela de tenant com tenant_id nulo. - RN11. destinoExige declara a exigência para fazer o que o tutorial ensina, não a para abrir a rota — e ela é no mínimo tão estrita quanto o porteiro da rota.

A diferença é real e o exemplo canônico a mostra: /credit-analyses/create é rota-filha de /credit-analyses, cujo ExigeAcesso pede credit_analyses:list, mas o tutorial ensina a criar — então destinoExige é credit_analyses:create. Um VIEWER, que tem list e não tem create, abriria o formulário e tomaria a recusa só ao salvar; com a regra escrita assim, a ação já vem desabilitada com a glosa, que é a promessa da matriz 10.1.

O make preflight falha se algum destino não corresponder a rota declarada no App.tsx. O método: percorrer os path= do arquivo compondo pai e filho — as rotas são JSX aninhado, e /credit-analyses/create só existe como composição —, montar o conjunto de caminhos completos e exigir que cada destino esteja nele. Falha também se a miniatura declarada não existir em apps/web/public/: catálogo apontando para PNG ausente passa em todos os outros portões e só aparece como bloco liso em produção. Que destinoExige seja de fato mais estrito que o porteiro não é verificado automaticamente — é revisão humana, e está dito para não parecer garantido. O erro nessa direção é conservador (botão desabilitado a mais); na direção oposta, produz uma tela que a pessoa abre e não consegue usar. - RN12. O vídeo é embutido por youtube-nocookie.com: o domínio sem cookie não planta rastreio antes do play, e a Ajuda é atravessada por todo mundo. - RN13. O roteiro nunca depende do vídeo para ser lido: vem do catálogo e continua na tela quando o player falha (7.3). - RN13a. /ajuda e /ajuda/<slug> abrem para garantidora ainda não ativada. O HomeGuard desvia a raiz de quem não concluiu o onboarding; a Ajuda fica de fora do desvio de propósito — é a única tela do app que serve a quem ainda não configurou nada, e o tutorial de configuração inicial é literalmente o que essa pessoa precisa ver. - RN14. Ao concluir o onboarding, o destino é /ajuda/configuracao-inicial se esse tutorial estiver publicado — isto é, se o catálogo existir e tiver youtubeId não nulo. Caso contrário, a raiz do app, como hoje.

A condição é a publicação, não a existência do arquivo, e a diferença não é sutil: existe uma janela real em que o catálogo está commitado e o vídeo ainda não subiu — é o estado da RN3, e ele se repete a cada tutorial novo e a cada regravação. Condicionar à existência mandaria quem acabou de ativar a garantidora para uma tela que diz "este tutorial não está mais disponível" — pior que o comportamento de hoje, e exatamente o que a Parte II proibiu. - RN15. Link direto para /ajuda ou /ajuda/<slug> sem sessão volta para lá depois do login, e isto já funciona hoje, sem uma linha de código. O CatchAllNavigate do Refine grava /login?to=<caminho> em toda rota protegida, e o useLogin (@refinedev/core) faz if (to) go(to) else if (redirectTo) go(redirectTo) — ou seja, o to vence e o redirectTo do authProvider é descartado.

Nada muda no auth.ts, na página de login, nas rotas ou nos guardas. Uma versão anterior desta spec mandava o auth.ts ler o to e aceitá-lo só sob /ajuda/; era errada em três pontos ao mesmo tempo — o parâmetro já era honrado, o recorte no authProvider não teria efeito porque o to é lido antes, e o recorte não faria sentido sozinho, já que o comportamento vale para o app inteiro desde sempre.

Duas observações que ficam registradas como pré-existentes, e não são desta spec: o to navega pelo router (é caminho interno, não URL externa), e ele precede os desvios de ADMIN_GLOBAL e de onboarding — então quem tem onboarding pendente e clica num link de tutorial cai no tutorial, não no onboarding. Para esta feature o efeito é benigno, e até desejável no caso do tutorial de configuração inicial; mudá-lo é assunto de outro trabalho.

A produção

  • RN25. Os metadados de publicação — título, descrição, visibilidade, playlist e público-alvo — vivem no bloco publicacao do roteiro versionado, não são compostos na hora do envio.

Não é organização por organização: são dois campos longos, digitados dentro de um formulário que remonta quando o envio do arquivo termina, apagando o que já estava escrito — sem erro e sem aviso. Escritos antes, ficam iguais a cada regravação, entram por cópia em vez de digitação, e o passo perigoso encolhe. A ordem também é regra: envio primeiro, campos depois de 100%, e conferência da tela antes de avançar.

  • RN24. Todo tutorial abre e fecha com vinheta da marca. A abertura traz logo, slogan e o título daquele tutorial — é o que faz o link solto se identificar em dois segundos; o fechamento traz logo, slogan e o endereço do produto, para quem assistiu sem estar logado saber para onde ir.

As vinhetas são páginas HTML animadas com CSS, gravadas pelo mesmo navegador que grava os tutoriais. Não é preciosismo: vinheta é tipografia, gradiente e ritmo — o que o CSS faz bem e o que, em filtro de ffmpeg, vira expressão ilegível que ninguém ajusta depois. E o navegador já está aqui, com as fontes e as cores do produto. As páginas vivem do lado do projeto, porque marca é do projeto.

  • RN24b. A legenda não é queimada no vídeo — revisado em 2026-08-22, e reverte a decisão original.

Ela era queimada porque, quando a spec foi escrita, não havia como pôr faixa de legenda no vídeo publicado: o envio era manual e ninguém ia digitar sessenta falas no Studio. Queimar garantia legenda sempre, ao custo de o texto ficar preso no arquivo — não se desliga, não se traduz, não se copia, e o vídeo tem de ser recodificado inteiro só para desenhá-lo.

O que mudou foi a premissa, não o valor da legenda: o YouTube legenda sozinho, e a publicação passou a ser por API — que aceita faixa de legenda com o texto exato que já temos no catálogo. Fica melhor dos dois lados, e de quebra o fechamento da montagem virou remux em vez de recodificação, porque não há mais nada a desenhar sobre o quadro.

A reserva do rodapé na gravação (faixaLegendaFracao) continua valendo: o player desenha a legenda exatamente onde a caixa ficava.

  • RN24a. A abertura desloca todas as marcas de tempo da legenda, e a trilha ganha silêncio do tamanho de cada vinheta.

É o detalhe que arruína o material inteiro se passar despercebido: os tempos das falas são medidos a partir do primeiro passo, e pôr quatro segundos na frente atrasa a imagem sem atrasar o texto — a legenda fica adiantada o vídeo todo, cada vez mais fora de lugar. Pela mesma razão, a miniatura sai de um quadro posterior à abertura: capa de vinheta é igual em todos os tutoriais e não diria qual é este.

  • RN16. A produção nunca publica sozinha. Dois portões humanos, ambos bloqueantes: o aceite do roteiro antes de gravar, e a conferência do arquivo antes de subir.
  • RN17. A gravação acontece exclusivamente no ambiente de desenvolvimento local da máquina de quem produz, num tenant dedicado à demonstração — garantidora própria, usuários próprios com e-mail fictício, dados encenados pela própria produção.

Nunca contra ambiente compartilhado, de homologação ou de produção. Decisão do usuário em 2026-08-20, e ela substitui o desenho anterior, que gravava contra o dev-remote. Três motivos, cada um bastando sozinho: a gravação mostra a tela inteira, inclusive registros que não criou, e a fila de um ambiente compartilhado acumula dado de outras suítes e de outras pessoas — que iria para um vídeo público; a produção cria e apaga dado, e localmente o estrago é de quem grava; e a síntese de voz é local, presa ao sistema de quem produz.

Encenar bem o que se cria não basta, porque a tela mostra o que já está lá — e é por isso que, mesmo localmente, o tenant é dedicado e não o de uso diário. - RN18. Nenhum passo captura a tela de login. A autenticação é injetada como token, como na /journeys, e a gravação começa com a sessão pronta. O Playwright grava o viewport da página, não a moldura do navegador, então a barra de endereço não aparece — mas o formulário de login apareceria, se algum passo passasse por ele. - RN19. O vídeo é público. Decisão do usuário em 2026-08-21, substituindo o "não listado" anterior: o material existe para ser encontrado, e link que só circula à mão não é achado por quem precisa dele.

A consequência que já valia passa a valer com mais força — antes o vídeo era, na prática, público (o youtubeId vai no bundle do frontend, que é público, e link não listado abre para qualquer um que o tenha); agora é público de direito, indexável e recomendável. Nada que apareça no vídeo pode ser confidencial, e é isso que dá peso às RN17 e RN18. - RN20. A conferência humana da RN16 inclui, explicitamente, procurar dado que não seja da encenação — nome, documento, imobiliária ou valor que a produção não tenha criado. Achou, não publica: corrige o isolamento e regrava. - RN21. Regerar substitui o vídeo no mesmo slug. A URL /ajuda/<slug> não muda; o youtubeId do catálogo é atualizado, e o vídeo antigo sai do ar depois de o novo estar publicado — nunca antes, senão quem abrir o link cai num vídeo removido enquanto o catálogo ainda aponta para ele. - RN21a. A substituição é registrada em arquivo versionado, não na memória de quem regravou: videos/publicacoes.json guarda o que está no ar, em que playlist, e as versões aposentadas marcadas A_REMOVER, cada uma apontando para a substituta e dizendo por quê.

A regra existe porque a plataforma não substitui o arquivo de um vídeo: regravar obriga a subir outro, com outro identificador, e o antigo continua alcançável por quem tem o link — ensinando a versão que acabou de ser aposentada. Sem registro, ele sobrevive por esquecimento, que é como ele sobrevive na prática.

Apagar continua sendo ato humano, e é decisão de desenho: remoção é permanente. A ferramenta lista o que apagar e por quê; quem apaga é uma pessoa, e quem quiser caminho de volta muda a visibilidade em vez de apagar. - RN21b. Os tutoriais são organizados numa playlist pública no canal, acompanhando a visibilidade dos vídeos (RN19). Playlist não listada sobre vídeo público esconderia a lista sem esconder nada — e a lista é justamente o que faz um tutorial levar ao seguinte. - RN21c. Ao republicar, o vídeo novo entra na playlist e o antigo sai dela — no mesmo ato. Tirar da playlist não apaga o vídeo (a remoção continua sendo humana, RN21a); o que se evita é a lista acumular versões do mesmo tutorial. Sem a regra, ela acumula: em três republicações a playlist chegou a quatro cópias do mesmo material, todas com o mesmo título, e quem abrisse não teria como saber qual assistir. - RN22. A produção desfaz o que tiver mexido em configuração compartilhada, mesmo falhando no meio (lição da spec 0025). - RN23. No ambiente local não há verificação de versão, e é correto que não haja: ele serve o próprio checkout, com recarga automática, então é o código atual por construção. O config declara ambienteLocal: true e a skill segue direto. Como a RN17 fixa o local como único alvo, este é o caminho normal.

Fora dele, se a skill algum dia apontar para ambiente construído, a produção recusa gravar contra versão diferente: a API expõe o commit que a imagem embute, e a skill o compara com o git rev-parse HEAD. Não é heurística de data — é o mesmo hash, ou não é.

Corrigido durante a implementação, e a razão importa. A primeira versão desta RN copiava a heurística da /journeys — data do último commit contra data de criação da imagem em docker images. Isso não funciona: o dev-remote não roda na máquina de quem grava (não há imagem, container nem volume dele lá), então a comparação sempre caía em "não consegui medir" e o portão virava aviso permanente. Comparar commit com commit resolve de vez, responde exato em vez de aproximado, e serve também ao /devremote-rebuild e ao /ship, que hoje adivinham pela data da imagem.

Gravar contra versão velha produz tutorial que ensina tela que já não existe, e o vídeo fica bonito do mesmo jeito — é o defeito mais caro desta spec, e o mais silencioso. - RN23a. Fora do ambiente local, quando a skill não consegue medir — API fora do ar, resposta sem o campo —, ela avisa e segue. O aviso diz que o portão não rodou; é diferente de dizer que o ambiente está em dia, e a distinção é o que impede o silêncio de passar por sucesso.

10. Autorização e isolamento

Superfície de API: inalterada. Nenhum endpoint novo, nenhum campo novo em resposta existente, nenhuma query nova. A tela consome o catálogo do próprio bundle e o useCurrentUser, que já existe e já expõe roles e permissions.

10.1 Matriz de papéis

Papel Pode Não pode
ADMIN_GLOBAL Abrir /ajuda e qualquer /ajuda/<slug> digitando a URL, e ler tudo. Ver o item no menu do painel do SaaS — árvore de navegação outra, recorte de relevância, não de acesso. E usar a ação principal de qualquer tutorial: ela vem sempre desabilitada para ele (RN10a), com a glosa "estas telas são do app da garantidora". Ele satisfaz quase toda permissão do rbac_seed, então a checagem da RN10 o aprovaria — e o levaria a uma tela de tenant com tenant_id nulo. A exceção é por papel, não por permissão.
ADMIN_TENANT Ver o item, a lista e todos os tutoriais, e usar a ação principal dos três — é o único papel que satisfaz as três exigências. —
ANALYST Ver o item, a lista e todos os tutoriais. Usar a ação do tutorial do analista (decisions:create). Usar a ação do tutorial do corretor — não tem credit_analyses:create (rbac_seed.py:209-222): analista julga análise, não abre análise — e a do de configuração inicial (não é ADMIN_TENANT).
OPERATOR Ver o item, a lista e todos os tutoriais. Usar a ação do tutorial do corretor — tem credit_analyses:create (rbac_seed.py:230), ao contrário do ANALYST. Usar a ação do tutorial do analista — não tem decisions:create — e a do de configuração inicial.
VIEWER Ver o item, a lista e todos os tutoriais, inclusive os que ensinam telas que não opera. Usar a ação principal dos três: não tem credit_analyses:create, não tem decisions:create (só decisions:read, rbac_seed.py:252) e não é ADMIN_TENANT.
AGENCY_USER Ver o item, a lista e todos os tutoriais, inclusive o do analista — decisão da jornada, não descuido. Usar a ação do tutorial do corretor (rbac_seed.py:277). Usar a ação do tutorial do analista — decisions:* fica fora dela de propósito — e a do de configuração inicial.
Não autenticado Assistir pelo link não listado do YouTube, sem conta (RN19). Alcançar /ajuda ou /ajuda/<slug> sem entrar — vai ao login e volta para o endereço pedido depois, pelo ?to= que o Refine já grava e já honra (RN15).

O negativo desta matriz é quase todo sobre a ação, não sobre o conteúdo — e isso é o desenho, não lacuna: o conteúdo é do produto e não tem dono.

10.2 Permissões

Nenhuma permissão nova, e é decisão explícita. Não se cria tutorials:read, e o item de menu não declara requerPermissao — o podeVerItem já trata item sem exigência como visível.

Criar permissão que todo papel receberia responderia "sim" à pergunta isto está protegido? sem proteger nada — o defeito que a spec 0035 encontrou. Se um dia um tutorial precisar de recorte, o recorte nasce com a necessidade.

As permissões citadas em destinoExige são as que já existem no rbac_seed.py; o catálogo as consome, não as cria.

10.3 Isolamento por garantidora (tenant)

Não se aplica na tela, e a afirmação é forte: não há FK recebida, não há registro consultado, não há tenant_id em lugar nenhum do fluxo. O catálogo é do produto e vem do bundle, igual para todas as garantidoras; a única leitura é o useCurrentUser, que a aplicação já faz em toda tela. Como não há registro alheio a alcançar, também não há o par 404/403 a decidir.

Onde o isolamento importa nesta spec é na gravação, não na tela — e ali ele é resolvido por construção: tenant dedicado, usuários próprios, dado encenado (RN17). É a diferença entre "esta tela não vaza" e "este vídeo não vaza", e só a segunda exigia regra nova.

10.4 Isolamento por imobiliária

O AGENCY_USER alcança todo o catálogo, por decisão da Parte II. Não há dado herdando escopo, porque não há dado.

O vazamento por imobiliária só poderia acontecer na gravação. O tutorial do analista mostra a fila de análises e uma análise aberta — telas que, gravadas contra o tenant compartilhado, exporiam inquilino, documento e imobiliária a qualquer pessoa com o link. É o conjunto RN17 (tenant dedicado) + RN19 (não listado não é privado) + RN20 (conferência procura dado alheio) que fecha isso, e é o único risco de acesso real desta spec.

10.5 Plataforma × cliente (SaaS admin)

Nada muda em /app/admin/*: nenhuma tela nova, nenhum dado agregado novo, nenhum item de menu. O ADMIN_GLOBAL que digitar /ajuda vê o mesmo catálogo que todo mundo, que não contém dado de garantidora nenhuma.

11. Impacto por app

apps/api

Uma linha de resposta, nenhuma rota nova, nenhuma migration. O /health — que já existe e já é público — passa a devolver também o commit que a imagem embute:

  • app/core/config.py — GIT_COMMIT: str = "desconhecido", lido do ambiente.
  • app/main.py — o /health acrescenta "commit" ao corpo.
  • infra/dev-remote/api.Dockerfile e infra/prd/api.Dockerfile — ARG GIT_COMMIT e ENV GIT_COMMIT, para o valor entrar na imagem no momento do build. Fora do build, o campo vale "desconhecido", e a skill trata isso como "não consegui medir" (RN23a).
  • Testes: o /health devolve o campo, e ele reflete a variável de ambiente.

A motivação deste campo mudou no meio da implementação, e ele ficou. Ele nasceu para a RN23, quando a gravação ainda apontava para o dev-remote. Com a RN17 fixando o ambiente local como único alvo, a RN23 deixou de precisar dele. Decisão do usuário: manter — são quatro linhas com teste verde, e resolvem um problema que já existia noutro lugar: o /ship e o /devremote-rebuild hoje descobrem "que versão está no ar" comparando data de imagem, que é aproximação. Com o campo, passam a comparar hash. Quem for usá-lo lá faz em spec própria; aqui ele fica disponível e testado.

Por que isto e não um endpoint de catálogo. O conteúdo dos tutoriais continua sem API: é idêntico para todas as garantidoras, não deriva do banco, e um endpoint só acrescentaria uma chamada de rede para devolver o que já está no bundle, mais um lugar onde autorização poderia estar errada. O que a API ganha aqui é outra coisa — a identidade da versão que está no ar, que é infraestrutura, não conteúdo.

Não é dado sensível: o /health já é aberto, e o hash de um commit não diz nada a quem não tem o repositório. Nenhuma permissão nova, nenhum filtro de tenant envolvido.

A consequência a assumir: publicar tutorial novo exige build e deploy do web. Com três tutoriais e um /ship de um comando, é troca aceitável — e a alternativa (catálogo em banco, com tela de gestão) é a spec que a Parte II decidiu não escrever.

apps/web

  • Catálogo: src/tutoriais/<slug>.json mais src/tutoriais/index.ts, que os reúne com import.meta.glob("./*.json", { eager: true }) — glob restrito à própria pasta, para não haver caminho pelo qual arquivo de outro lugar entre no bundle. Tipagem em src/tutoriais/tipos.ts, espelhando a RN2. Nenhum alias, nenhum server.fs.allow, nenhuma mudança nos Dockerfiles — foi o que a separação comprou.
  • Miniaturas: public/tutoriais/<slug>.png, servidas estaticamente.
  • Páginas:
  • src/pages/ajuda/index.tsx — a lista, PageHeader "Ajuda" e grade de TutorialCard.
  • src/pages/ajuda/TutorialPage.tsx — /ajuda/:slug, duas colunas no desktop e empilhado no mobile, primaryAction para o destino (desabilitada por RN10) e secondaryAction de volta à lista.
  • Componentes: src/components/tutorial-card/ (peça nova — não é RecordCard, que espera pares de rótulo e valor) e src/components/tutorial-roteiro/ (a lista de passos, compartilhada pelos dois viewports).
  • Player: <iframe> de youtube-nocookie.com com a IFrame API para o salto por passo. Sem biblioteca de terceiros.
  • Navegação: components/navigation/nav-groups.tsx exporta um AJUDA_ITEM avulso — fora dos grupos devolvidos por buildNavGroups, senão apareceria duas vezes no desktop. Ele é parâmetro da casca, não decisão dela: AppLayout passa a receber itemRodape?, repassado à custom-sider (que o ancora na base, fora do contêiner rolável) e à mobile-shell (que o põe no fim da gaveta).

O painel do SaaS usa a mesma casca (admin/components/AdminLayout.tsx chama AppLayout com os grupos do admin), então item renderizado pela casca por conta própria apareceria nas duas árvores e a CA12 falharia. Quem passa o itemRodape é o layout do app da garantidora; o AdminLayout não passa, e é só isso que mantém o item fora do painel do SaaS. Não entra em buildAdminNavGroups nem em buildNavTabs. - Rotas: App.tsx ganha /ajuda e /ajuda/:slug, ambas dentro do porteiro de autenticado, sem exigência de papel. - Login com retorno: sem impacto. Nada em providers/auth.ts, na página de login, nas rotas ou nos guardas — o retorno ao tutorial já é o comportamento do Refine hoje (RN15). - Onboarding: src/pages/onboarding/index.tsx — o navigate("/") de depois do activate() passa a consultar o catálogo (RN14). - data-testid novos: ajuda-lista, ajuda-card-<slug>, ajuda-vazio, ajuda-erro-listar, ajuda-tentar-de-novo, tutorial-player, tutorial-player-erro, tutorial-link-youtube, tutorial-roteiro, tutorial-passo-<n>, tutorial-ir-para-tela, tutorial-ir-para-tela-glosa, tutorial-voltar, nav-ajuda. - Responsividade e PWA: conforme 7.1 e 7.2. O service worker não pré-cacheia vídeo (nem é nosso); as miniaturas entram no cache de assets como qualquer imagem. Reabrir a tela instalada depois do deploy, para furar o cache do bundle.

apps/e2e

  • Specs: tests/e2e/ajuda.spec.ts — a lista, a abertura de um tutorial, o roteiro presente, a ação desabilitada para papel sem acesso ao destino, e a ordenação por papel. Nos dois viewports. Depende do /devremote-rebuild prévio, como toda a suíte: o spec lê o catálogo do disco e a tela vem do bundle embutido na imagem — sem o rebuild, os dois discordam e o vermelho seria lido como defeito da tela.
  • O spec deriva a expectativa do catálogo em disco, lendo apps/web/src/tutoriais/*.json e considerando publicados só os de youtubeId não nulo. É o que faz o mesmo arquivo de teste valer nos dois estados do produto — com catálogo vazio ele exige o estado vazio; com tutoriais publicados, exige os cartões correspondentes. Sem isso, o spec teria de fixar um número de tutoriais e passaria a mentir no dia seguinte ao primeiro publicado.
  • Seeds: nenhum. O catálogo vem do bundle e não depende de dado — é a rara feature cujo e2e não semeia nada. Os papéis usados já existem nas fixtures.

scripts/preflight.py

Cinco verificações novas, todas baratas e todas guardando erro caro — as quatro da RN4 (catálogo divergente do roteiro nos seis campos copiados, catálogo sem roteiro, catálogo incompleto contra o esquema da RN2, e arquivo de videos/ alcançável de apps/web/src) mais a da RN11 (destino correspondendo a rota declarada). A quarta é a de segurança; a terceira é a que evita /ajuda quebrar em runtime para todo mundo. A CA18 é a régua delas.

A skill (fora dos três apps)

  • .claude/skills/tutoriais/SKILL.md — o procedimento, sem citar rota, papel ou nome de projeto.
  • .claude/skills/tutoriais/scripts/ — narrar.mjs (texto → áudio e marcas de tempo por frase), gravar.mjs (percorre e grava um webm por passo), montar.mjs (junta, ajusta à narração, queima a legenda, extrai a miniatura, escreve o catálogo).
  • videos/publicacoes.json e videos/publicacoes.mjs — o controle do que está publicado e do que espera remoção (RN21a). Versionado, ao contrário do config: não tem segredo, e é justamente o que precisa sobreviver à máquina de quem gravou.
  • videos/config.json e videos/<slug>.json — o lado do projeto. O config.json tem a credencial do tenant de demonstração e fica fora do git: entra no .gitignore, e o que se versiona é um videos/config.example.json sem senha. A skill avisa, com a instrução de cópia, quando o arquivo real não existe.

Decisão do usuário em 2026-08-20, e ela diverge da skill irmã de propósito: o journeys/config.json está commitado com a senha dos usuários de teste em texto claro. O que muda aqui é a consequência — este tenant é quem grava material que fica público, e senha em texto claro não sai mais do histórico depois de entrar. Corrigir a /journeys é trabalho à parte: tirar do arquivo é fácil, tirar do histórico exige rotacionar a senha. - As ~200 linhas de autenticação, cliente de API e resolução de {{token}} são duplicadas da /journeys, por decisão registrada: skill precisa ser copiável para outro projeto sozinha.

12. Contrato de API

Nenhum endpoint novo, nenhum alterado, nenhuma migration — pelo motivo da seção 11. Não há códigos de erro, permissões de endpoint nem downgrade a descrever.

O contrato desta spec é o formato do catálogo, e ele é tão contrato quanto um endpoint: é o que a produção escreve e a tela lê.

Os três tutoriais, com destino e exigência decididos aqui — é desta tabela que a matriz 10.1 deriva, e não o contrário:

Tutorial destino destinoExige Quem satisfaz (rbac_seed.py)
analise-credito-imobiliaria (corretor) /credit-analyses/create permissão credit_analyses:create ADMIN_TENANT, OPERATOR, AGENCY_USER
analise-credito-analista (equipe interna) /credit-analyses permissão decisions:create ADMIN_TENANT, ANALYST
configuracao-inicial (garantidora nova) /settings papel ADMIN_TENANT ADMIN_TENANT

Por que decisions:create no do analista, e não credit_analyses:list: o destino é a fila, mas o que o tutorial ensina é conferir documento, apontar pendência e emitir o parecer — e é a exigência de fazer isso que decide se o botão promete algo verdadeiro (RN11). credit_analyses:list apenas abre a tela, e o VIEWER a tem: com ela, a ação viria habilitada para quem só consegue olhar.

// apps/web/src/tutoriais/analise-credito-imobiliaria.json — o que a tela lê
{
  "slug": "analise-credito-imobiliaria",
  "titulo": "Abrir uma análise de crédito",
  "frase": "Do cadastro do inquilino ao envio para análise, pelo lado da imobiliária.",
  "publico": ["AGENCY_USER"],
  "duracaoSegundos": 214,
  "youtubeId": "xxxxxxxxxxx",
  "miniatura": "tutoriais/analise-credito-imobiliaria.png",
  "destino": "/credit-analyses/create",
  // `create`, embora o porteiro da rota seja `credit_analyses:list` (a rota `create` é
  // filha de `/credit-analyses` no `App.tsx`). O que vale é a exigência para **fazer** o
  // que o tutorial ensina, e ela é mais estrita que a de abrir a tela — RN11.
  "destinoExige": {
    "tipo": "permissao",
    "valor": "credit_analyses:create",
    "glosa": "Abrir análise é do perfil da imobiliária e da equipe de crédito."
  },
  "roteiro": [
    { "t": 0,  "texto": "Nesta tela ficam as análises da sua imobiliária.",
      "passo": 1, "nome": "A fila" },
    { "t": 3.6, "texto": "Cada linha é uma análise, e a cor diz em que ponto ela está.",
      "passo": 1, "nome": "A fila" },
    { "t": 12, "texto": "Clique em Nova análise para começar.",
      "passo": 2, "nome": "Abrir a análise" }
  ]
}
// videos/analise-credito-imobiliaria.json — o que a produção usa, e que NÃO vai ao bundle
{
  "slug": "analise-credito-imobiliaria",
  "titulo": "Abrir uma análise de crédito",
  "frase": "Do cadastro do inquilino ao envio para análise, pelo lado da imobiliária.",
  "publico": ["AGENCY_USER"],
  "destino": "/credit-analyses/create",
  "destinoExige": { "tipo": "permissao", "valor": "credit_analyses:create",
                    "glosa": "Abrir análise é do perfil da imobiliária e da equipe de crédito." },
  "passos": [
    { "nome": "A fila", "rota": "/credit-analyses", "espera": "credit-analyses-list",
      "narracao": "Nesta tela ficam as análises da sua imobiliária.",
      "acoes": [{ "clicar": "credit-analysis-create" }] }
  ],
  "desfazer": []
}
  • A produção escreve o catálogo, derivando roteiro (as marcas de tempo só existem depois de o áudio ser medido), duracaoSegundos, youtubeId e miniatura; e copiando os seis campos slug, titulo, frase, publico, destino e destinoExige do roteiro. É exatamente essa cópia, campo a campo, que a RN4.1 verifica.
  • A tela nunca vê passos nem desfazer — e agora isso é verdade por onde os arquivos estão, não por disciplina de quem escreve o código.

13. Critérios de aceite

Como ler as marcas, ao fechar a spec. [x] foi verificado, e o como está dito na seção 17. [–] não é verificável como escrito, e o motivo está registrado — nos três primeiros, a redução de escopo para um tutorial. [ ] depende da execução da suíte e2e ou do PWA instalado, que a seção 18 registra como aberto.

Nenhum [ ] aqui significa "provavelmente passa". Um critério que ninguém exercitou é um critério em aberto, e escrevê-lo como aprovado é a única coisa que este documento não pode fazer.

  • [–] CA1. Com os três catálogos publicados, um ANALYST abre /ajuda e vê os três cartões — miniatura, título, frase, público e duração —, com o do analista antes dos outros dois.
  • [–] CA2. Um AGENCY_USER abre /ajuda e vê os mesmos três, com o do corretor primeiro. Nenhum está oculto para ele.
  • [–] CA3. Um AGENCY_USER abre /ajuda/configuracao-inicial: vídeo e roteiro inteiros, ação principal desabilitada exibindo a glosa do catálogo. Clicar não navega e não produz erro.
  • [ ] CA4. Um ADMIN_TENANT abre o mesmo tutorial: ação habilitada, e leva a /settings.
  • [ ] CA4a. No tutorial do analista (destinoExige: decisions:create), a ação está habilitada para ANALYST e ADMIN_TENANT, e desabilitada com glosa para OPERATOR, VIEWER e AGENCY_USER. É o CA que prova a tabela da seção 12 e a matriz 10.1 no caso em que elas mais divergem entre papéis.
  • [ ] CA5. Com o player bloqueado, a página mostra aviso e link direto no lugar do player; no desktop o roteiro continua na coluna da direita, sem duplicar; no mobile, logo abaixo do aviso.
  • [ ] CA6. Com a miniatura ausente, o cartão mostra o bloco liso na mesma proporção e a grade não muda de altura.
  • [ ] CA7. Com os catálogos presentes mas nenhum publicado (youtubeId: null em todos) — a janela da RN3, entre commitar o catálogo e subir o vídeo —, /ajuda mostra "Nenhum tutorial publicado ainda", sem cara de erro, e quem conclui o onboarding cai na raiz, não no tutorial. Vale igual com a pasta de catálogos vazia.
  • [ ] CA8. Com configuracao-inicial publicado, quem conclui o onboarding cai naquele tutorial.
  • [ ] CA9. /ajuda/nao-existe mostra "Este tutorial não está mais disponível", com link para a lista.
  • [ ] CA10. Sem sessão, abrir /ajuda/<slug> e também /ajuda leva ao login e, depois de entrar, o navegador para naquele endereço. É verificação de que o comportamento existente atende (RN15), não de código novo — e por isso vale para os dois caminhos.

    Não há CA11. Ele verificava o recorte do parâmetro to no login, e deixou de existir junto com o recorte — a RN15 apurou que o retorno já é comportamento do Refine e que esta spec não mexe nele. A numeração fica com o buraco de propósito: CA18 e CA19 são citados por nome em outras seções, e renumerar quebraria as referências.

  • [ ] CA12. Um ADMIN_GLOBAL não vê "Ajuda" no menu do painel do SaaS, e vê a lista ao digitar /ajuda.

  • [ ] CA12a. Num tutorial publicado cujo destinoExige ele satisfaria, a ação principal do ADMIN_GLOBAL está desabilitada, com a glosa da RN10a. É o CA negativo da RN10a — sem ele, a exceção existe só na prosa.
  • [ ] CA12b. Um VIEWER — e um usuário cujo único papel seja um perfil customizado criado em /roles, sem permissão nenhuma de análise — vê o item "Ajuda" no menu e abre /ajuda normalmente. É o lado positivo da RN7: ajuda não tem permissão, e quem tem menos acesso é justamente quem mais precisa dela.
  • [x] CA13. O item "Ajuda" aparece uma única vez no desktop, e está presente na gaveta do mobile.
  • [x] CA14. Em 375px: lista em coluna única sem rolagem horizontal (scrollWidth - clientWidth == 0), tutorial com vídeo e roteiro empilhados, vídeo não fixo, ações nas slots do topo com alvo de 44px.
  • [ ] CA15. Para o ADMIN_TENANT numa tela de 900px de altura, "Ajuda" está visível sem rolar a sider, com o grupo ADMINISTRAÇÃO inteiro presente.
  • [x] CA16. Tocar um passo do roteiro salta o vídeo para a marca daquele passo.
  • [x] CA17. Catálogo com youtubeId: null não aparece na lista, e sua URL responde como inexistente — é a janela da RN3, entre commitar o catálogo e subir o vídeo.
  • [x] CA18. make preflight falha nas quatro situações da RN4, uma a uma: titulo divergente do roteiro; catálogo sem roteiro; catálogo a que falte uma chave do esquema da RN2 (é a verificação que evita /ajuda quebrar em runtime para todo mundo); e arquivo de src/tutoriais/ contendo passos ou desfazer.
  • [x] CA18a. make preflight falha quando um destino não corresponde a rota composta do App.tsx, e quando a miniatura declarada não existe em apps/web/public/ (RN11).
  • [x] CA19. Nada de videos/ no bundle. Depois do build, grep -rl '"passos"' apps/web/dist/ e grep -rl '"desfazer"' apps/web/dist/ não retornam nada, e o valor literal da senha em videos/config.json não aparece em dist/. A âncora é essa, e não a palavra "senha": o produto tem "E-mail ou senha inválidos" em providers/auth.ts e mais seis ocorrências no i18n.ts, então um grep por "senha" acusaria em build limpo — e critério que sempre acusa é critério que se aprende a ignorar. Sem videos/config.json no checkout (o caso de quem só revisa, já que o arquivo está no .gitignore), a parte do segredo é pulada e as duas primeiras buscas continuam valendo — pular por ausência é diferente de passar por engano, e o critério diz qual dos dois aconteceu.
  • [ ] CA20. A skill, chamada sem argumento, lista e para — não grava, não cria dado.
  • [ ] CA21. A skill mostra o roteiro com as durações e não grava sem aceite explícito.
  • [ ] CA22. Fora do ambiente local, com o commit devolvido pelo /health diferente do git rev-parse HEAD, a skill recusa gravar e oferece o rebuild.
  • [ ] CA22a. Com a API fora do ar, ou devolvendo /health sem o campo commit, a skill avisa que não conseguiu verificar e segue — e o aviso não se parece com sucesso.
  • [x] CA22b. GET /health devolve commit, e o valor é o da variável de ambiente GIT_COMMIT; sem ela, "desconhecido".
  • [x] CA23. Falhando um passo (elemento inexistente), a skill para naquele passo, nomeia o que esperava, e não publica nada.
  • [x] CA24. A gravação roda no tenant de demonstração do ambiente local, e o vídeo produzido não contém nenhum registro que a produção não tenha criado — verificado na conferência da RN20.
  • [ ] CA24a. Com ambienteLocal: true, a skill não tenta verificar versão e não emite aviso a respeito. O ruído fixo é o que ensina a ignorar aviso.
  • [x] CA25. Nenhum quadro do vídeo mostra a tela de login.
  • [x] CA26a. Publicar uma versão nova de um tutorial já no ar marca a anterior como A_REMOVER em videos/publicacoes.json, apontando para a substituta, e atualiza o catálogo no mesmo ato — os dois não podem divergir, sob pena de a tela apontar para um vídeo que saiu do ar.
  • [x] CA26. duracaoSegundos do catálogo corresponde à duração real do mp4 — conferido na publicação, quando o arquivo e o catálogo estão na mesma mão. Não é verificável depois da entrega (o mp4 fica em docs/videos/, fora do git), e por isso é item do portão da RN16, não critério que alguém confira lendo o repositório.

14. Plano de implementação

A ordem canônica é api → web → e2e. Aqui não há apps/api, e quem ocupa o lugar dela é a skill: é ela que produz o catálogo, e o catálogo é o contrato que a tela consome. Sem um tutorial real produzido, a tela seria construída contra um arquivo inventado à mão.

  • [x] apps/api — GIT_COMMIT em config.py, o campo no /health, ARG/ENV nos dois Dockerfiles, args nos dois compose, e o teste (4 verdes, red-green feito)
  • [x] Skill — SKILL.md, narrar.mjs, gravar.mjs, montar.mjs, videos/config.example.json e a entrada no .gitignore
  • [x] Skill — o tenant de demonstração no banco local, com usuários próprios (RN17)
  • [x] Skill — o primeiro tutorial (o do corretor) produzido de ponta a ponta, com os dois portões humanos, publicado público no canal do time e com o catálogo commitado. Subiu pela API, não à mão — ver a revisão da seção 15
  • [x] scripts/preflight.py — as cinco verificações novas (as quatro da RN4 mais a da RN11), rodando sempre, não só sob --all. Cada uma verificada quebrando o arquivo de propósito e vendo reprovar: divergência de campo, catálogo órfão, chave ausente, chave de roteiro no catálogo, import subindo até videos/, destino fora das rotas do App.tsx e miniatura inexistente
  • [x] apps/web — tipagem e index.ts do catálogo, TutorialCard, lista /ajuda
  • [x] apps/web — página do tutorial, player, roteiro com salto, ações e estados
  • [x] apps/web — AJUDA_ITEM como itemRodape do <AppLayout> inline do App.tsx, rotas no App.tsx, desvio do onboarding
  • [x] apps/web — desktop e 375px, com data-testid em todo elemento interativo
  • [~] Skill — produzir os outros dois tutoriais: retirado do escopo em 2026-08-22, a pedido de quem pediu a spec (ver o Registro). O ferramental que os produz está pronto e provado por um tutorial real; os outros dois são conteúdo, não construção
  • [x] apps/e2e — ajuda.spec.ts nos dois viewports, sem seed. Escrito, não executado — a suíte exige o rebuild do dev-remote (regra do CLAUDE.md)
  • [x] make preflight limpo e pytest verde (o intermitente conhecido do partner_agency_status_filter, que esta branch não toca, à parte)
  • [x] Validado no browser em desktop e 375px por captura nos dois viewports. O PWA instalado não foi conferido — exige deploy e reabrir a tela instalada

15. Decisões em aberto

Nenhuma.

  • [x] Qual conta do YouTube hospeda os vídeos. Resolvido em 2026-08-20: o canal UCYV5aSQk1Y5w6JouKFgD9ug, do time. O channelId vai para o videos/config.json.
  • [x] Como o vídeo sobe. À mão, pela interface do YouTube, e não pela API. A decisão não é preguiça: projeto novo do YouTube Data API só publica em privado até passar pela auditoria de conformidade do Google, e não deixa promover a "não listado" nem pela interface. Automatizar hoje escreveria código que entregaria exatamente o bloqueio que já existe, com dependência de um prazo que não controlamos. São três vídeos; o passo manual custa menos que a auditoria.

    O que fica registrado para quando a auditoria sair: o upload por API precisa de OAuth (não de chave), escopo youtube.upload, tela de consentimento publicada — em modo de teste o refresh token expira em 7 dias —, e custa 1.600 unidades de cota por upload, contra 10.000 diárias. Vira spec própria, e o videos/config.json já é o lugar certo para as credenciais, por estar fora do git.

    Revisado em 2026-08-22, e a decisão caiu. O envio por API foi implementado (videos/publicar-youtube.mjs) e o primeiro vídeo subiu público, já na playlist, num cliente OAuth criado minutos antes e sem auditoria nenhuma. A premissa que sustentava esta decisão — "projeto novo só publica em privado" — não se confirmou neste canal.

    O erro não foi acreditar nela: ela consta na documentação e vale sob certas condições. O erro foi decidir duas vezes sobre um limite nunca testado, quando testar custava um comando. Fica como método: quando conferir é mais barato que escrever a justificativa, confira antes de escrever.

    O que a prática confirmou do resto: OAuth é obrigatório (chave de API só lê dado público, e conta de serviço não pode ser dona de canal), o consentimento é uma vez por máquina, e o refresh token vive no videos/config.json, fora do git.


16. Revisão do detalhamento

Rubrica .claude/rubrics/detalhamento.md, subagente de contexto limpo a cada rodada.

Rodada Critical Important Minor Veredicto
1 3 8 2 Reprovada
2 — 5 4 Reprovada
3 — 3 4 Reprovada
4 — 4 1 Reprovada
5 — 1 2 Reprovada
6 — 1 2 Reprovada

Rodada 1 — três Critical, e os três mudaram o desenho. O catálogo era um arquivo só, importado da raiz por alias: isso quebraria o build do container (os dois Dockerfiles fazem COPY apps/web/, então videos/ não entra na imagem) e, pior, um import.meta.glob casaria com videos/config.json, levando senha de ambiente em texto claro para o bundle público — que é o que o journeys/config.json tem hoje. Daí a separação em dois arquivos. O terceiro: as regras fechavam o dado que a produção cria, e não o que a tela mostra — a fila do dev-remote acumula registro de e2e, jornada e do time, e tudo isso entraria num vídeo que a própria spec classifica como público. Daí o tenant dedicado (RN17), a proibição de capturar login (RN18) e a conferência que procura dado alheio (RN20).

Rodada 2 — cinco Important, três deles criados pelas correções da rodada 1. O item "Ajuda" apareceria no painel do SaaS, porque a casca é a mesma; ?next= não tinha quem o escrevesse, já que o Refine grava ?to=; a CA19 acusaria em build limpo, por casar com "E-mail ou senha inválidos" do próprio produto; os critérios se excluíam (uns exigindo três catálogos, outros nenhum). Aqui o usuário decidiu o versionamento do config.json: fora do git, com exemplo versionado, divergindo da /journeys de propósito.

Rodada 3 — três Important, todos de rastreabilidade. Sobraram três ?next= depois da troca para ?to=, e o resíduo caía justo na CA11, que passaria verde contra qualquer implementação. A RN14 condicionava o desvio do onboarding à existência do catálogo, e não à publicação — na janela entre commitar o catálogo e subir o vídeo, isso mandaria o recém-ativado para "este tutorial não está mais disponível", exatamente o que a Parte II proibiu. E a exceção do ADMIN_GLOBAL existia só na matriz, sem RN nem CA: quem implementasse pelas RN9/RN10 habilitaria o botão. Viraram RN10a e CA12a. Dos Minor, o mais útil: o exemplo canônico do contrato declarava credit_analyses:create, mas o porteiro real da rota é credit_analyses:list — o molde divergia.

Rodadas 4 a 6 — as três de verificação. Não contam contra o limite: cada uma achou o defeito que a correção anterior criou, que é a exceção registrada no skill. A 4 pegou o pior de todos e era meu: a spec mandava o auth.ts recortar o ?to= do login, e isso não teria efeito nenhum — o useLogin do Refine lê o to antes do redirectTo, então o recorte teria de morar na página de login, e a própria spec afirmava que nenhuma página mudava. A apuração desfez o nó: o retorno ao tutorial já funciona hoje, sem código, e a spec deixou de mexer nisso.

As rodadas 5 e 6 acharam o mesmo tipo de coisa, sempre na matriz de papéis: o ANALYST não tem credit_analyses:create (analista julga análise, não abre), o OPERATOR tem, e o tutorial do analista nunca teve destinoExige declarado — o que tornava a matriz impossível de satisfazer com qualquer valor.

A correção da rodada 6 foi de desenho, não de texto: os três tutoriais passaram a ter destino e exigência fixados numa tabela única na seção 12, e a matriz 10.1 deriva dela. A causa das três rodadas era a mesma — a matriz guardava, à mão, conhecimento que existia em outro lugar.

Resultado: seis rodadas, nenhum Critical desde a primeira, nenhum achado em aberto — e sem aprovação formal. A sétima rodada, que confirmaria a correção de desenho da sexta, foi dispensada pelo usuário em 2026-08-20, que optou por seguir para a implementação. Fica registrado: o que a rodada 6 corrigiu (a tabela única da seção 12, de onde a matriz 10.1 deriva) não foi lido por revisor nenhum.

Pendências aceitas: —

Riscos aceitos:

  • destinoExige × porteiro real da rota não é verificado automaticamente (RN11): o preflight confere que a rota existe, não que a exigência bate. Divergir produz um /unauthorized inesperado depois do clique — conservador, não escalada.
  • Mostrar ação desabilitada a quem não alcança a tela ensinada tende a gerar pedido de acesso (decidido na Parte II).

17. Revisão da implementação

Como cada critério marcado foi verificado, para a marca não ser palavra:

Critério Como
CA13, CA14 captura nos dois viewports, com medição de rolagem horizontal (não houve)
CA16 um item por passo na tela, os treze do catálogo
CA17 youtubeId: null some da lista — é o filtro do index.ts, exercido pelo preflight
CA18, CA18a as sete situações quebradas de propósito, uma a uma, vendo o preflight reprovar
CA19 o glob restrito e a verificação de vazamento, com import de teste que reprovou
CA22b quatro testes, red-green feito
CA23 aconteceu sozinho, várias vezes: a gravação parou no passo, nomeou a ação e o testid
CA24, CA25 quadros amostrados do vídeo produzido: dado fictício, e nenhum com tela de login
CA26 catálogo diz 430s, ffprobe mede 429,5s
CA26a publicar a versão nova marcou a anterior A_REMOVER, e o aposentar a pôs privada

Os 17 em aberto não são "provavelmente passam". Quinze deles são o que o ajuda.spec.ts cobre e ninguém executou; um é o PWA instalado, que exige deploy; e o CA15 (o item "Ajuda" visível em tela de 900px sem rolar o menu) não foi medido. Cada um está marcado como [ ] de propósito.

18. Registro

O número mudou de 0041 para 0042, ao trazer a main

Duas specs receberam o mesmo número: esta e a 0041-equipe-e-perfis-no-mobile, que foi mesclada na main enquanto esta estava aberta. O contador é sequencial e nunca reutilizado, então quem chega depois renumera — e quem chegou depois foi esta.

O renumerar não é só mover a pasta: as duas specs se citam dentro do mesmo arquivo. O App.tsx, a custom-sider, a mobile-shell e o nav-groups têm comentários das duas, e uma substituição cega de 0041 teria reescrito as referências da outra spec. A troca foi feita linha a linha, distinguindo pelo assunto: as linhas que falam de Ajuda e tutorial são desta.

A fase de API não existia no plano, e voltou a existir

A seção 11 declarava apps/api sem impacto, e com razão quanto ao conteúdo dos tutoriais. A fase reapareceu por outro motivo, descoberto na revisão da fase da skill: a RN23 era inimplementável. Ela mandava comparar a data do último commit com a data de criação da imagem, imitando a /journeys — e quem grava não tem o ambiente na própria máquina. Não há imagem, container nem volume do dev-remote no host de quem produz, então a medição nunca acontecia e o portão virava aviso permanente.

Decisão do usuário: a API expõe o commit que a imagem embute, e a skill compara hash com hash. Responde exato em vez de aproximado, funciona de qualquer lugar, e serve também ao /devremote-rebuild e ao /ship, que hoje adivinham pela data. Custou uma linha no /health, um campo no Settings, ARG/ENV nos dois Dockerfiles e args nos dois compose.

O catálogo emite uma entrada por fala, não por passo (RN2a)

A RN2 dizia "passos com t e texto", e o exemplo da seção 12 já mostrava duas falas do mesmo passo — as duas leituras discordavam. Manteve-se o comportamento implementado: é a granularidade em que a legenda troca e a única em que saltar o vídeo faz sentido. Cada entrada ganhou passo e nome, para a tela agrupar.

Três defeitos de legenda que só apareceram olhando um quadro do vídeo

  1. A caixa tinha zero pixel — no ASS quem lhe dá tamanho é o campo Outline, não o BorderStyle.
  2. O alfa do ASS é invertido — 00 é opaco, FF é invisível. Valor "alto" por intuição produz caixa transparente, e a legenda some sobre área clara.
  3. tempoAss(3.999) devolvia 0:00:03.100 — centésimo 100 não existe, e o libass descarta a linha inteira em silêncio.

Defeitos que a revisão de fase pegou

  • Regravar despublicava, em silêncio, porque identificador nulo é estado legítimo.
  • O desfazer podia deixar o navegador aberto e mascarar o erro original: a resolução de token rodava fora do catch, dentro do finally.
  • Troca de papel no setup contaminava todos os passos seguintes — do passo em diante o vídeo seria gravado logado como outra pessoa, com a narração falando pela voz do protagonista.

O ferramental desta máquina diverge do CLAUDE.md

  • Existe uv e existe .venv em apps/api. O CLAUDE.md da raiz diz que não e manda rodar por docker exec. A suíte rodou com uv run pytest: 562 verdes, 2 vermelhos — os dois intermitentes já registrados. Evidência: nenhum dos dois arquivos foi tocado nesta branch, o diff não encosta no que eles exercitam, e isolados falharam 3 de 5 vezes, que é o sorteio descrito no CLAUDE.md. Não é regressão desta branch.
  • O dev-remote não roda nesta máquina, o que faz o make preflight falhar só na verificação de meta-testes (docker exec garantora-devremote-api-1). As outras quatro passam.

O escopo caiu de três tutoriais para um, e por quê

Pedido de quem pediu a spec, em 2026-08-22, depois de o primeiro tutorial estar no ar. A decisão é de conteúdo, não de construção: o que a spec existia para construir está construído e provado — a skill grava, narra e monta; a publicação é um comando; a tela lê o catálogo; o portão verifica o par de arquivos. Um tutorial real atravessou tudo isso de ponta a ponta, e é ele que prova o caminho.

Os outros dois — o do analista e o de configuração inicial — passam a ser trabalho de produzir vídeo, que não precisa de spec. Quando forem gravados, entram pelo mesmo caminho, sem uma linha de código nova.

O que isso muda no que já está escrito, e o que não muda:

  • A RN14 (concluir o onboarding leva a /ajuda/configuracao-inicial) continua correta e continua implementada. Ela já condicionava à publicação, não à existência do arquivo, justamente porque existe uma janela em que o catálogo está commitado e o vídeo não subiu. Sem o tutorial, acharTutorial devolve undefined e o destino é a raiz, como hoje. Nada a mudar, e é o desenho fazendo o que prometeu.
  • As CA1, CA2 e CA3, que exigiam três catálogos publicados, deixam de ser verificáveis como escritas. Ficam registradas como não verificadas por redução de escopo, e não como aprovadas — ver a seção 13.
  • O texto de desenho das seções 7 e 9 continua falando em três tutoriais, e isso é deliberado: ele descreve a decisão tomada com a informação da época — por que não há tela de administração, por que não há paginação, por que a ordenação não desempata entre papéis. Reescrever para "um tutorial" apagaria o raciocínio e faria a decisão parecer arbitrária. O número volta a três assim que os vídeos forem gravados.

Em aberto, ao fechar

Três coisas, e nenhuma bloqueia o merge:

  • O ajuda.spec.ts foi escrito e não executado. É a regra do repositório — a suíte roda contra o dev-remote e exige o rebuild antes. Fica para o momento em que alguém rodar a suíte inteira.
  • O PWA instalado não foi conferido. Exige deploy e reabrir a tela instalada, para furar o cache do bundle.
  • Cinco versões antigas do tutorial foram publicadas ao longo da produção. Quatro já foram apagadas e uma está privada, fora da playlist. Apagar de vez é permanente e é ato humano — node videos/publicacoes.mjs pendencias lista.