Pular para conteúdo

0015 — A recusa do servidor não aponta o campo em três formulários

Status CORRIGIDO
Branch fix/0015-recusa-da-api-nao-aponta-o-campo (mesclada na main em 5f690d5)
Aberto em 2026-08-29
Fechado em 2026-08-30 — v0.4.2 em produção
Severidade MÉDIA
Apps afetados web
Revisão do diagnóstico fechada na rodada 7 por decisão do usuário — ver a seção 5
Revisão da correção sem o laço automático do /fix implement (sem subagente de contexto limpo) — o usuário acompanhou e revisou os 16 itens em conversa direta e autorizou o fechamento explicitamente. Ver a seção 11.

Status: RASCUNHO → DIAGNOSTICADO → EM CORREÇÃO → CORRIGIDO

O fix atravessa dois portões, cada um revisado por um subagente de contexto limpo, em até 3 rodadas. Máquina do plugin superpowers, régua em .claude/rubrics/. Qualquer Critical reprova; Important em aberto reprova salvo decisão registrada do usuário.

  1. O diagnóstico (seções 1 a 4) é revisado na seção 5 (reprodução × causa raiz). É o portão barato: sem ele, o segundo mede o conserto contra um palpite.
  2. A correção (o diff e os testes) é revisada na seção 11 (correção × regressão), com duas perguntas — atacou a causa ou o sintoma, e o que prova que não volta.

1. Sintoma

A tela manda conferir detalhes que ela não mostra. Salvando um formulário cujo campo o servidor recusa, aparece uma faixa vermelha no topo com:

Alguns campos precisam ser corrigidos. Verifique os detalhes abaixo e tente novamente.

E não há nada abaixo. Nenhum campo fica vermelho, nenhuma mensagem diz qual é o problema, e o formulário fica idêntico ao que estava antes do clique. A pessoa relê a tela inteira sem ter o que reler.

Acontece em três dos seis formulários que a spec 0046 tratou — e, como a seção 4 mostra, em muitas outras superfícies de entrada de dados pelo produto afora, incluindo a decisão de crédito. As três abaixo são por onde o defeito foi encontrado:

Tela Arquivo
Minha Garantidora apps/web/src/pages/my-garantidora/index.tsx
Cadastro de inquilino apps/web/src/pages/renters/form.tsx
Onboarding, dados da garantidora apps/web/src/pages/onboarding/StepTenant.tsx

Nos outros três — imobiliária, imóvel e análise de crédito — a mesma recusa marca o campo e mostra a mensagem nele.

É o mesmo defeito que a spec 0046 corrigiu, e que a seção 1 da spec 0047 descreve como "o erro aparecer sem dizer o que fazer". A 0046 o corrigiu em três telas; nestas três não.

2. Reprodução

O campo é o Estado, e a escolha dele não é acaso — ver a nota do e-mail abaixo, que explica o caminho que não serve.

  1. Abrir Minha Garantidora autenticado como ADMIN_TENANT.
  2. No campo Estado, escrever São Paulo por extenso.
  3. Clicar em Salvar.

O campo é texto livre na tela (apps/web/src/pages/my-garantidora/index.tsx:337, um register("state") sem rules e sem limite de tamanho), e o servidor o declara max_length=2 (apps/api/app/schemas/tenant.py:67). Nove caracteres produzem um 422 real, com {"field": "state"} no details[].

  • Ambiente: local, dev-remote ou produção — mas o dev-remote precisa de um passo a mais, ver a pré-condição abaixo.
  • Superfície: os dois viewports, e igualmente. O defeito é de tratamento de resposta, não de layout; nada muda em 375px, e o PWA não agrava.
  • Dados/pré-condições, e esta é a parte que engana: a garantidora precisa ter Razão social, CNPJ válido e Celular já gravados. São os três campos com rules no formulário (apps/web/src/pages/my-garantidora/index.tsx:171, :198 e :244), e o handleSubmit reprova antes de chamar o envio se qualquer um faltar.

A garantidora que o seed_e2e cria não tem celular — apps/api/app/db/seed_e2e.py:208 monta o Tenant só com nome, CNPJ e onboarding_step. Num dev-remote recém-semeado, os passos acima produzem uma tela que não faz nada: nenhuma requisição sai, e o que se vê é o sósia do sintoma, por uma causa que nada tem a ver com este defeito.

Preencher o Celular é incondicional, e não uma alternativa. Uma versão anterior desta nota oferecia "ou reproduza em city, street ou neighborhood, que são register puro". A parte sobre register puro é verdadeira e não ajuda: o required do celular reprova o handleSubmit inteiro, qualquer que seja o campo em que se digite. Medido, no seed como está: digitar 120 caracteres em Cidade e salvar produz zero requisições. A alternativa levava de volta ao mesmo sósia que o parágrafo existe para evitar.

O e-mail parece o caminho e não é — o navegador intercepta antes. A primeira versão deste registro mandava pôr contato no e-mail comercial, com o argumento de que a tela não confere e-mail. A metade sobre rules é verdadeira; a conclusão é falsa: os campos são type="email" (apps/web/src/pages/my-garantidora/index.tsx:204 e apps/web/src/pages/my-garantidora/index.tsx:362), nenhum formulário do repositório usa noValidate — grep -rn noValidate apps/web/src devolve zero — e o Salvar é type="submit" dentro do <form>. A validação de restrição do navegador aborta o envio e mostra o balão "Inclua um '@' no endereço de e-mail": nenhuma requisição sai, e a faixa nunca aparece.

Isso importa além do roteiro: um critério de aceite escrito sobre aquele caminho passaria antes e depois da correção, medindo um trajeto que não chega ao servidor.

Também reproduz por teste, e ele já existe. A suíte da spec 0047 registra o estado defeituoso de propósito:

  • apps/web/src/pages/renters/form.test.tsx:117 afirma aria-invalid="false" e que a mensagem específica não está no DOM — é a asserção forte;
  • apps/web/src/pages/my-garantidora/index.test.tsx:156 afirma só que o texto do campo não é a mensagem. É mais fraca, e é ela que precisa virar vermelha naquela tela.

A resposta que o caso simula ali é um 422 nomeando o campo do CNPJ. CNPJ duplicado, no servidor, é 409 sem details[] — o raise ConflictError de apps/api/app/services/tenant_service.py:54. O defeito que o caso demonstra é real, e vale para qualquer 422 com details; o que não é real é o exemplo escolhido, que o servidor não produz para aquele campo.

3. Esperado × Atual

  • Esperado: cada mensagem de details[] aparece no campo a que pertence, marcando-o; o que não tiver onde ser lido — campo registrado sem error/helperText, ou nome que não existe na tela — vai para a faixa do topo, com o rótulo do campo em português. É o que a spec 0046 desenhou.
  • Atual: todas as mensagens de details[] são descartadas. A faixa exibe a frase genérica da API, que anuncia detalhes inexistentes — exceto na área admin, onde é pior: apps/web/src/admin/services/adminApi.ts:57 lê a chave detail, que este envelope não tem, e o que aparece é "Erro 422". Nem a mensagem do servidor sobrevive.

4. Causa raiz

Onde. Os três catch do envio, cada um numa linha só:

  • apps/web/src/pages/my-garantidora/index.tsx:126 — setSaveError(err instanceof Error ? err.message : "Tentar novamente")
  • apps/web/src/pages/renters/form.tsx:149 — setError(e instanceof Error ? e.message : "Tentar novamente")
  • apps/web/src/pages/onboarding/StepTenant.tsx:71 — setError(err instanceof Error ? err.message : "Tentar novamente")

O mecanismo. O erro chega às telas carregando duas coisas — .message e .details — e o catch lê só a primeira. Mas não há um transporte só, e essa é a informação que muda o alvo da correção — a tabela deles está no Eixo 1, adiante, com os cinco que existem hoje.

A segunda linha é a mais perigosa, e só apareceu na quinta rodada do portão. A primeira linha de aplicarErrosDaApi (apps/web/src/shared/utils/foco-no-erro.ts:223) é uma guarda de tipo — if (!(e instanceof ApiError) || e.details.length === 0) return false. Um ApiValidationError falha nessa guarda: a helper devolve false sem marcar campo nenhum, mesmo com details cheio. Chamar a helper ali compila e não faz nada — a faixa genérica continua sozinha, e o sintoma sobrevive num lugar onde o registro diz que foi entendido. A classe não é lida por ninguém hoje: grep -rn ApiValidationError apps/web/src acha só a declaração e o throw.

O .details do transporte principal vem de body.error?.details — e o servidor sempre preenche esse campo numa recusa de validação: apps/api/app/core/exceptions.py:158 transforma cada erro do Pydantic em {field, message} e devolve a frase "Alguns campos precisam ser corrigidos. Verifique os detalhes abaixo e tente novamente." como mensagem geral. O handler é global (apps/api/app/main.py:511), não por endpoint.

As três linhas acima leem só o .message. O .details existe no objeto, com o nome do campo e o texto em português, e é jogado fora ali — nada mais o lê depois. Por isso a faixa mostra exatamente a frase que promete detalhes, e não há detalhe nenhum: a promessa e o descarte são a mesma resposta HTTP, lida pela metade.

Não é regressão: é lacuna de origem. As três linhas nasceram com as próprias telas e não foram tocadas pela spec 0046 — conferido com git log -L sobre o trecho da Minha Garantidora, que só mudou na remoção de i18n da spec 0009.

Há dois tratamentos certos no repositório, e não um — isto muda o alvo da correção.

  • aplicarErrosDaApi (apps/web/src/shared/utils/foco-no-erro.ts:217) é chamada por duas telas: apps/web/src/pages/agencies/form.tsx:130 e apps/web/src/pages/properties/form.tsx:175. Ela marca cada campo que existe no DOM, devolve para a faixa o que não ficou com aria-invalid depois do repintar, e responde false quando o erro não é recusa por campo.
  • A análise de crédito trata corretamente e não a usa: tem implementação própria, tratarRecusaDaApi (apps/web/src/pages/credit-analyses/CreditAnalysisForm.tsx:548). O motivo está no cabeçalho da própria helper: aquele formulário guarda erro sob chave própria — renterDoc, propStreet — e precisa traduzir o nome da API antes de marcar.

Das três telas do sintoma, a helper pronta serve em duas. Em my-garantidora e em renters/form os campos são registrados com o nome da API (email, contact_email, cnpj, cpf, state) e o erro chega como ApiError — ali é chamar a função que existe. O StepTenant é servido pelo ApiValidationError e não passa na guarda: consertá-lo exige tratar aquele transporte também, e não só trocar a linha do catch.

O alcance é maior que três telas, e a régua que o define levou quatro rodadas de portão para ficar de pé. Vale escrever a régua antes da lista, porque foi a falta dela que fez a lista mudar quatro vezes.

A régua do escopo, uma só, e a unidade é o catch: todo catch que trata a recusa de um envio de dado digitado pelo usuário e lê só .message.

A unidade importa: um arquivo costuma ter vários pontos de tratamento, de naturezas diferentes. O DocumentsPanel tem cinco catch de bloco — observação digitada (dentro), substituição de arquivo, exclusão, download e pré-visualização (fora) —, mais dois .catch(); o InviteListPage tem quatro, e só a emissão do convite está dentro. Dizer "arquivo" faria a régua incluir carregamento de lista, que não envia nada.

E "catch" é estreito demais por pouco: apps/web/src/components/shared/DocumentsPanel.tsx:427 tratava a recusa do envio num Promise.allSettled lendo só a mensagem do erro, sem catch nenhum — corrigido no item 2 desta lista. Era o único allSettled do src, então o furo era de um sítio só, mas uma varredura por catch o teria perdido. A unidade é o ponto onde a recusa do envio é lida, e não a palavra-chave.

As três partes fazem trabalho:

  • "dado digitado" exclui tela de lista que só apaga ou alterna um booleano (pages/users/index.tsx, admin/pages/tenants/TenantDetailPage.tsx): não há campo, não há details[] possível, e não há como escrever regressão — só sobraria um teste que passa contra qualquer código;
  • "superfície", e não "arquivo com TextField", porque em vários casos o campo mora num diálogo filho e o catch mora na tela. O exemplo é apps/web/src/pages/credit-analyses/show.tsx:1276, o diálogo de reprovação: ele monta um ConfirmDialog sem reasonMaxLength, então o reason não tem limite na tela e alcança o max_length=500 do schema. (O PendDialog não serve de exemplo: ele espelha o limite em apps/web/src/pages/credit-analyses/components/PendDialog.tsx:284.) Buscar por campo no mesmo arquivo perde justamente esses — foi assim que a tela mais importante do produto ficou fora das três primeiras listas;
  • "lendo só .message" exclui quem já trata — agencies/form.tsx, properties/form.tsx e CreditAnalysisForm.tsx — e exclui quem apenas repassa o erro sem o descartar: providers/api-fetch.ts, que o constrói com o details inteiro, e pages/renters/api.ts, que devolve o que o apiReq levantou.

Não confundir "repassa" com "reconstrói", e a diferença tem nome próprio: os módulos da tabela de transportes — shared/api/documents.ts, admin/services/adminApi.ts e providers/auth.ts — constroem um erro novo e descartam o details antes de a tela existir. Uma versão anterior desta linha punha o documents.ts entre os que "repassam", e quem confiasse nela classificaria o painel de documentos no balde errado. Nesses três, o conserto é do módulo, e não de cada tela que ele serve.

E um caso ficava dentro pela intenção e fora pela letra: apps/web/src/pages/users/form.tsx:155 lia o details só para acender um booleano no campo, e descartava as mensagens — corrigido no item 15 desta lista. Não lia "só .message", e tinha o mesmo defeito: a pessoa via o campo vermelho sem saber o que havia de errado nele.

A correção NÃO é uniforme, e descrevê-la por "famílias" foi o terceiro defeito desta seção. A régua diz onde a causa morde; o que "tratar a recusa" significa depende de dois eixos independentes, e não de uma classificação só. Foi a mistura dos dois que produziu, na versão anterior, uma tabela que mandava chamar a helper em duas telas que não têm formulário — e tsc não pega: passar um setError de useState compila, a helper devolve true, e o que chega à faixa é ["email"], o nome cru do campo. Pior que o sintoma de hoje.

Eixo 1 — por qual transporte o erro chega. Decide se o details sequer alcança a tela.

Transporte Onde O que chega à tela Quem é servido
ApiError com .details apps/web/src/providers/api-fetch.ts:131 mensagem e details quase todo o produto
ApiValidationError — classe própria, não estende ApiError lançada em apps/web/src/pages/onboarding/api.ts:55, declarada em apps/web/src/pages/onboarding/api.ts:10 mensagem e details, num tipo que a helper não reconhece StepTenant, register, verify-email, accept-invite
Error cru, details descartado apps/web/src/shared/api/documents.ts:47 (mensagemDeFalha) só a mensagem DocumentsPanel, e todo upload de documento
Error cru, lendo a chave errada apps/web/src/admin/services/adminApi.ts:57 — lê err.detail, e este envelope usa {error:{…}} "Erro 422" — nem a mensagem do servidor toda a área admin, incluindo a emissão de convite
reconstruído no provedor de autenticação, details descartado apps/web/src/providers/auth.ts:79 (login), :170 (recuperação, que descarta até a mensagem do servidor), :196 (redefinição) só a mensagem, e numa delas nem isso login, recuperação e redefinição de senha

Os dois transportes do meio só apareceram na sétima rodada, e servem sítios que este mesmo registro nomeia em escopo: o Promise.allSettled do DocumentsPanel recebe o Error cru do documents.ts, e a emissão de convite do InviteListPage recebe o "Erro 422" do adminApi. Nos dois, chamar a helper compila e devolve false — a mesma forma do defeito da classe ApiValidationError. Medido: com o 422 real da API, o módulo de documentos entrega um Error sem details, e aplicarErrosDaApi não marca campo nenhum.

Eixo 2 — quem é dono do estado de erro do campo. Decide o que se faz com o details depois de tê-lo.

Dono do estado de erro Exemplos O que "tratar" exige
react-hook-form my-garantidora, renters/form, StepTenant, register, verify-email, accept-invite o setError do formulário — é o que aplicarErrosDaApi consome. Os quatro últimos são o cruzamento difícil: têm formulário e são servidos pelo ApiValidationError
estado próprio da página apps/web/src/pages/users/form.tsx:80, apps/web/src/pages/roles/form.tsx:36 (useState<string \| null>) a mensagem não tem onde pousar. O users/form chega a acender um booleano por campo (emailComErro, :89), mas o TextField não tem helperText: marca o campo e não diz o que há de errado nele
o campo mora num componente e o catch, noutro o ConfirmDialog de reprovação em apps/web/src/pages/credit-analyses/show.tsx:1270 prop nova no componente; hoje ele só aceita error da caixa inteira (apps/web/src/components/shared/ConfirmDialog.tsx:85), e o de reprovação nem isso recebe
campo por linha, com estado indexado DocumentsPanel, cujo notesDraft é por documento (:311) e cujo catch é do próprio painel (:460) conserto interno, sem prop nova — mas o estado de erro precisa ser mapeado por documento, e o ConfirmDialog (um campo, um diálogo) não é molde
não há campo listas que só apagam ou alternam fora de escopo

O conserto é o cruzamento dos dois, e é por isso que não há receita única. As telas de conta são o exemplo que derruba qualquer classificação de um eixo só: register, verify-email e accept-invite usam react-hook-form — e portanto parecem o caso fácil — e são servidas pelo ApiValidationError, que não passa na guarda da helper. Chamar aplicarErrosDaApi nelas compila e não faz nada.

O precedente da análise de crédito não serve para credit-analyses/show.tsx, e a versão anterior desta seção dizia que servia. O tratarRecusaDaApi (apps/web/src/pages/credit-analyses/CreditAnalysisForm.tsx:548) pressupõe uma página com formulário próprio: formRef, um estado setErrors que cada campo consome, e quatro mapas locais de nome e ordem. Ele resolve "tenho campos, sob chaves próprias em vez de react-hook-form". A tela de exibição da análise não tem campo nenhum — o único recusável mora dentro do ConfirmDialog, cujo estado é privado. Ali o conserto passa por mudar o componente compartilhado, e isso é trabalho de outra natureza.

A segunda régua, essa sim, é de teste e não de escopo. Onde não há 422 com details[] alcançável, a correção continua valendo — é barata e resiste ao dia em que o schema apertar —, mas não há o que afirmar num teste.

Prova de regressão, porém, só onde há 422 com details[] alcançável hoje — senão o teste não teria o que afirmar. Alcançável quer dizer: o schema da API tem restrição de campo (max_length, min_length, gt=, validador) que a tela não espelha, e nada intercepta antes — nem validação nativa do HTML, nem rules do formulário.

Confirmados por leitura de schema, e são a espinha da regressão:

Superfície O que estoura Onde está a restrição
pages/my-garantidora/index.tsx state com mais de 2 letras apps/api/app/schemas/tenant.py:67
pages/credit-analyses/show.tsx reason acima de 500 caracteres apps/api/app/schemas/decision.py:62 — o diálogo de reprovação (apps/web/src/pages/credit-analyses/show.tsx:1270) não recebe reasonMaxLength, então nada limita na tela
pages/register/index.tsx name acima de 255 caracteres apps/api/app/schemas/auth.py:52 — a tela espelha só o minLength: 2 (apps/web/src/pages/register/index.tsx:319)

Uma quarta linha saiu daqui, e o motivo é o mesmo erro das listas antigas. O BillingPayDialog estava na tabela por ter amount_paid: gt=0 no schema (apps/api/app/schemas/billing.py:50). Mas o critério tem duas metades — schema e tela —, e a tela espelha: apps/web/src/pages/guarantees/components/BillingPayDialog.tsx:43 calcula isValid = amountPaid && parseFloat(...) > 0 e o botão é disabled={!isValid || loading} (:71). Valor zero ou negativo não sai. A tabela tinha sido montada lendo só o schema, que é literalmente metade do critério que o parágrafo acima escreve.

Numa das três, o sintoma nem é o da seção 1, e quem escrever a regressão precisa saber: em credit-analyses/show.tsx o catch (:424) faz setError da página, mas o diálogo continua aberto — e o próprio apps/web/src/components/shared/ConfirmDialog.tsx:81-86 documenta que nesse caso a faixa fica atrás do backdrop. Ali não há faixa vazia: não há faixa visível nenhuma.

Quatro varreduras por padrão de texto produziram quatro listas (quinze, dezoito, vinte, e uma quarta com falso positivo), sempre pelo mesmo motivo: grep acha o arquivo e perde o vizinho, e não distingue quem envia dado digitado de quem só transporta o erro. O que fechou a enumeração não foi uma quinta varredura — foi trocar a fonte por uma lista fechada: o inventário de telas do produto, hoje documentado em apps/web/CLAUDE.md (seção "Inventário de telas — a unidade de escopo de uma correção") (todo item do menu do tenant, todo item do menu do SaaS admin, e as telas de fora do menu — login, registro, recuperação de senha, verificação de e-mail, aceite de convite, onboarding). Cada item do inventário foi lido e classificado pela régua acima, em vez de buscado por texto.

O resultado, tela a tela (Tenant, SaaS admin, Fora do menu). "OK"/"N/A" tem o porquê numa frase; "JÁ CORRIGIDO NESTE FIX" são as três telas do sintoma original, viradas no commit 4dc5c43; "DEFEITO" é o que resta.

Tenant

Item do inventário catch Veredicto Mecanismo
Painel apps/web/src/pages/operations-center/index.tsx:95 N/A só carregamento, sem campo
Análise de Crédito, lista pages/credit-analyses/index.tsx:197,212 N/A carregamento e disparo sem campo
Análise de Crédito, lista — cancelar pages/credit-analyses/index.tsx:217-225 DEFEITO ConfirmDialog com motivo (dado digitado) não recebe a prop error — a faixa fica atrás do backdrop
Análise de Crédito, formulário apps/web/src/pages/credit-analyses/CreditAnalysisForm.tsx:548 OK tratarRecusaDaApi nos quatro envios
Análise de Crédito, exibição pages/credit-analyses/show.tsx:417-425 DEFEITO run() (cancelar e reprovar) só lê .message; nenhum dos dois ConfirmDialog recebe error — o próprio arquivo já resolve isso certo no diálogo de desistência (:1337)
Garantias (lista, encerrar, notas, pagamento, criação, assinatura) pages/guarantees/*, components/Billing*Dialog.tsx, components/SignatureDialog.tsx OK/N/A campos sem restrição de schema alcançável, ou já mirrorizados no cliente, ou erro exibido no Alert do próprio diálogo
Inquilinos, formulário apps/web/src/pages/renters/form.tsx:164 JÁ CORRIGIDO aplicarErrosDaApi
Imóveis, formulário apps/web/src/pages/properties/form.tsx:175 OK aplicarErrosDaApi
Imobiliárias, formulário apps/web/src/pages/agencies/form.tsx:130 OK aplicarErrosDaApi
Minha Garantidora apps/web/src/pages/my-garantidora/index.tsx:150 JÁ CORRIGIDO aplicarErrosDaApi
Equipe, formulário pages/users/form.tsx:152-156 DEFEITO lê details só para acender booleano (emailComErro); TextField sem helperText
Perfis de Acesso, formulário pages/roles/form.tsx:151-152 DEFEITO só .message, em error de página; name/description sem marcação própria
Parâmetros pages/settings/index.tsx:486-487 DEFEITO estado próprio via useState; só .message
Tipos de Documento pages/settings/document-types/index.tsx:136-137 DEFEITO handleSave só .message (handleDelete é N/A, sem campo)
Tipos de Pendência pages/settings/pendency-types/index.tsx:147-153 DEFEITO nunca lê .details — decide por regex sobre o texto da mensagem (/nome/i, /TAKEN/i); funciona por sorte, não por leitura estruturada
Documentos da Análise pages/settings/credit-analysis-documents/index.tsx:80-81 N/A grade de toggle, sem campo de texto
Auditoria apps/web/src/pages/audit-logs/index.tsx:85 N/A carregamento; TextField são filtros, não envio validável
Ajuda — N/A nenhum catch no diretório
DocumentsPanel — upload components/shared/DocumentsPanel.tsx:394-406 DEFEITO Promise.allSettled, (r.reason as Error).message descarta .details
DocumentsPanel — observação components/shared/DocumentsPanel.tsx:448-463 DEFEITO estado por documento (:311), catch só .message em error de página
DocumentsPanel — substituir/excluir/download/preview components/shared/DocumentsPanel.tsx:488-603 N/A sem campo de texto validável

SaaS admin

Item do inventário catch Veredicto Mecanismo
Painel apps/web/src/admin/pages/dashboard/AdminDashboardPage.tsx:86 N/A carregamento
Garantidoras (suspender/reativar/exclusão) admin/pages/tenants/* N/A sem campo enviado ao servidor (a exclusão confere o nome só no cliente)
Convites — emissão admin/pages/invites/InviteListPage.tsx:165-172 DEFEITO contact_name com max_length=255 alcançável (apps/api/app/schemas/tenant_invite.py:28, sem espelho no cliente); só .message
Convites — revogação admin/pages/invites/InviteListPage.tsx:182-187 N/A sem campo
Usuários admin/pages/users/GlobalUserListPage.tsx:92-95 N/A só leitura, sem diálogo com campo

Fora do menu

Item do inventário catch Veredicto Mecanismo
SignIn (login) apps/web/src/pages/login/index.tsx:52 DEFEITO, baixo impacto só .message; hoje sem details[] alcançável (type="email" intercepta antes), mas o descarte é o mesmo mecanismo
SignUp (registro) pages/register/index.tsx:89-90 DEFEITO react-hook-form presente, mas o catch usa setError de página, não o do formulário, e não chama aplicarErrosDaApi; name acima de 255 é alcançável (apps/api/app/schemas/auth.py:52). O transporte (ApiValidationError) já está corrigido desde o 4dc5c43 — falta só a tela consumi-lo
SignRecover, passo 1 pages/forgotPassword/index.tsx:44,47 DEFEITO react-hook-form presente, sem setError usado
SignRecover, passo 2 pages/resetPassword/index.tsx:51,54 DEFEITO mesmo padrão
SignOut (logout) apps/web/src/providers/auth.ts:156 N/A sem campo, sem recusa de validação possível
Verificação de e-mail pages/verify-email/index.tsx:51-52 DEFEITO react-hook-form (register("code")), catch grava em error de página em vez de setError do formulário
Aceite de convite pages/accept-invite/index.tsx:47-48 DEFEITO mesmo padrão, register("password")/register("confirmPassword")
Onboarding apps/web/src/pages/onboarding/StepTenant.tsx:88 JÁ CORRIGIDO aplicarErrosDaApi; index.tsx:79-82 (handleCancel) é N/A, sem campo

Dezesseis defeitos confirmados, e é o número que substitui "vinte a trinta arquivos" da versão anterior desta seção — o inventário fechado tornou a contagem exata em vez de estimada:

  1. admin/pages/invites/InviteListPage.tsx:165-172
  2. components/shared/DocumentsPanel.tsx:394-406
  3. components/shared/DocumentsPanel.tsx:448-463
  4. pages/accept-invite/index.tsx:47-48
  5. pages/credit-analyses/index.tsx:217-225
  6. pages/credit-analyses/show.tsx:417-425
  7. pages/forgotPassword/index.tsx:44,47
  8. apps/web/src/pages/login/index.tsx:52
  9. pages/register/index.tsx:89-90
  10. pages/resetPassword/index.tsx:51,54
  11. pages/roles/form.tsx:151-152
  12. pages/settings/document-types/index.tsx:136-137
  13. pages/settings/index.tsx:486-487
  14. pages/settings/pendency-types/index.tsx:147-153
  15. pages/users/form.tsx:152-156
  16. pages/verify-email/index.tsx:51-52

Nenhum é falso positivo por nome de arquivo — cada um foi lido antes de entrar na lista, e cada linha é conferível no próprio arquivo hoje, antes do diff.

O que o defeito NÃO alcança, e vale delimitar porque a correção não pode quebrá-lo: erro de negócio sem details[] — como o ConflictError de CNPJ já cadastrado (apps/api/app/services/tenant_service.py:54, no caminho de atualização que esta tela usa) — continua correto hoje. Ele traz só uma mensagem, e a faixa do topo é o lugar dela. É exatamente o que o false de aplicarErrosDaApi preserva.

Escopo decidido pelo usuário em 2026-08-29: todas as superfícies, e não só as três.

A alternativa era corrigir as três telas do achado da spec 0047 — as que já têm teste afirmando o defeito — e registrar o resto como trabalho seguinte. Foi recusada, e o motivo é o que o portão apontou: aprovar um conserto de três deixa a mesma faixa vazia viva em Configurações, no cadastro de usuário e na decisão de crédito, com a causa idêntica e o defeito já entendido. Corrigir um pedaço de uma lacuna conhecida cria duas versões do mesmo comportamento no produto.

A contagem mudou quatro vezes — quinze, dezoito, vinte, e uma quarta lista com falso positivo — e por isso ela deixou de ser o escopo por busca de texto. O que a decisão do usuário fixou é o princípio: todas. O que fechou o número foi trocar a fonte — grep por inventário fechado (apps/web/CLAUDE.md) — e aplicar a régua item a item: 16 defeitos confirmados, listados acima, cada um lido antes de entrar na conta.

Fica como fix e não como spec porque é defeito — o comportamento existe, está desenhado desde a 0046, e não foi aplicado onde devia. Tamanho não muda a natureza do trabalho; muda o número de vezes que a mesma correção se repete. Prova de regressão só onde há 422 alcançável, pela régua acima — senão o fix geraria dezenas de testes que passariam contra qualquer código.

5. Revisão do diagnóstico

Preenchido pelo laço do /fix new, rubrica .claude/rubrics/diagnostico.md. Registrar todas as rodadas, inclusive as reprovadas.

Rodada Critical Important Minor Veredicto
1 1 2 2 REPROVADO
2 1 1 2 REPROVADO
3 — 3 1 REPROVADO
4 — 2 2 REPROVADO
5 1 2 3 REPROVADO
6 1 4 3 REPROVADO
7 1 1 3 REPROVADO

As três rodadas acharam a mesma classe de defeito, e nas três a causa foi a mesma: eu afirmei comportamento sem medir.

  • Rodada 1 — a reprodução mandava usar um e-mail malformado. O type="email" do HTML aborta o envio: nenhuma requisição sai.
  • Rodada 2 — trocada para o campo Estado, ela esbarrava no required do celular, que a garantidora do seed_e2e não tem. Também zero requisições, e o que se vê é o sósia do sintoma.
  • Rodada 3 — a reprodução principal passou a funcionar (medido: 1 PATCH, faixa aparece), mas a nota que documentava o sósia oferecia uma alternativa que caía nele de novo.

O revisor mediu nas três. Eu não medi em nenhuma antes de escrever, e é por isso que o mesmo defeito voltou três vezes com roupa diferente: uma reprodução plausível é indistinguível de uma que funciona, até alguém rodar.

E o alcance mudou em todas as quatro — quinze, dezoito, vinte, e uma quarta lista com falso positivo —, sempre com a omissão sendo irmã de um arquivo já listado. A rodada 4 achou a causa disso, e ela é estrutural, não desatenção: o registro usava duas réguas ao mesmo tempo — enumerava por "envia formulário e lê só .message" e excluía por "tem 422 com details[] alcançável". Com duas réguas, toda varredura acha mais um, para sempre.

O conserto foi separar as duas. Uma régua define o escopo (envia dado digitado, trata lendo só .message); a outra define onde há prova de regressão (422 com details[] alcançável hoje). A correção é inofensiva onde não há o que tratar — aplicarErrosDaApi devolve false com details vazio —, então aplicá-la amplamente não custa nada, e o teste vai só onde tem o que afirmar.

Resultado: fechado por decisão do usuário na rodada 7 (2026-08-29), e não "aprovado". As sete reprovaram, e as correções da sétima não passaram por revisão. O laço parou por decisão, não por convergência.

A decisão é defensável, e vale dizer por quê. O que o portão mede — reprodução, mecanismo, localização, alcance — está sólido nos três primeiros desde a rodada 3, e medido por revisores diferentes. O quarto, o alcance, é o que não fecha; e as sete rodadas mostraram por que ele não fecha num documento: cada uma acrescentou um sítio ou um transporte que a anterior não tinha aberto, e nenhuma reescrita evita isso — só abrir cada arquivo evita, que é o trabalho da implementação. Lá, cada afirmação vira código que compila ou não, e teste que fica vermelho ou verde.

O que fica em risco, dito sem eufemismo: a implementação vai encontrar sítios que este registro não previu, e possivelmente um sexto transporte. O registro dá a régua e os dois eixos para classificá-los; não dá a lista.

A rodada 5 achou o defeito mais grave das cinco, e ele não era de redação. A seção 4 fixava como alvo "chamar a helper que já existe", e isso compila e não faz nada em duas telas: apps/web/src/pages/onboarding/api.ts:55 lança ApiValidationError, uma classe própria que não estende ApiError — e a primeira linha de aplicarErrosDaApi (apps/web/src/shared/utils/foco-no-erro.ts:223) é uma guarda instanceof ApiError. StepTenant — uma das três telas do sintoma — e register são servidos por ela. A helper devolveria false, a faixa genérica continuaria sozinha, e o sintoma sobreviveria no lugar onde o registro afirmava tê-lo entendido.

Junto veio o segundo: a correção não é uniforme. aplicarErrosDaApi exige um UseFormSetError do react-hook-form, e credit-analyses/show.tsx — a tela que a seção 4 chama de mais importante do produto — não usa react-hook-form em lugar nenhum. A seção passou a enumerar quatro famílias de sítio, com o que "tratar a recusa" significa em cada uma.

É o que a lista fazia sem querer. Removê-la foi certo — quatro listas congeladas saíram erradas —, mas ela forçava a leitura de cada sítio, e é lendo cada um que se descobre que eles não são a mesma coisa. A régua sozinha não carregava isso; as famílias carregam.

A rodada 6 derrubou a tabela de famílias que a rodada 5 tinha feito nascer, e o Critical foi medido: duas telas listadas na família "página com react-hook-form" — users/form e roles/form — não usam react-hook-form. Passar o setError de um useState para aplicarErrosDaApi compila (tsc sai com zero), a helper devolve true, e o que chega à faixa é ["email"]: o nome cru do campo, sem mensagem. Pior que o sintoma de hoje, e dentro da tabela escrita para prevenir exatamente isso.

A causa foi misturar dois eixos independentes numa classificação só. O conserto depende de (a) por qual transporte o erro chega e (b) quem é dono do estado de erro do campo — e as telas de conta são a prova: register, verify-email e accept-invite usam react-hook-form (parecem o caso fácil) e são servidas pelo ApiValidationError, que não passa na guarda da helper. Qualquer família de um eixo só as classifica errado. A seção passou a ter as duas tabelas, e o conserto é o cruzamento.

Caíram junto três afirmações que eu não tinha conferido: o descarte no authProvider acontece em três lugares (apps/web/src/providers/auth.ts:79, :170, :196) e não num; o tratarRecusaDaApi não serve de precedente para credit-analyses/show.tsx, porque pressupõe formulário próprio e aquela tela não tem campo nenhum — o reason mora num ConfirmDialog que não expõe erro de campo; e há um sítio em escopo que não é catch (apps/web/src/components/shared/DocumentsPanel.tsx:404, um Promise.allSettled).

A rodada 7 achou mais dois transportes, e a forma do defeito foi idêntica à da rodada 5. apps/web/src/shared/api/documents.ts:47 e apps/web/src/admin/services/adminApi.ts:57 constroem um Error novo e descartam o details antes de a tela existir. O segundo é pior: lê a chave detail, que este envelope não tem, e o que chega à área admin é literalmente "Erro 422" — nem a mensagem do servidor. Os dois servem sítios que este registro já nomeava em escopo: o Promise.allSettled do painel de documentos e a emissão de convite.

São cinco transportes, e não três. E o registro chegou a afirmar que o documents.ts apenas "repassava" o erro — quem confiasse nessa frase classificaria o painel no balde errado.

O padrão das sete rodadas, dito sem enfeite. Três reprovaram a reprodução; quatro reprovaram o alcance ou o alvo. Em cada uma delas eu havia afirmado sem abrir o arquivo: que o e-mail chegava ao servidor, que o required não barrava, que havia três transportes, que duas telas usavam react-hook-form, que o documents.ts repassava. O revisor mediu em todas. O núcleo — reprodução, mecanismo, localização, delimitação — está estável desde a rodada 3; o que não estabiliza é a enumeração do que a causa alcança, e cada rodada acrescenta um sítio ou um transporte que a anterior não tinha aberto.

Pendências aceitas: nenhuma. Os achados das sete rodadas foram corrigidos. A enumeração dos sítios continua sendo produto da implementação, com os dois eixos para classificá-los — e com a advertência, agora medida sete vezes, de que abrir o arquivo é a única forma de saber.

6. Correção

Onde vai mexer, por camada. Um bug pode viver em um app só — seja explícito.

apps/api

  • Onde: nenhum.
  • O quê: sem impacto — o defeito é inteiro de tratamento de resposta no apps/web; a API já preenche details[] corretamente desde antes deste fix (exceptions.py:158).

apps/web

  • Onde: os cinco transportes (providers/api-fetch.ts, providers/auth.ts, shared/api/documents.ts, admin/services/adminApi.ts, pages/onboarding/api.ts) e os 16 pontos de tratamento listados na seção 4.
  • O quê: as duas metades, feitas. Primeira (commit 4dc5c43): os cinco transportes passam a preservar .details até a tela, e as três telas do sintoma original (Minha Garantidora, cadastro de inquilino, onboarding) chamam aplicarErrosDaApi. Segunda: os 16 pontos da checklist da seção 4, um commit por item — aplicarErrosDaApi onde o dono do erro é react-hook-form (SignUp, SignRecover ×2, verificação de e-mail, aceite de convite, SignIn); prop error/reasonMaxLength nova no ConfirmDialog onde o campo mora num diálogo filho (cancelamento e reprovação de análise de crédito); marcação por item onde o estado é indexado (DocumentsPanel, observação e upload); e tratamento local (sem react-hook-form) com helperText/mensagem completa onde antes só acendia um booleano ou decidia por regex sobre o texto (Equipe, Perfis de Acesso, Parâmetros, Tipos de Pendência, Tipos de Documento, emissão de convite no admin).

Dois achados no caminho, fora da lista original: o transporte de updatePassword (providers/auth.ts) calculava .details da resposta e não os incluía no objeto de erro devolvido — o comentário dizia que o campo viajava e o código ao lado não fazia isso; corrigido junto do item 10. E o e-mail de login, forgot-password, register e do cadastro de usuário passa por AppEmail na API, que confere entregabilidade em produção (validate_email(..., check_deliverability=True)) — um domínio sem MX recusa mesmo com formato válido, e o type="email" do navegador não pega isso. Descoberto nos itens 7 e 8 e replicado nos itens 9 e 15: é um 422 de campo genuinamente alcançável em produção, não hipotético como a maioria dos "sem alcançável hoje" desta lista.

Um achado relacionado, registrado e não corrigido: o diálogo "Devolver ao analista" (resumeOpen, credit-analyses/show.tsx) tem o mesmo run() sem error que os itens 5 e 6 corrigiam — mas sem campo de texto (é convite, não formulário), a régua da seção 4 não o inclui. Candidato a um próximo fix se a régua for reaplicada. - data-testid: acrescentados onde faltavam para tornar a correção testável — settings-* (nove campos e o botão Salvar, item 13) e settings-save-button não existiam antes deste fix. - Responsividade & PWA: a correção vale para desktop e mobile (não conserta um breakpoint quebrando o outro); se toca asset/estilo/layout, considerar o cache do service worker (PWA) na hora de validar

apps/e2e

  • Teste de regressão: o cenário que falha hoje e passa depois da correção

Se algum app não é afetado, escreva "sem impacto" explicitamente.

7. Risco / impacto colateral

Nenhuma migração, nenhum cálculo de dinheiro tocado — o fix é só como o apps/web lê e exibe uma resposta que a API já enviava certa. O risco real é regressão de comportamento correto em 16 arquivos espalhados por login, cadastro, análise de crédito e configurações: cada item tem teste próprio com red-green (passa → reverte a correção → falha → restaura → passa), e a suíte inteira do apps/web (113 casos, 27 arquivos) foi rodada depois do último item — verde. render.tsx, helper compartilhado por toda a suíte, ganhou um parâmetro (rota) no item 4; a suíte inteira foi rodada logo depois para confirmar que nenhum caso existente regrediu.

Uma mudança de comportamento deliberada, não colateral: providers/auth.ts e a tela de Login/Registro/Esqueci a senha agora recusam por campo quando o servidor rejeita o e-mail por domínio não entregável (AppEmail) — antes essa recusa também acontecia, só não dizia onde.

8. Critérios de aceite

Verificável por quem não escreveu a correção. O primeiro deve ser sempre "o bug não reproduz mais".

  • [x] CA1. Seguindo os passos da seção 2, o comportamento agora é o esperado — digitar São Paulo em Estado (Minha Garantidora) marca o campo com a mensagem do servidor, sem a faixa genérica.
  • [x] CA2. Garantia central deste fix — mensagem no topo sem marca no campo não pode mais acontecer. Verificado nos 16 pontos, um teste por item com red-green.
  • [x] CA3. Cancelamento e reprovação de análise de crédito (ConfirmDialog) recebem error; a mensagem aparece dentro do diálogo aberto (itens 5 e 6).
  • [x] CA4. Nome que a API manda e a UI não registra continua indo para a faixa com o rótulo em português — comportamento preexistente de aplicarErrosDaApi, não regredido em nenhum dos 16 pontos (testado explicitamente no item 1).
  • [x] CA5. Erro de negócio sem details[] (ConflictError de nome duplicado em Tipos de Documento e Tipos de Pendência) continua só na faixa — verificado nos itens 12 e 14, que tratam exatamente esse limite.

9. Plano

  • [x] Escrever o teste que reproduz o bug e vê-lo falhar — 16 arquivos de teste, um por item, cada um visto vermelho antes da correção
  • [x] Corrigir a causa raiz — só apps/web, sem apps/api nem apps/e2e neste fix
  • [x] Red-green verificado: os 16 itens, cada um passa → reverte → falha → restaura → passa, com o comando e a saída registrados no commit do item
  • [x] make test-web sem apontamento novo — suíte inteira rodada ao final (113 casos, 27 arquivos, verde). make lint não roda nesta máquina (sem uv no host — ver Estado do ferramental do CLAUDE.md); tsc --noEmit limpo, checado arquivo a arquivo a cada item
  • [x] Validar no browser de verdade, desktop e mobile, contra o dev-remote — /devremote-rebuild web (o pnpm build local já tinha passado limpo antes), depois amostragem de 3 dos 16 itens com Playwright disparando o 422 real da API (não simulado): Parâmetros (item 13), Perfis de Acesso (item 11, desktop e mobile) e Registro (item 9, desktop e mobile). Os três marcam o campo com a mensagem do servidor. Não é os 16 — é amostra, a pedido do usuário.

10. Decisões em aberto

Nenhuma — o diagnóstico fechou na rodada 7 (seção 5) e a correção não exigiu decisão nova: os 16 pontos seguiram os padrões já presentes no repositório (aplicarErrosDaApi, tratarRecusaDaApi, a prop error do ConfirmDialog).

11. Revisão da correção

Sem o laço automático do /fix implement. Não houve subagente de contexto limpo julgando o diff contra .claude/rubrics/correcao.md — o trabalho inteiro (os 16 itens, o deploy no dev-remote e depois em produção) aconteceu em conversa direta com o usuário, que acompanhou cada correção e pediu explicitamente o fechamento (2026-08-30, "Pode fechar então"), depois de ver a v0.4.2 no ar e a amostragem em browser confirmando três das telas corrigidas.

Rodada Critical Important Minor Veredicto
— — — — Sem rodada de subagente; fechado por decisão direta do usuário

O teste sobreviveria à reversão? Sim, nos 16 itens — não por impressão: cada commit desta lista registra o comando do red-green (revertida a correção, o teste do item volta a falhar pelo mecanismo exato do defeito; restaurada, passa) na própria mensagem, e a seção 9 resume o total (113 casos, 27 arquivos, verde ao final).

Defeitos aceitos: nenhum apontado — não houve revisão formal para apontar algo a aceitar. O que ficou fora do escopo desta lista está registrado na seção 6 (o diálogo "Devolver ao analista", resumeOpen em credit-analyses/show.tsx, mesmo run() sem error dos itens 5 e 6, mas sem campo de texto — candidato a um próximo fix).

12. Registro

Uma aresta de ambiente, encontrada ao rodar o portão — 2026-08-29.

O make preflight reprovou com a suíte vermelha, e ela estava verde. A causa não era teste: o pnpm install --frozen-lockfile saía com ERR_PNPM_IGNORED_BUILDS, porque o pnpm 11 desta máquina não lê o campo pnpm.onlyBuiltDependencies do package.json — ele avisa que ignorou — enquanto o pnpm 9 do container de produção lê, e é de lá que veio a correção que desbloqueou o deploy da 0.4.1.

Tentei as três formas de escrever a decisão para o pnpm 11 — pnpm-workspace.yaml com e sem packages:, e only-built-dependencies[] no .npmrc — e nenhuma foi aceita. O que resolveu aqui foi rodar a instalação uma vez com --config.dangerouslyAllowAllBuilds=true: o script do esbuild correu, e as instalações seguintes passam.

Fica registrado como aresta, e não como resolvido: num checkout novo com pnpm 11, o make test-web provavelmente vai pedir esse passo uma vez. Não mexi de novo no pnpm-workspace.yaml de propósito — foi ele que derrubou o build da imagem de produção, e o package.json sozinho está provado em produção. Trocar uma aresta de máquina de desenvolvimento por um deploy quebrado seria péssimo negócio.

Anotações durante a correção: o que era, o que virou, o que divergiu do plano. Preencher com /fix implement.

Os 16 pontos corrigidos, um commit por item, em conversa direta com o usuário — 2026-08-29.

Não passou pelo laço automático do /fix implement (sem subagente revisor de contexto limpo); por isso a seção 11 segue com uma rodada vazia, e o Status do cabeçalho continua DIAGNOSTICADO em vez de CORRIGIDO — essa transição pede o portão da seção 11, que não rodou. O que aconteceu:

  • Cada um dos 16 itens da seção 4 ganhou teste próprio (*.test.tsx) e correção, nesta ordem: escrever o teste, vê-lo falhar, corrigir, ver passar, reverter só a correção para confirmar que volta a falhar, restaurar. Um commit por item, citando o mecanismo exato.
  • Dois achados no caminho, fora da lista original de 16, registrados na seção 6: o transporte de updatePassword tinha o mesmo defeito que o commit anterior já dizia ter corrigido (calculava .details e não os devolvia); e o e-mail de quatro telas (login, registro, esqueci a senha, cadastro de usuário) tem um 422 de campo genuinamente alcançável em produção via AppEmail — achado que a seção 4 original tinha catalogado como "baixo impacto" para o login e não tinha visto nos outros três.
  • render.tsx, o preparo compartilhado da suíte, ganhou um parâmetro (rota) para telas que leem useSearchParams — usado pela primeira vez no item 4. Suíte inteira rodada logo depois (93 casos) e de novo ao final (113 casos, 27 arquivos): verde nas duas vezes.
  • O make preflight pegou 16 citações arquivo:linha erradas na seção 4 depois da correção — 13 sem o prefixo de app, duas apontando para código que a própria correção substituiu. Corrigidas num commit à parte.
  • Não feito: a validação no browser contra o dev-remote (Plano, último item) e o laço formal de revisão da seção 11. Ambos pedem decisão do usuário — o primeiro custa um /devremote-rebuild, o segundo é o portão que decide CORRIGIDO.

Build, deploy no dev-remote e amostragem — 2026-08-29/30.

Antes do rebuild, cd apps/web && pnpm build (mesmo comando do Dockerfile, tsc -p tsconfig.build.json && refine build) rodou limpo — EXIT:0, sem apontamento novo. /devremote-rebuild web reconstruiu e recriou o container (api não muda neste fix); app=200 e o hash do bundle (index-2HTIk57M.js) confirmam que o dev-remote está servindo o build novo, não o cacheado.

A validação em browser pedida foi por amostragem, não os 16 itens: um script Playwright avulso (fora do e2e — não é a suíte, é o script de screenshot que o CLAUDE.md já descreve para conferir tela sem pagar a suíte inteira), logado como gestor@e2e.test contra https://dev.garantora.com.br/app, disparando o 422 real da API em três telas — Parâmetros, Perfis de Acesso e Registro — e capturando desktop e mobile. Os três confirmam o campo marcado com a mensagem do servidor. O script rodou de dentro de apps/e2e/ (para resolver o playwright do node_modules de lá) como arquivo temporário, removido depois — não ficou artefato no repositório, e nenhuma das três tentativas foi salva de fato (as três terminam em 422, por desenho).

Ainda não fechado, até aqui: o laço formal de revisão da seção 11 (sem subagente de contexto limpo) — por isso o Status do cabeçalho seguia DIAGNOSTICADO.

Merge, deploy em produção e fechamento — 2026-08-30.

fix/0015-recusa-da-api-nao-aponta-o-campo mesclada na main por fast-forward (nada a resolver — a main não tinha andado desde que a branch nasceu dela) e empurrada. /ship publicou a v0.4.2: sem migration, make seed rodou (RBAC + admin idempotente, sem retirar permissão nenhuma), health respondeu {"status":"ok","db":"ok","commit":"5f690d5...", "version":"0.4.2"} e a raiz HTTP 200 — as duas verificações de fora que autorizam a tag, que foi criada e empurrada (v0.4.2) só depois de as duas passarem.

O usuário pediu o fechamento explicitamente, ciente de que a seção 11 não passou pelo laço automático — ver o que ficou registrado lá. Status agora CORRIGIDO.