Pular para conteúdo

0010 — Parametrização de documentos por área: botões e seleção do tipo

Status CONCLUÍDA
Branch spec/0010-parametrizacao-documentos-por-area
Criada em 2026-08-01
Apps afetados api (sem impacto) · web · e2e

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

1. Problema

A tela Documentos da Análise de Crédito (/settings/credit-analysis-documents) apresenta, dentro de cada aba de área, a lista inteira dos tipos de documento cadastrados no tenant — hoje 20 tipos vindos da ativação, agrupados em 7 grupos — cada um com um seletor "Não se aplica / Opcional / Obrigatório".

Três dores vêm daí:

  1. A tela mente sobre o que está configurado. Um tenant recém-ativado, sem exigência nenhuma, mostra 20 linhas preenchidas com "Não se aplica". O gestor rola uma lista longa procurando os poucos itens que realmente valem, e não consegue responder de relance "o que eu exijo do inquilino PF?".
  2. O trabalho é de exclusão, não de inclusão. Configurar 3 documentos exige passar por 20 seletores. A lista é fixa e imposta; o gestor queria escolher o tipo e dizer se é obrigatório ou opcional — não percorrer o catálogo inteiro.
  3. As abas não comunicam estado. Cinco abas idênticas não dizem qual área já foi parametrizada e qual está vazia. Só clicando uma a uma se descobre.

Some-se que o gestor precisa clicar Salvar no rodapé para gravar: a tela acumula pendências e, ao trocar de aba, não é óbvio que o que ficou para trás ainda está por salvar.

2. Objetivo

Trocar as abas por botões de área que mostram quantas exigências cada uma tem, e transformar o painel de configuração em uma lista do que foi cadastrado — vazia até que o gestor adicione, escolhendo o tipo de documento e o grau (obrigatório ou opcional). Cada alteração grava na hora.

3. Fora de escopo

Foi cogitado e fica de fora:

  • Mudar o contrato da API. GET/PUT /api/v1/settings/credit-analysis-documents continuam como estão (ver §5 apps/api). Enxugar o GET para devolver só as exigências configuradas foi avaliado e recusado: mexeria em service, testes e no aviso de exclusão de document-types, sem ganho para o usuário.
  • Criar tipo de documento a partir desta tela. O gestor continua indo a Tipos de Documento. A tela só ganha uma mensagem apontando para lá quando não há tipo disponível para adicionar.
  • Reordenar exigências (drag-and-drop, prioridade). A ordem é derivada de grupo + nome do tipo.
  • Regra de conferência, bloqueio na aprovação, justificativa do gestor e snapshot — tudo o que a spec 0008 definiu segue idêntico. Esta spec não toca em decision_service, build_checklist nem no DocumentChecklist.
  • Copiar exigências entre áreas ("usar o mesmo do Inquilino PF no PJ").
  • Confirmação ao remover uma exigência. Remover é reversível re-adicionando; um diálogo por remoção atrapalharia mais do que protege.

4. Regras de negócio

  • RN1. As cinco áreas (LESSEE_PF, LESSEE_PJ, CO_RESPONSIBLE, PROPERTY, ADDITIONAL) são apresentadas como botões-cartão, não como abas. Cada botão exibe o rótulo da área e a contagem de exigências configuradas nela (ex.: "Inquilino PF · 3 documentos", "Inquilino PJ · nenhum documento").
  • RN2. Ao carregar a tela nenhuma área está aberta — só os botões aparecem. Clicar num botão abre o painel daquela área; clicar no botão da área já aberta fecha o painel. Uma área aberta por vez. Onde o painel entra depende da largura:
  • desktop (grade de 2 ou 3 colunas): abaixo da grade inteira;
  • mobile (coluna única): logo abaixo do botão clicado, como acordeão — no fim da lista ele abriria a três ou quatro cartões de distância, fora da tela.
  • RN3. O painel de uma área lista apenas as exigências já configuradas nela. Um tipo sem registro naquela área não aparece — é a leitura literal da RN2 da spec 0008 ("ausência de registro = não se aplica"). A lista fixa de todos os tipos deixa de existir na tela.
  • RN4. Área sem nenhuma exigência mostra um estado vazio explícito ("Nenhum documento exigido nesta área. Nada será cobrado do gestor na aprovação."), seguido da linha de adição.
  • RN5. A linha de adição fica ao final do painel e tem três campos: tipo de documento (obrigatório), exigência (Obrigatório | Opcional, padrão Obrigatório) e validade em dias (opcional, inteiro ≥ 1; vazio = não vence). O botão Adicionar fica desabilitado enquanto não houver tipo escolhido.
  • RN6. O seletor de tipo da linha de adição oferece somente os tipos ativos ainda não configurados naquela área, agrupados pelo group do tipo. Um tipo já presente na lista some do seletor — não há como cadastrar o mesmo tipo duas vezes na mesma área (o UniqueConstraint do banco já garante; a UI impede antes).
  • RN7. Esgotados os tipos disponíveis, a linha de adição é substituída pela mensagem "Todos os tipos de documento já estão configurados nesta área." Se o tenant não tem nenhum tipo cadastrado, a mensagem aponta para Tipos de Documento (comportamento atual do ca-doc-no-types, preservado).
  • RN8. Cada alteração grava na hora, com uma chamada PUT de um item só:
  • adicionar → grava ao clicar em Adicionar;
  • editar → grava ao confirmar a linha (ver RN16);
  • remover → grava ao clicar no ícone de remoção (envia requirement: null).

O botão Salvar global e o contador de "alterações não salvas" do rodapé deixam de existir: o que grava é sempre a ação de uma linha, nunca um lote acumulado. - RN16. A exigência configurada é exibida como valor, não como formulário: nome do tipo, grau (Obrigatório em destaque, Opcional discreto), validade (vence em N dias / não vence) e grupo, mais os botões editar e remover. Os controles só aparecem na linha que o usuário mandou editar, e ela ganha confirmar e cancelar próprios. Uma linha em edição por vez. Cancelar descarta o rascunho sem chamar a API; Esc cancela e Enter confirma. - RN9. Enquanto uma gravação está em curso, a linha afetada fica em estado de espera (controles desabilitados). Falha na gravação reverte o estado local para o que a API devolveu por último e mostra o erro — a tela nunca fica mostrando algo que não foi gravado. - RN10. A resposta do PUT é a grade completa e substitui o estado local: é ela quem manda, não o otimismo da UI. - RN11. Validade inválida (não numérica, 0 ou negativa) não é enviada: o campo mostra erro e o valor anterior é restaurado. O ge=1 do schema segue como segunda barreira. - RN12. Permissões inalteradas em relação à spec 0008 — credit_analysis_document_requirements:list para ler, :update para gravar (só ADMIN_TENANT e ADMIN_GLOBAL). Novo: quem não tem :update (ANALYST, OPERATOR, VIEWER, AGENCY_USER) vê a tela em modo leitura — sem linha de adição, sem botão de remover, seletores desabilitados e um aviso "Somente o gestor pode alterar as exigências". Hoje esses papéis veem tudo editável e só descobrem o 403 ao salvar. - RN13. A contagem no botão da área e a lista do painel refletem o mesmo estado — gravou, o número no botão muda junto, sem recarregar a página. - RN14. Nada muda no dado. Exigências já parametrizadas continuam valendo e aparecem na área correspondente; não há migration nem alteração de contrato.

5. Impacto por app

apps/api

Sem impacto. Nenhum model, schema, service, router ou migration é alterado. O contrato existente já sustenta a tela nova:

  • GET /api/v1/settings/credit-analysis-documents devolve todos os tipos ativos com areas mapeando cada área para a célula ou null. A tela usa a mesma resposta de duas formas: as células não nulas viram as linhas do painel (RN3), e os tipos com célula nula naquela área alimentam o seletor de adição (RN6).
  • PUT /api/v1/settings/credit-analysis-documents já é upsert parcial por célula: células não enviadas ficam como estão, e requirement: null apaga. Um items de um elemento só é exatamente o que a RN8 pede.

Nenhum teste de tests/test_credit_analysis_documents.py precisa mudar.

apps/web

  • Página: src/pages/settings/credit-analysis-documents/index.tsx — reescrita da camada de apresentação. api.ts fica inalterado.
  • Componentes internos (mesma pasta, se o arquivo passar de ~250 linhas): AreaButtons.tsx (os cinco botões-cartão com contagem) e AreaPanel.tsx (lista + linha de adição).
  • Estado: o edits: Record<string, CellValue> acumulado morre com o botão Salvar. Fica rows (verdade da API), openArea: DocumentArea | null e savingKey: string | null para o estado de espera por linha (RN9).
  • Aproveitar: AREA_LABELS e AREA_HELP seguem como estão (o AREA_HELP vira o subtítulo do painel aberto). useCurrentUser() (src/shared/hooks/useCurrentUser.ts) fornece isAdminTenant/isAdminGlobal para o modo leitura da RN12.
  • data-testid:
Testid Onde Situação
ca-doc-area-buttons container dos cinco botões novo
ca-doc-area-button-{AREA} botão-cartão da área novo (substitui ca-doc-area-tab-{AREA})
ca-doc-area-count-{AREA} contagem dentro do botão novo
ca-doc-requirements-form painel da área aberta mantido (era o container da lista)
ca-doc-area-empty estado vazio da área novo
ca-doc-requirement-row-{typeId} linha de exigência configurada mantido
ca-doc-requirement-label-{typeId}-{area} grau em leitura (RN16) novo
ca-doc-validity-label-{typeId}-{area} validade em leitura (RN16) novo
ca-doc-edit-{typeId}-{area} botão editar da linha novo
ca-doc-requirement-{typeId}-{area} seletor Obrigatório/Opcional — só em edição mantido
ca-doc-validity-{typeId}-{area} validade — só em edição mantido
ca-doc-save-{typeId}-{area} confirmar edição da linha novo
ca-doc-cancel-{typeId}-{area} cancelar edição da linha novo
ca-doc-remove-{typeId}-{area} botão remover da linha novo
ca-doc-add-type seletor de tipo da linha de adição novo
ca-doc-add-requirement seletor de grau da linha de adição novo
ca-doc-add-validity validade da linha de adição novo
ca-doc-add-submit botão Adicionar novo
ca-doc-add-exhausted mensagem da RN7 novo
ca-doc-read-only aviso do modo leitura (RN12) novo
ca-doc-requirements-saved feedback de gravação mantido (vira feedback por ação)
ca-doc-requirements-error erro de gravação/carga mantido
ca-doc-no-types tenant sem nenhum tipo mantido
ca-doc-area-tabs — removido
ca-doc-area-tab-{AREA} — removido
ca-doc-requirements-save — removido (não há mais Salvar)
  • Sider: sem mudança. A página já está registrada em src/App.tsx (rota /settings/credit-analysis-documents) e em src/components/navigation/nav-groups.tsx (grupo ADMINISTRAÇÃO, item "Documentos da Análise"). Módulo não é novo.
  • Responsividade: os botões-cartão fluem em grade — 2 ou 3 colunas no desktop, coluna única em 375px, cada botão ocupando a largura toda. No mobile o painel é intercalado logo após o botão aberto (RN2); quem decide a posição é o AreaButtons, que recebe o painel por prop — o useIsMobile() (< 600px) é o mesmo corte em que a grade vira coluna única, então as duas decisões nunca saem de sincronia. No painel, cada linha de exigência empilha em xs (nome, grau, validade, remover) e vira row a partir de sm, como a linha atual já faz. A linha de adição segue o mesmo empilhamento, com o botão Adicionar em largura total no mobile. Sem scroll horizontal (scrollWidth - clientWidth <= 1). O rodapé sticky sai junto com o botão Salvar, devolvendo altura útil ao mobile.
  • PWA: sem impacto — nenhum asset novo, nenhuma tela offline, nenhuma mudança no service worker. Vale a ressalva de sempre: no dev-remote o bundle é cacheado, então a mudança só aparece após rebuild do container web.

apps/e2e

  • Spec existente a atualizar: tests/e2e/credit-analysis-documents.spec.ts
  • CA1 ("o gestor parametriza pela tela e a marcação persiste") — o fluxo muda: não há mais linha pré-existente para trocar o seletor nem botão Salvar. Passa a abrir a área pelo botão, usar a linha de adição e conferir via API.
  • CA14 (usabilidade em 375px) — troca ca-doc-area-tabs / ca-doc-area-tab-CO_RESPONSIBLE pelos botões novos.
  • CA3/CA4, CA8/CA9, CA11 não tocam nesta tela pela UI (parametrizam via API) — seguem sem alteração.
  • Cenários novos (mesmo arquivo):
  • área vazia mostra o estado vazio e nenhuma linha de exigência;
  • adicionar grava sem clicar em Salvar e a contagem do botão sobe;
  • o tipo recém-adicionado some do seletor de adição;
  • remover apaga a exigência e a contagem do botão desce;
  • papel sem :update (analista) vê a tela em modo leitura;
  • a linha nasce em leitura e os controles só aparecem ao editar (RN16);
  • estado interceptado com page.route(), para o que não dá para montar no tenant compartilhado: catálogo de tipos vazio (CA10), PUT falhando (CA12) e validade inválida sem chamada à API (CA7).
  • Seeds: nenhum novo. O helper ensureDocType e a limpeza no finally (setRequirement(..., null)) do arquivo atual continuam valendo — e ficam ainda mais necessários, já que a tela agora reflete exatamente o que está gravado.

6. Contrato de API

Inalterado. Reproduzido aqui só para deixar claro como a tela nova usa o que já existe.

Carga da tela — uma chamada, que serve tanto à lista quanto ao seletor:

GET /api/v1/settings/credit-analysis-documents
Authorization: Bearer <token>
{
  "items": [
    {
      "document_type_id": "8f1c…",
      "name": "CPF",
      "group": "Identificação",
      "areas": {
        "LESSEE_PF": { "requirement": "REQUIRED", "validity_days": null },
        "LESSEE_PJ": null,
        "CO_RESPONSIBLE": null,
        "PROPERTY": null,
        "ADDITIONAL": null
      }
    }
  ]
}

Na área LESSEE_PF esse tipo é uma linha do painel; em LESSEE_PJ ele é uma opção do seletor de adição.

Adicionar / alterar (RN8) — um item por chamada:

PUT /api/v1/settings/credit-analysis-documents
Authorization: Bearer <token>

{ "items": [ { "document_type_id": "8f1c…", "area": "LESSEE_PF",
               "requirement": "REQUIRED", "validity_days": 90 } ] }

Remover:

{ "items": [ { "document_type_id": "8f1c…", "area": "LESSEE_PF",
               "requirement": null, "validity_days": null } ] }

A resposta dos dois é o mesmo corpo do GET (grade completa), e substitui o estado local (RN10).

Erros previstos:

Situação Resposta Tela
Sem permissão :update 403 Não deve acontecer — RN12 esconde os controles. Se ocorrer, alerta em ca-doc-requirements-error e reversão (RN9)
validity_days < 1 422 Barrado antes na UI (RN11)
Tipo inexistente/soft-deletado 404 / erro do service Alerta + recarga da grade
Rede/500 — Alerta + reversão do estado local (RN9)

7. Critérios de aceite

Marcação: [x] = verificado de fato — pela suíte e2e executada, pelo browser, ou pelo git (CA14). Todos os quinze estão marcados.

  • [x] CA1. Dado um tenant sem exigência nenhuma, quando o gestor abre a tela, então vê os cinco botões de área, todos com contagem zero, nenhum painel aberto e nenhuma lista de tipos.
  • [x] CA2. Dado que o gestor clica no botão "Inquilino PF", então o painel dessa área abre abaixo dos botões, mostrando o texto de ajuda da área, o estado vazio e a linha de adição.
  • [x] CA3. Dado o painel aberto, quando o gestor escolhe o tipo "CPF", mantém "Obrigatório" e clica em Adicionar, então a exigência aparece na lista sem que ele clique em Salvar (não existe botão Salvar na tela) e a contagem do botão "Inquilino PF" passa a 1.
  • [x] CA4. Dado que "CPF" já está na lista da área, então ele não aparece mais entre as opções do seletor de adição daquela área — mas continua disponível nas outras áreas.
  • [x] CA5. Dado que o gestor clica no botão da área já aberta, então o painel fecha; e clicar em outra área fecha a anterior e abre a nova (uma por vez).
  • [x] CA6. Dado uma exigência configurada, quando o gestor troca "Obrigatório" por "Opcional", então a alteração é gravada na hora e persiste após recarregar a página.
  • [x] CA7. Dado uma exigência configurada, quando o gestor digita 90 em "Validade (dias)" e confirma, então grava; recarregando a página o valor continua 90. Digitar 0 mostra erro no campo e não dispara chamada.
  • [x] CA8. Dado uma exigência configurada, quando o gestor clica em remover, então a linha some, a contagem do botão desce e o GET deixa de trazer a célula.
  • [x] CA9. Dado que todos os tipos do tenant já estão configurados na área, então a linha de adição dá lugar à mensagem de "todos já configurados".
  • [x] CA10. Dado um tenant sem nenhum tipo de documento cadastrado, então a tela mostra o aviso apontando para Tipos de Documento (comportamento atual preservado).
  • [x] CA11. Dado um usuário ANALYST, quando abre a tela, então vê as exigências em modo leitura — sem linha de adição, sem remover, seletores desabilitados — e um aviso de que só o gestor altera.
  • [x] CA12. Dado que a API falha ao gravar, então a tela mostra o erro e volta ao estado anterior — não fica exibindo a alteração que não foi gravada.
  • [x] CA13. Dado o viewport de 375px, então os botões de área ficam em coluna única, a linha de adição empilha e não há scroll horizontal (scrollWidth - clientWidth <= 1).
  • [x] CA14. Dadas exigências parametrizadas antes desta mudança, então elas aparecem nas áreas corretas e o bloqueio da aprovação (spec 0008) segue idêntico — nenhum teste de tests/test_credit_analysis_documents.py precisou mudar.
  • [x] CA15. (RN16) Dada uma exigência recém-adicionada, então ela aparece como valor — grau e validade legíveis, sem nenhum controle de formulário. Clicando em editar, e só então, os controles surgem naquela linha; cancelar descarta sem chamar a API e confirmar grava e volta para leitura.

8. Plano de implementação

Ordem obrigatória no monorepo: api → web → e2e. Aqui a api não entra.

  • [x] apps/api — sem impacto, confirmado por git status (ver Registro)
  • [x] apps/api — migration: não há
  • [x] apps/api — docstrings/Swagger: sem alteração
  • [x] apps/web — botões-cartão de área com contagem (RN1, RN2, RN13)
  • [x] apps/web — painel da área: lista só do configurado + estado vazio (RN3, RN4)
  • [x] apps/web — linha de adição com seletor de tipos disponíveis (RN5, RN6, RN7)
  • [x] apps/web — gravação imediata por ação, com estado de espera e reversão (RN8, RN9, RN10, RN11); removidos o rodapé sticky e o botão Salvar
  • [x] apps/web — modo leitura para quem não tem :update (RN12)
  • [x] apps/web — data-testid da tabela em §5 completos
  • [x] apps/e2e — CA1 e CA14 reescritos; cenários novos escritos
  • [x] tsc do web limpo; make test não roda nesta máquina — ver Registro
  • [x] Validado no browser em desktop e 375px por script de screenshot Playwright

9. Decisões em aberto

Nenhuma. As quatro dúvidas levantadas na entrevista foram decididas pelo usuário e estão registradas como regras:

  • Área abre em painel expandido abaixo dos botões, não em modal nem sub-rota (RN2)
  • Adição por linha inline ao final da lista, não por dialog (RN5)
  • Gravação imediata, sem botão Salvar (RN8)
  • Só tela — contrato de API inalterado (§5 apps/api)

8b. O que foi entregue

Arquivo O quê
apps/web/src/pages/settings/credit-analysis-documents/index.tsx Reescrita: carga, gravação por ação, permissão, orquestra os dois componentes
apps/web/src/pages/settings/credit-analysis-documents/AreaButtons.tsx novo — os cinco botões-cartão com contagem
apps/web/src/pages/settings/credit-analysis-documents/AreaPanel.tsx novo — lista da área + linha de adição
apps/web/src/pages/settings/credit-analysis-documents/labels.ts novo — AREA_LABELS/AREA_HELP, agora usados por dois componentes
apps/e2e/tests/e2e/credit-analysis-documents.spec.ts CA1 e CA14 reescritos; 3 cenários novos; describes separados por spec
apps/web/.../api.ts inalterado, como planejado
apps/api/** inalterado — git status confirma

10. Registro

Divergências do planejado

  1. Modo leitura por permissão, não por papel. A §5 dizia usar useCurrentUser().isAdminTenant/isAdminGlobal. O código usa user.permissions.includes("credit_analysis_document_requirements:update"), que é o padrão do resto do web (persons:delete, properties:delete) e checa exatamente o que a API exige — se um papel novo ganhar a permissão, a tela acompanha sem edição.

  2. data-testid dos selects foi para SelectDisplayProps, não inputProps. No Select do MUI o inputProps cai no input nativo escondido, que não recebe clique — o e2e da 0008 contornava isso com getByRole('combobox'). Pondo o testid no display, o atributo passa a valer para o que o usuário de fato clica. Vale para ca-doc-requirement-{typeId}-{area}, ca-doc-add-type e ca-doc-add-requirement. Os campos de texto seguem com inputProps, como manda o CLAUDE.md do web.

  3. Reversão da RN9 saiu de graça. Como nada é escrito de forma otimista — rows só muda com resposta da API — não há o que desfazer no erro. O único estado local que diverge é o rascunho da validade, descartado ao fim da tentativa. Não foi preciso guardar estado anterior.

  4. ListSubheader do seletor de tipos é lido como option. Descoberto ao dirigir a tela: getByRole('option').first() clica no cabeçalho do grupo, não no primeiro tipo. Não afeta o usuário (o cabeçalho não é clicável) e o spec e2e filtra por nome, então ficou como está — mas fica registrado para quem for escrever teste novo aqui.

  5. Ajuste pedido depois da primeira entrega: painel colado ao botão no mobile. A primeira versão punha o painel abaixo de todos os botões, nos dois viewports. No desktop está certo; no mobile, com os cinco cartões empilhados, clicar no primeiro abria o painel a quatro cartões de distância — fora da tela, parecendo que o clique não fez nada. O AreaButtons passou a receber o painel por prop e a posicioná-lo: intercalado após o botão aberto quando useIsMobile(), depois da grade quando não. A RN2 foi reescrita para registrar a diferença. Medido depois da correção: 12px entre botão e painel no mobile (é o gap da grade) contra 98px no desktop, que continua com o painel embaixo da grade inteira.

  6. Espaçamento no mobile. O gap de 8px (xs: 1) herdado da linha da spec 0008 servia para uma linha com um campo rotulado; com três empilhados, o label flutuante de um encostava na borda do campo de cima. Subiu para 20px na linha de adição (xs: 2.5) e 16px na linha de exigência (xs: 2), com o py acompanhando. O desktop não muda: os valores sm continuam os mesmos. No mesmo passo, o botão de remover passou a alinhar à direita no mobile (alignSelf: flex-end) — empilhado e sozinho à esquerda, lia como elemento solto.

  7. Destaque dos grupos no seletor de tipo. O ListSubheader sem estilo sai com peso parecido com o das opções, e o usuário lê "Identificação" como um tipo selecionável. Ganhou versalete, cor secondary, fundo tênue e divisor — a mesma linguagem do cabeçalho de grupo do painel — e as opções ganharam recuo (pl: 3). Isso convive com o item 4 acima: o cabeçalho segue exposto como option para a árvore de acessibilidade; o que mudou é só o visual.

  8. Lista em leitura, edição sob demanda (RN16, pedido depois da entrega). A primeira versão deixava toda linha configurada como formulário aberto — três controles por exigência, o tempo todo. Com poucas exigências já ficava ruidoso: não dava para ler o que estava exigido sem interpretar campos. A linha passou a mostrar valores, com editar e remover; os controles surgem só na linha em edição, que ganhou confirmar e cancelar próprios.

Isso estreita a RN8, e de propósito: a gravação continua imediata e por linha (não há Salvar global), mas dentro da edição existe um passo de confirmação — que é o que devolve o cancelar, impossível quando cada mexida no seletor já gravava. Sumiu junto a gravação da validade no blur, que era a parte mais frágil do desenho anterior.

  • make test não roda nesta máquina: não há uv, .venv nem pytest no host. A suíte foi executada dentro do container (docker exec garantora-devremote-api-1 uv run pytest) — 173 passed.

Correção. Uma versão anterior deste registro dizia que a imagem era anterior à spec 0009 e que a árvore teria 187 testes, tratando o resultado como sinal fraco. Está errado: a imagem não tem app/core/i18n.py nem o model de translations, ou seja, é pós-0009 — e a própria 0009 registrou "173 passed". Os 187 do CLAUDE.md é que estão velhos (medidos antes de a 0009 remover os testes de i18n). O número bate com a árvore, e o resultado vale. - tsc do web limpo (npx tsc --noEmit, zero erros). ruff não se aplica: nenhum arquivo Python mudou. - Validado no browser de verdade, contra a API do dev-remote, com um Vite temporário proxiando /api (a API de lá só libera CORS para dev.garantora.com.br). O config temporário foi removido ao final. Cinco telas conferidas em 1440px e 375px: painel fechado, área vazia, adição concluída (contagem sobe, snackbar, sem botão Salvar), mobile em coluna única e modo leitura do analista. scrollWidth - clientWidth == 0 nos cinco. A exigência criada na validação foi removida ao final, para não contaminar a suíte e2e. - Suíte e2e executada. npx playwright test tests/e2e/credit-analysis-documents.spec.ts --project=chromium → 9 passed em 35s, de primeira: os 3 cenários da 0008 que seguem valendo, os 4 novos da 0010 e o mobile. Exigiu rebuild do container web do dev-remote antes (docker compose build web && docker compose up -d web), porque lá o front é build, não dev server — o dev-remote está rodando o código desta branch. Reexecutada depois dos ajustes 5–8, com o container rebuildado: 9 passed em 38s, incluindo os cenários novos de leitura/edição (a linha nasce como valor, editar revela os controles, cancelar não grava) e a asserção de folga botão→painel do CA13.

Com os três cenários de interceptação, o arquivo passou a 12 passed em 37s. - Suíte e2e completa (npm run test:fast, chromium — o mesmo motor do gate): 60 passed, 2 skipped em 2,1 min. Os 2 pulados são os semeadores em massa (cadastrar todas as imobiliárias, cadastrar todos os 20 imóveis), já marcados test.skip no repositório por não serem idempotentes contra o banco compartilhado — não têm relação com esta spec. Nenhuma regressão fora da tela alterada.

Firefox e WebKit não rodaram: o gate cross-browser triplica o tempo e, na VPS, o WebKit não tem as dependências de sistema (mesma limitação registrada na spec 0009). - Os três critérios que tinham ficado sem cobertura foram fechados com page.route(). O que os travava era o estado exigido — catálogo vazio, API falhando — e não a dificuldade do cenário: interceptar a rota produz os dois sem tocar no tenant compartilhado e sem deixar resíduo no banco. - CA10 — GET devolvendo {items: []}: a tela mostra o aviso de Tipos de Documento e não desenha os botões de área. - CA12 — PUT devolvendo 500 na remoção: alerta de erro, e a linha continua com grau e validade originais, confirmado também pelo GET seguinte. - CA7 (metade negativa) — validade 0 com contador de requisições: erro no campo e zero PUT; cancelar devolve o valor gravado.

Sobre o CA12, uma precisão: quando a falha acontece durante a edição, a linha permanece em edição com o que o usuário digitou. Isso é deliberado e não contraria o critério — o que a RN9 proíbe é a tela apresentar como salvo algo que não foi, e um formulário aberto não faz essa afirmação. Perder o que a pessoa digitou por causa de um 500 seria pior. - O CA9 segue sem cenário na suíte, mas foi observado no browser: com os dois tipos do tenant configurados em ADDITIONAL, a linha de adição deu lugar à mensagem de "todos já configurados". Automatizá-lo exigiria um tenant com catálogo pequeno e controlado.