Pular para conteúdo

0037 — Pendências tipificadas na análise de crédito, e um histórico que diz o que mudou

Status CONCLUÍDA
Branch spec/0037-tipos-de-pendencia-na-analise
Criada em 2026-08-13
Apps afetados api · web · e2e
Nota do problema 8,0 — laço esgotado; pendências aceitas na seção 6
Nota da experiência 8,5 (tentativa 3 de 3)
Nota do detalhamento 8,4 (tentativa 2 de 3) — segurança 9,0
Nota da implementação 8,0 — laço esgotado; defeitos corrigidos, um em aberto

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

A spec tem três partes, escritas em ordem, e quatro portões — cada um com nota de 0 a 10 dada por um subagente verificador de contexto limpo, em até 3 tentativas, passando acima de 8:

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

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


Parte I — O problema

Aqui não entra solução: nem tela, nem endpoint, nem botão. Só o que dói, para quem, e o que se faz hoje. Se você já sabe qual é a tela, guarde para a Parte II.

1. Dor

A devolução é o caminho de volta da análise de crédito: o analista interrompe a avaliação, diz o que falta, e a imobiliária corrige e devolve. A spec 0036 abriu esse caminho e o tornou mão dupla. O que ela deixou aberto é o que dói aqui — cinco defeitos, todos no mesmo trecho do fluxo, todos conferíveis no produto de hoje.

Sobre a base de comparação usada nesta seção. O Garantora ainda não tem cliente operando (a produção subiu na spec 0028 com apenas o usuário admin do seed), mas há piloto com nome e data: a TXC Garantidora, prevista para o fim de agosto de 2026 — hoje ela opera sobre outro software, o GarantePay. A escala citada abaixo é expectativa de negócio declarada pelo responsável do produto, não medição instrumentada: da ordem de 50 análises/mês por garantidora, cerca de metade voltando para correção (~25 devoluções/mês), com ~10 imobiliárias parceiras e 1 a 2 analistas de crédito. Onde a spec usa número medido (contagem no código), o comando está citado para poder ser reproduzido.

D1 — A imobiliária lê uma caixa vazia que nunca foi para ela

Quem abre uma análise sem a permissão decisions:read recebe o painel "Parecer da análise" renderizado na tela, com a frase "Nenhum parecer registrado". São dois papéis, e só dois: a imobiliária (AGENCY_USER) e o OPERATOR da própria garantidora — conferido em apps/api/app/db/rbac_seed.py, onde ADMIN_TENANT, ANALYST e VIEWER têm a permissão e por isso não sofrem este defeito.

Acontece em SUBMITTED, IN_ANALYSIS, PENDING_DOCS, APPROVED e REJECTED — todos os estados menos DRAFT e CANCELLED. O painel aparece porque a tela decide exibi-lo pelo status (apps/web/src/pages/credit-analyses/show.tsx:448-449), e o conteúdo dele nunca é buscado porque a permissão falta (show.tsx:294); as duas decisões são tomadas em lugares diferentes e não se conversam.

O efeito para quem lê: durante toda a fase de análise, a imobiliária vê um bloco em destaque anunciando um parecer que ela nunca vai ler, dizendo que não há nenhum. Ela tem duas leituras possíveis, e as duas estão erradas — "o analista ainda não escreveu nada" (quando pode já ter escrito) ou "está quebrado". A informação correta seria não existir esse bloco na tela dela.

Alcance: toda análise que a imobiliária ou o operador abrir, do envio ao desfecho — com a escala esperada, da ordem de 50 análises/mês por garantidora, abertas mais de uma vez cada.

Custo de adiar: baixo, e a spec assume isso. É defeito de renderização; corrigi-lo daqui a um ano custa o mesmo que hoje. O que ele custa é agora, e por ser a primeira coisa que um usuário externo vê (ver O custo de não fazer).

D2 — O motivo da devolução é redigido do zero

Ao devolver, o analista preenche um campo de texto livre. Quando a análise tem documento obrigatório faltando, o campo nasce com os nomes dos documentos; em qualquer outro caso — dado divergente, renda que não fecha, comprovante ilegível, imóvel com informação errada — ele nasce vazio (show.tsx:1081-1085), e cada analista redige com as próprias palavras.

Alcance: da ordem de 25 devoluções/mês por garantidora. Que fração delas é documental — e portanto já cai no caminho pré-preenchido, que funciona — não se sabe, e não vou estimar; o que se sabe é que tudo o que não é documento cai no campo em branco.

Duas consequências, e elas não têm o mesmo peso:

  • Agora: a mesma pendência sai com palavras diferentes a cada análise. Com 1 a 2 analistas por garantidora, o problema não é tanto um discordar do outro — é que o jeito de comunicar da casa não existe fora da cabeça dessas uma ou duas pessoas. Não fica disponível para o colega, nem para elas na próxima vez, nem para a empresa quando saírem; e as ~10 imobiliárias parceiras recebem instrução cuja forma depende de quem pegou a análise naquele dia.
  • Depois, e por isso mais barato de tratar agora: prosa livre não vira dado retroativamente. Enquanto o motivo for texto corrido, não existe resposta para "qual pendência mais devolve análise" — e as devoluções já ocorridas nunca passarão a ter essa resposta. É a mesma irreversibilidade do D4, aplicada a um dado menos crítico. A tela que agregaria isso está fora de escopo (seção 5); o que esta spec quer é que o dado exista para quando ela for feita.

Custo de adiar: moderado, e por um motivo só — o da irreversibilidade acima. O argumento de que "um catálogo introduzido depois competiria com um hábito formado" é conjectura sobre analistas que ainda não existem, e não sustenta prioridade sozinho.

D3 — Quem recebeu a pendência não tem saída, só o caminho de ida

A imobiliária que recebe uma devolução tem exatamente uma ação disponível: corrigir e devolver ao analista. Desistir não é possível para ela — credit_analyses:cancel não está entre as permissões do papel AGENCY_USER (apps/api/app/db/rbac_seed.py, linhas 189–211).

E desistir é parte do negócio: o inquilino escolheu outro imóvel, o proprietário fechou com outro candidato, a locação caiu. Nesses casos a análise fica parada aguardando uma correção que nunca virá, ocupando a fila de quem espera e a lista de quem acompanha, até que alguém da garantidora perceba e cancele por ela — o que exige que a imobiliária avise por um canal que o sistema não oferece.

Alcance: a fração das ~25 devoluções/mês em que a locação não se concretiza. Cada uma dessas análises é um registro que só sai da fila por intervenção do outro lado.

D4 — O histórico conta trocas de status, e mais nada

O histórico da análise registra apenas transições de estado. A tabela credit_analysis_status_history tem quatro campos úteis — from_status, to_status, changed_by, reason — e nenhum deles carrega o que mudou dentro da análise.

Contado no código: 8 endpoints alteram uma análise de crédito ou seus participantes (grep -c '@router.\(post\|patch\|put\|delete\)' nos dois routers). Destes, PATCH /credit-analyses/{id} grava o registro e sai sem escrever linha nenhuma de histórico nem de auditoria (credit_analysis_service.py:364-395), e o serviço de participantes não tem nenhuma chamada de auditoria — zero, no arquivo inteiro. Quer dizer: alterar a renda declarada de um inquilino, trocar o valor do aluguel ou substituir um participante não deixa rastro em lugar algum.

O que isso custa, em perguntas que hoje não têm resposta:

  • "A renda declarada mudou depois que o analista pediu o holerite? Era quanto antes?"
  • "Quem alterou o valor do aluguel entre a submissão e a aprovação?"
  • "Este participante estava na análise quando ela foi devolvida, ou entrou depois?"

Quem pergunta isso, e quando. No dia a dia, o próprio analista: com ~25 devoluções/mês retornando, a primeira coisa que ele quer ao retomar uma análise é o que mudou desde que a devolvi — e hoje a única forma é reler a análise inteira comparando com a memória. As demais perguntas são de auditoria, e é característico delas serem feitas depois de algo dar errado — quando o dado já teria de estar gravado.

A fronteira do D4, dita em termos de pergunta. O histórico precisa responder quem mudou o quê, quando, de que valor para que valor, para qualquer coisa que altere uma análise de crédito — os campos dela, seus participantes e seus documentos, que só existem no contexto dela. Não é "guardar tudo o que acontece no sistema": é fechar as três perguntas acima para os 8 endpoints de escrita já contados, dos quais hoje só as transições de status deixam rastro. O que fica de fora está na seção 5 (outras entidades, e retroatividade).

Todo campo entra, e isso é decisão tomada — não só os que mudam a decisão de crédito. Foi cogitado restringir aos campos financeiros (aluguel, pacote locatício, renda declarada, prazo), o que reduziria bastante a implementação; recusado porque a fronteira "campo relevante" precisa ser mantida à mão para sempre, e um campo classificado como irrelevante hoje é exatamente o que se quer auditar depois. A regra é: se alterou algo que a análise persiste, registra.

Custo de adiar: alto, e é o único defeito irreversível. O valor anterior de um campo não existe em lugar nenhum para ser recuperado depois. Todo período de operação real sem esse registro é um trecho de histórico permanentemente cego — e é o trecho mais antigo, que é justamente o que auditoria costuma querer.

D5 — O nome do estado mente sobre o que ele significa

O estado chama-se PENDING_DOCS, e a interface o rotula "Aguardando documentos". Isso era verdade até a spec 0036, quando a ação se chamava "Solicitar documentos". A própria 0036 renomeou a ação para "Devolver à imobiliária" — com a justificativa registrada de que "solicitar documentos" descrevia um dos motivos, não o que a ação faz — mas parou aí: o estado que a ação produz continua com o nome antigo.

Hoje, portanto, uma devolução por CPF divergente coloca a análise num estado que a tela apresenta como "aguardando documentos", e o texto ao lado explica que o problema é outro. O rótulo contradiz o motivo logo abaixo dele.

Custo de adiar: cresce com o primeiro cliente. Hoje o identificador aparece em 122 linhas / 124 ocorrências, em 32 arquivos dos três apps (grep -rn 'PENDING_DOCS' apps/; 110 dessas linhas em código-fonte, o resto em testes e migration antiga). É um rename e uma migration de enum sem dados. Depois do primeiro cliente, a mesma migration precisa reescrever linhas já gravadas em credit_analyses.status e nos dois campos de estado do histórico.

O custo de não fazer

Não há cliente nenhum ainda, então o custo não se mede em horas de operação perdidas. Mede-se em duas coisas, e as duas são verificáveis:

  1. Há piloto com nome e data: a TXC Garantidora, prevista para o fim de agosto de 2026 — cerca de duas semanas a contar desta spec (2026-08-13). Ela opera hoje em outro software (GarantePay) e vai migrar para o Garantora. A devolução é o caminho obrigatório de toda análise que não é decidida de primeira — pela expectativa declarada, metade delas —, e os cinco defeitos aparecem em sequência numa única passagem do fluxo: a caixa vazia (D1), o campo em branco (D2), a imobiliária sem saída (D3), o histórico mudo (D4) e o rótulo errado (D5). Quem chega vindo de um sistema em produção encontra, na primeira análise devolvida, um painel vazio sem explicação e uma parceira que precisa telefonar para desistir.

  2. Um dos cinco defeitos é irreversível, e dois ficam mais caros com o tempo. O histórico de alterações (D4) não é retroativo: o que não foi gravado não pode ser reconstruído. O rename do estado (D5) passa de migration sem dados a migration com backfill. O motivo tipificado (D2) só passa a ser dado a partir do momento em que existir; as devoluções anteriores continuam prosa para sempre. Os outros dois — D1 e D3 — não ficam mais caros de corrigir depois; entram aqui porque são do mesmo trecho do fluxo e porque o item 1 acima os torna bloqueantes do mesmo jeito.

Por que os cinco numa spec só, e por que o D4 fica junto

A pergunta é justa: D1 é uma condição de renderização, D3 é uma linha no arquivo de papéis, e D4 — trilha de alterações campo a campo sobre a análise, seus participantes e seus documentos — tem tamanho de spec inteira sozinho. Agrupá-los precisa de razão, e "mesmo trecho do fluxo" só explica D1, D2 e D5.

A razão do D4 é outra, e é a irreversibilidade. Ele é o único defeito cuja correção tardia não recupera o que se perdeu: cada dia de operação sem registro de valor anterior é um dia de histórico que nunca existirá. Isso o torna o mais urgente dos cinco, não o mais adiável — e é por isso que ele não vai para a spec seguinte, mesmo sendo o maior. Fazer só ele agora e deixar os outros quatro para depois seria coerente com esse argumento; o que não é coerente é o inverso, entregar os quatro baratos e deixar o irreversível esperando.

Os outros quatro entram junto porque são a mesma passagem do fluxo, porque o piloto os encontra em sequência, e porque somados custam uma fração do D4.

2. Quem sente

Papel O que sente Quando
AGENCY_USER (imobiliária) D1 — caixa vazia em toda análise que ela acompanha; D2 — recebe instruções sem padrão; D3 — não consegue desistir; D4 — não consegue provar o que alterou nem quando Em toda análise que ela cadastra, do envio ao desfecho (~50/mês por garantidora)
ANALYST D2 — redige do zero a cada devolução não documental e não tem de onde puxar o texto que já funcionou; D4 — não consegue ver o que mudou desde que devolveu, que é a primeira pergunta ao retomar; D5 — o estado que ele produziu tem nome errado A cada devolução e a cada retomada (~25 de cada, por mês)
ADMIN_TENANT D2 — não tem como padronizar o que a garantidora comunica às parceiras; D4 — a auditoria da própria operação não existe Ao definir o processo, e sempre que precisa auditar
OPERATOR D1 — é o outro papel sem decisions:read, e recebe a mesma caixa vazia dentro da própria garantidora Ao acompanhar qualquer análise
VIEWER Nada de novo — tem decisions:read e lê o parecer normalmente —
ADMIN_GLOBAL Nada — é papel de plataforma; não opera análise de crédito —

Quem sente mais é a imobiliária, por dois motivos: ela é atingida por quatro dos cinco defeitos, e é o único papel externo à garantidora — o que ela vê de estranho na tela não tem quem explique do outro lado do balcão.

Para quantas pessoas. Hoje: nenhuma — não há operação. Na escala esperada de uma garantidora típica (expectativa declarada pelo responsável do produto): ~10 imobiliárias parceiras e 1 a 2 analistas de crédito, para as ~50 análises/mês.

Isso muda o peso de duas dores, e vale registrar:

  • D2 é menos sobre divergência entre analistas do que eu supunha. Com 1 ou 2 pessoas analisando, o texto de um não conflita com o de muitos. O que pesa é o outro lado da mesma moeda: o padrão da garantidora mora na cabeça de uma ou duas pessoas e sai com elas — e as ~10 imobiliárias do outro lado recebem instrução que depende de quem pegou a análise naquele dia, sem que exista um "jeito da casa" escrito em lugar nenhum.
  • D1 e D3 atingem os ~10 parceiros externos, que são justamente quem não tem a quem perguntar quando a tela faz algo estranho.

3. A jornada de hoje

São duas, e as duas importam porque o piloto vai passar de uma para a outra em duas semanas: como a TXC Garantidora resolve isto hoje, fora deste produto; e como o Garantora entrega esse mesmo caminho hoje, depois da spec 0036.

3.1 Como a TXC opera hoje, fora do Garantora

Relatado pelo responsável do produto a partir da operação da TXC, que roda hoje sobre o software GarantePay. É a jornada que o piloto vai trocar por esta.

  1. O analista da TXC encontra o problema — CPF divergente, renda que não fecha, documento ilegível.
  2. Ele entra em contato com a imobiliária ou envia e-mail. A comunicação da pendência sai do sistema e vira mensagem direta a uma pessoa.
  3. Não existe texto-padrão. Não há modelo de e-mail, planilha de motivos, nem documento combinado com as parceiras: cada pendência é redigida na hora.

    ⚠️ Este é o D2 antes do Garantora. O defeito não é do nosso produto — é o estado atual da operação que vai migrar, e o produto hoje o reproduz em vez de resolver.

  4. A imobiliária avisa que o inquilino desistiu pelo mesmo caminho — e-mail ou contato direto. Não há ação de desistir; há um aviso a uma pessoa.

    ⚠️ Este é o D3 antes do Garantora, com a mesma observação.

  5. Saber que a renda declarada mudou é perguntar. Não há registro consultável do valor anterior; a resposta vem de alguém que lembra, ou do que a imobiliária informar quando perguntada.

    ⚠️ Este é o D4 antes do Garantora. É o único dos três em que o Garantora, hoje, está no mesmo lugar que a operação que ele deveria substituir: nenhum dos dois guarda o valor anterior.

O que essa jornada prova, e o que ela não prova. Prova que D2, D3 e D4 são dores reais de uma operação real, não deduções do que o software não faz — a TXC convive com as três hoje, e por isso migrar não as resolve sozinho. Não prova D1 nem D5, que são defeitos nossos: nascem de decisões tomadas dentro deste produto e não têm correspondente lá fora.

3.2 Como o Garantora entrega esse mesmo caminho hoje

Todos os passos abaixo foram lidos no código, com arquivo e linha citados — é o que a TXC vai encontrar ao migrar.

Gatilho: a imobiliária cadastra uma análise e a envia (SUBMITTED).

  1. A imobiliária abre a análise para acompanhar. Vê o trilho de status, a linha de situação ("Enviada para a garantidora. Aguardando o início da análise") e, logo abaixo, um painel intitulado "Parecer da análise" dizendo "Nenhum parecer registrado".

    ⚠️ Quebra 1 (D1). Esse painel é do analista. Ela não tem permissão para o conteúdo dele, e por isso ele aparece permanentemente vazio. Ela não tem como saber que aquilo nunca vai preencher.

  2. O analista assume a análise e ela vai para IN_ANALYSIS.

  3. O analista encontra um problema e decide devolver. Abre o diálogo "Devolver à imobiliária", que exige um motivo.

  4. Se falta documento obrigatório, o campo já vem com os nomes dos documentos — este é o caso que funciona.
  5. Nos demais casos — renda que não fecha, CPF divergente, contracheque ilegível, dado do imóvel errado — o campo vem vazio.

    ⚠️ Quebra 2 (D2). Ele redige do zero. O que escreve não fica disponível para a próxima vez — nem para ele, nem para o colega. É o passo 3 da jornada da TXC, reproduzido: lá também não existe texto-padrão, e migrar para cá não muda isso.

  6. A análise entra em PENDING_DOCS. A tela passa a rotulá-la "Aguardando documentos".

    ⚠️ Quebra 3 (D5). Se a devolução foi por CPF divergente, o rótulo do estado contradiz o motivo escrito logo abaixo dele.

  7. Ninguém é avisado. Não há e-mail nem notificação nesta transição — é decisão registrada da 0036 (decision_service.py, docstring de pend), com a justificativa de que os dois lados descobrem pela lista e pela tela. Continua valendo nesta spec (seção 5).

    Registro como risco conhecido do desenho atual, não como problema que esta spec ataca — mas note que é justamente o passo 2 da TXC: hoje eles avisam por e-mail ou contato direto. Quem migra sai de um aviso ativo para nenhum aviso. Foi decidido nesta rodada manter assim; se o piloto reclamar, é a spec seguinte.

  8. A imobiliária abre a análise devolvida e lê o motivo na linha de situação. A partir daí, dois desfechos:

6a. Ela vai corrigir. Edita a análise, anexa o documento, devolve ao analista ("Devolver ao analista" → IN_ANALYSIS).

⚠️ Quebra 4 (D4). Nada do que ela alterou fica registrado. PATCH /credit-analyses/{id} grava e sai; o serviço de participantes não tem uma única chamada de auditoria. O analista recebe a análise de volta e não tem como ver o que mudou — precisa reler a análise inteira comparando com a memória do que viu na primeira passagem.

6b. Ela não vai corrigir — o inquilino desistiu, ou o imóvel foi alugado para outro.

⚠️ Quebra 5 (D3). Ela não tem a ação de cancelar (permissão ausente no papel). A análise fica parada em PENDING_DOCS indefinidamente; para sair dali, é preciso que alguém da garantidora cancele por ela. É o passo 4 da TXC, reproduzido: lá a imobiliária avisa por e-mail ou contato direto que o inquilino desistiu. O Garantora não melhora isso hoje — apenas troca o canal informal por uma análise que fica parada até alguém agir.

  1. O ADMIN_TENANT vai conferir o que aconteceu numa análise — provocado por uma reclamação da imobiliária, por uma garantia que virou sinistro, ou pela conferência de rotina de uma decisão de crédito. Abre o Histórico da análise e encontra uma lista de transições de estado com data e motivo — sem nome de quem agiu (proibido na 0036, §8.7, porque a imobiliária lê esse mesmo histórico) e sem uma linha sequer sobre o que foi alterado dentro da análise.

    ⚠️ Quebra 6 (D4). As perguntas da seção 1 não têm resposta, e não terão retroativamente.

O que já se tentou

O contorno em uso é o da seção 3.1, e ele é humano: na TXC, a pendência é comunicada por e-mail ou contato direto, sem texto-padrão, e a desistência chega pelo mesmo caminho. Não é um contorno que "não pegou" — é o processo vigente, e é justamente o que a migração para o Garantora deveria substituir por algo melhor, não reproduzir. Que não exista nenhum texto-padrão lá é a evidência mais barata de que o D2 é real: uma operação com ~10 parceiras e 1 a 2 analistas nunca chegou a formalizar o que comunica, e não vai formalizar sozinha ao trocar de software.

Do lado do produto, quatro tentativas, todas conferíveis:

  • O pré-preenchimento documental (spec 0036). O diálogo de devolução já nasce com os documentos bloqueantes listados. Resolveu o caso documental e só ele — foi a tentativa mais próxima de padronizar o motivo, e mostrou que o caminho funciona: quando há texto pronto, o analista confere e confirma em vez de redigir. O que faltou foi cobrir os motivos que não são documento.
  • Renomear a ação, e parar aí (spec 0036). "Solicitar documentos" virou "Devolver à imobiliária" justamente porque o nome antigo descrevia um motivo só. Foi tentativa deliberada de corrigir o vocabulário, e funcionou na ação; o estado que a ação produz ficou com o nome antigo. A lição é que renomear metade de um par deixa a contradição mais visível, não menos — hoje o botão diz uma coisa e o estado resultante diz outra.
  • Esconder as ações por permissão (spec 0036, RN19). Os botões de decidir já são ocultados de quem não tem decisions:update — a RN19 estabelece que as ações do topo derivam de permissão, nunca de autoria. A caixa do parecer não entrou nessa regra, e é o resto da mesma ideia.
  • audit_logs como trilha genérica. A tabela existe, tem campo payload livre, e é usada em pontos esparsos — 2 chamadas no serviço de análise, 3 no de decisão, 3 no de documentos, 0 no de participantes. Nenhuma delas grava valor anterior, e a tela da análise não lê essa tabela. Como trilha de auditoria da análise, não pegou.

4. Resultado esperado

O que passa a ser possível, e como se confere:

  1. Quem não tem acesso ao parecer não vê o espaço dele. Abrir uma análise como AGENCY_USER ou OPERATOR, em qualquer estado, não mostra bloco algum referente ao parecer — e quem tem decisions:read (ANALYST, VIEWER, ADMIN_TENANT) continua vendo o painel como hoje. Confere-se abrindo a mesma análise com os dois grupos.

  2. Devolver deixa de exigir redação. O analista não escreve do zero um motivo que a garantidora já sabe comunicar; parte do que já está definido e ajusta o que é específico daquele caso. Confere-se devolvendo uma análise por um motivo não documental sem redigir do zero.

  3. O motivo da devolução vira dado, não só prosa. Duas devoluções pelo mesmo motivo passam a ser reconhecíveis como tal, sem ler o texto de cada uma. Confere-se agrupando as devoluções registradas por motivo — o dado responde, mesmo sem tela que o agregue (fora de escopo).

  4. A imobiliária resolve ou desiste, sem precisar de canal externo. Ela tem os dois desfechos dentro do sistema. Confere-se desistindo de uma análise devolvida como AGENCY_USER e verificando que ela sai da fila.

  5. O histórico responde "o que mudou". Toda alteração numa análise fica registrada com data, hora, quem fez, o campo tocado, o valor anterior e o novo. Confere-se alterando a renda declarada de um participante e lendo, no histórico, o valor de antes e o de agora.

  6. O rótulo do estado corresponde ao que aconteceu. Uma devolução por dado divergente não é anunciada como "aguardando documentos". Confere-se lendo o trilho de status depois de uma devolução não documental.

Sinal de que não resolveu: o analista continuar escrevendo o motivo do zero porque o que existe não cobre o caso dele; ou a imobiliária continuar dependendo da garantidora para encerrar uma análise que ela mesma desistiu.

5. Fora de escopo

  • Notificação por e-mail da devolução. Decidido nesta rodada: o solicitante lê o motivo na tela da análise, como hoje. A decisão de não notificar é da 0036 e continua valendo; reverter isso é spec própria, com destinatário e template — e não é pré-requisito de nada aqui.
  • Relatório ou painel de pendências por motivo. Esta spec faz o dado passar a existir; a tela que o agrega, não. Sem operação real, um painel seria desenhado sobre zero linhas.
  • Reabrir análise reprovada ou cancelada. REJECTED e CANCELLED continuam terminais. Foi cogitado junto com a desistência pela imobiliária e fica de fora: mexer na terminalidade da máquina de estados é mudança de risco maior que tudo o que está aqui.
  • Cada analista definir o próprio padrão de comunicação. Quem define o que a garantidora comunica às parceiras é a administração dela. Cogitado e recusado: padrão que qualquer um amplia deixa de padronizar — que é exatamente a dor do D2.
  • Tratar a comunicação da pendência como documento. O que a garantidora manda à parceira é instrução curta para ser lida na tela e resolvida — não peça formal, não anexo, não contrato. Cogitado e recusado: elevar isso a documento traz versionamento, aprovação e manutenção que a dor não pede, e a dor é que hoje não existe texto nenhum (seção 3.1).
  • Histórico de alterações fora da análise de crédito. Inquilino, imóvel, imobiliária, garantia e cobrança continuam como estão. O que entra é o que muda dentro de uma análise — inclusive seus participantes e documentos, que só existem no contexto dela.
  • Retroatividade. Registros já existentes não ganham detalhe de "antes e depois": o valor anterior não existe para ser recuperado. O histórico novo começa do zero, e isso é aceito.

6. Validação do problema

Preenchido pelo laço de validação do /spec new (subagente spec-problem-validator). Registrar todas as tentativas, inclusive as reprovadas.

Tentativa Necessidade Jornada de hoje Final Veredicto
1 8,0 7,0 7,0 REPROVADA — escala não declarada; jornada sem separar observado de deduzido; custo da demo pela metade; dois números que não conferiam
2 8,0 7,8 7,8 REPROVADA — jornada externa ausente (só o produto descrito); pessoas por garantidora em aberto; prioridade sem lastro; VIEWER afirmado sem decisions:read quando tem
3 8,5 7,8 7,8 REPROVADA — jornada da TXC só pelo lado do analista, sem o lado da imobiliária e sem gatilho/desfecho; escala ainda por expectativa quando há base real no GarantePay; frações de D2 e D3 sem tamanho

| 4 | 8,5 | 8,0 | 8,0 | REPROVADA por 0,1 — mesmos apontamentos: números medidos no GarantePay, jornada pelo lado da imobiliária, custo por unidade |

Nota sobre a rodada 4. Entre a 3ª e a 4ª tentativa, a régua foi alterada (.claude/agents/spec-problem-validator.md, .claude/skills/spec/SKILL.md e o CLAUDE.md da raiz): o portão deixou de perguntar se a feature vale a pena — decisão que já está tomada quando o /spec new é chamado — e passou a perguntar apenas se o problema está entendido o bastante para desenhar contra ele. Na prática, os critérios N2 e J3 foram reorientados de justificativa para dimensionamento, e ausência de métrica de uma operação que ainda não roda deixou de custar nota. A rodada 4 não refletiu essa mudança: o subagente respondeu citando os nomes antigos dos critérios ("Custo de não fazer", "Frequência e escala"), sinal de que a definição estava em cache da sessão. A régua nova vale a partir da próxima sessão.

Resultado: melhor nota 8,0 (necessidade 8,5 · jornada 8,0) — 0,1 abaixo do corte, sob a régua antiga. Todas as afirmações verificáveis foram conferidas no código pelo validador ao longo das quatro rodadas e nenhuma falhou. O que travou foi sempre a mesma coisa: métrica de operação e jornada de terceiros que exigiriam consultar a TXC.

Pendências aceitas — decisão do usuário, registrada:

  1. Números medidos no GarantePay não foram buscados. A escala usada (~50 análises/mês, ~25 devoluções, ~10 imobiliárias, 1–2 analistas) é expectativa declarada pelo responsável do produto, não medição. Aceito: a decisão de fazer já estava tomada, e o número mudaria a justificativa, não o desenho.
  2. A jornada de hoje da imobiliária não foi levantada. A seção 3.1 conta a operação atual da TXC pelo lado do analista. O que a imobiliária faz ao receber a pendência — onde anota, como responde, quanto demora — continua desconhecido. Isto pode voltar a morder na Parte II, ao desenhar a tela que ela usa; se voltar, é conversa com a TXC, não suposição.
  3. A fração documental das devoluções é desconhecida. Se a maioria for documental, o pré-preenchimento da 0036 já cobre o caso comum e o catálogo de tipos vale menos do que esta spec supõe. Registrado como decisão em aberto (seção 15) por poder alterar o recorte.
  4. O custo não tem unidade (minutos por devolução redigida, análises paradas por mês). Aceito pela mesma razão do item 1.

Parte II — A experiência

Não preencher antes da nota acima de 8 na seção 6.

7. Jornada pretendida

São três entradas, e a do meio é a que precisa existir antes das outras duas.

  • A garantidora define o vocabulário. ADMIN_TENANT chega pela lateral, grupo ADMINISTRAÇÃO, num item novo "Tipos de Pendência", vizinho de "Tipos de Documento" — mesmo grupo, mesmo desenho, porque é a mesma natureza de cadastro: uma lista curta que a garantidora mantém e o resto do sistema consome.
  • O analista devolve. Continua chegando pela análise em andamento, pela ação que já existe no topo ("Devolver à imobiliária"). Nada muda no caminho até o diálogo; muda o que o diálogo pede.
  • A imobiliária resolve ou desiste. Continua chegando pela análise devolvida, da lista ou do link direto. Ganha uma saída que hoje não tem.

A jornada, caso feliz

Antes de tudo, uma vez: o gestor abre "Tipos de Pendência" e cadastra os motivos que a garantidora usa — cada um com nome curto (é o que vira rótulo na tela e agrupador depois) e texto padrão (é o que a imobiliária vai ler). O texto aceita ênfase simples: negrito, itálico, sublinhado. Nada além disso — é instrução para ser lida e resolvida, não documento.

A garantidora não começa do zero. Uma garantidora nova nasce com cinco tipos já cadastrados, editáveis e removíveis. Sem isso, a primeira devolução de todo cliente novo esbarra num cadastro vazio, e o tipo é obrigatório — o produto travaria exatamente no passo que esta spec veio destravar. É a mesma lição do estado vazio (7.3), resolvida antes de acontecer.

O conjunto inicial, aprovado pelo responsável do produto:

Nome Texto padrão
Documentos pendentes — marcado como o tipo documental Faltam documentos para concluir a análise. Anexe os itens listados abaixo e devolva a análise para seguirmos.
Documento ilegível ou vencido O documento anexado não permite conferência — está ilegível, cortado ou fora do prazo de validade. Anexe uma nova via legível e dentro da validade.
Dados divergentes do documento As informações cadastradas não conferem com o documento apresentado. Confira nome, CPF/CNPJ e data de nascimento e corrija o cadastro.
Renda insuficiente ou não comprovada A renda declarada não atinge o mínimo exigido, ou não está comprovada pelos documentos anexados. Inclua um corresponsável ou anexe comprovação adicional.
Dados do imóvel incompletos Faltam informações do imóvel ou do proprietário para concluir a análise. Complete o cadastro do imóvel e devolva a análise.

Não há um tipo "Outros", e é decisão: o tipo é obrigatório justamente para que a devolução seja agrupável depois, e um coringa cadastrado vira o item mais usado da lista — esvaziando o agrupamento que justifica o campo. Quando um caso não se encaixa, o caminho é o gestor cadastrar o tipo que faltava; é assim que o vocabulário da casa cresce a partir do que de fato acontece, e não de uma lista imaginada na estreia.

A devolução, do começo ao fim:

  1. O analista aciona "Devolver à imobiliária" numa análise em andamento. O diálogo abre pedindo duas coisas, nesta ordem: o tipo e o texto.
  2. Ele escolhe o tipo. Ao escolher, o texto padrão daquele tipo preenche o campo de texto, já formatado, e fica editável — ele completa com o que é específico daquele caso ("o holerite de março está ilegível") ou apaga tudo e escreve o seu. O tipo é obrigatório; o texto, também — devolver sem dizer o que fazer é o que a spec 0036 já tinha proibido.

Achar o tipo não pode virar o custo novo. O catálogo começa com cinco itens e cresce com o uso; uma garantidora rodada pode chegar a algumas dezenas. Então a seleção é ordenada alfabeticamente, com um campo de busca que filtra por nome enquanto se digita — e o tipo marcado como documental vem no topo, fora da ordem, quando há documento faltando, porque nesse caso ele já é a resposta. A busca existe nos dois viewports: no desktop porque uma lista longa de nomes parecidos se varre mal com o olho, no mobile porque rolar uma lista de tela cheia com o polegar é pior ainda.

Trocar de tipo depois de já ter escrito não apaga o que a pessoa digitou sem avisar. Se o campo de texto ainda estiver exatamente como o tipo anterior o deixou, trocar substitui direto — não há trabalho a perder. Se tiver sido editado, a troca pergunta ("Substituir o texto pelo padrão de Renda insuficiente?") com a opção de manter o que está escrito. Substituir três linhas digitadas por causa de um clique de correção é a maneira mais rápida de a pessoa parar de usar o campo. 3. Quando há documento obrigatório faltando, o diálogo já vem resolvido: o tipo documental vem pré-selecionado e o texto vem com o padrão dele seguido da lista dos documentos que faltam, como hoje. O caso mais comum continua sendo conferir e confirmar, sem digitar nada — esse ganho da 0036 não pode regredir por causa de um campo novo.

Como o sistema sabe qual é "o tipo documental", se o catálogo é livre e o gestor pode renomear ou apagar qualquer item: o gestor marca um deles. No cadastro há um marcador "usar quando faltar documento obrigatório", e no máximo um tipo pode tê-lo — marcar um segundo desmarca o primeiro, e a tela diz isso. Se nenhum estiver marcado (ou o marcado tiver sido removido), o diálogo abre sem pré-seleção, mas o texto ainda traz a lista de documentos faltantes: perde-se o clique economizado, nunca a informação. Nada de adivinhar pelo nome do tipo. 4. Ele confirma, e vê o resultado na hora. O diálogo fecha, a tela recarrega, e a análise passa para "Pendente" — não mais "Aguardando documentos", rótulo que contradizia toda devolução que não fosse sobre documento. A linha de situação passa a dizer o que ele acabou de fazer: "Devolvida a Imobiliária X em 13/08. Motivo: '…'. Aguardando a correção — a vez é da imobiliária." É a confirmação de que a bola trocou de lado; sem ela, a única evidência seria o botão ter sumido. 5. A imobiliária abre a análise. Lê, no topo, o tipo e o texto da pendência: o tipo dá o assunto em duas palavras, o texto diz o que fazer. E não vê mais o bloco "Parecer da análise" — ele desaparece para quem não tem acesso ao parecer, em vez de aparecer vazio. 6. Ela escolhe entre dois desfechos, os dois dentro do sistema: - Resolver — corrige, anexa, e devolve ao analista. É o caminho que já existe. - Desistir — quando a locação caiu. Detalhado abaixo.

A desistência, passo a passo (é o desfecho novo, e o único do usuário externo):

6a. Ela aciona "Desistir desta análise", no rodapé da página. 6b. Abre uma confirmação intitulada "Desistir desta análise", dizendo o que vai acontecer em uma frase — "A análise é encerrada e não poderá ser retomada. Para seguir com esta locação depois, será preciso cadastrar uma análise nova." — com um campo "Motivo (obrigatório)", curto, de até 200 caracteres. Obrigatório porque é a única coisa que a garantidora vai ler para saber por que a análise morreu; sem ele, o registro diz que alguém desistiu e não diz nada mais. O confirmar chama-se "Desistir da análise", em cor de erro; ao lado, "Voltar". 6c. Confirmado, a análise passa a "Cancelada" — o estado terminal que já existe, não um estado novo. Ela some das filas de quem espera, dos dois lados. 6d. A tela não redireciona. Recarrega na mesma análise, agora encerrada, com a linha de situação dizendo "Cancelada em DD/MM. Motivo: '…'. Estado final: esta análise não volta atrás." — quem acabou de desistir muitas vezes precisa copiar um dado dali para a análise nova, e jogá-la de volta na lista tiraria isso da mão.

Em que estados ela pode desistir: Rascunho, Enviada e Pendente. A regra não é "onde a bola está com ela" — em Enviada a bola é da garantidora, e ela pode desistir assim mesmo. A regra é: enquanto ninguém começou a trabalhar naquilo. Desistir de uma análise que ainda não foi aberta não desperdiça o trabalho de ninguém; desistir depois, sim.

Por isso Em análise fica de fora: o analista está com a análise na mão naquele momento, e encerrá-la por baixo dele é diferente de desistir do que está parado. Se a locação caiu enquanto a análise está sendo avaliada, o caminho é a garantidora cancelar — e o gatilho disso é conversa entre as partes, que esta spec não tenta substituir. Nos estados terminais, nada. 6e. A garantidora descobre pela própria análise, sem ninguém avisar — e isso precisa estar escrito, porque o motivo é obrigatório justamente para ela ler. A análise sai das filas de trabalho e aparece como Cancelada; quem a abrir lê, na linha de situação, "Cancelada pela imobiliária em 13/08. Motivo: '…'. Estado final: esta análise não volta atrás." — e a mesma linha no histórico.

> **O texto de hoje mente nesse caso e precisa mudar.** A frase que o lado que não
> decide lê hoje num cancelamento é *"Cancelada … **pela garantidora**"* — escrita
> quando cancelar era ação só da garantidora. Com a desistência, **quem cancelou passa
> a ser dito de verdade dos dois lados**: "pela imobiliária" quando foi ela, "pela
> garantidora" quando foi a outra parte. Dizer o lado não é dizer a pessoa — o nome de
> quem agiu continua restrito a quem é da garantidora.
  1. O analista retoma e, no histórico, vê o que mudou desde que devolveu: cada alteração com data, hora, quem fez, o campo tocado, o valor de antes e o de agora. Não precisa reler a análise inteira comparando com a memória.

E a tela mostra onde começa "desde que devolvi". Uma linha do tempo cronológica que mistura transições e alterações não responde isso sozinha — o analista teria de ler de cima até topar com a transição e contar dali. Então o histórico ganha um separador logo acima do primeiro evento posterior à última devolução, escrito "Desde a devolução de 13/08". Aparece apenas quando houve devolução e há evento depois dela; some quando não há o que separar. É uma linha de texto, não um filtro: o resto do histórico continua visível abaixo, porque a pergunta seguinte costuma ser "e da vez anterior, o que tinha sido pedido?".

Custo de uso

O que o sistema já sabe e não deve perguntar de novo:

  • Os documentos que faltam — o checklist já sabe; continua preenchendo o texto, agora junto do tipo correspondente.
  • Quem está devolvendo, para quem, e quando — não se digita nada disso.
  • O texto do tipo escolhido — é o cadastro que responde; o analista só ajusta.

O que ele precisa digitar, e é só isto: o que é específico daquele caso. No caso documental, nada. No caso "CPF divergente", a diferença entre o padrão e a situação real — uma frase.

Os limites, ditos onde a pessoa escreve — todo campo de texto mostra o contador quando se aproxima do teto:

Campo Quem escreve Teto
Nome do tipo gestor 60 — é rótulo de tela e de agrupamento, não frase
Texto padrão do tipo gestor 500 — o mesmo teto do campo que ele vai preencher; maior que isso entregaria ao analista um texto que não cabe
Texto da devolução analista 500 (o de hoje)
Motivo da desistência imobiliária 200 — é uma frase, não um relatório

O teto do texto padrão ser igual ao do campo de destino não é coincidência: se fosse maior, o gestor escreveria um padrão que chega truncado ou recusado na mão do analista, e descobriria isso pelo analista.

O contador conta o que a pessoa escreveu, não a marcação. Um texto de 300 caracteres com metade em negrito continua sendo 300 no contador. Contar a marcação faria o limite encolher conforme a pessoa formata — ela veria o contador saltar ao clicar em negrito, sem ter digitado nada, e concluiria que o campo está quebrado. (Isso tem consequência para o armazenamento, e ela é problema da Parte III, não de quem escreve.)

Do lado do gestor, o custo é uma vez: cadastrar os tipos que a casa usa. Do lado da imobiliária, zero digitação para resolver; uma frase curta para desistir.

O que ficou deliberadamente mais caro, e por quê: devolver passou a exigir uma escolha a mais (o tipo). É um clique que não existia. Ele se paga porque devolve o texto pronto no mesmo gesto — e porque sem ele a devolução continua sendo prosa que ninguém consegue agrupar depois.

7.1 Desktop

Um aviso para quem for implementar: o editor de ênfase é padrão novo neste app. Não existe editor de texto rico em apps/web/src hoje, e docs/ui-patterns.md não descreve nenhum. Esta spec introduz um — a barra de três botões — usado em dois diálogos (cadastro do tipo e devolução) e lido em um lugar (a linha de situação da análise). Não é objeção: é registro de que se está criando padrão, e de que ele deve nascer como componente compartilhado com o seu .prompt.md, não como duas implementações que divergem no primeiro ajuste — que foi exatamente a história do design system antes da spec 0017.

O diálogo de devolução. Cresce um campo, e a ordem importa: "Tipo de pendência" (seleção, obrigatório) acima de "O que precisa ser corrigido" (texto longo, obrigatório). O texto é uma área de várias linhas com uma barra de três botões — negrito, itálico, sublinhado — e um contador de caracteres, porque o limite é 500 e hoje a pessoa só descobre que estourou ao confirmar. Rodapé com "Cancelar" e "Devolver", este último em destaque de atenção. Tudo cabe sem rolar: são dois campos.

A tela da análise devolvida. A linha de situação, que hoje diz Motivo: "…", passa a abrir com o nome do tipo em destaque e o texto formatado embaixo — assunto primeiro, instrução depois. É a mesma posição de hoje, logo abaixo do trilho de status, sem bloco novo competindo com ela.

O que some. Para quem não tem acesso ao parecer, o painel "Parecer da análise" deixa de ser renderizado — e o que estava abaixo dele sobe. A página fica mais curta para a imobiliária, que é o correto: ela nunca teve o que fazer ali.

A saída da imobiliária. "Desistir desta análise" fica num rodapé discreto ao fim da página, com borda superior separando-o do conteúdo, em texto de atenção e tamanho pequeno — nunca no topo. Ação destrutiva não disputa o topo com "Devolver ao analista" ou "Enviar para análise", que é o que se espera dela.

Isso diverge de onde a garantidora cancela hoje, nos dois estados novos — e é deliberado. Hoje "Cancelar análise" é ação destrutiva do topo em Rascunho e Enviada, e só desce para o rodapé do painel de parecer em Pendente, porque ali o topo já está lotado com quatro candidatas (spec 0025, RN11). Para a imobiliária o cálculo é outro: ela não tem painel de parecer (ele deixou de ser renderizado), e "Desistir" é a única ação destrutiva que ela tem em qualquer estado. Uma ação que muda de lugar conforme o estado obriga a procurá-la; posição fixa nos três estados vale mais do que espelhar a colocação do outro lado, que ela nunca vê. Na mesma análise, portanto, a garantidora pode ter "Cancelar" no topo e a imobiliária ter "Desistir" no rodapé — são duas telas diferentes para a mesma análise, e sempre foram.

O histórico. Continua sendo a linha do tempo vertical no fim da página, e ganha um segundo tipo de linha. Hoje há só transições ("Em análise → Pendente"). Passa a haver também alterações, com um glifo próprio para não se confundirem à distância. Cada alteração mostra o campo, o valor anterior e o novo, na forma "Renda declarada: R$ 4.500 → R$ 6.200" — o antes riscado ou esmaecido, o depois em peso normal, para a leitura de relance responder "mudou para quanto". Data e hora sempre; o nome de quem fez só para quem é da garantidora — a imobiliária lê o mesmo histórico e não recebe a estrutura interna da garantidora, que é a decisão da spec 0036, §8.7, mantida aqui.

Quando uma edição tocou muitos campos de uma vez, as alterações daquele momento aparecem agrupadas sob um cabeçalho único (quem, quando), com os campos listados dentro — senão uma correção de cinco campos vira cinco linhas iguais e empurra o resto do histórico para fora da tela.

Nem toda alteração é "de X para Y", e cada forma tem a sua linha. São quatro:

O que aconteceu Como a linha lê
Campo alterado Renda declarada: ~~R$ 4.500~~ → R$ 6.200
Valor preenchido pela primeira vez Renda declarada: (vazio) → R$ 6.200
Item adicionado ou removido Participante adicionado: Maria Souza (corresponsável) · Documento removido: Comprovante de renda — João Lima
Item substituído Documento substituído: Comprovante de residência — João Lima (o anterior continua registrado)

Adicionar e remover não se espremem no formato de par: "participante: nada → Maria" seria uma leitura pior que a frase direta. O que todas as quatro compartilham é o resto: data, hora, e nome só para quem é da garantidora.

O histórico agora cresce, e a tela precisa aguentar. Antes ele tinha uma linha por transição de status — meia dúzia numa análise inteira. Agora tem uma por alteração, e uma correção de análise devolvida pode gerar dez. O painel mostra os 20 eventos mais recentes, do mais novo ao mais antigo, com "Ver histórico completo" ao fim quando há mais — que expande no lugar, sem sair da página. Não é paginação com setas: o histórico se lê de cima para baixo, e quebrar isso em páginas destrói justamente a leitura de sequência que a linha do tempo entrega.

O cadastro de Tipos de Pendência. Mesma composição da tela "Tipos de Documento", que é o vizinho dele no menu: painel único, título no topo com o botão "Novo tipo", e uma tabela de quatro colunas — Nome, Texto padrão (as primeiras linhas, truncadas com reticências), Uso e Ações (editar, remover). Sem filtro, sem busca, sem paginação: o catálogo tem dezenas de itens, não milhares, e cabe numa tela.

A coluna Uso existe por causa do marcador: é onde se lê a etiqueta "Documental" no único tipo que a carrega. Sem coluna própria, o marcador só apareceria dentro do diálogo de edição, e o gestor teria de abrir os tipos um a um para descobrir qual está marcado — sendo que a pergunta ("qual é o documental?") é justamente de relance.

O diálogo de cadastro tem três coisas: Nome, o mesmo editor de três botões do diálogo de devolução (para o gestor ver exatamente o que o analista vai receber), e a caixa de marcação "Usar quando faltar documento obrigatório", com a explicação em letra miúda logo abaixo — "Este tipo virá pré-selecionado quando a análise tiver documento obrigatório faltando. Apenas um tipo pode ter esta marca." Ao marcar um segundo tipo, a confirmação diz de quem a marca está saindo: "'Documentos pendentes' deixará de ser o tipo documental." — a troca é visível no momento em que acontece, não descoberta depois pelo analista.

Depois de salvar, o diálogo fecha e a lista recarrega com o item recém-salvo destacado por um instante, para o olho achar onde ele entrou numa lista ordenada por nome. Sem toast: a mudança está visível na própria lista, e um aviso flutuante repetiria o que já se vê.

Editar um tipo não reescreve o passado. O texto que a devolução mandou é guardado com ela no instante do envio — é o que aquela pessoa leu, e mudar isso retroativamente falsificaria o histórico. É a mesma razão pela qual a aprovação guarda o checklist documental do momento (spec 0008, RN11). O nome, ao contrário, segue o cadastro: ele é rótulo e agrupador, e renomear "Renda insuficiente" para "Renda não comprovada" é dizer que o conceito passou a se chamar assim — devoluções antigas passam a exibir o nome novo, que é o que faz o agrupamento continuar funcionando.

Remover não congela nada — o tipo removido continua existindo por trás (a remoção esconde, não apaga), então o nome de uma devolução antiga continua resolvendo normalmente. O que muda é só que ele deixa de ser oferecido em devoluções novas.

Remover um tipo não apaga história. Devoluções antigas continuam mostrando o tipo com que foram feitas; o tipo removido apenas deixa de ser oferecido em devoluções novas. A tela diz isso na confirmação da remoção, em vez de deixar o gestor descobrir depois.

"Pendente" troca o rótulo em toda superfície onde ele aparece — não só no detalhe. São cinco, e deixar qualquer uma para trás recria a contradição que o D5 descreve, só que em outra tela:

  1. O selo de status na lista de análises — hoje "Aguardando documentos".
  2. O filtro de status dessa mesma lista, onde é uma das opções.
  3. O Centro de Operações, que agrupa análises por estado.
  4. O trilho de status no topo da análise.
  5. O histórico, onde a transição lê "Em análise → Pendente".

Onde couber, o rótulo é "Pendente"; onde a coluna for larga o bastante e o contexto pedir, "Pendente com a imobiliária" — porque o que a palavra sozinha não diz é de quem é a bola, e essa é a pergunta de quem varre uma lista.

7.2 Mobile (375px)

O diálogo de devolução é o ponto crítico, porque é onde o teclado virtual entra em cena. Em 375px ele ocupa a tela inteira, de borda a borda: a seleção de tipo no topo, a área de texto abaixo. Com o teclado aberto sobram ~180px de altura útil, então o rodapé com "Devolver" fica ancorado ao fim do diálogo e visível o tempo todo — não rola junto com o conteúdo. É a diferença entre confirmar e ficar procurando o botão com o polegar. A barra de negrito/itálico/sublinhado fica colada acima do teclado, com alvos de 44px, pelo mesmo motivo.

A seleção de tipo vira lista de tela cheia, não um menu suspenso de 200px: cada item com nome e a primeira linha do texto padrão, porque escolher com o polegar numa lista de nomes curtos apertada é onde se erra o item vizinho.

A busca fica fixa no topo dessa lista, abaixo do título, e não rola com ela. Com o teclado aberto sobram ~180px, o que dá três itens visíveis — e é o bastante, porque quem está digitando está filtrando: cada letra reduz a lista, e o item certo sobe. Fechado o teclado, a lista ocupa a tela inteira. O que não pode acontecer é a busca rolar para fora junto com os itens, obrigando a subir de volta para corrigir uma letra.

A situação da análise já é texto corrido e continua sendo — o tipo em destaque, o texto abaixo. Não trunca: é a informação que a pessoa veio buscar. Ocupa as linhas que precisar.

O histórico é o que mais muda de forma. "Renda declarada: R$ 4.500 → R$ 6.200" não cabe em uma linha de 375px, e rolagem horizontal está proibida. Então quebra em duas: o nome do campo numa linha, antes → depois na seguinte, com recuo. Quando o valor é longo — um endereço, uma observação —, cada valor ocupa a própria linha, com "de" e "para" como rótulos, e o texto muito longo é truncado com "ver mais" que expande no lugar.

E é no mobile que o volume dói mais, porque cada alteração ocupa duas alturas em vez de uma. As três defesas do desktop existem aqui, mais apertadas:

  • O agrupamento por momento de edição vem colapsado. Uma correção de cinco campos aparece como uma entrada — "Cadastro atualizado · 5 alterações · 13/08 14:22" — que expande ao toque. No desktop o grupo já vem aberto, porque lá cabe; em 375px, cinco campos abertos são dez linhas antes de qualquer outra coisa.
  • O teto cai de 20 para 10 eventos, com o mesmo "Ver histórico completo" ao fim. Vinte eventos em 375px são umas cinco alturas de tela só de histórico, e ele é o último bloco da página — quem chegou até ali já rolou tudo.
  • O separador "Desde a devolução de 13/08" aparece igual, e no mobile ele vale ainda mais: é o que permite parar de rolar quando se encontrou o que se procurava.

O cadastro de Tipos de Pendência troca a tabela por RecordCard: um cartão por tipo, com o nome como título, as duas primeiras linhas do texto padrão como corpo, o marcador documental como etiqueta discreta quando houver, e as ações de editar/remover como ícones de 44px no canto do cartão. O botão "Novo tipo" é a ação primária do topo, que em 375px já vira botão preenchido de 44×44 — comportamento que o PageHeader entrega sozinho.

O diálogo de cadastro em 375px tem o mesmo problema de teclado que o da devolução, e a mesma resposta: tela cheia de borda a borda, "Nome" no topo, o editor abaixo ocupando o que sobra, a barra de negrito/itálico/sublinhado colada acima do teclado com alvos de 44px, e o rodapé "Cancelar / Salvar" ancorado ao fim, visível com o teclado aberto. Não é um caso mais fácil por ser tela de gestor: é o mesmo gesto de escrever texto longo num aparelho pequeno.

A confirmação de "Desistir" também pede digitação em 375px — são 200 caracteres, mas com teclado aberto valem as mesmas regras: diálogo de tela cheia, campo visível acima do teclado, e o botão "Desistir da análise" ancorado no rodapé. Um diálogo destrutivo cujo botão de confirmar se esconde atrás do teclado é a pior combinação possível: a pessoa rola às cegas procurando o que confirma o que não dá para desfazer.

As ações da imobiliária no topo já viram ícone de 44px sem rótulo. São duas (lápis = corrigir, avião de papel = devolver ao analista), e continuam duas: "Desistir" não sobe para o topo no mobile. Fica onde está no desktop, no rodapé da página, com rótulo escrito por extenso — um ícone sem rótulo para a ação mais destrutiva da tela é exatamente o erro que a spec 0036 documentou ao enumerar os ícones na linha de situação.

O rótulo do estado, em 375px, é sempre "Pendente" — a forma curta. A regra do desktop ("'Pendente com a imobiliária' onde a coluna for larga o bastante") não sobrevive aqui: no cartão da lista o selo divide a linha com o código da análise, e no FilterPill o rótulo longo estoura a pílula e força quebra. A informação de quem tem a bola já é dada pela linha de situação dentro da análise, escrita por extenso; repeti-la num selo de 90px custaria o layout e não acrescentaria nada.

O que não aparece no celular: nada é escondido. O que muda é a forma. As duas concessões são o texto padrão nos cartões do cadastro, limitado a duas linhas (quem precisa do texto inteiro toca em editar), e o grupo de alterações que vem colapsado.

O vizinho de menu não é modelo no mobile, e isso é de propósito. "Tipos de Documento" mantém a tabela em 375px — ela não tem tratamento responsivo hoje. Aqui a tabela vira RecordCard, e quem for implementar não deve copiar a tela vizinha por analogia: a semelhança entre as duas vale no desktop, onde a composição é a mesma, e termina no mobile. Uma tabela de quatro colunas em 375px produz rolagem horizontal, que é justamente o que este app proíbe.

A linha de situação nomeia o que fazer, como já faz hoje: em 375px ela diz "toque no lápis" em vez de citar um rótulo que não existe na tela. Com "Desistir" no rodapé, ela passa a mencionar as duas saídas — a de corrigir pelo ícone, a de desistir pelo fim da página.

7.3 Estados

Vazio — e este é o estado que decide se a feature funciona.

  • Cadastro de tipos sem nenhum item: não deve acontecer, porque a garantidora nasce com um conjunto inicial. Se ainda assim acontecer (o gestor apagou todos), a tela mostra o vazio que diz o que fazer, não só que está vazio: "Nenhum tipo cadastrado. Sem ao menos um, o analista não consegue devolver análise." com o botão de criar ao lado.
  • Diálogo de devolução sem tipos disponíveis: o analista não pode ser deixado sem saída num beco criado por outra tela. O diálogo mostra o aviso e diz a quem pedir — "Nenhum tipo de pendência cadastrado. Peça a um gestor para cadastrar em Administração → Tipos de Pendência." (é onde o item fica na lateral; não existe seção "Configurações" no menu — a de /settings chama-se "Parâmetros"). Para quem é gestor, o mesmo aviso traz o link direto. Confirmar fica desabilitado, e o motivo está escrito ao lado, não três telas abaixo.
  • Histórico sem alterações: uma análise que só mudou de status mostra as transições, como hoje. Não se inventa linha de "nada mudou".

O histórico que não carregou não pode se passar por histórico vazio. É o estado mais importante desta lista, porque o histórico é o entregável do D4 e porque hoje a tela engole a falha em silêncio — a busca cai num catch que devolve lista vazia, e o resultado na tela é indistinguível de uma análise sem eventos. Alguém auditando leria "nada aconteceu aqui" diante de uma requisição que falhou. Passa a mostrar, no lugar da linha do tempo, "Não foi possível carregar o histórico" com "Tentar de novo" ao lado — a mesma distinção que a spec faz para o cadastro, e pela mesma razão: vazio e não carregou levam a ações opostas.

Carregando. O diálogo de devolução abre já utilizável: a lista de tipos costuma ser curta e vem com a tela. Enquanto não chega, a seleção mostra um esqueleto e o botão de confirmar espera — nunca um diálogo em branco que aceita confirmação sem tipo. O histórico carrega com esqueleto de três linhas, no lugar onde as linhas vão aparecer. A lista do cadastro também: esqueleto de três linhas de tabela (ou três cartões, no mobile), nunca a tabela vazia — que se leria como "não há tipos cadastrados" e mandaria o gestor criar o que já existe.

Faltando preencher. Confirmar sem tipo, ou sem texto, não desabilita o botão — o erro aparece no campo, em vermelho, com a rolagem indo ao primeiro que faltou, que é o padrão de formulário deste app (docs/ui-patterns.md) e o que o diálogo de devolução já faz hoje com o motivo. Botão cinza obriga a pessoa a adivinhar qual dos dois campos o está travando; erro no campo diz.

Erro.

  • Falha ao carregar os tipos: o diálogo diz que não conseguiu carregar e oferece tentar de novo ali mesmo, sem fechar e perder o que já foi digitado.
  • Falha ao devolver: o diálogo continua aberto com o texto preservado — é o comportamento que o ConfirmDialog já tem hoje, e ele existe porque perder três linhas de motivo por uma rede que caiu é o oposto do que a tela promete.
  • Texto acima do limite: o contador fica em cor de atenção antes de estourar, e a confirmação avisa ao lado do campo — não depois da viagem ao servidor.
  • Desistência recusada (a análise mudou de estado enquanto ela olhava): mensagem dizendo o que aconteceu — "esta análise já foi decidida pela garantidora" — e a tela recarrega no estado novo, em vez de repetir uma ação impossível.

Erro no cadastro de tipos — a tela do gestor tem os mesmos três, e nenhum deles é exótico:

  • Falha ao carregar a lista: a tela mostra o que houve e um "Tentar de novo" no lugar da tabela, não uma tabela vazia que se confunde com "não há tipos cadastrados". A diferença entre vazio e não carregou é a diferença entre cadastrar e esperar.
  • Falha ao salvar: o diálogo fica aberto com o que foi digitado — mesma regra do diálogo de devolução, e pelo mesmo motivo.
  • Nome repetido: é o erro previsível deste cadastro, porque a lista cresce aos poucos e ninguém relê antes de criar. O aviso aparece ao lado do campo Nome — "Já existe um tipo com este nome" — e não como faixa no topo, para a correção acontecer onde o olho já está.

Sem permissão.

  • Quem não tem acesso ao parecer não vê o bloco de parecer. Não é aviso de "sem permissão": é ausência. Aviso de permissão negada aqui informaria a existência de algo que não é da conta de quem lê — o mesmo defeito que a spec 0035 corrigiu no menu.
  • Quem não decide não vê as ações de decidir, como já hoje.
  • Quem não é gestor não vê o item "Tipos de Pendência" na lateral — e, se digitar a URL, encontra a tela de acesso negado, não o cadastro. Esconder o menu não protege a rota.
  • A imobiliária vê "Desistir" apenas em Rascunho, Enviada e Pendente. Nos demais estados a ação não existe na tela, em vez de existir e ser negada.

Falha ao remover um tipo. A remoção tem confirmação própria; ela também precisa de caminho de erro, e pelo mesmo motivo dos outros dois: o diálogo continua aberto com a mensagem do que houve, e a lista não some nem se reordena — o gestor precisa saber que o tipo continua lá, não descobrir na próxima devolução que ele voltou.

8. Validação da experiência

Tentativa Jornada Viewports Final Veredicto
1 7,3 7,5 7,3 REPROVADA — "Desistir" anunciado sem descrição; histórico só com forma de antes → depois; tipo documental sem regra; rótulo "Pendente" só no detalhe; estados do cadastro e diálogo mobile ausentes
2 8,2 8,0 8,0 REPROVADA por 0,1 — histórico sem estado de erro; volume do histórico não resolvido em 375px; marcador documental fora da tela de cadastro; "Desistir" × "Cancelar" em DRAFT/SUBMITTED; ciclo de vida do tipo ao editar
3 8,5 8,5 8,5 APROVADA

Resultado: aprovada na tentativa 3 com nota 8,5 (jornada 8,5 · viewports 8,5).

Melhorias aplicadas depois da aprovação (o validador apontou 8 itens mesmo aprovando; 7 foram corrigidos na hora): o outro lado da desistência ganhou tela e o texto "Cancelada pela garantidora" passou a dizer quem de fato cancelou; a contradição entre "pode desistir em Enviada" e "só onde a bola está com ela" foi resolvida pela regra real (enquanto ninguém começou a trabalhar naquilo); o passo do retorno da devolução saiu de dentro do bloco da desistência; o editor de ênfase foi registrado como padrão novo do app; a busca da lista de tipos ganhou tratamento em 375px; o contador passou a declarar que conta caracteres, não marcação; e a falha ao remover um tipo ganhou caminho de erro.

Pendências aceitas: uma, e ela é de acesso — o que a imobiliária lê nas linhas de alteração do histórico. A Parte II decidiu só o nome de quem agiu (restrito à garantidora); falta decidir se a parceira externa vê todas as alterações, só as que ela mesma fez, ou nenhuma. Levado para a Parte III, seção 10.4 (isolamento por imobiliária), que é onde essa decisão pertence e onde o portão a mede com rigor.


Parte III — Detalhamento

Não preencher antes das notas acima de 8 nas seções 6 e 8.

9. Regras de negócio

O catálogo de tipos

  • RN1. Um Tipo de Pendência pertence a uma garantidora (tenant_id) e tem nome (obrigatório, único dentro da garantidora, até 60 caracteres visíveis) e texto padrão (obrigatório, até 500 caracteres visíveis, com ênfase opcional). Nome repetido no mesmo tenant → 409 PENDENCY_TYPE_NAME_TAKEN.

Era 422 nesta spec, e virou 409 na implementação — a spec é que estava errada. O código já tinha convenção para isto: DOCUMENT_TYPE_NAME_TAKEN, do cadastro vizinho, usa ConflictError (409). 409 é o código de conflito de unicidade, e divergir dele criaria duas respostas para a mesma situação em telas irmãs. - RN2. No máximo um tipo por garantidora carrega a marca is_documental. Marcar um segundo desmarca o primeiro na mesma transação — nunca resulta em dois marcados, nem em erro pedindo que o usuário desmarque antes. Não é opção de configuração: é a identidade do tipo que o sistema pré-seleciona. - RN3. Remover um tipo é soft delete. O registro permanece para que as devoluções antigas continuem resolvendo o nome; ele apenas deixa de ser listado e de ser aceito em devoluções novas. Remover o tipo documental é permitido — a garantidora fica sem pré-seleção, e a RN12 cobre esse caso. - RN4. Toda garantidora nova nasce com os cinco tipos da Parte II, o primeiro com is_documental = true. São dados de tenant, criados junto com as TenantSettings no onboarding — não é seed global. - RN5. O texto padrão é copiado, nunca referenciado, no momento da devolução. Editar o texto de um tipo não altera o que devoluções passadas exibem. Já o nome é resolvido pela referência: renomear um tipo muda o rótulo exibido nas devoluções antigas, porque é o mesmo conceito com outro nome — e é o que mantém o agrupamento íntegro. (Mesmo raciocínio do documents_snapshot da spec 0008, RN11.)

A ênfase no texto

  • RN6. O texto padrão e o texto da devolução aceitam três marcações e nada mais: negrito, itálico e sublinhado. São armazenados como HTML restrito, e o servidor sanitiza na escrita com allowlist de <strong>, <em>, <u> — nenhum atributo, nenhuma outra tag. Marcação fora da lista é removida, preservando o texto interno; a requisição não é recusada por isso.
  • RN7. O limite de 500 conta caracteres visíveis — o texto com a marcação removida. A coluna guarda até 2.000 para caber o HTML; quem valida é o serviço, sobre o texto limpo. Acima disso → 422 com o campo nomeado.
  • RN8. O cliente nunca injeta esse HTML por dangerouslySetInnerHTML. A renderização percorre a marcação e produz elementos React. A sanitização do servidor é a primeira barreira; não confiar nela no cliente é a segunda, e as duas são baratas.

A devolução

  • RN9. Devolver exige tipo (obrigatório) e texto (obrigatório, como já era na spec 0036, RN7). Sem tipo → 422 VALIDATION_ERROR com pendency_type_id nomeado em details (é o schema que valida, como em qualquer campo obrigatório do app).
  • RN10. O pendency_type_id recebido no corpo é resolvido no escopo da garantidora de quem chama antes de qualquer escrita. Tipo de outro tenant, ou removido → 404, nunca 403 (spec 0034, RN4/RN5): responder 403 confirmaria que o identificador existe.
  • RN11. O tipo e o texto enviado ficam gravados na linha do histórico de status daquela devolução (pendency_type_id + reason), que é a única estrutura com uma linha por devolução — a Decision guarda só a última. É o que permite agrupar por motivo ao longo do tempo.
  • RN12. Quando a análise tem documento obrigatório faltando e existe tipo documental, a API devolve, junto do checklist, qual é o tipo a pré-selecionar. Sem tipo documental, o campo vem nulo e a interface abre sem pré-seleção — o texto com a lista de documentos continua sendo montado do mesmo jeito. A ausência do marcador nunca bloqueia a devolução.
  • RN12a. O campo suggested_pendency_type_id do checklist é preenchido apenas para quem tem decisions:update; para os demais vem null. O checklist é lido com credit_analyses:read, que OPERATOR e AGENCY_USER têm — e a matriz da seção 10.1 mantém os dois fora do catálogo. Sem esta cláusula, um campo novo entregaria a eles, de graça, o identificador de um tipo de pendência que não devem alcançar. Quem não pode devolver não tem o que fazer com a sugestão.
  • RN13. Sem nenhum tipo cadastrado (todos removidos), devolver é impossível e a API responde 422 NO_PENDENCY_TYPE_AVAILABLE, com mensagem que diz o que fazer. É consequência aceita da RN9; a RN4 existe para que não aconteça.

O estado renomeado

  • RN14. CreditAnalysisStatus.PENDING_DOCS passa a chamar-se PENDING. É rename do valor do enum, não estado novo: as transições permitidas, as guardas de edição e o fluxo continuam idênticos. DecisionStatus.PENDING já existe e é outro enum — não há colisão.
  • RN15. O rótulo em português passa a "Pendente", e a forma longa "Pendente com a imobiliária" só onde couber (Parte II). Nenhuma superfície fica para trás: selo, filtro, Centro de Operações, trilho e histórico.

A desistência

  • RN16. A imobiliária encerra a própria análise em DRAFT, SUBMITTED e PENDING. Em IN_ANALYSIS e nos terminais → 422 WITHDRAW_NOT_ALLOWED_IN_STATE. O estado de destino é CANCELLED, o terminal que já existe.
  • RN17. Motivo obrigatório, até 200 caracteres, não vazio nem só espaço. Ausente → 422 WITHDRAW_REASON_REQUIRED. É simétrico à devolução (0036, RN7) e pela mesma razão: é o texto que o outro lado vai ler.
  • RN18. Só desiste quem é do lado que cadastrou: o usuário precisa ter partner_agency_id, e ele precisa coincidir com a imobiliária vinculada à análise. Caso contrário → 403 WITHDRAW_WRONG_SIDE, espelhando o RESUME_WRONG_SIDE da spec
  • O vínculo vem do perfil de quem chama, nunca do corpo (0034, RN10/RN12).
  • RN19. A garantidora não usa esta operação — ela tem credit_analyses:cancel, que continua como está. São ações distintas de propósito: o negativo explícito é o que permite testar que uma não vira atalho para a outra.
  • RN20. A linha de situação e o histórico passam a dizer qual lado encerrou — "pela imobiliária" ou "pela garantidora". Hoje o texto afirma "pela garantidora" sempre, e a RN16 o tornaria falso. Lado, não pessoa: o nome de quem agiu continua restrito (RN24).

O histórico de alterações

  • RN21. Toda escrita que altere uma análise de crédito, seus participantes ou seus documentos grava uma ou mais linhas de alteração, com: quem (changed_by), quando, o que (entidade + campo), valor anterior e valor novo. São seis endpoints, nominalmente:
Endpoint Entidade Operações
PATCH /credit-analyses/{id} ANALYSIS UPDATED
POST /credit-analyses/{id}/participants PARTICIPANT ADDED
PATCH /credit-analyses/{id}/participants/{pid} PARTICIPANT UPDATED
DELETE /credit-analyses/{id}/participants/{pid} PARTICIPANT REMOVED
POST /documents (quando credit_analysis_id está preenchido) DOCUMENT ADDED
PUT /documents/{doc_id}/content (idem) DOCUMENT REPLACED
PATCH /documents/{doc_id} (idem — metadado: nome, tipo) DOCUMENT UPDATED
DELETE /documents/{doc_id} (idem) DOCUMENT REMOVED

Esta tabela é a lista fechada — são oito, e é ela que vale onde o texto desta spec disser outro número.

PUT /content é endpoint novo, criado nesta spec. Ele entrou por causa da própria RN23: REPLACED estava declarada e nenhum caminho a emitia, porque a API não tinha substituição de arquivo — o conteúdo era imutável e trocar significava excluir e subir de novo. A regra prometia uma operação que o produto não sabia fazer.

Podíamos ter apagado a promessa. Criar a operação foi a escolha, e o motivo é o catálogo inicial desta mesma spec: "Documento ilegível ou vencido" é um dos cinco tipos, e resolvê-lo é justamente mandar a via nova do mesmo documento. Pelo caminho antigo, a imobiliária perdia a classificação (o document_type_id que satisfaz a exigência do checklist) e o histórico registrava dois eventos sem relação. Ver RN28.

Os endpoints de transição de estado ficam de fora, e não é esquecimento: submit, resume, cancel, withdraw, pend, approve e reject não alteram campo nenhum — mudam o estado, e isso já é registrado no histórico de status desde sempre. Duplicá-los no log de alterações produziria duas linhas para o mesmo evento na mesma linha do tempo. - RN22. Uma requisição que altera vários campos gera um grupo (change_group_id comum) com uma linha por campo. É o que permite à interface colapsar "5 alterações" numa entrada só sem inventar agrupamento na leitura. - RN23. Quatro operações, e nem todas têm par de valores: UPDATED (antes e depois), ADDED e REMOVED (só o rótulo do item — "Maria Souza (corresponsável)"), REPLACED (rótulo, e a linha anterior continua registrada). Adicionar não vira null → valor. - RN24. O nome de quem agiu só é resolvido para quem tem decisions:read. Para os demais, a resposta traz o identificador e o lado (garantidora ou imobiliária), nunca o nome — é a §8.7 da spec 0036 mantida, agora numa estrutura nova. - RN25. Valor sensível não entra no log em claro. O change_log_service mantém uma allowlist negativa fechada — hoje cpf, cnpj, rg, os mesmos que exigem renters:update_sensitive para serem escritos (app/schemas/renter.py). Campo nela grava a linha (mudou, quem, quando) com old_value e new_value em null; a interface lê "CPF alterado", sem os valores.

Esta regra é preventiva, e é honesto dizer isso. Nenhum dos endpoints da RN21 altera esses três campos hoje — eles vivem em Renter, que muda por PATCH /renters/{id}, endpoint que a seção 5 deixa fora de escopo (o log é do que muda dentro de uma análise). A regra existe porque a decisão de o que não vai para o log precisa estar tomada antes de alguém precisar dela: no dia em que o log alcançar o cadastro do inquilino, a omissão já é o padrão, em vez de ser lembrada por quem estiver escrevendo o código naquele dia.

Quando esse dia chegar, duas coisas terão de ser decididas na spec e não no código: contra qual análise a linha é gravada (um inquilino participa de várias, de imobiliárias diferentes) e qual actor_agency_id ela carrega. Errar isso faz uma alteração atravessar o filtro da seção 10.4 e aparecer para a imobiliária errada. Por isso o log não se estende a Renter nesta spec: não é esquecimento, é a decisão de não improvisar essa parte.

A lista é fechada e vive num só lugar, porque campo sensível que depende de cada chamador lembrar de omiti-lo é campo que vaza na terceira chamada. - RN28. Substituir o arquivo de um documento preserva o registro — PUT /documents/{doc_id}/content, permissão documents:update. Trocam-se conteúdo, tipo MIME, tamanho e nome; não mudam document_type_id, notes nem o vínculo com a entidade. Emite REPLACED (RN23) quando o documento é de uma análise. Mesmas validações do upload; documento de outro tenant ou de outra imobiliária → 404.

  • RN29. A validade documental reinicia na substituição. O checklist conta o prazo da data de anexo; a coluna nova documents.content_updated_at guarda o instante da troca, e o checklist prefere-a a created_at.

Sem isto a operação não resolveria o caso que a motivou: a via nova de um comprovante vencido continuaria vencida, o analista devolveria por "documento vencido", a imobiliária anexaria, e o checklist continuaria acusando — um laço sem saída. created_at não é reescrito: ele diz quando o registro nasceu e é o que a auditoria lê; mentir nele para consertar o checklist trocaria um problema por outro pior.

  • RN26. O log é imutável: só inserção. Não há endpoint de edição nem de remoção, e o downgrade da migration remove a tabela inteira — não há remoção seletiva.

10. Autorização e isolamento

10.1 Matriz de papéis

Papel Pode Não pode
ADMIN_GLOBAL Administrar tipos de pendência informando tenant_id explicitamente; ler qualquer análise Listar tipos de pendência sem informar tenant (→ 400 TENANT_REQUIRED); desistir de análise (não tem imobiliária)
ADMIN_TENANT CRUD completo dos tipos da própria garantidora; devolver, aprovar, reprovar, cancelar; ver o histórico completo com nomes Ver tipo ou análise de outra garantidora (→ 404); usar withdraw
ANALYST Ler os tipos (para escolher no diálogo); devolver com tipo; ver o histórico completo com nomes Criar, editar ou remover tipo (→ 403); usar withdraw
OPERATOR Criar e editar análise; ver o histórico sem nomes (não tem decisions:read) Ler o catálogo de tipos; devolver; usar withdraw; ver o painel de parecer
VIEWER Ler análise e histórico com nomes (tem decisions:read) Qualquer escrita; ler o catálogo de tipos
AGENCY_USER Desistir da própria análise em DRAFT/SUBMITTED/PENDING; corrigir e devolver ao analista; ver as próprias linhas de alteração Ler o catálogo de tipos (→ 403); devolver análise; ver o painel de parecer; ver alterações feitas pela garantidora; ver nome de quem agiu; desistir de análise de outra imobiliária (→ 404)

O negativo do AGENCY_USER é o que mais vira teste: são seis proibições, e cinco delas já falharam em produto parecido — é a lição das specs 0034 e 0035.

10.2 Permissões

Novas (entram no rbac_seed.py):

Permissão Papéis
pendency_types:list ADMIN_TENANT, ANALYST
pendency_types:create ADMIN_TENANT
pendency_types:update ADMIN_TENANT
pendency_types:delete ADMIN_TENANT
credit_analyses:withdraw AGENCY_USER

São quatro, não cinco: não existe pendency_types:read. Foi cogitada e recusada — não há endpoint de leitura por id nesta spec (o diálogo de edição usa o item que a lista já trouxe), e permissão sem endpoint que a exija responde "sim" a isto está protegido? sem proteger. Além do achado da spec 0035, isso quebra um teste que já existe: tests/test_authorization_coverage.py::test_toda_permissao_definida_e_exigida.

ADMIN_GLOBAL recebe as quatro automaticamente — o rbac_seed.py monta o papel dele com todas as permissões ({f"{r}:{a}" for r, a, _ in _PERMISSIONS}), então não há como "não conceder". O que o segura não é permissão, é a exigência de escopo da seção 10.5: sem tenant_id resolvível ele não passa em nenhuma das quatro operações. É por isso que a coluna acima lista só quem opera de fato.

Por que withdraw não é cancel. Reusar credit_analyses:cancel para a imobiliária daria a ela, de uma vez, o cancelamento em todos os estados que a garantidora pode cancelar — a guarda de estado passaria a ser a única coisa entre ela e uma análise em análise. Permissão separada mantém o negativo explícito e testável: AGENCY_USER continua recebendo 403 em POST /cancel, e isso é um teste, não uma suposição.

ANALYST lê o catálogo, não o administra. Sem pendency_types:list o diálogo de devolução não tem o que oferecer; com create ele passaria a definir o vocabulário da casa, que é o oposto da RN1. OPERATOR e AGENCY_USER não leem o catálogo: nenhum dos dois devolve análise, e a lista revela como a garantidora classifica o trabalho dela.

Nenhuma permissão nova sem endpoint que a exija. As seis acima são exigidas nos endpoints da seção 12 — permissão declarada e não usada responde "sim" a isto está protegido? sem proteger, que foi o achado da spec 0035.

10.3 Isolamento por garantidora (tenant)

  • PendencyType tem tenant_id e é lido pelo BaseRepository, que filtra sozinho. Isso cobre a leitura; não cobre a FK que chega pelo corpo.
  • pendency_type_id do corpo da devolução é resolvido no escopo de quem chama antes de gravar (RN10) — um get_by_id(tipo, tenant_id=<do solicitante>). Sem isso, um identificador adivinhado de outra garantidora entraria por escrita, e o nome dela apareceria na tela de outra: é exatamente o furo de leitura-por-escrita que a RN5 da spec 0034 descreve.
  • Registro de outro tenant → 404, nunca 403. Vale para tipo, análise e linha de histórico.
  • A tabela de alterações carrega tenant_id próprio, e não só a FK da análise. Custa uma coluna e evita que uma consulta que esqueça o join devolva log de outra garantidora.

10.4 Isolamento por imobiliária

A decisão que a Parte II deixou em aberto, agora tomada: o AGENCY_USER vê, no histórico de alterações, apenas as linhas produzidas pelo próprio lado dela — e as transições de status, como já hoje. O que a garantidora alterou internamente não aparece para ela.

  • Como se filtra: cada linha grava actor_agency_id — a imobiliária de quem fez a alteração, derivada do perfil de quem chama no instante da escrita, null quando foi alguém da garantidora. A consulta do AGENCY_USER filtra por actor_agency_id == <a dele>. Guardar no momento da escrita, e não resolver depois pelo usuário, é o que mantém o histórico correto quando alguém troca de imobiliária ou é desligado.
  • Por que não "todas as alterações": ela veria a garantidora corrigindo renda declarada e valores de pacote locatício — a operação interna do cliente do SaaS, exposta a um parceiro externo. É a mesma natureza do que a spec 0035 tirou do menu.
  • Por que não "nenhuma": ela perderia a prova de que corrigiu o que foi pedido, que é metade do valor do D4 para o lado dela.
  • O alcance dela continua sendo o de hoje: só análises da própria imobiliária; qualquer outra → 404. O vínculo vem do perfil, nunca do corpo (0034, RN10/RN12).
  • Documento herda o escopo do dono (0034, RN13): a linha "documento anexado" segue a mesma regra das demais — se foi ela quem anexou, ela vê; se foi a garantidora, não.
  • O catálogo de tipos é invisível para ela, inclusive o nome do tipo? Não: o nome do tipo aparece na pendência que ela recebeu — é o assunto da mensagem dirigida a ela. O que ela não alcança é a lista, que mostraria todos os motivos pelos quais aquela garantidora reprova trabalho, inclusive os que nunca lhe foram aplicados.

10.5 Plataforma × cliente (SaaS admin)

  • Tipo de pendência é dado de negócio do cliente, não da plataforma: é o vocabulário operacional de uma garantidora. Por isso, ADMIN_GLOBAL não recebe cross-tenant aqui — e a regra vale para as quatro operações, não só para a leitura.

RN27 — Toda operação do catálogo exige um escopo de garantidora resolvido, e ele nunca vem do corpo. O serviço resolve o escopo assim, antes de qualquer leitura ou escrita:

Quem chama De onde vem o tenant_id Se não der
Quem tem tenant próprio (ADMIN_TENANT, ANALYST) do perfil, sempre — o parâmetro tenant_id, se vier, é ignorado, nunca honrado —
ADMIN_GLOBAL do parâmetro tenant_id da query, obrigatório nas quatro operações 400 TENANT_REQUIRED

E as escritas por id conferem o escopo antes de gravar: PATCH e DELETE fazem get_by_id(id, tenant_id=<escopo resolvido>) e respondem 404 quando o tipo é de outra garantidora. Sem essa cláusula o BaseRepository não protegeria nada nesse caminho — ele não filtra para quem não tem tenant, e o id sozinho alcançaria o catálogo de qualquer cliente. POST grava com o escopo resolvido; tenant_id no corpo não existe no schema, então não há campo a ignorar.

Isso é o oposto do padrão tenant_id = None if is_admin_global usado em outros serviços, e é deliberado: aquele padrão é exatamente o caminho pelo qual o SaaS admin vira atalho para o dado do cliente (spec 0034). - O histórico de alterações segue a mesma régua. É lido sempre no contexto de uma análise, que já é escopada; ADMIN_GLOBAL alcança porque alcança a análise, não porque o log seja cross-tenant. - A tela do SaaS admin (/app/admin/*) não muda. Nada de tipo de pendência, contagem de devoluções ou histórico aparece lá. Ela administra a plataforma. - Nenhuma tela da garantidora aceita tenant_id de fora (0034, RN7). O parâmetro do primeiro item existe só para o ADMIN_GLOBAL e é ignorado para os demais — quem tem tenant é servido pelo próprio, sempre.

11. Impacto por app

apps/api

Modelos e migrations — três migrations, nesta ordem:

  1. rename_pending_docs_to_pending — ALTER TYPE credit_analysis_status RENAME VALUE 'PENDING_DOCS' TO 'PENDING'. As linhas existentes acompanham o rename; não há UPDATE. downgrade: o rename inverso. Em SQLite (testes) o enum é VARCHAR e a migration é inócua — o valor vem do Python.
  2. add_pendency_types — tabela pendency_types (UUIDMixin, TenantMixin, TimestampMixin, SoftDeleteMixin): name String(60), default_text String(2000), is_documental Boolean default false. Índice único parcial em (tenant_id, lower(name)) where deleted_at is null — é a RN1 no banco, não só no serviço. Coluna pendency_type_id (FK nullable, ondelete=RESTRICT) em credit_analysis_status_history. downgrade: derruba a coluna e a tabela.
  3. add_credit_analysis_change_log — tabela credit_analysis_change_log (UUIDMixin, TenantMixin, TimestampMixin): credit_analysis_id FK indexado, change_group_id UUID indexado, changed_by FK, actor_agency_id FK nullable, entity Enum(ANALYSIS|PARTICIPANT|DOCUMENT), operation Enum(UPDATED|ADDED|REMOVED|REPLACED), field String(80) null, old_value Text null, new_value Text null, item_label String(160) null. downgrade: derruba a tabela.

O downgrade da primeira é o que importa de verdade. Um deploy revertido com o enum já renomeado deixa a API antiga lendo um valor que o Python dela não conhece — e o erro aparece longe, na desserialização. Exercitar o downgrade, não só escrevê-lo, é parte da entrega (spec 0028).

Camadas:

  • models/pendency_type.py; models/credit_analysis_change_log.py; alterações em models/credit_analysis.py (rename do valor do enum + FK no histórico).
  • schemas/pendency_type.py — PendencyTypeCreate/Update/Response. Validador de campo que sanitiza e mede o texto limpo (RN6/RN7).
  • schemas/credit_analysis.py — CreditAnalysisChangeResponse; pendency_type_id em AnalysisPendRequest; CreditAnalysisWithdrawRequest (motivo obrigatório, 200).
  • repositories/pendency_type_repo.py; repositories/change_log_repo.py (com o filtro por actor_agency_id).
  • services/pendency_type_service.py — RN1 a RN5, incluindo a desmarcação em transação (RN2) e o seed dos cinco tipos, chamado do onboarding_service.
  • services/change_log_service.py — um registrar(antes, depois, ...) que produz o grupo de linhas a partir de dois estados do objeto. Um lugar só: espalhar a montagem do diff pelos serviços é como metade dos campos fica de fora.
  • services/decision_service.py — RN9 a RN13 no pend().
  • services/credit_analysis_service.py — withdraw() (RN16 a RN20) e chamadas ao change_log_service no update().
  • services/credit_analysis_participant_service.py — chamadas ao change_log_service (hoje o arquivo não tem nenhuma linha de auditoria).
  • services/document_service.py — chamadas ao change_log_service na criação, na substituição, na alteração de metadado e na remoção, apenas quando o documento pertence a uma análise de crédito (credit_analysis_id preenchido). Documento de inquilino ou de imóvel não entra: o escopo desta spec é o que muda dentro de uma análise (seção 5).
  • api/v1/routers/pendency_types.py (novo, registrado em api/v1/__init__.py e com entrada em TAGS_METADATA); alterações em routers/credit_analyses.py e routers/decisions.py.
  • db/rbac_seed.py — as seis permissões da seção 10.2.

Testes — caminho feliz e, sobretudo, os negativos:

  • RN2 (marcar o segundo desmarca o primeiro), RN5 (editar texto não muda devolução antiga; renomear muda o rótulo), RN7 (500 contados sem marcação), RN6 (<script> some, texto interno fica).
  • RN10: devolver com pendency_type_id de outro tenant → 404.
  • RN13: sem tipos, devolver → 422.
  • RN16/RN18: withdraw em IN_ANALYSIS → 422; AGENCY_USER da imobiliária B na análise da A → 404; usuário da garantidora chamando withdraw → 403.
  • RN19: AGENCY_USER em POST /cancel → 403.
  • RN21/RN22/RN23: PATCH de dois campos gera duas linhas com o mesmo change_group_id; adicionar participante gera ADDED sem old_value.
  • RN24: resposta para quem não tem decisions:read não traz nome.
  • RN25: alterar cpf gera linha com old_value/new_value nulos — e um teste que varre a resposta inteira procurando o valor antigo, para o dia em que alguém acrescentar um campo de conveniência que o traga de volta.
  • RN12a: OPERATOR e AGENCY_USER recebem suggested_pendency_type_id nulo.
  • RN27 / 10.4 / 10.5: AGENCY_USER lê /changes e recebe só as linhas do próprio actor_agency_id; ADMIN_GLOBAL sem tenant_id → 400 nas quatro operações; e com tenant_id de A, PATCH no id de B → 404.

apps/web

Páginas e componentes:

  • pages/settings/pendency-types/ — lista + diálogo, no molde de document-types, com a coluna Uso e RecordCard no mobile.
  • components/shared/RichTextField/ — componente novo e compartilhado, com o .prompt.md correspondente em design-system/prompts/. Barra de três botões, contador de caracteres visíveis, e o renderizador de leitura (RichText) que não usa dangerouslySetInnerHTML.
  • pages/credit-analyses/show.tsx — showDecisionPanel passa a exigir canReadDecision (é o D1, uma condição); ação "Desistir" no rodapé; merge cronológico de transições e alterações.
  • components/credit-analyses/PendDialog — o ConfirmDialog de hoje não comporta dois campos com dependência entre eles (escolher tipo preenche texto, com confirmação ao sobrescrever). Vira diálogo próprio; o ConfirmDialog continua servindo cancelar, reprovar e reenviar.
  • HistoryTimeline — as quatro formas de linha, o agrupamento colapsável, o separador "Desde a devolução de DD/MM", o teto (20 desktop / 10 mobile) e o estado de erro.
  • situacao.ts — "Pendente"; o lado que encerrou (RN20); a menção à saída de desistir.
  • StatusBadge/CREDIT_ANALYSIS_STATUS, filtro de status, Centro de Operações — RN15.

data-testid novos: pendency-types-form, pendency-type-name-input, pendency-type-text-input, pendency-type-documental-checkbox, pendency-types-form-submit, pendency-types-new-button, pendency-type-row-{id}, pendency-type-edit-{id}, pendency-type-delete-{id}, pend-dialog, pend-type-select, pend-type-search, pend-text-input, pend-dialog-confirm, credit-analysis-withdraw-button, credit-analysis-withdraw-dialog, credit-analysis-withdraw-reason-input, credit-analysis-withdraw-confirm, credit-analysis-history-error, credit-analysis-history-more, credit-analysis-history-group-{id}.

Sider e rota: item "Tipos de Pendência" em nav-groups.tsx, grupo ADMINISTRAÇÃO, entre "Tipos de Documento" e "Documentos da Análise", requerPermissao: "pendency_types:create" — o mesmo critério do vizinho, de modo que o ANALYST (que só lê) não vê o item. Rota em App.tsx sob <ExigeAcesso permissao="pendency_types:create">: esconder o menu não protege a rota (spec 0035).

Responsividade e PWA: tudo o que a seção 7.2 descreve. Sem impacto de PWA — não há tela offline nova nem mudança de assets; o service worker serve o bundle novo no próximo carregamento, como sempre.

apps/e2e

Specs:

  • pendency-types-crud.spec.ts — cadastrar, editar, marcar documental (e ver o primeiro desmarcar), remover com o aviso, nome repetido.
  • devolucao-com-tipo.spec.ts — devolver escolhendo tipo; conferir que o texto padrão chegou; conferir a pré-seleção documental quando há documento faltando.
  • desistencia-imobiliaria.spec.ts — desistir em PENDING; conferir que a garantidora lê "Cancelada pela imobiliária" com o motivo; conferir que a ação não existe em IN_ANALYSIS.
  • historico-alteracoes.spec.ts — alterar dois campos, ver o grupo e os valores antes/ depois; conferir que o AGENCY_USER não vê a alteração feita pela garantidora.
  • Ajuste dos três specs que hoje citam PENDING_DOCS e o rótulo antigo.

Seeds: tipos de pendência no tenant de e2e, e uma análise em PENDING com histórico de alterações já povoado.

A parametrização de documentos é do tenant e é compartilhada. O spec da pré-seleção documental precisa de documento obrigatório configurado — e tem de desfazer no finally, ou derruba testes que nada têm a ver com ele. Aconteceu na spec 0025.

12. Contrato de API

Catálogo de tipos

GET /api/v1/pendency-types?tenant_id=<uuid>
Authorization: Bearer <token>
Permissão pendency_types:list. Não há GET /{id} — e por isso não há pendency_types:read (§10.2). tenant_id é ignorado para quem tem tenant próprio e obrigatório para ADMIN_GLOBAL, nas quatro operações (RN27).

[{ "id": "uuid", "name": "Documentos pendentes",
   "default_text": "Faltam documentos… <strong>anexe</strong>…",
   "is_documental": true, "created_at": "…", "updated_at": "…" }]
POST   /api/v1/pendency-types?tenant_id=<uuid>        → 201   (pendency_types:create)
PATCH  /api/v1/pendency-types/{id}?tenant_id=<uuid>   → 200   (pendency_types:update)
DELETE /api/v1/pendency-types/{id}?tenant_id=<uuid>   → 204   (pendency_types:delete)

{ "name": "Renda insuficiente", "default_text": "<em>…</em>", "is_documental": false }

O PATCH é parcial: campo omitido é preservado, não zerado. Enviar {"name": "…"} muda só o nome. is_documental: false desmarca de verdade — e a garantidora fica sem tipo documental, que é o caso previsto pela RN12 (o diálogo abre sem pré-seleção e o texto ainda traz os documentos). Enviar is_documental: true dispara a RN2: o tipo que estava marcado é desmarcado na mesma transação.

O corpo não tem tenant_id, e isso é do desenho (RN27): campo que decide escopo não entra pelo corpo. O parâmetro de query existe só para o ADMIN_GLOBAL, é ignorado para quem tem tenant próprio, e PATCH/DELETE buscam o registro já no escopo resolvido — tipo de outra garantidora responde 404, não 403. O DELETE é soft.

Erros: 422 PENDENCY_TYPE_NAME_TAKEN (nome repetido no tenant, comparando sem diferenciar maiúsculas) · 422 com campo nomeado (texto acima de 500 visíveis) · 404 para tipo de outro tenant, nas três operações por id · 403 para ANALYST em create/update/delete · 400 TENANT_REQUIRED para ADMIN_GLOBAL sem tenant_id, nas quatro.

Devolução com tipo

POST /api/v1/credit-analyses/{id}/analysis/pend
Authorization: Bearer <token>

{ "pendency_type_id": "uuid", "reason": "Falta o holerite de <strong>março</strong>." }
Permissão decisions:update (inalterada). Resposta: a AnalysisResponse de hoje.

Erros: 422 VALIDATION_ERROR com pendency_type_id nomeado em details quando ele falta — quem valida é o schema, como em qualquer campo obrigatório do app, e tirar a obrigatoriedade dali só para emitir um código próprio seria pior que ajustar a promessa · 404 para tipo de outro tenant ou removido (RN10) · 422 NO_PENDENCY_TYPE_AVAILABLE (RN13) · 422 com reason nomeado (vazio ou acima de 500 visíveis).

Leitura da devolução — o que a tela usa para mostrar o tipo

O histórico de status é onde a devolução mora (RN11), e é ele que a linha de situação e a linha do tempo leem. GET /api/v1/credit-analyses/{id}/history, permissão credit_analyses:read (inalterada), ganha três campos:

[{ "id": "uuid", "from_status": "IN_ANALYSIS", "to_status": "PENDING",
   "changed_by": "uuid", "created_at": "…",
   "reason": "Falta o holerite de <strong>março</strong>.",
   "pendency_type_id": "uuid",
   "pendency_type_name": "Documentos pendentes",
   "pendency_type_active": false }]
  • reason continua sendo o texto gravado no envio (RN5) — nunca o texto atual do tipo.
  • pendency_type_name é resolvido pela referência, e por isso funciona para tipo removido: a remoção é soft (RN3), a linha continua no banco, e o nome resolve como qualquer outro. pendency_type_active: false existe só para a interface poder diferenciar, se quiser — não muda o que é exibido.
  • Os três campos vêm null nas linhas que não são devolução (aprovação, cancelamento, submissão).
  • Quem lê isso é todo mundo que lê a análise, inclusive a imobiliária: o nome do tipo é o assunto da mensagem dirigida a ela (seção 10.4). O que ela não alcança é a lista do catálogo, que é outro endpoint e outra permissão.

O checklist ganha um campo

GET /credit-analyses/{credit_analysis_id}/document-checklist, permissão credit_analyses:read (inalterada), passa a devolver suggested_pendency_type_id: uuid | null — a RN12. Vem null em dois casos: quando não há tipo documental cadastrado, e para quem não tem decisions:update (RN12a) — o que mantém OPERATOR e AGENCY_USER, que têm credit_analyses:read, fora do catálogo mesmo com o campo novo na resposta deles.

Desistência

POST /api/v1/credit-analyses/{id}/withdraw
Authorization: Bearer <token>

{ "reason": "Inquilino desistiu — imóvel alugado para outro candidato." }
Permissão credit_analyses:withdraw. Resposta: CreditAnalysisResponse em CANCELLED.

Erros: 422 WITHDRAW_REASON_REQUIRED · 422 WITHDRAW_NOT_ALLOWED_IN_STATE (IN_ANALYSIS e terminais) · 403 WITHDRAW_WRONG_SIDE (usuário sem partner_agency_id, ou de outra imobiliária que ainda assim alcança a análise) · 404 para análise de outra imobiliária.

Substituição de arquivo (RN28/RN29)

PUT /api/v1/documents/{doc_id}/content
Authorization: Bearer <token>
Content-Type: multipart/form-data

file=@comprovante-novo.pdf
Permissão documents:update (já existente; a imobiliária a tem, e é ela quem resolve a pendência documental).

{ "id": "uuid", "original_filename": "comprovante-novo.pdf",
  "document_type_id": "uuid — inalterado",
  "created_at": "2026-08-01T… — inalterado, é quando o registro nasceu",
  "content_updated_at": "2026-08-13T… — é daqui que a validade passa a contar" }

Erros: 404 para documento de outro tenant, de outra imobiliária ou já excluído · 422 INVALID_FILE_TYPE · 422 FILE_TOO_LARGE · 422 EMPTY_FILE.

Migration w6y8a0d2g4h5: acrescenta documents.content_updated_at (nulo para todo documento existente — nenhum foi substituído, e a validade deles segue contando de created_at). downgrade: derruba a coluna, e com ela o instante das trocas já feitas — documentos substituídos podem reaparecer como vencidos, que é um custo visível (o checklist volta a pedir o que já foi entregue) e não silencioso.

Histórico de alterações

GET /api/v1/credit-analyses/{id}/changes?limit=20&skip=0
Authorization: Bearer <token>
Permissão credit_analyses:read, mais o filtro de 10.4.

[{ "id": "uuid", "change_group_id": "uuid",
   "entity": "PARTICIPANT", "operation": "UPDATED",
   "field": "declared_income", "field_label": "Renda declarada",
   "old_value": "4500.00", "new_value": "6200.00",
   "item_label": "João Lima",
   "changed_by": "uuid", "changed_by_name": "Ana Prado", "actor_side": "GARANTIDORA",
   "created_at": "2026-08-13T14:22:00-03:00" }]

changed_by_name vem apenas para quem tem decisions:read; para os demais é null e o actor_side é o que a interface mostra (RN24). Ordenação created_at desc, id desc — OFFSET/LIMIT sem ordem estável repagina o mesmo registro (regra do CLAUDE.md da API).

field_label é montado na API, não no web — um mapa único {"declared_income": "Renda declarada", "rental_value": "Valor do aluguel", …} no change_log_service. Poderia morar no cliente, e a razão de não morar é que ele tem dois consumidores hoje (a tela e o e2e) e um terceiro amanhã: com o rótulo no web, cada um reconstrói a própria lista e elas divergem no primeiro campo novo — que é a história do design system antes da spec 0017. Na API, o Swagger o documenta e o teste o cobre.

Campo sensível (RN25) volta com old_value e new_value em null e field_label normal — a linha existe, os valores não.

13. Critérios de aceite

Catálogo

  • [ ] CA1. Dado um ADMIN_TENANT, quando cadastra um tipo com nome e texto, então ele aparece na lista e passa a ser oferecido no diálogo de devolução.
  • [ ] CA2. Dado um tipo já marcado como documental, quando o gestor marca outro, então o primeiro deixa de estar marcado e a lista mostra a etiqueta em um só.
  • [ ] CA3. Dado um nome já usado na garantidora, quando o gestor tenta salvar, então recebe 409 e a mensagem aparece ao lado do campo Nome.
  • [ ] CA4. Dado um texto com <script>alert(1)</script> e negrito, quando salvo, então o negrito permanece, a tag some, o texto interno permanece, e a tela não executa nada.
  • [ ] CA5. Dado um texto de 480 caracteres com metade em negrito, quando salvo, então é aceito — o contador conta caracteres visíveis, não marcação.
  • [ ] CA6. Dado um tipo removido, quando se abre uma devolução antiga que o usou, então o nome e o texto continuam sendo exibidos.
  • [ ] CA7. Dado um tipo renomeado, quando se abre uma devolução antiga, então ela exibe o nome novo e o texto original.

Devolução

  • [ ] CA8. Dado um analista em IN_ANALYSIS, quando devolve escolhendo um tipo, então o texto padrão preencheu o campo e a análise foi para "Pendente" — nunca "Aguardando documentos", em nenhuma das cinco superfícies.
  • [ ] CA9. Dada uma análise com documento obrigatório faltando, quando o diálogo abre, então o tipo documental vem pré-selecionado e o texto traz a lista dos documentos.
  • [ ] CA10. Dado que o gestor removeu o tipo documental, quando o diálogo abre com documento faltando, então não há pré-seleção e o texto ainda traz a lista.
  • [ ] CA11. Dado um analista, quando confirma sem escolher tipo, então o erro aparece no campo, o diálogo continua aberto e o texto digitado é preservado.

Desistência

  • [ ] CA12. Dado um AGENCY_USER numa análise da própria imobiliária em PENDING, quando desiste com motivo, então a análise fica CANCELLED e a tela recarrega nela mesma, encerrada.
  • [ ] CA13. Dado o analista da garantidora, quando abre essa análise, então lê "Cancelada pela imobiliária" e o motivo — nunca "pela garantidora".
  • [ ] CA14. Dado um AGENCY_USER numa análise em IN_ANALYSIS, então não existe botão de desistir na tela, e POST /withdraw responde 422.

Histórico

  • [ ] CA15. Dado um PATCH que altera aluguel e prazo, quando se abre o histórico, então há um grupo com duas linhas, cada uma com o valor anterior e o novo.
  • [ ] CA16. Dado um participante adicionado, então a linha lê "Participante adicionado: " — sem par null → valor.
  • [ ] CA17. Dado que a API de histórico falha, então a tela mostra "Não foi possível carregar o histórico" e "Tentar de novo" — nunca a lista vazia.
  • [ ] CA18. Dada uma análise devolvida e depois corrigida, quando o analista a reabre, então há o separador "Desde a devolução de DD/MM" acima do primeiro evento posterior.

Substituição de arquivo (RN28/RN29)

  • [ ] CA36. Dado um documento anexado a uma análise, quando o arquivo é substituído, então o id e o document_type_id são os mesmos e o histórico ganha uma linha REPLACED com os dois nomes — sem que a linha do anexo original desapareça.
  • [ ] CA37. Dado um documento vencido pela parametrização, quando o arquivo é substituído, então o checklist deixa de acusá-lo — é o que torna a pendência "Documento ilegível ou vencido" resolvível.
  • [ ] CA38. Dado um documento de outra imobiliária, quando se tenta substituir, então a resposta é 404.
  • [ ] CA39. Arquivo de tipo não aceito ou vazio → 422 com o código correspondente, as mesmas validações do upload.

Negativos de autorização — um por papel afetado

  • [ ] CA19. AGENCY_USER em GET /pendency-types → 403; e o item "Tipos de Pendência" não aparece na lateral dele.
  • [ ] CA20. AGENCY_USER em POST /credit-analyses/{id}/cancel → 403 (a desistência não é atalho para o cancelamento da garantidora).
  • [ ] CA21. AGENCY_USER da Imobiliária B em POST /withdraw na análise da A → 404.
  • [ ] CA22. AGENCY_USER em GET /changes de uma análise dele → recebe só as linhas do próprio lado; a alteração feita pela garantidora não está na resposta.
  • [ ] CA23. ANALYST em POST /pendency-types → 403; em GET /pendency-types → 200.
  • [ ] CA24. OPERATOR abre uma análise → não existe o bloco "Parecer da análise" na tela, e em GET /changes os nomes vêm null.
  • [ ] CA25. VIEWER abre a mesma análise → vê o bloco de parecer e os nomes (tem decisions:read).
  • [ ] CA26. ADMIN_GLOBAL sem tenant_id → 400 TENANT_REQUIRED nas quatro operações do catálogo (GET, POST, PATCH, DELETE); com tenant_id → alcança só aquela garantidora.
  • [ ] CA27. Devolver com pendency_type_id de outra garantidora → 404, nunca 403.
  • [ ] CA28. ADMIN_GLOBAL com tenant_id da Garantidora A, chamando PATCH/DELETE no id de um tipo da Garantidora B → 404. (É o caminho que o BaseRepository não protege sozinho para esse perfil.)
  • [ ] CA29. ADMIN_TENANT da Garantidora A passando ?tenant_id=<B> → o parâmetro é ignorado e ele opera em A. Nunca em B.
  • [ ] CA30. OPERATOR e AGENCY_USER em GET /checklist → o campo suggested_pendency_type_id vem null, mesmo havendo tipo documental e documento faltando; para o ANALYST, vem preenchido.
  • [ ] CA31. (teste de serviço, não de endpoint — RN25 é preventiva e nenhum endpoint da RN21 toca esses campos hoje.) Chamando o change_log_service com uma alteração de cpf, cnpj ou rg, a linha é criada com old_value e new_value em null, e o valor antigo não aparece em nenhum campo do registro produzido.
  • [ ] CA32. Nenhuma permissão declarada fica sem endpoint que a exija — o teste test_toda_permissao_definida_e_exigida continua passando depois desta spec.

Mobile (375px)

  • [ ] CA33. Em 375px, nenhuma das telas novas rola horizontalmente (scrollWidth - clientWidth == 0): cadastro, diálogo de devolução, diálogo de desistência e histórico.
  • [ ] CA34. Com o teclado aberto, o botão de confirmar dos três diálogos continua visível.
  • [ ] CA35. Em 375px o grupo de alterações vem colapsado e expande ao toque.

14. Plano de implementação

Ordem obrigatória: api → web → e2e.

  • [x] apps/api — migration 1 (rename do enum) e ajuste dos usos de PENDING_DOCS
  • [x] apps/api — PendencyType: model → schema → repo → service → router; seed dos cinco tipos no onboarding
  • [x] apps/api — sanitização e contagem de texto visível (RN6/RN7), com teste próprio
  • [x] apps/api — pend() com tipo (RN9–RN13) e suggested_pendency_type_id no checklist
  • [x] apps/api — withdraw() (RN16–RN20)
  • [x] apps/api — change_log_service e as chamadas nos sete endpoints da tabela da RN21 (RN21–RN26)
  • [x] apps/api — rbac_seed.py: as permissões (cinco, não seis — pendency_types:read saiu por não ter endpoint que a exija)
  • [x] apps/api — migrations 2 e 3 escritas com downgrade
  • [x] apps/api — docstrings + Swagger (summary, description, response_description)
  • [x] apps/api — testes, com os negativos da seção 13 (33 testes novos em dois arquivos)
  • [x] apps/web — RichTextField + RichText compartilhados
  • [x] apps/web — tela de Tipos de Pendência, App.tsx e nav-groups.tsx
  • [x] apps/web — PendDialog; painel de parecer condicionado; "Desistir"; rótulo em todas as superfícies
  • [x] apps/web — HistoryTimeline com as quatro formas, grupo, separador, teto e erro
  • [x] apps/e2e — um spec novo com 15 testes (em vez de quatro arquivos), e o ajuste dos três que citam o estado antigo
  • [ ] make test e make lint (sem apontamento novo nos arquivos tocados)
  • [ ] Validado no browser em desktop e mobile

15. Decisões em aberto

  • [ ] A fração documental das devoluções na TXC. Se a maioria das devoluções for por documento faltando, o caminho pré-preenchido da spec 0036 já resolve o caso comum e o catálogo de tipos entrega menos do que esta spec supõe — o que não invalida a feature (o agrupamento e o rename seguem valendo), mas mudaria a ênfase da entrega. Não trava a implementação; trava a avaliação do resultado. Quem responde: o responsável do produto, com a TXC.
  • [ ] A jornada da imobiliária no processo atual (pendência aceita da Parte II). Pode voltar a morder ao ajustar a tela dela depois do piloto. Quem responde: a TXC.

16. Validação do detalhamento

Tentativa Segurança Clareza Final Veredicto
1 8,0 8,4 8,0 REPROVADA por 0,1 — escritas do catálogo por ADMIN_GLOBAL sem escopo definido; pendency_types:read órfã; checklist alterado sem permissão declarada; endpoints do log não enumerados; RN25 sem CA
2 9,0 8,4 8,4 APROVADA

Resultado: aprovada na tentativa 2 com nota 8,4 (segurança 9,0 · clareza 8,4). A segurança subiu de 8,0 para 9,0 entre as rodadas — o que fechou foi a RN27 (escopo obrigatório nas quatro operações do catálogo, e conferência de escopo no PATCH/DELETE por id), a RN12a (campo novo do checklist fechado para quem não decide) e a remoção da permissão órfã pendency_types:read.

Melhorias aplicadas depois da aprovação (6 apontamentos, todos corrigidos): o contrato de leitura da devolução passou a existir (/history com pendency_type_id/name/active, que é o que a tela da imobiliária lê e nenhum endpoint declarava); a RN25 passou a declarar-se preventiva, com o motivo pelo qual o log não se estende a Renter nesta spec e o que precisará ser decidido quando se estender; a contagem dos endpoints do log foi unificada em sete, com a tabela como lista fechada; o path do checklist foi corrigido para /document-checklist; a semântica do PATCH parcial foi dita; e a frase "o nome congela quando o tipo é removido" foi corrigida — com soft delete não há congelamento.

Riscos aceitos: nenhum. Os dois riscos concretos apontados foram fechados, não aceitos.

17. Validação da implementação

Tentativa Aderência Qualidade Testes Final Veredicto
1 7,2 7,5 6,0 6,0 REPROVADA — RN20 sem implementação; marcação de ênfase saindo crua; ADMIN_GLOBAL com FK cross-tenant; alerta atrás do backdrop; IntegrityError no log de documento; três negativos e a RN5 sem teste
2 8,0 7,8 7,9 7,8 REPROVADA por 0,2 — decision_reason curta demais para o texto com marcação (regressão do fix 0006); Centro de Operações com o rótulo antigo; .catch(() => []) do /history; dois códigos de erro documentados e não emitidos; RN4/RN13 sem teste
3 8,2 8,0 8,1 8,0 REPROVADA por 0,1 — laço esgotado.
4 8,5 8,0 8,3 8,0 REPROVADA por 0,1 — rodada extra, a pedido do usuário, para cobrir a substituição de arquivo (RN28/RN29), acrescentada depois de o laço fechar. Três defeitos, todos corrigidos.

Resultado: laço esgotado nas 3 tentativas, melhor nota 8,0 (aderência 8,2 · qualidade 8,0 · testes 8,1). A progressão foi 6,0 → 7,8 → 8,0, e cada rodada encontrou defeito real — a 1ª uma regra escrita e não implementada (RN20), a 2ª a regressão do fix 0006, a 3ª uma regressão que eu próprio introduzira ao corrigir a 2ª.

Os defeitos da 3ª rodada foram corrigidos, sem revalidação — o laço acabou, e gastar uma 4ª chamada seria burlar o limite. O que ficou:

Defeito Estado
Falha do /history derrubava a página inteira (regressão minha, introduzida na 2ª rodada) corrigido — a promessa não rejeita mais, e o erro é tratado no lugar da linha do tempo, com "Tentar de novo"
pendency_types.default_text sem teto bruto → HTTP 500 no PostgreSQL corrigido — max_length=2000 no schema, como o campo irmão já tinha
RN12: lista de documentos sumia sem tipo documental corrigido — a lista entra sempre que há documento faltando, como a regra dizia
Duas asserções que passavam em qualquer cenário corrigidas — a do checklist verifica a lista, não o eco do PATCH; a do e2e verifica o texto que chega ao campo
Rótulo antigo em CreditAnalysisForm corrigido
skip × offset no contrato corrigido na spec
Ordenação de import (5 arquivos meus) corrigida. Os 16 restantes em app/ são preexistentes — comparado com a main, que já acusa 8 nos mesmos arquivos

Defeitos aceitos: nenhum. O [SPEC INCOMPLETA] foi resolvido pelo usuário, e a decisão foi criar a operação em vez de apagar a promessa — ver abaixo.

A rodada extra: o mesmo erro meu, pela terceira vez

A validação da substituição foi pedida pelo usuário, não é quarta tentativa do laço — ele já tinha fechado. Cobriu trabalho acrescentado depois, e a aderência subiu de 8,2 para 8,5 (a RN28/RN29 foi reconhecida). Os três defeitos:

O motivo da desistência não chegava à tela. motivoDoEstado extraía o motivo para PENDING e REJECTED e devolvia null no resto — então o analista abria uma análise encerrada pela parceira e lia "Cancelada pela imobiliária" sem o porquê, que é a metade que importa. E isso esvaziava a RN17: o motivo é obrigatório porque é o que a garantidora vai ler. Meu próprio e2e falharia — a terceira vez nesta spec em que escrevi o teste certo para um comportamento que não implementei.

A mensagem de erro da desistência ficava atrás do backdrop — exatamente o defeito que eu tinha corrigido no diálogo de remoção de tipo duas rodadas antes, e repeti aqui. O ConfirmDialog ganhou reasonMaxLength, então os 200 caracteres da RN17 passaram a ser sentidos enquanto se digita, em vez de voltarem do servidor para o mesmo ponto cego.

A forma longa do rótulo alimentava o FilterPill de 375px, contra a §7.2 que a exclui do mobile. Duas listas agora: longa no menu do desktop, curta na pílula.

Mais dois testes (RN9 e as linhas REMOVED, que existiam no código sem asserção), a consulta duplicada de User unificada em UserRepository.get_many, e o diagrama de status do apps/api/CLAUDE.md atualizado.

O [SPEC INCOMPLETA], e o que a decisão revelou

ChangeOperation.REPLACED estava declarada e nenhum caminho a emitia: a API não tinha substituição de arquivo. O conteúdo era imutável, e o Swagger dizia com todas as letras "para substituir o arquivo, exclua e faça um novo upload". A regra prometia uma operação que o produto não sabia fazer.

Duas saídas: apagar a promessa, ou criar a operação. O usuário decidiu criar — e ao implementar apareceu por que a escolha era essa:

O caminho antigo não resolvia a pendência que esta mesma spec criou. "Documento ilegível ou vencido" é um dos cinco tipos iniciais, e resolvê-lo é mandar a via nova do mesmo documento. Excluindo e subindo de novo, a imobiliária perdia o document_type_id que satisfaz a exigência do checklist — o obrigatório voltava a constar como faltando, e ela não teria como saber por quê.

E apareceu um segundo defeito, que a substituição sozinha não resolveria. A validade documental conta da data de anexo (spec 0008, RN7). Trocar o arquivo sem tocar nisso deixaria o documento vencido mesmo com a via nova anexada: o analista devolveria por "documento vencido", a imobiliária anexaria, o checklist continuaria acusando — um laço sem saída, e a pendência mais previsível do catálogo seria a única impossível de resolver. Daí a RN29 e a coluna content_updated_at. created_at não é reescrito: ele diz quando o registro nasceu e é o que a auditoria lê.

O teste que prova isso (test_substituir_arquivo_reinicia_a_validade) monta um documento vencido, confere que o checklist o acusa, substitui, e confere que parou. Sem ele, a operação existiria e não serviria para nada.

18. Registro

O que divergiu do planejado, e por quê

Cinco permissões novas, não seis. A Parte III já havia recusado pendency_types:read por não haver endpoint que a exija; o plano da seção 14 ainda dizia "seis". Confirmado na implementação: o teste test_authorization_coverage.py::test_toda_permissao_definida_e_exigida existe e teria falhado.

Um spec de e2e, não quatro. A seção 11 previa quatro arquivos (pendency-types-crud, devolucao-com-tipo, desistencia-imobiliaria, historico-alteracoes). Entregue como um arquivo com 15 testes agrupados por describe, porque os quatro dividiriam os mesmos helpers de arranjo (montar análise até um estado, criar tipo com nome único) e a duplicação seria maior que a separação. Os grupos preservam a leitura por assunto.

_ensure_withdraw_side levanta 403, não 422. A RN18 dizia 403 WITHDRAW_WRONG_SIDE, e o espelho dela — _ensure_resume_side, da spec 0036 — usa BusinessRuleError (422). Mantive o 403 da spec: não é estado inválido nem dado mal preenchido, é a pessoa errada pedindo. Quem alcança a análise mas é de outra imobiliária já morreu no 404 de _get_authorized; o que sobra neste ponto é usuário da garantidora tentando usar a saída da parceira.

Dois defeitos que os testes existentes pegaram

1. O router do catálogo nasceu sem route_class=CommitRoute. Sem ele a resposta sai antes do COMMIT: o cadastro responderia 201 com o tipo não gravado. Pego pelo test_transaction_boundary.py::test_toda_rota_da_aplicacao_comita_na_requisicao, que existe exatamente para isso. É o tipo de defeito que passaria em qualquer teste de caminho feliz feito na mesma requisição.

2. Um teste de isolamento passou a parar no 422 antes do 404 que ele prova. test_outra_garantidora_nao_alcanca_a_analise mandava /decision/pend sem pendency_type_id; com a RN9, a validação do corpo responde 422 antes de o serviço chegar ao 404. Não é vazamento — o 422 é o mesmo para qualquer chamador —, mas o teste deixaria de exercitar o que existe para provar. Corrigido mandando um tipo válido da própria garantidora B contra uma análise da A, o que o torna mais forte do que era.

Uma intermitência preexistente, agora com causa provada

test_partner_agency_status_filter.py::test_alcanca_quem_esta_fora_da_primeira_pagina falhou numa execução e passou na seguinte. Não é desta spec: o arquivo é idêntico à main e passa 3/3 isolado. A causa é a mesma que o CLAUDE.md documenta para test_partner_agency_search.py — cria 9 imobiliárias no mesmo segundo, o created_at empata em SQLite e o desempate é id desc sobre UUID aleatório, então a "escondida" cai na primeira página em ~1/3 das execuções. É uma segunda instância da mesma intermitência, e o CLAUDE.md só registrava uma.

O que o portão da implementação encontrou, e que eu tinha deixado passar

Cinco defeitos reais na primeira rodada. Registro os três que mais ensinam:

RN20 estava escrita e não existia. O ramo encerradaPelaImobiliaria foi escrito em situacao.ts, e nenhum chamador o preenchia — pior, não havia de onde preencher: o GET /history devolvia changed_by e nada sobre o lado. A linha dizia "Cancelada pela garantidora" para toda desistência, justamente para quem precisava saber o contrário. O conserto foi de ponta a ponta: actor_side no schema, _lados_de no serviço, e a ligação na tela. Escrever o ramo não é implementar a regra — e o e2e que eu mesmo tinha escrito para a CA13 teria falhado.

A marcação de ênfase saía crua para quem recebeu a devolução. O RichText nasceu nesta mesma branch e foi usado só no histórico; a linha de situação e o formulário de correção imprimiam o texto direto, então a imobiliária lia <strong>holerite</strong> literal — que é o exemplo do próprio contrato da §12. A causa era estrutural: o motivo era interpolado dentro da frase de acao, e string interpolada não tem como ser renderizada como texto rico. O conserto foi separar o motivo do texto da frase.

ADMIN_GLOBAL gravaria FK de outra garantidora. _resolver_tipo_no_escopo escopava pelo current_user.tenant_id — que funciona para todo perfil que tem tenant e falha exatamente para quem não tem: ali o valor é None, o BaseRepository para de filtrar, e um tipo da Garantidora B entraria numa análise da A. É o padrão tenant_id = None if is_admin_global que a própria §10.5 desta spec rejeita nominalmente, e ele reentrou por uma porta que a seção não previa. O escopo passou a ser o da análise. E o teste que provaria isso (CA27) era um dos que faltavam — não é coincidência.

A segunda rodada: o fix 0006 voltando por uma porta nova

O achado mais caro das três rodadas, e ele não aparecia em teste nenhum.

O fix 0006 subiu decisions.decision_reason de 100 para 500 caracteres porque um motivo de 143 estourava no PostgreSQL e virava HTTP 500. Esta spec acrescentou ênfase ao mesmo campo e passou a medir o limite sobre o texto visível — 500 caracteres mais as tags. Resultado: um motivo de 484 visíveis com um trecho em negrito grava 501, o validador aprova (mediu 484) e o banco recusa. Mesmo defeito, outro gatilho.

A RN7 tinha previsto a folga da coluna — "guarda até 2.000 para caber o HTML" — e ela foi aplicada a pendency_types.default_text e não ao segundo destino do mesmo texto.

Por que nada pegou: a suíte roda em SQLite, onde String(500) não é imposto, e o teste herdado do fix 0006 usa "x" * 501 puro — que morre antes, no validador. O teste novo (test_motivo_com_marcacao_cabe_na_coluna) compara o comprimento gravado com o teto declarado no model, o que funciona nos dois bancos.

Migration v5x7z9c1f3g4, com downgrade que trunca antes de encolher — um ALTER para 500 com linhas mais longas falharia, e o caminho de volta não pode ser o passo que trava a reversão.

Duas descobertas menores, do meu próprio teste: os papéis da garantidora são barrados no porteiro (não têm credit_analyses:withdraw), não na guarda de lado — o WITHDRAW_WRONG_SIDE só é alcançável pelo ADMIN_GLOBAL, que tem todas as permissões e nenhuma imobiliária. E o PENDENCY_TYPE_REQUIRED prometido na §12 nunca existiu: quem valida é o schema, e o que sai é VALIDATION_ERROR com o campo nomeado. Corrigi a documentação, não o código — tirar a obrigatoriedade do schema só para emitir um código próprio seria pior que ajustar a promessa.

As duas suítes, executadas — e a dívida da 0036 paga

make test: 364 passaram, 0 falharam.

make e2e: 221 passaram, 7 falharam, 2 pulados (21,4 min, contra o dev-remote depois do rebuild). Os 22 testes desta spec passaram todos — nenhum aparece entre as falhas, e o Playwright as enumera integralmente ao fim.

Das sete:

  • Duas eram minhas. Troquei o diálogo de devolução pelo PendDialog e não atualizei os dois testes da spec 0036 que o dirigem — procuravam credit-analysis-pend-reason-input, que deixou de existir. Corrigidas, e mais fortes: uma delas chamava /documents/checklist, caminho inexistente, e o .ok() engolia o 404 — ela caía sempre no ramo "sem bloqueantes", medindo metade do que dizia medir e passando por isso.
  • Uma é instável: admin-interface:87 falhou na suíte cheia e passou na reexecução isolada, sem alteração de código.
  • Quatro em áreas fora deste diff, e aqui a spec 0036 tinha deixado escrito: "Não provei que são pré-existentes: isso exigiria uma rodada de baseline." Foi feita.

O baseline. Com o trabalho commitado, git checkout main + rebuild do web + as mesmas quatro specs: 10 falharam, 27 passaram — mais falhas que na branch (4). Confirmadas preexistentes, com o mesmo erro (element(s) not found) e sem nenhuma linha desta branch envolvida:

Teste Branch 0037 main
credit-analysis-documents:366 — modo leitura ✘ ✘
isolamento-imobiliaria:76 — gestor vê as duas ✘ ✘
properties:156 — cadastrar imóvel ✘ ✘

Ressalva do método, e ela é contra a limpeza do resultado. O baseline reconstruiu só o web; a API continuou sendo a desta branch, porque reverter o banco exigiria migrations que só existem aqui. Nessa combinação mista, o admin-interface falhou sete vezes na main contra uma ou duas na branch — ruído da configuração, não medição. A leitura que se sustenta ali é negativa: a branch produz menos falhas de admin que a main sob a mesma API, então não há regressão introduzida aqui. Por que a main falha mais nessa combinação, este teste não responde, e não se inventa explicação.

O downgrade, exercitado e não só escrito

A spec 0028 pede que o downgrade seja exercitado. Foi: as três migrations desceram até r1t3v5y7b9c0 e voltaram a head, limpo, contra o PostgreSQL do dev-remote. O rename do enum volta com RENAME VALUE inverso; as duas tabelas são derrubadas.

Efeito colateral do seed (RN4): o seed_initial roda no onboarding, então tenants que já existiam não recebem o catálogo. Os sete do dev-remote foram semeados à mão pelo próprio serviço (idempotente). Em produção, o mesmo passo será necessário para qualquer garantidora anterior a esta spec — hoje, só a criada pelo seed do admin.