Pular para conteúdo

0026 — Jornadas visuais por processo

Status CONCLUÍDA
Branch spec/0026-jornadas-visuais-por-processo
Criada em 2026-08-08
Apps afetados nenhum — é ferramental (.claude/skills/, journeys/, docs/journeys/)

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

1. Problema

Não existe uma forma de ver um processo inteiro do sistema. Para saber como está hoje a jornada da análise de crédito — da inclusão à aprovação, passando por pendência de documentos — é preciso abrir a aplicação, criar dado à mão em cada estado e navegar tela por tela, nos dois viewports. Leva dezenas de minutos e ninguém repete com frequência.

Três consequências concretas, todas já observadas neste repositório:

  1. Defeito de layout só aparece quando alguém tropeça nele. As specs 0016, 0018, 0020, 0022 e 0024 nasceram de problemas de tela — título truncado, campo estourando, ação no lugar errado — encontrados por acaso, não por varredura.

  2. A validação visual é refeita do zero a cada spec. Nas specs 0025 e anteriores o procedimento foi sempre o mesmo: escrever um script de Playwright descartável, montar dado pela API, tirar print nos dois viewports, olhar. O script morre no fim da spec e a próxima começa de novo — e uma das cópias chegou a poluir a configuração do tenant de e2e e derrubar três testes que nada tinham a ver com a mudança.

  3. Mudança de fluxo não deixa rastro visível. A spec 0025 moveu o parecer do sétimo para o segundo bloco da página e levou as ações de decisão para o cabeçalho. O git diff mostra isso como TSX; ninguém consegue olhar o antes e o depois da jornada.

2. Objetivo

Um comando que, para um processo escolhido, encena um caso verossímil, percorre a jornada inteira em desktop e mobile, e entrega uma página navegável com o caminho percorrido e os prints de cada passo — repetível, versionável no que importa e sem deixar sujeira no ambiente.

Verossímil não é detalhe de acabamento: é o que separa um material que se mostra a alguém de um material que só serve para conferir se a tela subiu.

3. Fora de escopo

  • Substituir o e2e. A suíte Playwright afirma comportamento e falha quando ele quebra. Esta skill descreve e mostra; ela não é gate de nada e não roda em automação.
  • Comparação automática de imagens (visual regression). Fica registrado como possibilidade futura — o mapa e os prints são a matéria-prima —, mas comparar pixel a pixel exige baseline, tolerância e triagem de falso positivo, e é spec própria.
  • Mapear todos os processos de uma vez. A entrega exige um processo funcionando fim a fim (§9). Os demais entram depois, e cada um é só um mapa novo.
  • Criar tela ou endpoint no produto. apps/api, apps/web e apps/e2e não mudam.
  • Rodar contra a infra/local. O alvo é o dev-remote, pela mesma razão do e2e (CLAUDE.md): o que se valida tem que ser o que vai para o ar.
  • Publicar a página em algum lugar. Ela é gerada no repositório e aberta no browser; hospedar é outra conversa.

4. Regras de negócio

Onde a coisa mora

  • RN1. A skill vive no projeto, em .claude/skills/journeys/, mas é escrita para ser portável: SKILL.md e scripts/ não citam Garantora, rota, papel nem data-testid deste sistema. Tudo que é daqui vive em journeys/config.json e nos mapas. Levar a outro projeto é copiar a pasta da skill e escrever um config.json.

Foi decidido pôr tudo no projeto em vez de deixar a skill em ~/.claude. O custo assumido é manter cópias em sincronia quando ela for para um segundo projeto; a contrapartida é que a skill entra em PR, é revisada e versionada junto do que ela fotografa.

  • RN2. A árvore:
.claude/skills/journeys/
  SKILL.md              genérico — o procedimento
  scripts/capture.mjs   genérico — executa um mapa e gera a página
journeys/
  config.json           do projeto — alvo, autenticação, papéis, viewports
  dados.js              do projeto — o pool de personas e valores verossímeis (RN8c)
  <processo>.json       do projeto — o mapa de cada jornada (versionado)
docs/journeys/
  index.html            a entrega (gerada)
  <processo>/*.png      os prints (gerados)
  • RN3. docs/journeys/ não é versionado (entra no .gitignore). A verdade versionada é o mapa: dele a página se regenera inteira. Um diff de PNG é ilegível, e uma execução completa produz dezenas de imagens — versionar isso incharia o repositório sem dar em troca nada que o diff do mapa não diga melhor.

A configuração do projeto

  • RN4. journeys/config.json declara o que a skill não pode adivinhar:
{
  "baseUrl": "https://dev.garantora.com.br/app",
  "apiUrl": "https://dev.garantora.com.br/api/v1",
  "auth": {
    "loginPath": "/auth/login",
    "tokenField": "access_token",
    "storageKey": "garantora-access-token"
  },
  "roles": { "gestor": "gestor@e2e.test", "analista": "analista@e2e.test" },
  "password": "S21d2qu1",
  "defaultRole": "gestor",
  "viewports": [
    { "nome": "desktop", "width": 1440, "height": 900 },
    { "nome": "mobile", "width": 375, "height": 812 }
  ],
  "beforeRun": "invocar /devremote-rebuild se o código mudou"
}

Os papéis e a senha são os mesmos do apps/e2e/fixtures/roles.ts — a skill lê essa convenção do config, não a reimplementa.

O mapa da jornada

  • RN5. Cada processo tem um mapa versionado em journeys/<processo>.json, com passos em ordem. Um passo declara: nome, rota (ou a ação de navegação que chega nela), setup opcional (chamadas de API que produzem o estado), ações opcionais (interações por data-testid antes do print) e espera (o data-testid que precisa estar visível para o print valer).
{
  "processo": "Análise de crédito",
  "papel": "gestor",
  "passos": [
    {
      "nome": "Rascunho criado",
      "setup": [
        { "metodo": "POST", "rota": "/renters",
          "corpo": { "name": "{{inquilino.nome}}", "cpf": "{{inquilino.cpf}}" },
          "guardarComo": "inquilino" },
        { "metodo": "POST", "rota": "/credit-analyses", "corpo": "…", "guardarComo": "analise" }
      ],
      "rota": "/credit-analyses/{{analise.id}}",
      "espera": "credit-analysis-timeline"
    },
    {
      "nome": "Enviada para análise",
      "setup": [{ "metodo": "POST", "rota": "/credit-analyses/{{analise.id}}/submit" }],
      "rota": "/credit-analyses/{{analise.id}}",
      "espera": "credit-analysis-timeline"
    }
  ]
}
  • RN6. A primeira execução de um processo analisa antes de navegar. O modelo lê as rotas (apps/web/src/App.tsx), as páginas envolvidas, a máquina de estados correspondente na API e os data-testid disponíveis, e escreve o mapa. Só então executa. Sem mapa, a skill não navega — declarar o caminho antes é o que torna a execução repetível e o resultado revisável.

  • RN7. Com o mapa existindo, a execução não reanalisa: lê e roda. --reanalisar refaz a análise e mostra o diff contra o mapa atual, pedindo confirmação antes de gravar. Sobrescrever em silêncio apagaria ajuste feito à mão, que a RN6 espera que aconteça.

A execução

  • RN8. O dado de cada passo é criado pela API, antes de navegar — a mesma técnica dos specs e2e, pelo mesmo motivo: um print que depende do que houver no banco mostra coisa diferente a cada execução, e pode simplesmente não achar o estado que precisa. A unicidade que o banco exige vem do documento gerado (RN8c), não de um sufixo no nome, então rodar duas vezes não colide e os nomes continuam bonitos.

  • RN8a. O dado é uma simulação verossímil, não um preenchimento técnico. O print é para ser mostrado — a alguém do time, a um cliente, numa revisão de fluxo —, e Inquilino Shot 1786211919068 não sobrevive a isso. O que o mapa monta tem que passar por um caso real:

Não vale Vale
Nome Inquilino Shot 1786… Marina Albuquerque Rocha
Documento vazio, ou 11111111111 CPF/CNPJ com dígito verificador válido
Imobiliária Imob shot 1786… Vitória Imóveis · CNPJ válido
Endereço Rua V, 1 Rua Doutor Rafael de Barros, 209 · Paraíso · São Paulo/SP · 04003-042
Valores 2500 / 0 / 0 aluguel 3.200 · condomínio 680 · IPTU 190
Renda número redondo qualquer compatível com a profissão e com o desfecho da jornada
Parecer Screenshot spec 0025 texto que um analista escreveria
Vigência ausente 12 meses a partir do primeiro dia do mês seguinte

Campo vazio aparece na tela como —, e uma jornada cheia de travessões não mostra o produto: mostra o formulário mal preenchido. Todo campo que a tela exibe é preenchido, a menos que a ausência seja justamente o que o passo quer demonstrar.

  • RN8b. Os valores servem ao desfecho que o passo demonstra. A jornada da análise de crédito termina em aprovação, então a renda declarada tem que ficar dentro da política (≥ 3× o Pacote Locatício, spec 0025/RN7) — senão o print mostra "FORA" numa jornada que aprova, e o material fica incoerente. Um mapa que queira demonstrar o caso reprovado declara isso e escolhe a renda para caber nele. O mapa diz qual cenário encena; os números obedecem.

  • RN8c. Os dados vêm de um pool do projeto, não do núcleo da skill. journeys/dados.js (lado do projeto, RN1) exporta um gerar() que devolve personas, imobiliárias, imóveis e valores coerentes — com CPF/CNPJ gerados com dígito verificador válido e CEP de logradouro existente. O mapa referencia por token ({{inquilino.nome}}, {{imovel.cep}}); o núcleo só substitui. Assim a skill continua portável: outro projeto escreve o próprio pool, com os documentos do próprio país.

A unicidade que o banco exige — CNPJ de imobiliária — vem do documento gerado, não de um sufixo no nome. É o que permite nome bonito e execução repetível ao mesmo tempo.

  • RN8d. Os registros da skill são identificáveis no banco e invisíveis no print. Com dado verossímil, quem abrir o dev-remote não distingue mais o que é da skill do que é de alguém testando. A marca vai num campo que a tela do processo não exibe — um sufixo convencionado no e-mail de contato da imobiliária (+journeys) —, o suficiente para varrer e limpar depois sem sujar nenhuma captura.

  • RN9. O que for configuração compartilhada, a skill desfaz. Parametrização de documentos, ajuste de TenantSettings e afins são do tenant, não da jornada — e o tenant é compartilhado com o e2e. Um passo que mexa nisso declara o desfazer, executado em finally mesmo se a jornada falhar no meio. Esta regra existe porque já aconteceu: na spec 0025 um script de screenshot deixou um documento obrigatório parametrizado e derrubou três testes e2e que nada tinham a ver com a mudança.

  • RN10. A autenticação é por injeção do token no localStorage, não pela tela de login: é mais rápida e não quebra quando a tela de login muda. Uma jornada cujo objeto seja o login declara "auth": "pela-tela" e aí passa pelo formulário.

  • RN10a. A captura estica o viewport até o conteúdo caber, em vez de usar fullPage. Aplicação cujo layout rola por dentro — qualquer casca com barra fixa — não rola o documento, e fullPage: true devolve exatamente o viewport: a jornada mostraria só o topo de cada tela. Esticar a altura resolve sem seletor de projeto e mantém o cabeçalho na imagem. Teto configurável (alturaMaximaPrint), e a rolagem horizontal continua medida no viewport declarado — é a largura real do dispositivo que decide se há defeito.

  • RN11. Cada passo é fotografado em todos os viewports do config — desktop e mobile por padrão. Em cada print a skill também afere scrollWidth - clientWidth == 0 e registra a falha no relatório sem interromper a jornada: encontrar rolagem horizontal é metade do valor da ferramenta, e abortar no primeiro defeito esconderia os seguintes.

  • RN12. Erro de console (console.error, pageerror) é coletado por passo e aparece no relatório. Mesmo tratamento: anota, não interrompe.

  • RN13. O alvo é o dev-remote, e a skill avisa quando o código local está à frente da imagem (comparando o HEAD com a data da imagem). Não sobe ambiente isolado — um ambiente montado à mão diverge no build, no service worker e no roteamento do Caddy, e o print deixaria de ser sobre o que vai para o ar.

  • RN14. Cada jornada roda com um papel, declarado no mapa ou herdado do defaultRole. Comparar o mesmo processo entre papéis (o que o AGENCY_USER vê e o ANALYST não) é feito com dois mapas, não com um mapa multi-papel — cada jornada continua sendo uma sequência linear, que é o que a torna legível.

A entrega

  • RN15. A saída é uma página HTML navegável em docs/journeys/index.html, com menu de processos à esquerda. Escolhido um processo, o corpo mostra os passos em ordem; cada passo traz nome, rota, o que o setup fez, e os prints dos dois viewports lado a lado.

  • RN15a. O clique no print abre um visualizador com zoom, não uma aba nova. O print tem a página inteira (RN10a), então na miniatura nada é legível e o tamanho natural em aba nova não dá controle nenhum — o zoom é a única forma de ler um campo.

O visualizador abre ajustado à tela e oferece roda do mouse, arraste para mover, botões − / + / Ajustar / 100%, duplo clique alternando ajuste ↔ tamanho real, e teclado (Esc, +, −, 0, 1).

Fecha só pelo botão ou por Esc — clique nunca fecha. Há um × no canto e um Fechar na barra de controles. Não existe fechar-por-clique porque ele é incompatível com o arraste: setPointerCapture, que é o que mantém o pan inteiro quando o ponteiro sai da imagem, faz o browser redirecionar o click para o elemento que capturou — então um clique sobre a própria imagem chega indistinguível de um clique no fundo, e o visualizador fechava no meio da análise. Entre perder o arraste e perder o atalho de fechar, perde-se o atalho.

O arraste exige desligar o arraste nativo de imagem. <img> é arrastável por padrão: pressionar sobre o print inicia um drag-and-drop do browser, que dispara pointercancel e mata o pan depois de alguns pixels. São três desligamentos, e falta qualquer um deles para o defeito voltar: draggable="false" no elemento, -webkit-user-drag: none + user-select: none no CSS, e preventDefault() no pointerdown.

O zoom acontece sob o cursor. Com transform-origin: 0 0, um ponto p da imagem cai em s·p + t; manter parado o ponto q sob o cursor exige t' = q − (q − t)·(s'/s). Sem isso o zoom foge para o canto, e navegar numa captura de página inteira vira tentativa e erro. Continua sem CDN e sem dependência (RN16).

  • RN16. A página é autocontida e offline: CSS embutido, sem CDN, imagens como arquivos ao lado. Abre com um duplo-clique, sem servidor.

  • RN16a. No dev-remote ela também é servida em /journeys, pelo Caddy da borda, via bind mount de docs/journeys/ — não entra em imagem nenhuma. Recapturar é rodar o skill: o Caddy passa a servir a versão nova sem rebuild e sem restart, que é o oposto do que vale para web e api. handle_path remove o prefixo, senão toda imagem responderia 404.

A pasta é ignorada pelo git, então num clone novo o Docker cria o ponto de montagem vazio e /journeys responde 404 até a primeira captura — comportamento correto, e o motivo de a rota não depender de arquivo versionado.

Fica aberta, como o /mail. São capturas de um ambiente de desenvolvimento com dados fictícios, mas expõem a interface a quem souber a URL; o Caddyfile traz o basic_auth comentado para quando isso deixar de ser aceitável.

  • RN17. O cabeçalho de cada processo mostra quando foi capturado, contra que baseUrl, em que commit (git rev-parse --short HEAD) e com que papel — um print sem essas quatro coisas não permite dizer se ainda vale.

  • RN18. Os defeitos encontrados (RN11, RN12) aparecem no topo da página, com contagem e ligação para o passo. Uma execução limpa diz isso explicitamente, em vez de só não mostrar nada.

  • RN19. A skill não roda sozinha — nem em hook, nem em CI, nem por iniciativa do modelo dentro de outra tarefa. É invocada pelo usuário (/journeys <processo>), porque monta dado no ambiente compartilhado e leva minutos. Sem argumento, lista os processos mapeados e os disponíveis para mapear.

5. Impacto por app

apps/api

Sem impacto. Nenhum endpoint, modelo ou migration. A skill consome a API pública com um token de usuário de e2e, como qualquer cliente.

apps/web

Sem impacto no código. A skill depende dos data-testid que o CLAUDE.md do web já torna obrigatórios — se um passo não encontra o testid de que precisa, isso é achado da skill (a tela não está instrumentada), não motivo para mudar a tela dentro desta spec.

apps/e2e

Sem impacto. A suíte não muda. A skill espelha convenções dela — usuários de fixtures/roles.ts, montar mundo pela API, alvo no dev-remote — mas não importa código: o config.json redeclara o que precisa, para a skill continuar portável (RN1).

Ferramental (o que realmente muda)

  • .claude/skills/journeys/SKILL.md — o procedimento: analisar, mapear, executar, relatar.
  • .claude/skills/journeys/scripts/capture.mjs — Node + playwright-core (já disponível em apps/e2e/node_modules), sem dependência nova. Lê um mapa, executa, grava PNGs e emite o JSON de resultado.
  • .claude/skills/journeys/scripts/render.mjs — o JSON de resultado vira index.html.
  • journeys/config.json — a configuração do Garantora.
  • journeys/dados.js — o pool verossímil: personas com CPF válido, imobiliárias com CNPJ válido, imóveis com endereço e CEP reais, e valores coerentes entre si (RN8a–RN8c).
  • journeys/analise-credito.json — o primeiro mapa (§9).
  • .gitignore — acrescenta docs/journeys/.
  • infra/dev-remote/ — Caddyfile, Caddyfile.behind-edge e docker-compose.yml: a rota /journeys e o bind mount da pasta de saída (RN16a).
  • CLAUDE.md da raiz — uma linha na seção de validação apontando a skill como o caminho para "ver a jornada", ao lado do script de screenshot pontual.

6. Contrato de API

Nenhum endpoint novo. A skill consome os existentes. O "contrato" que esta spec cria é o formato do mapa (RN5) e o do resultado que alimenta a página:

{
  "processo": "Análise de crédito",
  "capturadoEm": "2026-08-08T14:21:00Z",
  "baseUrl": "https://dev.garantora.com.br/app",
  "commit": "707416c",
  "papel": "gestor",
  "passos": [
    {
      "nome": "Enviada para análise",
      "rota": "/credit-analyses/6f2a…",
      "setup": ["POST /credit-analyses/6f2a…/submit"],
      "prints": { "desktop": "analise-credito/02-desktop.png", "mobile": "analise-credito/02-mobile.png" },
      "overflow": { "desktop": 0, "mobile": 0 },
      "errosDeConsole": []
    }
  ],
  "defeitos": []
}

Erros previstos, todos relatados sem derrubar a execução: passo cujo espera não aparece no prazo (PASSO_NAO_CARREGOU), chamada de setup que falha (SETUP_FALHOU, e a jornada para ali porque os passos seguintes dependem dela), papel sem permissão para a rota (SEM_PERMISSAO).

7. Critérios de aceite

  • [x] CA1. /journeys sem argumento lista os processos com mapa e os disponíveis para mapear, sem navegar nem criar dado.
  • [x] CA2. /journeys analise-credito num repositório sem o mapa analisa primeiro, grava journeys/analise-credito.json e só então executa.
  • [x] CA3. Com o mapa existindo, a execução não reanalisa — o mapa não é reescrito.
  • [ ] CA4. /journeys analise-credito --reanalisar mostra o diff contra o mapa atual e só grava após confirmação.
  • [x] CA5. A execução cria o próprio dado: rodar duas vezes seguidas produz duas jornadas completas, sem colisão de registro.
  • [x] CA6. Um passo que altere configuração de tenant a desfaz ao final — verificável rodando a suíte e2e depois da skill e obtendo o mesmo resultado de antes.
  • [x] CA7. Cada passo tem print de desktop e de mobile, com as dimensões do config.
  • [x] CA7a. Nenhum print da jornada exibe carimbo técnico — nome com número de timestamp, lorem ipsum, teste, shot ou spec. Verificável lendo os prints.
  • [x] CA7b. Os documentos gerados passam na validação de dígito verificador: o CPF do inquilino e o CNPJ da imobiliária são aceitos pela API e aparecem formatados na tela.
  • [x] CA7c. Nenhum campo exibido pela tela do processo aparece como —, salvo quando a ausência for o que o passo demonstra (RN8a).
  • [x] CA7d. A jornada da análise de crédito termina aprovada e o quadro de política mostra DENTRO em todos os passos em que aparece — os números servem ao desfecho (RN8b).
  • [x] CA8. Uma tela com rolagem horizontal aparece como defeito no relatório, e a jornada continua até o último passo.
  • [x] CA9. Erro de console num passo aparece no relatório atrelado àquele passo.
  • [x] CA10. docs/journeys/index.html abre por duplo-clique, sem servidor e sem rede, com todos os prints visíveis.
  • [x] CA19. https://dev.garantora.com.br/journeys responde com a página, e /journeys sem barra redireciona para /journeys/. /app e /api seguem intactos.
  • [x] CA20. Clicar num print abre o visualizador sem abrir aba nova; ele nasce ajustado à tela, amplia por roda, botão e teclado, move por arraste e fecha com Esc (RN15a).
  • [x] CA22. Clicar na imagem, no fundo ou dar duplo clique não fecha o visualizador; ele fecha pelo ×, pelo Fechar da barra e por Esc.
  • [x] CA21. O arraste move a imagem 1:1 com o mouse — deslocar o ponteiro em (dx, dy) desloca a imagem exatamente em (dx, dy) —, não dispara dragstart nem pointercancel, não altera a escala, e um arraste que termina fora da imagem não fecha o visualizador.
  • [x] CA11. A página tem menu de processos; escolher um troca o corpo sem recarregar.
  • [x] CA12. O cabeçalho de cada processo mostra data, baseUrl, commit e papel.
  • [x] CA13. Uma execução sem defeitos diz isso explicitamente no topo.
  • [x] CA14. docs/journeys/ está no .gitignore e git status fica limpo depois de uma execução (fora o mapa, quando for a primeira vez).
  • [x] CA15. SKILL.md e scripts/ não contêm nenhuma rota, papel, data-testid, URL ou nome próprio do Garantora — verificável por grep.
  • [x] CA16. O processo "Análise de crédito" está mapeado fim a fim: rascunho, envio, início da análise, pendência de documentos, retomada, aprovação e contrato — com um caso verossímil do começo ao fim (RN8a–RN8c).
  • [x] CA18. Os registros criados pela skill são localizáveis no banco pela marca da RN8d, e a marca não aparece em nenhum print.
  • [x] CA17. A skill avisa quando o HEAD local é mais novo que a imagem do dev-remote, antes de capturar.

8. Plano de implementação

Não vale a ordem api → web → e2e desta vez: nenhum dos três é tocado. A ordem aqui é a da dependência — formato, executor, renderizador, mapa.

  • [x] apps/api — sem impacto
  • [x] apps/web — sem impacto
  • [x] apps/e2e — sem impacto
  • [x] journeys/config.json — configuração do Garantora (RN4)
  • [x] journeys/dados.js — pool verossímil, com geradores de CPF/CNPJ válidos (RN8a–RN8c)
  • [x] .claude/skills/journeys/scripts/capture.mjs — executa um mapa, grava PNGs e o JSON de resultado (RN8–RN14)
  • [x] .claude/skills/journeys/scripts/render.mjs — JSON → index.html com menu (RN15–RN18)
  • [x] .claude/skills/journeys/SKILL.md — o procedimento, incluindo a análise da primeira vez e o --reanalisar (RN6, RN7, RN19)
  • [x] journeys/analise-credito.json — o primeiro mapa, derivado do código (CA16)
  • [x] .gitignore — docs/journeys/
  • [x] CLAUDE.md da raiz — a linha apontando a skill
  • [x] Execução real contra o dev-remote reconstruído, com a página aberta e conferida
  • [x] Suíte e2e rodada depois da skill, para provar a RN9/CA6

9. Decisões em aberto

Nenhuma. A única que havia foi decidida:

  • [x] Qual processo entra primeiro. Decidido pelo usuário em 2026-08-08: Análise de crédito (CA16) — entidade central do domínio, máquina de estados mais rica, e acabou de ser reorganizada pela spec 0025, então é onde a jornada visual paga mais rápido. Os candidatos seguintes, em ordem sugerida e fora do escopo desta spec: Onboarding da garantidora, Contrato e cobranças, Cadastro de inquilino e imóvel, Parametrização de documentos.

10. Registro

Implementada em 2026-08-08. A jornada da análise de crédito roda fim a fim contra o dev-remote: 7 passos, 14 prints, zero rolagem horizontal.

Achados da própria ferramenta

A skill existe para encontrar isto, e encontrou já na primeira execução. Nenhum deles é corrigido aqui — a spec declara apps/api e apps/web sem impacto, e mudar isso transformaria uma spec de ferramental numa spec de produto. Ficam registrados para virar /fix ou spec própria:

  1. POST /decision/pend devolve 500 com motivo longo. Decision.decision_reason é String(100) no banco e o schema não limita, então um motivo de 143 caracteres estoura como StringDataRightTruncationError — HTTP 500, não 422. Vale para pend, reject e approve, que compartilham o campo. Correção provável: max_length no AnalysisDecisionRequest.reason.

  2. A tela de detalhe registra um 404 no console em DRAFT e SUBMITTED. É o GET /credit-analyses/{id}/decision que a spec 0025 passou a chamar em todos os estados para alimentar o quadro de política. O 404 é resposta esperada — não há decisão nesses estados —, mas chega ao console como erro e polui qualquer monitoramento. São os 4 defeitos que a página relata.

  3. CNPJ aparece sem formatação em tela. _validate_cnpj (app/schemas/common.py:34) recusa pontuação, então todo CNPJ existe cru no banco — e nenhuma tela o formata na exibição. O CPF não tem esse validador e por isso sai formatado. A incoerência entre os dois documentos fica visível lado a lado no painel da imobiliária.

  4. PENDING_DOCS → IN_ANALYSIS é transição morta. Está em ALLOWED_TRANSITIONS (credit_analysis_service.py:70), mas o único ponto que grava IN_ANALYSIS é a criação da decisão, que exige SUBMITTED. Não há endpoint que a dirija. A retomada real do produto é anexar o documento e aprovar direto de PENDING_DOCS — que é como o mapa ficou, e funciona.

Divergências do planejado

  • RN10a é nova. O plano supunha fullPage: true. Medido no dev-remote, o <main> tem 2484px de conteúdo em 843px de altura e o documento não rola — fullPage devolvia exatamente o viewport, e a jornada mostrava só o topo de cada tela. A captura passa a esticar a altura do viewport até o conteúdo caber.

  • Três primitivas nasceram da necessidade, não do plano: seletor de lista no token ({{tipos.items[name=X].id}}), tolerarErro e arquivo (multipart). As três são genéricas — "garantir que existe" e "anexar um documento" são necessidades de qualquer jornada, não deste projeto.

  • A etiqueta da RN8d mudou de campo. Ela estava no email da imobiliária, que é exibido no painel — apareceu no primeiro print, violando a própria regra. Foi para contact_email, que nenhuma tela do processo mostra.

  • CPF com máscara, CNPJ sem. Não é escolha estética: a API recusa CNPJ pontuado e aceita CPF de qualquer forma, e a tela exibe o que recebeu. Verossímil aqui é obedecer ao que cada campo aceita — ver o achado 3.

Verificação

  • Duas execuções seguidas: 7 passos cada, sem colisão, mapa não reescrito.
  • Estado compartilhado limpo depois de cada uma — inclusive depois da execução que falhou no meio (o 500 do achado 1), que é onde o desfazer prova o que vale.
  • index.html aberto por file://: 7 passos, 14 imagens carregadas, metadados completos, zero erro de console na própria página.
  • Fronteira genérica conferida por grep: nenhum termo do projeto em SKILL.md ou scripts/.
  • Visualizador com zoom (RN15a, pedido do usuário depois da entrega): abre em 48% (ajuste), + leva a 105%, 100% a 100%, roda a 246%, Ajustar volta a 48%, Esc fecha. Zero abas novas e zero erro de console. Como a RN16a serve a pasta por bind mount, o modal entrou no ar sem rebuild e sem restart.

  • O arraste saiu quebrado na primeira versão, e o teste não pegou. O <img> mantinha o arraste nativo do browser, que cancela a sequência de ponteiro — no uso real o pan parava depois de alguns pixels. O teste passou mesmo assim porque só verificava "a transformação mudou", e porque evento sintético de automação não inicia drag-and-drop nativo. Corrigido nas três frentes (draggable, CSS, preventDefault) e o teste virou medida: o pan tem que ser 1:1 com o mouse. Aferido em três deslocamentos (−240,−160 · 300,220 · −90,410), todos exatos, sem dragstart nem pointercancel, com a escala intacta e sem o modal fechar por engano.

A lição vale além deste caso: asserção que só pergunta "mudou?" passa em qualquer implementação errada. A que pergunta "mudou quanto?" é a que pega. (Com tolerância: a comparação estrita reprovou um pan de -240.0000000000001 que estava correto.)

  • O fechar-por-clique caiu, e a causa é a mesma captura de ponteiro. Ele filtrava por e.target === palco supondo distinguir fundo de imagem — mas com setPointerCapture ativo o browser redireciona o click para o elemento capturante, então todo clique chegava como clique no fundo e fechava o visualizador. Fechar passou a ser só ×, Fechar e Esc (RN15a). As duas coisas não coexistem sem abrir mão do arraste.

  • Crase em comentário dentro de template literal quebrou o render.mjs três vezes. O HTML inteiro é uma template string, e escrever `assim` num comentário fecha a string no meio. Todos os comentários dentro do template passaram a ser texto puro.

  • Publicação no dev-remote (RN16a, pedido do usuário depois da entrega): /journeys → 302 para /journeys/; /journeys/ → 200; imagem → 200; /app/ e /api/docs inalterados. Carregada pela URL pública: 7 passos, 14 imagens, nenhuma requisição com erro, nenhum erro de console. O Caddyfile e o Caddyfile.behind-edge receberam a rota — o dev-remote roda o segundo (modo co-hospedado), e deixar os dois divergirem é como o modo standalone quebraria sem ninguém perceber.

  • Suíte e2e depois da skill: 29 de 29 nos quatro specs de análise de crédito — os mesmos que a poluição da spec 0025 derrubou. É a CA6 fechada onde ela importa.

Não verificado

  • CA4 (--reanalisar mostra o diff e pede confirmação) é procedimento do modelo, descrito no SKILL.md, não comportamento de script — não há o que executar para provar. Fica por conferir no primeiro uso real do flag.