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.
- 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.
- A Parte II diz como se usa o que vai existir. Revisada na seção 8 (jornada × viewports).
- 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.
- 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 apenasTooltipem 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¶
- A garantidora fecha o contrato de parceria e cria o acesso do usuário da imobiliária.
- 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.
- Na chamada, compartilha-se a tela e percorre-se o fluxo: abrir análise, cadastrar inquilino, anexar documento, acompanhar o retorno.
- 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.
- 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.
- 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¶
- Recebe acesso e senta com alguém do time, presencialmente ou em call.
- Percorre-se o sistema por cima; o que não couber vira "qualquer coisa me chama".
- Nos primeiros dias, interrompe colegas a cada dúvida — cada interrupção custa duas pessoas.
- 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
HelpDialogem 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¶
- Chega ao onboarding — por convite em link ou por cadastro próprio, já que a 0031 deixou
POST /auth/registeraberto de propósito (RN10). As duas portas terminam no mesmo lugar. - 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. - 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. - Para a primeira análise rodar, ela precisa descobrir Configurações sozinha. Chegando lá, a ajuda existente funciona: o
HelpDialogexplica taxa, LMI, carência e vencimento, com exemplo. - 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
/journeysexiste 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
/journeysjá 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:
- 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.
- 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.
- 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.
- 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
/journeysentrega, 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
HelpDialogcontinua 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:
- 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_TENANTrecé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.
- 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.
- 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:
- 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:
- 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. - 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>.jsone versionado: é ele, não o vídeo, a fonte da verdade. - 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
.mp4emdocs/videos/, fora do git — é artefato, e se refaz. - 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.
- Publicação — dois atos, nesta ordem. Sobe-se o
.mp4ao 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. - 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
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á
MobileFilterBarnemFilterPill, 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 oaria-labelsai 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 apenasapps/web/) e arrastaria para o bundle público oconfig.jsonda skill, que tem senha de ambiente em texto claro, como o da/journeystem 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 deapps/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, comt,texto,passoenome— ver a nota abaixo), maisyoutubeId(string | null) edestinoExige(objeto ounull, quando a rota é aberta a qualquer autenticado). - RN2a. O
roteirodo 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 carregapasso(o número) enome(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: nullsignifica "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 preflightfalha 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,destinoedestinoExige, entreapps/web/src/tutoriais/<slug>.jsonevideos/<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 emvideos/. - Catálogo incompleto — valida o arquivo contra o esquema da RN2, chave por chave. Sem isto, catálogo sem
roteiropassa no portão e quebra/ajudapara todo mundo em runtime. - Vazamento de
videos/para o app — duas buscas emapps/web/src: qualquerimportcujo caminho suba atévideos/, e qualquer arquivo desrc/tutoriais/que contenha as chavespassosoudesfazer, 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
publicoconté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
publicacaodo 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/healthacrescenta"commit"ao corpo.infra/dev-remote/api.Dockerfileeinfra/prd/api.Dockerfile—ARG GIT_COMMITeENV 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
/healthdevolve 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
/shipe o/devremote-rebuildhoje 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>.jsonmaissrc/tutoriais/index.ts, que os reúne comimport.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 emsrc/tutoriais/tipos.ts, espelhando a RN2. Nenhum alias, nenhumserver.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 deTutorialCard.src/pages/ajuda/TutorialPage.tsx—/ajuda/:slug, duas colunas no desktop e empilhado no mobile,primaryActionpara odestino(desabilitada por RN10) esecondaryActionde volta à lista.- Componentes:
src/components/tutorial-card/(peça nova — não éRecordCard, que espera pares de rótulo e valor) esrc/components/tutorial-roteiro/(a lista de passos, compartilhada pelos dois viewports). - Player:
<iframe>deyoutube-nocookie.comcom a IFrame API para o salto por passo. Sem biblioteca de terceiros. - Navegação:
components/navigation/nav-groups.tsxexporta umAJUDA_ITEMavulso — fora dos grupos devolvidos porbuildNavGroups, senão apareceria duas vezes no desktop. Ele é parâmetro da casca, não decisão dela:AppLayoutpassa a receberitemRodape?, 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-rebuildpré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/*.jsone considerando publicados só os deyoutubeIdnã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.jsonevideos/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.jsonevideos/<slug>.json— o lado do projeto. Oconfig.jsontem a credencial do tenant de demonstração e fica fora do git: entra no.gitignore, e o que se versiona é umvideos/config.example.jsonsem 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.jsonestá 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,youtubeIdeminiatura; e copiando os seis camposslug,titulo,frase,publico,destinoedestinoExigedo roteiro. É exatamente essa cópia, campo a campo, que a RN4.1 verifica. - A tela nunca vê
passosnemdesfazer— 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
ANALYSTabre/ajudae 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_USERabre/ajudae vê os mesmos três, com o do corretor primeiro. Nenhum está oculto para ele. - [–] CA3. Um
AGENCY_USERabre/ajuda/configuracao-inicial: vídeo e roteiro inteiros, ação principal desabilitada exibindo aglosado catálogo. Clicar não navega e não produz erro. - [ ] CA4. Um
ADMIN_TENANTabre o mesmo tutorial: ação habilitada, e leva a/settings. - [ ] CA4a. No tutorial do analista (
destinoExige: decisions:create), a ação está habilitada paraANALYSTeADMIN_TENANT, e desabilitada com glosa paraOPERATOR,VIEWEReAGENCY_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: nullem todos) — a janela da RN3, entre commitar o catálogo e subir o vídeo —,/ajudamostra "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-inicialpublicado, quem conclui o onboarding cai naquele tutorial. - [ ] CA9.
/ajuda/nao-existemostra "Este tutorial não está mais disponível", com link para a lista. -
[ ] CA10. Sem sessão, abrir
/ajuda/<slug>e também/ajudaleva 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
tono 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_GLOBALnão vê "Ajuda" no menu do painel do SaaS, e vê a lista ao digitar/ajuda. - [ ] CA12a. Num tutorial publicado cujo
destinoExigeele satisfaria, a ação principal doADMIN_GLOBALestá 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/ajudanormalmente. É 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_TENANTnuma 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: nullnã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 preflightfalha nas quatro situações da RN4, uma a uma:titulodivergente do roteiro; catálogo sem roteiro; catálogo a que falte uma chave do esquema da RN2 (é a verificação que evita/ajudaquebrar em runtime para todo mundo); e arquivo desrc/tutoriais/contendopassosoudesfazer. - [x] CA18a.
make preflightfalha quando umdestinonão corresponde a rota composta doApp.tsx, e quando aminiaturadeclarada não existe emapps/web/public/(RN11). - [x] CA19. Nada de
videos/no bundle. Depois do build,grep -rl '"passos"' apps/web/dist/egrep -rl '"desfazer"' apps/web/dist/não retornam nada, e o valor literal da senha emvideos/config.jsonnão aparece emdist/. A âncora é essa, e não a palavra "senha": o produto tem "E-mail ou senha inválidos" emproviders/auth.tse mais seis ocorrências noi18n.ts, então um grep por "senha" acusaria em build limpo — e critério que sempre acusa é critério que se aprende a ignorar. Semvideos/config.jsonno 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
/healthdiferente dogit rev-parse HEAD, a skill recusa gravar e oferece o rebuild. - [ ] CA22a. Com a API fora do ar, ou devolvendo
/healthsem o campocommit, a skill avisa que não conseguiu verificar e segue — e o aviso não se parece com sucesso. - [x] CA22b.
GET /healthdevolvecommit, e o valor é o da variável de ambienteGIT_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_REMOVERemvideos/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.
duracaoSegundosdo 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 emdocs/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_COMMITemconfig.py, o campo no/health,ARG/ENVnos dois Dockerfiles,argsnos dois compose, e o teste (4 verdes, red-green feito) - [x] Skill —
SKILL.md,narrar.mjs,gravar.mjs,montar.mjs,videos/config.example.jsone 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,importsubindo atévideos/, destino fora das rotas doApp.tsxe miniatura inexistente - [x]
apps/web— tipagem eindex.tsdo catálogo,TutorialCard, lista/ajuda - [x]
apps/web— página do tutorial, player, roteiro com salto, ações e estados - [x]
apps/web—AJUDA_ITEMcomoitemRodapedo<AppLayout>inline doApp.tsx, rotas noApp.tsx, desvio do onboarding - [x]
apps/web— desktop e 375px, comdata-testidem 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.tsnos dois viewports, sem seed. Escrito, não executado — a suíte exige o rebuild do dev-remote (regra doCLAUDE.md) - [x]
make preflightlimpo epytestverde (o intermitente conhecido dopartner_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. OchannelIdvai para ovideos/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 ovideos/config.jsonjá é 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/unauthorizedinesperado 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¶
- A caixa tinha zero pixel — no ASS quem lhe dá tamanho é o campo
Outline, não oBorderStyle. - 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. tempoAss(3.999)devolvia0: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
desfazerpodia deixar o navegador aberto e mascarar o erro original: a resolução de token rodava fora docatch, dentro dofinally. - Troca de papel no
setupcontaminava 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
uve existe.venvemapps/api. OCLAUDE.mdda raiz diz que não e manda rodar pordocker exec. A suíte rodou comuv 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 noCLAUDE.md. Não é regressão desta branch. - O dev-remote não roda nesta máquina, o que faz o
make preflightfalhar 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,acharTutorialdevolveundefinede 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.tsfoi 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 pendenciaslista.