Pular para conteúdo

0039 — Emitir a garantia conferindo, e sem poder divergir da análise

Status CONCLUÍDA
Branch spec/0039-emissao-de-garantia-na-analise
Criada em 2026-08-17
Apps afetados api · web · e2e
Revisão do problema aprovada na rodada 3 (com decisões do usuário)
Revisão da experiência aprovada na rodada 7 (com decisões do usuário)
Revisão do detalhamento aprovada na rodada 4
Revisão da implementação api: rodada 2 · web: rodada 5 · e2e: 3 rodadas (4ª dispensada pelo usuário) · branch: 1 rodada, o Important corrigido

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

A spec tem três partes, escritas em ordem, e quatro portões — cada um revisado por um subagente de contexto limpo, em até 3 rodadas. A máquina de revisão é do plugin superpowers; a régua é a rubrica correspondente em .claude/rubrics/. Achados vêm classificados: qualquer Critical reprova, Important em aberto reprova salvo decisão registrada do usuário, e Minor vira pendência aceita.

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

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


Parte I — O problema

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

1. Dor

Quem emite uma garantia confirma no escuro, e o sistema aceita o que ele confirmar.

A emissão congela valores que não se corrigem: taxa, mensalidade, taxa de setup, total de entrada e o LMI ficam gravados na garantia. E não há segunda chance — há uma garantia por análise de crédito, com unicidade no banco, e o botão de emitir some assim que ela existe (inclusive depois de encerrada). Consertar um número errado exige refazer a análise de crédito inteira, com nova decisão do analista.

Esses números também saem do sistema: é com eles que a garantidora redige o termo enviado ao inquilino para assinar. Um valor errado não fica parado numa tela — vira documento assinado.

Duas coisas doem, e produzem efeitos diferentes:

A primeira: não dá para conferir sem desfazer. O modal de emissão não mostra de quem é a garantia, sobre qual imóvel, nem sobre qual valor a análise foi aprovada. Quem quer conferir precisa fechar o modal para reler a tela atrás dele — e fechar apaga o que já foi preenchido, porque o diálogo é desmontado ao fechar. O desenho cobra por conferir e não cobra nada por não conferir: de um lado, redigitar; do outro, confirmar de memória e olhar depois, com o valor já congelado. Não há operação para dizer qual caminho as pessoas tomam — o que se pode afirmar é qual dos dois o produto tornou barato.

A segunda: o formulário aceita divergir da análise aprovada. Início, fim e valor garantido vêm pré-preenchidos com dados da análise e são livremente editáveis. O efeito de cada um é diferente, e vale separar:

  • A vigência estende a exposição além do que foi decidido. Emitir 24 meses sobre uma análise aprovada para 12 dobra o tempo em que a garantidora responde pelo inquilino, com uma decisão de crédito que avaliou metade disso. E, dependendo dos parâmetros da garantidora, os meses também entram no cálculo da taxa de setup — quando a base é o total garantido —, então a divergência pode chegar ao dinheiro congelado. Nada recusa, nada avisa.
  • O valor garantido não move preço nenhum — e é isso que o torna traiçoeiro. Todo o cálculo sai do Pacote Locatício da análise, então o que se digita no campo é gravado como registro divergente e decorativo: a garantia passa a declarar um valor que o crédito não aprovou, sem que nada na tela ou no preço denuncie. O campo tem um caminho de fallback no código para análises sem o Pacote congelado, mas ele é letra morta na emissão: o snapshot é gravado na criação e na atualização da análise, e nenhuma das 3.327 análises do ambiente de desenvolvimento está sem ele — nem uma aprovada sequer.

Somadas: a pessoa não consegue conferir, e o sistema não confere por ela.

Um terceiro incômodo, de outra natureza: o botão que abre tudo isso usa o ícone de "+". É o sinal de acrescentar item a uma lista, e emitir garantia não é acrescentar um registro — é fechar um contrato irreversível. No desktop o rótulo "Emitir garantia" acompanha e salva a leitura; em 375px o rótulo some e sobra o "+" sozinho, que é onde a crítica é literal. Não causa erro, mas ensina a coisa errada sobre o peso da ação, no lugar exato onde o peso importa.

2. Quem sente

Quem emite hoje: ADMIN_TENANT (gestor, sócio) e OPERATOR — o papel que o sistema descreve como responsável por cadastros e pela condução das análises, e que ganhou a permissão de emitir na spec 0038. Para eles vale a dor da seção 1, com a mesma ressalva: o produto não cobra nada por confirmar sem conferir, e cobra redigitação de quem quer conferir.

O ADMIN_GLOBAL também emite, porque recebe todas as permissões — mas ele administra a plataforma, não o negócio do cliente, e não é ele quem sente esta dor.

ANALYST (analista de crédito) sente de outro jeito: ele decide e não conclui. Hoje ele não tem permissão de emitir. Decide o crédito, passa adiante, e quem emite é alguém que não participou da decisão — e que, por isso, precisa reconstruir o contexto alheio: abrir a análise que outro conduziu, ler tomador, imóvel e valores, e só então abrir um modal que não mostra nada disso. É a mesma dor da seção anterior, agravada por não ter sido quem decidiu.

Poderia-se argumentar que esse segundo emissor é um segundo par de olhos sobre um ato irreversível. Hoje ele não é: o modal não lhe mostra de quem é, qual imóvel nem sobre que valor, então não há o que ele confira ali. Decisão do usuário nesta spec: o analista passa a emitir — a conferência passa a estar na ferramenta, não numa segunda pessoa que também está no escuro. O como é assunto da Parte III.

É caminho obrigatório, não exceção. Toda análise aprovada que vira garantia passa por aqui: não existe outra porta de emissão no produto.

Não há volume a medir, e isso é limite declarado. O produto está em produção desde a spec 0028 e ainda não tem cliente — o ambiente subiu, a base instalada não. Não existe histórico de emissões de onde tirar frequência ou taxa de erro. O alcance se sustenta pelo desenho: é a única porta de emissão, o ato é irreversível sem refazer a análise, e o número congelado vira documento assinado.

A imobiliária (AGENCY_USER) não sente — não emite garantia, e a tela da análise não lhe oferece a ação.

3. A jornada de hoje

Gatilho: o analista aprova a análise de crédito. A emissão não muda o estado dela — ela chega em APPROVED pela decisão do analista, e continua APPROVED depois.

  1. O analista não emite: ele passa adiante. Sem permissão para concluir, a análise aprovada fica esperando que um gestor (ou o OPERATOR) a abra. Quem abrir não conduziu a análise e precisa reconstruir o contexto: ler quem é o tomador, qual o imóvel e quais os valores aprovados, antes de conseguir julgar se o que vai emitir está certo.
  2. Na tela da análise, clica em "Emitir garantia", no topo — o botão do ícone de "+".
  3. Abre um modal com três campos preenchidos: início e fim (copiados da análise) e valor garantido (preenchido com o aluguel mensal, não com o Pacote Locatício, que é a base de cálculo real). Há também um campo de notas, vazio.
  4. Abaixo dos campos, o bloco "O que será congelado" mostra taxa, mensalidade, taxa de setup e total de entrada — os números de preço. Ele entrou na spec 0038 e resolveu metade do problema: ninguém mais confirma sem ver quanto vai custar.
  5. O que ele ainda não vê é sobre o quê. Tomador, imóvel, número da análise e o valor que o crédito aprovou não estão no modal.
  6. Aqui a jornada quebra, de dois jeitos:
  7. Para conferir, fecha o modal. A tela atrás tem tudo — tomador, imóvel, resumo financeiro. Lê, e reabre. O modal reabre pré-preenchido de novo — o que é pior que reabrir vazio: as datas voltam ao padrão da análise e qualquer ajuste se perde, porque o componente é desmontado ao fechar. Conferir custa refazer.
  8. Ou não fecha. Confia no que lembra da tela que acabou de ler, confirma, e confere depois — na tela da garantia, com o valor já congelado.
  9. Confirmado, o sistema emite e o navegador vai direto para a tela da garantia. A conferência que sobrou acontece aqui, quando já não muda nada.
  10. Fora do sistema, logo depois: a garantidora redige o termo — que precisa do número da garantia e dos valores que só a emissão congela — e o envia ao inquilino para assinar. É o passo que a spec 0038 mapeou, e é o que transforma um número errado em documento assinado.

O que já se tentou.

  • A spec 0038 atacou esta mesma jornada e entregou o bloco de preço do passo 3. Antes dela, confirmava-se sem ver nenhum número calculado. Foi melhoria real, e é o que torna o problema restante visível: agora se vê o preço, e fica evidente que não se vê sobre o que ele incide.
  • A liberdade do campo de valor já foi questionada — na própria 0038, com decisão registrada. A revisão daquela fase levou a escolha ao usuário e o veredicto foi "o Pacote manda"; a consequência anotada lá foi que o campo não recalcula o resumo, e que a tela precisaria dizer isso ou o campo sairia. Escolheu-se dizer: hoje o campo traz o aviso "A base de cálculo é o Pacote Locatício da análise. Este valor não altera o resumo acima." Este é o contorno em uso, e ele já nomeia a alternativa que faltou executar.
  • Nada foi tentado sobre a vigência. Os campos nasceram editáveis na spec 0027, quando o contrato virou garantia e a emissão era um cadastro como outro qualquer, e a liberdade deles nunca foi revista.

Não foi identificado contorno fora do sistema — e, sem operação, isso é leitura do desenho e não observação: o processo cabe inteiro nestas duas telas, e é a lacuna entre elas que dói.

4. Resultado esperado

Some o passo 5. Quem emite consegue conferir dentro do modal, sem fechá-lo e sem redigitar. A conferência deixa de ser "depois, na garantia emitida" e passa a acontecer antes de confirmar — o único momento em que ela ainda muda alguma coisa.

Some o passo 0. Quem decide o crédito consegue concluir a emissão, sem passar o bastão para alguém que vai ter de reconstruir o contexto do zero.

A garantia emitida deixa de poder divergir da análise aprovada. Duas decisões do usuário fixam o que "não divergir" quer dizer:

  • O valor da garantia passa a ser o Pacote Locatício da análise — a mesma grandeza que já é base de todo o cálculo — em vez de um número digitado. Hoje o campo abre com o aluguel mensal e aceita qualquer coisa por cima, então o valor declarado e a base do cálculo são duas coisas diferentes.
  • A duração respeita o prazo da locação registrado na análise — o número de meses que a análise declara e sobre o qual o crédito foi decidido. (Não é um campo do parecer: o analista arbitra taxa e setup, não prazo.) O início continua livre — a locação pode começar depois do previsto —; o que se limita é por quanto tempo a garantidora responde. Duração menor continua valendo; maior, não.

O botão para de dizer "adicionar". Quem chega na tela lê o peso do ato antes de clicar, em vez de ler o ícone de acrescentar item a uma lista.

Como se sabe que resolveu, sem métrica instrumentada:

  • Abrir o modal e responder, sem sair dele, três perguntas que hoje exigem fechá-lo: de quem é?, qual imóvel?, sobre qual valor?
  • Tentar emitir com duração maior que a registrada na análise e ser recusado, com o limite dito por extenso.
  • Comparar o valor gravado na garantia com o Pacote Locatício da análise, em qualquer emissão, e encontrar o mesmo número.
  • Um analista de crédito consegue emitir a garantia da análise que ele mesmo decidiu.
  • Olhar a tela em 375px, onde só o ícone aparece, e não confundir a ação com "adicionar".

5. Fora de escopo

  • Reabrir a decisão de crédito. Se o valor aprovado está errado, o conserto é na análise, não na emissão. Esta spec não cria caminho para corrigir crédito a partir da garantia.
  • Alterar valores depois de emitida. Continua valendo a 0038: uma análise gera uma garantia só, e o que congelou não se edita. Não entra edição de garantia vigente, nem reemissão.
  • Emissão em lote, ou a partir da lista de análises. A porta continua sendo uma só.
  • Mudar o cálculo. Taxa, mensalidade, setup e LMI seguem exatamente como a 0038 os congela. Esta spec muda de onde vem o valor garantido e o que a tela mostra, não como os preços são computados.
  • O resumo financeiro da própria análise (spec 0035) não muda. O que se discute aqui é o que aparece dentro do modal.
  • Preencher notas automaticamente. O campo continua livre e opcional.
  • Rever os ícones das outras ações da tela. Só o de emitir garantia entra; os demais ficam como estão, mesmo que algum mereça a mesma crítica.
  • Mexer nas datas previstas de locação registradas na análise, hoje informativas. Elas continuam como estão — a régua da vigência passa a ser o prazo em meses da análise, não elas.

6. Revisão do problema

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

Rodada Critical Important Minor Veredicto
1 2 5 3 reprovada — laço interrompido por [PERGUNTAR AO USUÁRIO]
2 0 3 6 reprovada — laço interrompido por [PERGUNTAR AO USUÁRIO]
3 0 0 7 aprovada

Resultado: aprovada na rodada 3, com duas decisões do usuário registradas.

As duas rodadas reprovadas cobraram fatos, não redação. A rodada 1 pegou dois Critical que eram afirmações minhas apresentadas como verdade: a jornada narrada com o ANALYST como protagonista, quando ele não tem permissão de emitir (e há teste garantindo o 403), e um caminho de conserto — "encerrar e emitir de novo" — que a própria spec 0038 exclui, com unicidade no banco. A rodada 2 pegou a contradição entre dizer que o emissor de hoje é um "segundo par de olhos" e propor eliminá-lo na mesma spec.

Decisões do usuário neste portão:

Decisão Escolha
Quem emite O analista passa a emitir — o segundo par de olhos de hoje é ficção, porque o modal não lhe mostra nada para conferir
O que trava hoje Retrabalho de contexto: quem emite não conduziu a análise e reconstrói tudo antes de julgar
Valor da garantia O Pacote Locatício da análise, não o aluguel mensal digitado
Régua da vigência O prazo em meses registrado na análise; início livre, duração menor aceita, maior não. Complementada no portão da experiência: mínimo de 1 mês, e início pode ser retroativo sem limite
Mobile Emite-se em 375px. A forma foi revista no portão seguinte: decidiu-se colapsar o resumo aqui, e depois — ao descobrir que o painel de identificação não existia quando essa escolha foi feita — decidiu-se não colapsar nada. Vale a segunda; o comportamento decidido está em 7.2

Pendências aceitas:

  • A Parte I registra decisões que são de Parte II/III — quem emite, de onde vem o valor, qual é a régua do prazo. O revisor apontou, e está certo pelo cabeçalho da parte. Ficam onde estão porque são respostas do usuário dadas neste portão, e movê-las para depois esconderia de quem lê a Parte I por que ela mudou de forma entre as rodadas. O que não entrou foi mecanismo: nenhum nome de coluna, endpoint ou componente aparece na Parte I.
  • A justificativa para o analista emitir tem prazo de validade, e vale dizê-lo. Ela se apoia em o segundo emissor não conseguir conferir nada hoje. Depois desta feature ele conseguiria — o modal passa a mostrar tomador, imóvel e valor. Ou seja: a spec remove a segregação de funções no mesmo movimento em que cria a condição que a tornaria útil. A decisão é do usuário e está registrada; fica anotado para quem reabrir o assunto não achar que ninguém percebeu.

Pendências aceitas: o que a revisão ainda apontou e se decidiu não resolver, com o porquê. Vazio se não houver.


Parte II — A experiência

Não preencher antes da revisão da seção 6 estar aprovada.

7. Jornada pretendida

Ponto de entrada: o mesmo de hoje — a tela da análise de crédito aprovada, botão no topo. Não se cria porta nova; a que existe passa a dizer melhor o que faz e a mostrar o que hoje esconde.

Jornada, caso feliz:

  1. O analista aprova a análise — ou abre uma já aprovada — e clica em "Emitir garantia". Ele mesmo, sem passar o bastão.
  2. O modal abre mostrando sobre o que é a garantia e quanto ela vai custar. Ele lê sem sair dali.
  3. Informa quando a locação começa. O fim da vigência aparece sozinho, calculado com o prazo que a análise aprovou.
  4. Escreve uma nota, se quiser.
  5. Confirma. Vai para a tela da garantia recém-emitida.

Custo de uso: uma data. É o passo 3, e é o único. O fim é derivado; o valor não se digita; o resto é leitura.

Por que uma e não zero. As datas de locação da análise são opcionais e quase nunca preenchidas: das 525 análises aprovadas do ambiente de desenvolvimento, 511 não têm data nenhuma. Preencher a vigência a partir delas — que é o que o modal tenta hoje — funciona em 3% dos casos e deixa os outros 97% com dois campos vazios. O que a análise sempre tem é o prazo em meses, obrigatório desde sempre. Então é dele que se deriva, e o que sobra para a pessoa é dizer o dia em que começa.

O que o sistema já sabe e por isso não pergunta: tomador, imóvel, número da análise, Pacote Locatício, prazo, taxa aprovada e todos os valores derivados. E o valor da garantia, que deixa de ser pergunta e vira informação — hoje ele é um campo que aceita qualquer número e não muda preço nenhum.

O ícone: certificate, e não só no botão.

O vocabulário de ícones está inconsistente hoje, e trocar só o botão manteria a confusão: certificate significa Garantias no menu — que alimenta tanto a barra lateral do desktop quanto a gaveta do celular —, mas a aba inferior do celular, o cabeçalho da lista, o vazio da lista e o cabeçalho do detalhe usam file-text, que é também o ícone de Análise de Crédito. O conflito já existe hoje dentro do mesmo viewport: em 375px, a gaveta mostra certificate e a barra de abas mostra file-text para o mesmo módulo. Trocar só o botão acrescentaria um terceiro símbolo à mesma tela.

E não é símbolo novo: o app já declara certificate como o ícone de garantias no seu mapa normativo de ícones, e o ui-patterns.md já exige que o ícone do cabeçalho de página seja o mesmo do menu. Os cinco pontos abaixo violam essas duas regras hoje — a decisão realinha, não inventa.

Decisão do usuário: certificate passa a significar garantia nos cinco lugares — o botão de emitir, a aba do celular, o cabeçalho e o vazio da lista, e o cabeçalho do detalhe. file-text volta a ser só análise de crédito. São cinco linhas, e é o que faz a troca do botão significar alguma coisa.

Isto é mais escopo do que a Parte I anunciava, e vale dizer em vez de deixar passar: lá só o botão entrava, e a seção 5 excluiu "rever os ícones das outras ações desta tela". As quatro linhas a mais são de outro módulo, e entram porque trocar só o botão deixaria o símbolo novo brigando com o antigo no mesmo viewport. É decisão registrada do usuário, não escopo que cresceu sozinho.

Existe um sexto ponto, e ele fica de fora: o meta.icon do recurso de garantias na configuração do Refine. Ele não é renderizado — o app usa menu e cabeçalho próprios —, então trocá-lo não muda nada que alguém veja.

E file-text continua existindo com outro sentido, legitimamente: é o ícone de documento genérico no painel de anexos, e o botão "Ver análise de crédito" dentro da tela da garantia segue usando-o — ali ele aponta mesmo para uma análise. A afirmação desta seção é sobre a identidade do módulo Garantias, não sobre cada ocorrência da string no código.

7.1 Desktop

O botão continua onde está, no topo da tela da análise, como ação principal — muda o ícone e o que acontece ao clicar.

O modal passa a ter duas metades, nesta ordem: o que se confere e o que se preenche.

Metade de cima — "Confira antes de emitir". Um painel de leitura, sempre visível, com dois rótulos escritos na tela — A quem se refere e O que será congelado —, no mesmo estilo de rótulo de seção que o app usa nos painéis de detalhe:

  • A quem se refere — tomador (nome e documento), imóvel (endereço) e o número da análise. Só o tomador principal, mesmo quando a análise tem co-responsáveis — decisão do usuário: a garantia é do inquilino com a garantidora, e quem quiser a composição inteira do crédito abre a análise, que é a tela atrás do modal. Responde duas das três perguntas que hoje obrigam a fechar o modal; a terceira — sobre qual valor? — é o valor da garantia, que está na fileira seguinte.
  • O que será congelado — valor da garantia, LMI, custos de saída, taxa, mensalidade, taxa de setup e total de entrada. Os quatro últimos já existem hoje; os três primeiros são o que entra.

LMI é o limite máximo de indenização — o teto do que a garantidora paga num sinistro — e custos de saída é a cobertura de reparos, multa e taxas de desligamento. Os dois derivam do Pacote por multiplicadores da garantidora, os dois ficam gravados sem correção, e um painel chamado "o que será congelado" que os omitisse contradiria a tese desta spec. Aparecem junto do valor da garantia, não junto dos valores de preço: são naturezas diferentes — estes são o que se cobre, aqueles são o que se cobra.

Multiplicador zero quer dizer coisas opostas nos dois, e o painel escreve o que o app já escreve:

  • LMI zero é cobertura sem teto → "Ilimitado".
  • Custos de saída zero é ausência de cobertura → "Não cobre", em cinza.

Não é detalhe de redação: trocar um pelo outro faria o painel anunciar cobertura ilimitada de reparos, multa e taxas exatamente quando a garantidora não cobre nada disso — o oposto do número que vai ser gravado, num painel cuja única razão de existir é que isso não aconteça. As duas frases são as que o resumo financeiro da análise já usa, lado a lado.

O número exibido tem de ser o que vai ser gravado. Vale para os sete, e o LMI e os custos de saída são os que mais pedem cuidado: eles não são copiados da análise na emissão — são recalculados com os multiplicadores vigentes da garantidora. Se um desses parâmetros mudou entre a análise e a emissão, o valor que a análise guarda e o que a garantia vai congelar são diferentes, e exibir o primeiro reencena o defeito que este painel existe para impedir. A Parte III escolhe a fonte; a Parte II exige a propriedade.

Consequência de estado, e ela é da Parte II: por não estarem no que a tela já tem, esses dois números também esperam — como os de preço. Enquanto não chegam, suas linhas mostram "Calculando…"; se a busca falhar, o painel diz isso e o confirmar não habilita. Congelar cobertura que ninguém viu é o mesmo defeito que congelar preço que ninguém viu.

O valor da garantia é o Pacote Locatício da análise — não são dois números, e por isso não aparecem duas vezes. O rótulo diz "Valor da garantia" porque é o que fica gravado; o painel esclarece, em texto pequeno, que ele é o Pacote Locatício (aluguel + condomínio + IPTU), que é de onde saem todos os outros valores. Aparece como dado, não como campo: ele deixa de ser digitável porque é derivado, e um campo desabilitado pareceria defeito. O valor da garantia aparece assim que o modal abre — a tela já o tem. O LMI e os custos de saída não: precisam ser buscados (ver a nota acima), e até chegarem mostram "Calculando…". Nenhum dos três depende da vigência; os quatro valores de preço, sim. Quem precisa de outro valor precisa de outra análise, e isso não se resolve neste modal.

Metade de baixo — o formulário, com o que de fato se decide aqui:

  • Início da vigência — o único campo que a pessoa preenche. Abre vazio, e não com uma data sugerida: emitir com a data errada por inércia é mais caro que digitar oito dígitos. Pode ser retroativo, sem limite — a locação é um fato do mundo, e pode ter começado antes de alguém emitir a garantia. Retroativo não é erro e não gera aviso. No futuro também é livre, pela mesma razão: emite-se hoje a garantia de uma locação que começa no mês que vem. Não há limite nos dois sentidos.
  • Fim da vigência — presente desde o início, desabilitado e vazio enquanto não houver data de início: some o salto de dois para três campos, e o campo desabilitado aqui não parece defeito, porque a pessoa acabou de ver que falta preencher o de cima. Informado o início, ele se preenche com o prazo registrado na análise e fica editável.
  • Notas — livre e opcional.

O fim é editável só entre o piso e o teto. O teto é o prazo registrado na análise — o número de meses sobre o qual o crédito foi decidido, e que não é campo do parecer. O piso é um mês — abaixo disso a cobrança arredonda para um mês inteiro de qualquer forma, e uma garantia de cinco dias custaria como uma de trinta. O calendário só oferece o intervalo válido; digitar fora dele mostra o erro no campo, com os dois limites. Quando a análise registra um mês, o piso encosta no teto e sobra um único fim possível — o campo se preenche com ele e não há o que escolher. Não acontece hoje (todas as análises aprovadas do ambiente têm 12 meses), mas o cadastro aceita, e um calendário com um dia selecionável não pode parecer defeito.

Mudar o início depois de já ter encurtado o fim recalcula o fim, voltando ao prazo registrado na análise. A duração escolhida antes não se preserva: ela foi decidida contra outra data de início, e mantê-la em silêncio produziria uma vigência que ninguém escolheu. A pessoa vê o campo mudar.

Os dois campos de data passam a usar o seletor de data do app, com máscara DD/MM/AAAA e calendário — o mesmo do formulário da análise de crédito, inclusive no campo "Início da locação". Hoje o modal usa o seletor nativo do navegador, que mostra a data no formato do sistema operacional e destoa do resto do produto.

Isto também é escopo além do que a Parte I listou, e entra declarado, como os ícones. A Parte I registra três dores — conferir sem desfazer, divergir da análise, e o "+" — e a troca do seletor não é nenhuma delas. Ela entra por consequência: o comportamento que esta seção exige (calendário limitado ao intervalo válido, fim derivado, máscara DD/MM/AAAA) não se obtém do seletor nativo. Trocar é o meio, não um fim que a spec descobriu no caminho.

A conta que deriva o fim e a conta que o valida têm de ser a mesma. Não é detalhe de implementação: se elas divergirem, a tela oferece uma data e o sistema a recusa, que é pior do que não oferecer nada. O risco é concreto — o produto mede duração em blocos de 30 dias, então doze meses de calendário (365 dias) contam como treze. A Parte III precisa escolher uma régua e usá-la nos dois lados; a Parte II só exige que o que a pessoa lê nunca seja recusado.

O texto abaixo do campo de fim diz o limite por extenso — a duração aprovada e as duas datas, a mínima e a máxima — e se atualiza quando o início muda. Antes de haver início, o campo está desabilitado e o texto diz apenas a duração aprovada — "a análise aprovou 12 meses" —, sem datas, porque não há de onde contá-las. A data manda sobre o número: se a régua escolhida na Parte III fizer "12 meses" terminar num dia que não é o mesmo dia do mês doze meses depois, é a data que a pessoa lê e é ela que o sistema tem de aceitar. Dizer "12 meses" ao lado de uma data que não fecha doze meses de calendário é confuso, mas é honesto; oferecer uma data e recusá-la, não.

Tudo cabe sem rolar numa janela de altura comum: são três blocos curtos — identificação com três itens, valores com seis, e o formulário com três campos —, num diálogo estreito onde cada bloco ocupa poucas linhas. Se em alguma altura não couber, o que rola é o corpo do diálogo; o rodapé com as ações fica ancorado, como já é hoje.

Data incompleta ou inexistente — oito dígitos pela metade, ou 31/02 — deixa o campo em erro e o confirmar desabilitado, como qualquer campo obrigatório mal preenchido. Enquanto o início não for uma data válida, o fim continua desabilitado e o preço não é calculado.

Ações no rodapé: Cancelar e Emitir garantia. O confirmar fica desabilitado enquanto faltar um início válido ou enquanto os valores de preço não puderem ser calculados — com o motivo à vista, não em cinza mudo.

7.2 Mobile (375px)

Emite-se do celular — decisão registrada do usuário. Não é tela só de escritório.

O modal já é tela cheia em 375px, com o rodapé ancorado, e continua assim. O que muda:

  • Nada colapsa, e a ordem se inverte: identificação → formulário → valores → notas. No desktop os valores vêm antes do formulário, porque tudo cabe na tela ao mesmo tempo; em 375px isso os colocaria acima do campo que os produz, fora de vista no momento em que passam a existir. Com a ordem invertida, os números nascem logo abaixo da data que os calculou — a pessoa digita e lê, sem rolar de volta.

Isto é o defeito da Parte I tentando voltar por outra porta, e a inversão sozinha não o fecha — vale dizer com todas as letras. Sem inverter, os valores se calculavam acima do campo, fora de vista. Invertendo, eles nascem logo abaixo dele. Nos dois casos o confirmar habilita no rodapé ancorado, ao alcance do polegar, e nada obriga a pessoa a rolar até os números. O que a inversão entrega é que eles passam a estar no caminho de leitura em vez de fora dele. É melhor, e não é garantia.

  • O que fecha o buraco é o rodapé: em 375px, acima do Emitir garantia, o rodapé ancorado mostra os dois números que decidem — total de entrada e mensalidade. Quem confirma lê, porque está no mesmo lugar onde toca. O painel completo com os sete valores continua acima, para quem quiser o detalhe; o rodapé carrega o que não pode passar batido.

E ele é também onde o motivo aparece quando não há número. Ao abrir, antes da data, a linha do rodapé diz "Informe o início da vigência" — é o motivo de o confirmar estar desabilitado, e no celular ele precisa estar junto do botão, porque o painel que o explicaria está a duas telas de rolagem. Enquanto recalcula, diz "Calculando…". Se o cálculo falhar, diz que não foi possível calcular e leva ao painel, onde está a saída (link para os parâmetros, pedir a um administrador, ou tentar de novo).

Erro vence a frase de espera. Numa garantidora sem parâmetros, os dois motivos coexistem ao abrir — falta a data e o cálculo já é impossível. A linha mostra o erro, porque a regra dela é dizer por que o confirmar está cinza, e informar a data não destravaria nada.

A linha tem altura fixa nos quatro estados — número, frase de espera, "Calculando…" e erro ocupam a mesma faixa. Um rodapé que muda de altura a cada recálculo empurra o conteúdo sob o dedo de quem está digitando.

Não se trava o botão para forçar a leitura. Um confirmar cinza porque a pessoa ainda não rolou até certa altura é motivo invisível, que é o que esta spec condena em todos os outros pontos. Decisão do usuário.

Decisão do usuário, e ela reverte a que estava registrada na seção 6. Lá se decidiu "resumo colapsado" — mas naquele momento o único bloco era o de preço, o painel de identificação ainda não existia, e colapsá-lo esconderia justamente as três perguntas que a feature existe para responder. Revista com o desenho à vista, a escolha passou a ser não colapsar nada.

O custo é real e não se disfarça: são cerca de duas telas de rolagem, e a pessoa rola de qualquer jeito, porque o campo de início está depois delas e o confirmar fica desabilitado até ele ser preenchido. O rodapé ancorado não economiza a rolagem — ele só garante que, depois de preencher, o botão não precise ser procurado. O que se ganha em troca é que quem e qual imóvel aparecem assim que o modal abre, sem exigir um toque, e que some um estado de aberto/fechado do desenho e dos testes. A terceira pergunta — sobre qual valor? — fica no bloco de valores, depois do formulário: é o preço de inverter a ordem, e é o que o resumo do rodapé compensa. - Os títulos acompanham a inversão. O painel deixa de ser um bloco só chamado "Confira antes de emitir": em 375px a identificação abre a tela sem título — é o cabeçalho do próprio modal que já diz o que é —, e o bloco de valores, agora depois do formulário, leva o título "O que será congelado", que é o que ele de fato é. - Os sete valores empilham em coluna única, rótulo e número em linhas próprias — não em duas colunas. Valores em BRL com rótulos longos ("Total de entrada", "Taxa de setup") não cabem lado a lado em 375px sem encurtar um dos dois, e encurtar número é o que não se faz. São sete linhas quando há sete números: ao abrir, os quatro de preço ainda são a frase que pede a data, e o bloco tem três linhas e um aviso. - O endereço do imóvel quebra em linhas em vez de encurtar — encurtar daria duas verdades diferentes para a mesma garantia, dependendo do aparelho. - A data se escolhe pelos dois caminhos: digitando os oito dígitos com máscara, ou pelo calendário, que em 375px abre como diálogo próprio sobre o modal. Os limites do fim são impostos no calendário — só os dias entre o mínimo (um mês) e o máximo (o prazo da análise) são selecionáveis, os dois lados —, então quem usa o calendário não chega a errar. - Todo campo rola para ficar acima do teclado ao receber foco — não só as notas. Com o teclado aberto sobram cerca de 180px, e o campo que mais importa aqui é o início da vigência: ele é o único obrigatório, fica depois de duas telas de rolagem, e é digitado a oito dígitos. Um campo de data sob o teclado é um campo que não se confere enquanto se escreve. Vale igual para o fim e para as notas. - As notas ficam por último, encostadas no rodapé ancorado — é o único campo de digitação longa, e o último a ser preenchido. - Nada some no celular. As mesmas informações do desktop estão lá. - A tela não rola na horizontal.

No botão que abre tudo isso, em 375px o cabeçalho da página troca rótulo por ícone: some o texto "Emitir garantia" e sobra o símbolo. É por isso que o ícone importa mais aqui do que no desktop — é a única coisa que a pessoa lê, e é o argumento que sustenta corrigir os cinco lugares em vez de um.

7.3 Estados

Ao abrir, antes de qualquer digitação. A identificação aparece na hora — tomador, imóvel e número já estão na tela. O valor da garantia também: é o Pacote Locatício da análise, que a tela tem em mãos.

LMI e custos de saída não aparecem na hora, e é consequência direta da exigência de 7.1: eles são recalculados com os multiplicadores vigentes, então não são o que a análise guarda. Suas linhas mostram "Calculando…" até chegarem. Diferente dos valores de preço, eles não dependem da vigência — chegam sem esperar data nenhuma.

Os quatro valores de preço — taxa, mensalidade, taxa de setup e total de entrada — esperam o início da vigência, porque a duração entra no cálculo: até lá ele diz "Informe o início da vigência para ver os valores", não um espaço vazio nem um "calculando" eterno. O confirmar fica desabilitado.

Calculando o preço. Informado o início, o bloco mostra "Calculando…" enquanto a conta não volta. E recalcula sempre que a vigência muda — inclusive quando o fim é encurtado, que é o caso que mais importa: dependendo dos parâmetros da garantidora, a duração entra no cálculo da taxa de setup (quando a base é o total garantido), e aí encurtar de 12 para 6 meses muda o total de entrada. Recalcula sempre, e não só quando entraria — a tela não tem por que saber qual é a base configurada. Enquanto recalcula, os quatro valores de preço saem da tela — o valor da garantia, o LMI e os custos de saída ficam, porque não mudam com a vigência — e o confirmar desabilita. Deixá-los à vista seria mostrar o preço de 12 meses num modal que vai congelar o de 6 — exatamente o defeito contra o qual a spec 0038 construiu este bloco.

Preço não pôde ser calculado — e a saída depende do motivo. O cálculo é uma chamada ao servidor, e ela falha por razões de naturezas diferentes:

  • Parâmetros da garantidora ausentes ou fora de faixa. O painel diz isso, e a saída depende de quem olha: quem administra a garantidora recebe um link para os parâmetros; quem não administra lê que precisa pedir a um administrador.
  • A chamada não completou — rede caída ou erro de servidor. Aqui a saída é tentar de novo, e o painel oferece isso. (Sessão expirada não chega aqui: o app renova ou manda para o login antes de a tela ver o erro.)

A mesma bifurcação vale para o LMI e os custos de saída — e eles a alcançam antes, porque não esperam a data. Numa garantidora sem parâmetros, é pelo LMI que a pessoa descobre o problema, com o modal recém-aberto e o bloco de preço ainda pedindo a vigência. As duas linhas dizem o motivo pela mesma régua: parâmetro ausente leva ao link ou ao "peça a um administrador"; chamada que não completou oferece tentar de novo. Oferecer "tentar de novo" para parâmetro ausente é mandar repetir o que nunca vai funcionar — e mandar revisar os parâmetros porque a rede piscou é a mesma falha ao contrário.

Nos dois, o confirmar segue desabilitado com o motivo lido, nunca em cinza mudo.

Existe uma segunda causa no sistema — análise sem o Pacote Locatício congelado — e ela é letra morta: nenhuma das 3.327 análises do ambiente está sem ele, e o valor é gravado na criação e na atualização. Hoje a saída para esse caso era digitar o valor à mão, e esta spec remove esse campo. Se ele ocorrer, a emissão fica bloqueada com o motivo dito — "esta análise não tem Pacote Locatício registrado" — e o caminho é a análise, não este modal. Registrar isso é o que impede alguém de reintroduzir o campo depois para "resolver" o caso.

Vigência fora do intervalo válido. Pelo calendário não acontece: só os dias entre o mínimo e o máximo são selecionáveis. Digitando, o campo Fim fica em erro — "a vigência tem de terminar entre e " — e o confirmar desabilita. Corrigir a data limpa o erro na hora. Vale para os dois lados: menos de um mês recusa igual a mais que o prazo da análise.

Início no passado não é estado de erro. Não há aviso, não há confirmação extra: a locação pode ter começado antes da emissão, e isso é o mundo, não engano. Uma garantia cuja vigência inteira já passou nasce mesmo assim — ela nasce aguardando assinatura, e a spec 0038 já cuida de recusar a ativação fora do período.

Emitindo. Entre confirmar e chegar na garantia, o botão vira "Emitindo…" e fica desabilitado, junto do Cancelar. Num ato irreversível e sem desfazer, é o que impede o duplo toque virar duas tentativas.

Falha ao emitir. O erro aparece dentro do modal, acima do formulário — nunca no corpo da página, que ficaria atrás do diálogo. No celular, a tela rola até ele: o confirmar é tocado no rodapé ancorado, com duas telas de conteúdo acima, e uma mensagem renderizada "acima do formulário" nasceria fora de vista — o botão voltaria de "Emitindo…" sem explicação visível. É o mesmo tratamento que o app dá ao primeiro campo com erro. Vale também para o caso da garantia que passou a existir: o link para vê-la não serve se ela não o alcança. O modal continua aberto e o que foi preenchido permanece: a data informada e as notas escritas não se perdem por causa de uma rede que caiu. É a inversão do defeito narrado na Parte I.

Sem permissão. Quem não pode emitir não vê o botão — a tela não oferece o que vai ser negado. Não há estado de erro a desenhar, porque não há caminho até ele.

A análise já tem garantia. O botão não aparece; no lugar dele, a ação de ver a garantia existente. É o comportamento de hoje e não muda.

A garantia passou a existir com o modal aberto. Outra aba, ou outra pessoa, emitiu primeiro. O confirmar falha, e a saída aqui não é tentar de novo — tentar de novo falha sempre, porque há uma garantia por análise. O modal diz que a garantia já foi emitida e oferece vê-la. É o único caso de falha em que a saída não é repetir.

Vazio. Não se aplica: o modal nunca abre sem dados — nasce de uma análise aprovada, que por definição tem tomador, imóvel, prazo e valores.

8. Revisão da experiência

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

Rodada Critical Important Minor Veredicto
1 0 7 5 reprovada — laço interrompido por [PERGUNTAR AO USUÁRIO]
2 1 3 9 reprovada — laço interrompido por [PERGUNTAR AO USUÁRIO]
3 0 2 7 reprovada — laço interrompido por [PERGUNTAR AO USUÁRIO]
4 0 2 8 reprovada — laço interrompido por [PERGUNTAR AO USUÁRIO]
5 0 4 5 reprovada — laço interrompido por [PERGUNTAR AO USUÁRIO]
6 0 2 3 reprovada
7 0 0 5 aprovada

Resultado: aprovada na rodada 7.

Os cinco Minor da rodada 7 foram corrigidos, não descartados: o LMI deixou de ser descrito como "aparece na hora" (ele é buscado, e o critério de aceite tinha copiado a frase errada); a contagem de valores passou de seis para sete; o rodapé do celular ganhou regra de precedência (erro vence a frase que pede a data); o painel passou a dizer que mostra só o tomador principal, por decisão do usuário; e ficou registrado que o calendário escolhe a variante por tipo de ponteiro, não por largura — o e2e da CA22 precisa emular toque, ou mede o desktop achando que mede o celular.

Pendências aceitas: nenhuma.


Parte III — Detalhamento

Não preencher antes das revisões das seções 6 e 8 estarem aprovadas.

9. Regras de negócio

RN1 — O valor da garantia é o Pacote Locatício da análise. A emissão grava, em Guarantee.guaranteed_value, o financial_rental_package da análise de crédito. O cliente não informa esse valor.

A coluna passa a ter dois sentidos, e não haverá backfill. As garantias emitidas antes desta spec guardam o aluguel mensal; as novas guardarão o Pacote Locatício. A lista ordena por essa coluna, então garantias antigas e novas ficam lado a lado com grandezas diferentes. Não se corrige: não há base instalada — o produto não tem cliente, e o que existe é dado de teste. Registrado para que a divergência não seja lida depois como defeito.

RN2 — guaranteed_value sai do corpo de POST /guarantees — e só dele. No simulate ele permanece: é o que sustenta a simulação sem análise, que a imobiliária e a garantidora usam para estimar antes de existir crédito aprovado. O que deixa de existir é o campo na emissão.

Enviado no create, é ignorado em silêncio — não é 422: quebrar o contrato para um cliente que já não deveria mandá-lo transforma um campo obsoleto em erro de integração, e o único cliente é o apps/web, versionado junto.

RN3 — Análise sem Pacote Locatício não emite. Se financial_rental_package for nulo, a emissão responde 422 NO_CALCULATION_BASE. É o caminho que hoje é coberto pelo valor digitado — e que a RN2 fecha. Nenhuma das 3.327 análises do ambiente está nesse estado, e o snapshot é gravado na criação e na atualização da análise; a regra existe para que a remoção do campo não crie um buraco silencioso.

RN3a — Garantidora sem parâmetros não emite. Responde 422 SETTINGS_NOT_FOUND. Hoje o create embrulha o cálculo num if settings: sem else, e uma garantidora sem parâmetros emite uma garantia com LMI, mensalidade e setup nulos — uma garantia que não cobra nem cobre nada, irreversível. Não é regra que esta spec inventa por gosto: a RN10 exige que os valores exibidos sejam os gravados, e sem parâmetros não há valor nenhum a exibir.

RN4 — A duração da vigência não excede o prazo registrado na análise. Emitir com duração maior que credit_analysis.guarantee_months responde 422 GUARANTEE_PERIOD_EXCEEDS_ANALYSIS, com a duração aprovada e a duração pedida na mensagem.

RN5 — A duração mínima é um mês de calendário. Abaixo disso, 422 GUARANTEE_PERIOD_TOO_SHORT. Uma garantia de cinco dias custaria como uma de um mês, porque a cobrança arredonda para cima de qualquer forma.

RN6 — A duração passa a ser contada em meses de calendário, e a fórmula é esta.

Tudo se apoia numa operação só, somar_meses(data, k) — "a mesma data, k meses depois":

  • Dia inexistente no mês de destino grampeia no último dia do mês. somar_meses(31/12/2026, 2) é 28/02/2027, não 03/03/2027. É o que uma pessoa entende por "dois meses depois", e é o oposto do transbordo que o Date do JavaScript faz por padrão — a cópia do web não pode usar setMonth cru.

Sobre ela:

  • O último dia coberto por k meses a partir de início:
alvo = somar_meses(início, k)
fim  = alvo − 1 dia,  se alvo cair no mesmo dia do mês que o início
       alvo,          se o grampeamento mudou o dia

De 01/09/2026, doze meses terminam em 31/08/2027. De 31/12/2026, dois meses terminam em 28/02/2027.

As duas linhas existem porque nenhuma sozinha acerta. alvo − 1 dia é o caso normal — "até a véspera do mesmo dia do mês" —, mas quando o dia do início não existe no mês de destino o grampeamento já pousou no último dia do mês, e subtrair mais um descobriria o último dia da locação: de 31/12/2026, dois meses dariam 27/02/2027. Subtrair antes de somar conserta esse caso e estraga um maior: de 01/03/2027, um mês daria 28/03 em vez de 31/03, porque o dia anterior ao início é o fim de um mês curto. O ramo do grampeamento é o que fecha os dois.

Casos que a implementação precisa acertar, nos três lugares: 01/09+12 → 31/08, 01/03+1 → 31/03, 01/03+12 → 29/02 (bissexto), 01/05+6 → 31/10, 31/12+2 → 28/02, 31/01+1 → 28/02.

  • A duração de uma vigência, para precificar, é o menor k tal que fim ≤ último dia coberto por k. Mês parcial arredonda para cima: 01/09/2026 a 15/03/2027 são 7 meses.

RN4 e RN5 se validam por comparação de datas, não pelo k — e é isso que faz a RN5 existir de verdade:

  • Teto (RN4): fim ≤ último dia coberto por prazo_da_análise.
  • Piso (RN5): fim ≥ último dia coberto por 1.

Por que não pelo k. Qualquer contagem inteira que arredonde mês parcial para cima devolve 1 para uma vigência de vinte dias — então k < 1 nunca dispara, e a RN5 existiria no papel sem existir no código. A comparação de datas é o que a torna verificável, e é também o que a Parte II já pressupõe ao dizer que, numa análise de um mês, "o piso encosta no teto e sobra um único fim possível".

A tela usa exatamente estas fórmulas para derivar o fim (o último dia coberto pelo prazo da análise), para os limites do calendário e para o texto sob o campo. É o que garante que a data oferecida nunca seja recusada.

Isto muda a precificação da spec 0038, e é decisão do usuário. Pela conta antiga, doze meses de calendário (365 dias) davam 13 blocos — então nenhuma vigência de um ano seria emissível sob a RN4, e o exemplo mais óbvio do contrato seria recusado. As alternativas eram piores: manter a conta antiga faria "12 meses" cobrir 360 dias, deixando cinco dias da locação anual descobertos; validar por calendário e precificar por blocos faria quem aprovou 12 pagar setup de 13, que é a família de defeito que a spec 0038 existe para impedir.

O que muda na prática: a taxa de setup, e só quando a base configurada é o total garantido — nos demais parâmetros o número de meses não entra na conta. Não há base instalada a preservar (o produto não tem cliente), então as garantias existentes são dado de teste.

A conta vive em três lugares e os três mudam juntos: _months_between na API, mesesEntre no diálogo de emissão e prazoEmMeses no detalhe da garantia.

Existe um quarto cálculo de mês no app, e ele fica de fora de propósito: o formulário da análise deriva a data de fim prevista com setMonth — o transbordo que a RN6 proíbe. As datas da análise estão fora de escopo pela seção 5, e o simulate só as usa como fallback quando não recebe prazo. Fica registrado para não parecer omissão.

E hoje a conta não tem teste nenhum. A fórmula antiga vive em comentário, não em asserção — nenhum teste da suíte exercita _months_between nem depende do resultado dele. Então a RN6 não "muda um teste": ela cria a primeira rede que essa conta vai ter. A CA6a é essa rede, e ela precisa cobrir a aresta do grampeamento, não só o caso feliz.

RN7 — O início da vigência é livre nos dois sentidos. Passado e futuro, sem limite. A locação é um fato do mundo e pode ter começado antes da emissão. Uma garantia cuja vigência inteira já passou nasce mesmo assim — ela nasce PENDING_SIGNATURE, e a RN8a da spec 0038 já recusa ativá-la fora do período.

RN8 — end_date posterior a start_date. Igual ou anterior responde 422 INVALID_GUARANTEE_PERIOD. É o piso absoluto, abaixo da RN5.

RN9 — O simulate passa a devolver LMI e custos de saída. Os dois são congelados pela emissão e hoje não aparecem em lugar nenhum antes dela. Vêm da mesma resposta que já traz taxa, mensalidade, setup e total de entrada.

RN9a — A tela chama o simulate duas vezes, e usa metade de cada resposta.

  • Na abertura, sem guarantee_months. Da resposta usa só lmi_value e exit_costs_value — eles não dependem da vigência. Os quatro valores de preço dessa resposta são descartados: eles saem do prazo da análise, e a pessoa ainda não escolheu a vigência.
  • Quando a vigência muda, com guarantee_months derivado das datas. Dessa resposta usa os quatro de preço.

Sem esta regra, quem implementa tem duas saídas plausíveis e a errada mostra o preço de um prazo num modal que vai congelar outro — o defeito que o bloco existe para impedir.

RN10 — Os valores exibidos são os que serão gravados. O simulate calcula LMI e custos de saída com os multiplicadores vigentes da garantidora — lmi_months_multiplier e exit_costs_multiplier —, exatamente como o create faz. Não se copia o financial_lmi_value da análise: ele foi congelado quando a análise foi criada, e se o parâmetro mudou desde então a tela mostraria um número e a emissão gravaria outro.

RN11 — Multiplicador zero tem significados opostos, e a resposta os distingue. Zero no LMI é cobertura ilimitada e o campo vem null; zero em custos de saída é ausência de cobertura e o campo também vem null. Quem lê distingue pelo rótulo, não pelo valor — e a tela escreve "Ilimitado" para um e "Não cobre" para o outro, como o resumo financeiro da análise já faz.

RN12 — O ANALYST passa a emitir garantia. Ganha guarantees:create. Não ganha mais nada: guarantees:sign, guarantees:update e guarantees:annotate continuam fora.

RN13 — POST /guarantees recusa o ADMIN_GLOBAL. Responde 403 PLATFORM_ADMIN_CANNOT_EMIT, e a guarda vem antes de resolver a FK — não se consulta a análise de um tenant para recusar quem não pode emitir em tenant nenhum. Emitir é ato de negócio do cliente, e o SaaS admin administra a plataforma — ver a seção 10.5, onde isto deixa de ser guarda de tela e passa a ser do servidor.

RN14 — O ícone de garantia é certificate em toda superfície visível. Botão de emitir, aba do celular, cabeçalho e vazio da lista, cabeçalho do detalhe. file-text volta a ser só análise de crédito — exceto onde aponta mesmo para uma análise (o botão "Ver análise de crédito" dentro da garantia) ou para documento genérico (o painel de anexos).

10. Autorização e isolamento

10.1 Matriz de papéis

Só a emissão muda. As demais ações da garantia seguem como a spec 0038 as deixou.

Papel Emitir garantia (POST /guarantees) Simular com análise (POST /guarantees/simulate)
ADMIN_GLOBAL não → 403 PLATFORM_ADMIN_CANNOT_EMIT (RN13) sim, mas sem parâmetros do tenant a conta falha — ver 10.5
ADMIN_TENANT sim, como hoje sim
OPERATOR sim, como hoje (spec 0038) sim
ANALYST sim — passa a poder (RN12) sim — consequência da mesma permissão
VIEWER não → 403 não → 403
AGENCY_USER não → 403 não → 403

O negativo que mais importa: o ANALYST ganha uma permissão. Continua sem registrar assinatura, sem encerrar e sem editar notas — a separação que a 0038 desenhou entre decidir e concluir se estreita, não desaparece.

A imobiliária não emite, e isso não muda. Ela lê as próprias garantias desde a 0038; emitir é ato da garantidora.

10.2 Permissões

Permissão Endpoint Quem passa a ter
guarantees:create POST /guarantees e o simulate com credit_analysis_id ANALYST (novo). Os demais já tinham

Nenhuma permissão nova é criada. A mudança é uma concessão a mais numa permissão que já existe — e é ela que faz o botão aparecer na tela para o analista, porque a tela condiciona por can("guarantees:create").

O porteiro do simulate é por parâmetro, e continua sendo. Sem credit_analysis_id, ele exige só guarantees:read; com a análise, exige guarantees:create, porque a resposta passa a carregar a taxa negociada e o setup arbitrado daquela análise. Isso é da spec 0038 e não muda — mas agora o ANALYST satisfaz a segunda condição, e é assim que ele vê o resumo.

10.3 Isolamento por garantidora (tenant)

credit_analysis_id passa a ser resolvido no escopo nos dois endpoints. O simulate já usa resolve_in_scope — que aplica tenant e imobiliária, com 404 (nunca 403) para o que está fora. O create não usa: ele chama get_by_id(..., tenant_id=...), e está assim desde antes desta spec (a 0038 já registrou que ele não é modelo a copiar). Como esta spec mexe no create de qualquer forma (RN1, RN2, RN4), ele passa a usar resolve_in_scope também. Não é refactor gratuito: é a mesma linha que já vai ser tocada.

resolve_in_scope não protege contra o ADMIN_GLOBAL, e isso é por desenho. tenant_scope devolve None para ele — "é o único perfil que opera cross-tenant" —, e o repositório trata None como "sem filtro". Por isso a fronteira plataforma × cliente não se fecha com escopo: fecha-se com a guarda da RN13, na seção seguinte.

Os dados novos da resposta não abrem porta nenhuma, e o argumento vale nos dois caminhos do simulate:

  • Com credit_analysis_id (exige guarantees:create): LMI e custos de saída derivam do Pacote de uma análise que o chamador já pode ler, com multiplicadores do próprio tenant.
  • Sem credit_analysis_id (exige só guarantees:read, que o AGENCY_USER tem): eles derivam de um valor que o próprio chamador informou, com os mesmos multiplicadores — e a imobiliária já os lê hoje, por tenant_settings:read, que está no seed dela. Nada que ela não pudesse calcular sozinha.

10.4 Isolamento por imobiliária

Sem mudança de acesso. O AGENCY_USER não emite e não simula com análise — 403 nos dois, antes de qualquer resolução de escopo. A garantia emitida herda a imobiliária do imóvel, como hoje.

Um campo a mais chega até ela, e é inofensivo. exit_costs_value entra na GuaranteeResponse, e a resposta dela herda tudo o que não é explicitamente omitido — então ela passa a ver os custos de saída da própria garantia. É o mesmo argumento de 10.3: a imobiliária já lê exit_costs_multiplier por tenant_settings:read e já vê o Pacote, então não há nada aí que ela não pudesse calcular sozinha. Nota: ela não vê notes, termination_reason nem terminated_by, que continuam omitidos.

10.5 Plataforma × cliente (SaaS admin)

Havia um buraco aqui, e esta spec o fecha.

O ADMIN_GLOBAL não tem tenant_id, e tenant_scope devolve None para ele — logo nem o resolve_in_scope da seção anterior o filtra. Hoje ele pode fazer POST /api/v1/guarantees com o credit_analysis_id de qualquer garantidora: os parâmetros são resolvidos pelo tenant da análise, e a garantia nasce, com número, na base de um cliente — irreversível, sem segunda chance, e sem passar por tela nenhuma. A tela o impede duas vezes — o app o redireciona para /admin antes mesmo da tela da análise, e o resumo não calcularia sem tenant. A rota, não.

A spec 0038 encontrou a mesma dívida e a declarou como anterior, sem consertar. Esta a conserta: pela RN13, POST /guarantees responde 403 PLATFORM_ADMIN_CANNOT_EMIT para o ADMIN_GLOBAL. Guarda de interface não protege rota — é a lição que esta família de specs já pagou, e escrever "é decisão" apoiado só no que a tela faz seria repeti-la.

Decisão do usuário, e o limite dela: fecha-se a emissão. Registrar assinatura, encerrar e anotar continuam alcançáveis pelo ADMIN_GLOBAL — é a mesma classe de dívida, em quatro rotas que esta spec não ia tocar, e fechá-las seria escopo que ela não anunciou. Fica registrado como risco aceito na seção 15.

Consequências: a tela do SaaS admin (/app/admin/*) não muda; nenhum teste desta spec usa ADMIN_GLOBAL como emissor; e quem no time usava o admin global para emitir em ambiente de teste passa a precisar de um usuário da garantidora.

11. Impacto por app

apps/api

  • schemas/guarantee.py — GuaranteeCreate perde guaranteed_value (RN2); GuaranteeSimulateResponse ganha lmi_value e exit_costs_value, os dois Decimal | None (RN9, RN11). GuaranteeResponse ganha exit_costs_value, que a coluna já tem e o schema não expunha — sem ele a CA10 não é verificável, e a tela da garantia não teria como mostrar o que a emissão congelou.
  • services/guarantee_service.py — create passa a derivar o valor do Pacote (RN1), a recusar análise sem Pacote (RN3), a validar a duração contra o prazo da análise (RN4, RN5, RN6, RN8) e a resolver a FK por resolve_in_scope (10.3). simulate passa a calcular e devolver LMI e custos de saída (RN9, RN10).
  • db/rbac_seed.py — guarantees:create entra no ANALYST (RN12).
  • Um teste da spec 0038 é revogado, não consertado. test_analista_e_visualizador_nao_escrevem afirma que o ANALYST toma 403 ao emitir — era a CA12 daquela spec, e a RN12 a revoga. O teste passa a afirmar o que a CA14 desta define: o ANALYST emite, e continua tomando 403 em assinar, encerrar e anotar. Dizer isto aqui é o que impede alguém de "consertar" o teste na direção errada.
  • Sem migration. Nada muda no schema do banco: os campos já existem, e o que muda é de onde vêm os valores e quem pode chamar. A concessão de RBAC não é migration — é seed, e o /ship passou a rodá-lo em todo deploy.
  • PLATFORM_ADMIN_CANNOT_EMIT exige um 403 com código próprio. Hoje ForbiddenError fixa code="FORBIDDEN" e é o único 403 da API — a RN13 não se resolve só com uma guarda no serviço; a camada de erro precisa aceitar um código. É uma linha, mas não é de graça.
  • Swagger e docstrings, na mesma entrega — é regra do CLAUDE.md da API. Hoje o decorator do POST /guarantees documenta guaranteed_value como campo do corpo, e a descrição do simulate não conhece os dois campos novos. As duas mudam junto com o código, mais os erros novos (PLATFORM_ADMIN_CANNOT_EMIT, GUARANTEE_PERIOD_EXCEEDS_ANALYSIS, GUARANTEE_PERIOD_TOO_SHORT, INVALID_GUARANTEE_PERIOD).
  • Testes: um por RN, com o negativo de papel do VIEWER e do AGENCY_USER, o negativo do ADMIN_GLOBAL (CA17a), e o positivo do ANALYST emitindo.

apps/web

  • pages/credit-analyses/show.tsx — o ícone do botão (RN14).
  • pages/credit-analyses/components/GuaranteeCreateDialog.tsx — a reescrita da Parte II: painel de conferência com identificação e os sete valores, campo de valor removido, datas pelo seletor do app com piso e teto, fim derivado do início, ordem invertida em 375px, resumo no rodapé do celular, e os estados da 7.3.
  • components/navigation/nav-groups.tsx, pages/guarantees/index.tsx, pages/guarantees/show.tsx — o ícone nos outros quatro lugares (RN14).
  • pages/guarantees/components/GuaranteeSections.tsx — ganha a linha de custos de saída, ao lado do LMI, que hoje não aparece em lugar nenhum da tela da garantia.

"Valor garantido" e "Pacote Locatício" continuam como duas linhas, e isso é decisão, não esquecimento: nas garantias novas elas mostram o mesmo número (RN1), mas nas antigas não — lá o valor é o aluguel mensal e o pacote é o pacote. Colapsá-las esconderia a divergência exatamente onde ela existe. O rótulo do primeiro passa a "Valor da garantia", para casar com o modal. - pages/guarantees/api.ts — GuaranteeSimulation ganha os dois campos novos; create perde guaranteed_value. - pages/guarantees/index.tsx — o rótulo da coluna acompanha o detalhe: "Valor da garantia". Não faz sentido a lista dizer um nome e a tela de detalhe outro para o mesmo campo. - data-testid em todo elemento interativo novo, inclusive o resumo do rodapé no celular.

apps/e2e

  • Spec nova cobrindo: o analista emitindo (papel que hoje não consegue); o valor gravado batendo com o Pacote; a recusa de duração maior que a aprovada e menor que um mês; o fim derivado do início; os sete valores no painel; e 375px — ordem invertida, resumo no rodapé, sem rolagem horizontal.
  • Sem seed novo. As análises aprovadas do tenant de e2e servem; o que o teste cria, ele cria pela API, como o spec da 0038.

12. Contrato de API

Emitir garantia

POST /api/v1/guarantees          Permissão: guarantees:create

{ "credit_analysis_id": "uuid", "start_date": "2026-09-01", "end_date": "2027-08-31",
  "notes": "opcional" }

guaranteed_value não é mais aceito (RN2). A resposta é a GuaranteeResponse de sempre.

Erro Código Quando
403 — papel sem guarantees:create no próprio tenant
403 PLATFORM_ADMIN_CANNOT_EMIT quem chama é ADMIN_GLOBAL (RN13)
404 — análise de outro tenant ou de outra imobiliária
409 GUARANTEE_EXISTS a análise já tem garantia (como hoje)
422 CREDIT_ANALYSIS_NOT_APPROVED análise não aprovada (como hoje)
422 NO_LESSEES análise sem inquilino participante (como hoje)
422 NO_CALCULATION_BASE análise sem Pacote Locatício congelado (RN3)
422 SETTINGS_NOT_FOUND garantidora sem parâmetros configurados — regra nova, ver RN3a
422 INVALID_GUARANTEE_PERIOD end_date ≤ start_date (RN8)
422 GUARANTEE_PERIOD_TOO_SHORT duração menor que um mês (RN5)
422 GUARANTEE_PERIOD_EXCEEDS_ANALYSIS duração maior que o prazo da análise (RN4)

Simular

POST /api/v1/guarantees/simulate
Permissão: guarantees:read — e guarantees:create quando vem `credit_analysis_id`

{ "credit_analysis_id": "uuid", "guarantee_months": 12 }

Sem guarantee_months, o serviço deriva o prazo das datas da análise quando as duas existem, e só cai no prazo em meses quando não existem — e, como 511 das 525 análises aprovadas não têm data, o segundo caminho é o normal. É o que a RN9a usa na abertura do modal, aproveitando só o LMI e os custos de saída da resposta e descartando os quatro de preço.

Erro Código Quando
403 — veio credit_analysis_id e o papel não tem guarantees:create
404 — análise de outro tenant ou de outra imobiliária
422 SETTINGS_NOT_FOUND garantidora sem parâmetros — inclusive o ADMIN_GLOBAL, que não tem tenant
422 ANALYSIS_NOT_APPROVED a análise informada não está aprovada
422 NO_CALCULATION_BASE análise informada, mas sem Pacote Locatício congelado
422 — (validação do schema) sem análise e sem guaranteed_value/guarantee_months — para no schema, sem código de negócio

A resposta ganha dois campos, ambos anuláveis (RN9, RN11):

{
  "guarantee_rate_percent": "10.00",
  "monthly_value": "250.00",
  "lmi_value": "75000.00",
  "exit_costs_value": "12500.00",
  "initial_fee": "1000.00",
  "first_month_upfront": "0.00",
  "total_upfront": "1000.00"
}

lmi_value: null significa cobertura ilimitada; exit_costs_value: null significa sem cobertura de custos de saída. São significados opostos para a mesma representação, e é por isso que a RN11 existe.

13. Critérios de aceite

O valor e o prazo

  • [ ] CA1. Emitida uma garantia, guaranteed_value é igual ao financial_rental_package da análise — sem que o cliente informe valor nenhum.
  • [ ] CA2. POST /guarantees com guaranteed_value no corpo emite normalmente, ignorando o campo, e o valor gravado continua sendo o Pacote.
  • [ ] CA3. Análise com Pacote nulo → 422 NO_CALCULATION_BASE.
  • [ ] CA3a. Garantidora sem parâmetros → 422 SETTINGS_NOT_FOUND, e nenhuma garantia é criada. Hoje ela seria criada, com LMI, mensalidade e setup nulos (RN3a).
  • [ ] CA4. Análise de 12 meses, vigência de 01/09/2026 a 30/09/2027 (13 meses) → 422 GUARANTEE_PERIOD_EXCEEDS_ANALYSIS.
  • [ ] CA5. Vigência de 20 dias → 422 GUARANTEE_PERIOD_TOO_SHORT.
  • [ ] CA6. Vigência de 01/09/2026 a 31/08/2027 sobre análise de 12 meses → 201. É o caso que a régua antiga (blocos de 30 dias) recusaria, contando 13 — e é o motivo de a RN6 existir. Vigência de 6 meses sobre a mesma análise também → 201.
  • [ ] CA6a. A garantia emitida em CA6 é precificada como 12 meses, não 13: o guarantee_months que alimenta o cálculo do setup vem da mesma conta que validou.
  • [ ] CA6b. A conta acerta a aresta do grampeamento, nos seis casos que a RN6 lista — inclusive 01/03+12 → 29/02 (bissexto) e 31/01+1 → 28/02. É o teste que a fórmula nunca teve, e é onde Python e TypeScript mais têm chance de divergir.
  • [ ] CA7. end_date igual a start_date → 422 INVALID_GUARANTEE_PERIOD.
  • [ ] CA8. Início um ano no passado → 201. Início um ano no futuro → 201.

Os valores da simulação

  • [ ] CA9. POST /guarantees/simulate com análise devolve lmi_value e exit_costs_value.
  • [ ] CA10. Os dois batem, número a número, com o que o POST /guarantees grava na garantia.
  • [ ] CA11. Multiplicador de LMI zero → lmi_value: null. Multiplicador de custos de saída zero → exit_costs_value: null. Os dois na mesma resposta, sem confundir um com o outro.
  • [ ] CA12. Alterado o multiplicador do tenant depois da análise, o simulate devolve o valor novo — não o congelado na análise — e a emissão grava o mesmo.

Autorização

  • [ ] CA13. ANALYST emite garantia com sucesso (novo).
  • [ ] CA14. ANALYST recebe 403 ao registrar assinatura, ao encerrar e ao editar notas.
  • [ ] CA15. VIEWER recebe 403 ao emitir.
  • [ ] CA16. AGENCY_USER recebe 403 ao emitir e ao simular com credit_analysis_id.
  • [ ] CA17. Análise de outra garantidora → 404 ao emitir, nunca 403.
  • [ ] CA17a. ADMIN_GLOBAL emitindo, com uma análise aprovada de qualquer garantidora → 403 PLATFORM_ADMIN_CANNOT_EMIT, e nenhuma garantia é criada. É o buraco que a seção 10.5 descreve, e o teste que impede alguém de reabri-lo.
  • [ ] CA17c. ADMIN_GLOBAL chamando o simulate com a análise de qualquer garantidora → 422 SETTINGS_NOT_FOUND, e a resposta não carrega taxa aprovada, setup arbitrado, LMI nem custos de saída.

    Correção de uma afirmação desta spec, feita na implementação. Esta CA dizia que a proteção valia "por ordem" — as settings buscadas antes de a análise ser resolvida — e que inverter entregaria a precificação alheia ao SaaS admin. Foi medido, e não entrega: com a busca movida para depois do bloco da análise, o 422 continua saindo antes de qualquer valor ser calculado ou devolvido. O que protege é a checagem de parâmetros existir, não a posição dela. A CA continua valendo pelo que prova. - [ ] CA17b. ADMIN_GLOBAL com um credit_analysis_id inexistente → 403, não 404. É o critério que trava a ordem: a guarda vem antes de resolver a FK, e sem ele o teste de CA17a passa com a guarda nos dois lugares.

A tela

  • [ ] CA18. O modal responde, sem ser fechado: de quem é, qual imóvel, qual análise, e os sete valores.
  • [ ] CA18a. Ao abrir, antes de qualquer data, o LMI e os custos de saída chegam sem esperar data nenhuma, e os quatro valores de preço não — eles pedem a vigência (RN9a).
  • [ ] CA19. Não existe campo de valor garantido no modal.
  • [ ] CA20. Informado o início, o fim se preenche com o prazo da análise; encurtar é aceito, esticar é recusado no campo.
  • [ ] CA21. O botão de emitir usa certificate, e os outros quatro lugares também.
  • [ ] CA22. Em 375px: a ordem é identificação → formulário → valores → notas, o rodapé mostra total de entrada e mensalidade acima do botão, e a tela não rola na horizontal.
  • [ ] CA23. Falha ao emitir mantém o modal aberto com o preenchido, e no celular a tela rola até a mensagem.

14. Plano de implementação

Na ordem do monorepo — o contrato desce da API.

  1. apps/api — RN1 a RN11 no schema e no serviço, resolve_in_scope no create, RN12 no seed de RBAC, testes por RN com os negativos de papel e o positivo do ANALYST.
  2. apps/web — o diálogo reescrito (Parte II), o ícone nos cinco lugares, o cliente da API.
  3. apps/e2e — o spec do fluxo, nos dois viewports.

Ordem dentro da fase da API, e ela importa: a RN2 (remover guaranteed_value do corpo) quebra o apps/web até a fase seguinte. É aceitável porque o único cliente é ele, versionado junto — mas os dois precisam entrar no mesmo merge.

15. Decisões em aberto

Nenhuma. As dezesseis decisões tomadas durante os portões estão registradas nas seções 6 e 8, com o que foi revisto e por quê.

Riscos aceitos:

  • A segregação de funções entre decidir crédito e emitir garantia deixa de existir. O ANALYST passa a fazer os dois. A justificativa registrada é que o segundo emissor de hoje não confere nada, porque o modal não lhe mostra nada — e esta spec conserta justamente isso, então a justificativa deixa de valer no dia em que a feature entra. Decisão do usuário, registrada na seção 6.
  • A fronteira plataforma × cliente fecha só na emissão. O ADMIN_GLOBAL continua podendo registrar assinatura, encerrar e anotar garantia de qualquer garantidora pela API — mesma classe de dívida, em quatro rotas que esta spec não anunciou tocar. Decisão do usuário: fecha-se a emissão agora, e o resto fica registrado para quem for atacá-lo.
  • Em 375px, ainda é possível confirmar sem ter rolado até o painel de valores. O resumo no rodapé mitiga — total de entrada e mensalidade ficam junto do botão —, mas os outros cinco números não obrigam leitura. Decisão do usuário: não travar o botão por posição de rolagem.

16. Revisão do detalhamento

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

Rodada Critical Important Minor Veredicto
1 0 4 7 reprovada — laço interrompido por [PERGUNTAR AO USUÁRIO]
2 0 1 7 reprovada
3 0 1 6 reprovada
4 0 0 6 aprovada

Resultado: aprovada na rodada 4.

Riscos aceitos: os três da seção 15 — a segregação entre decidir crédito e emitir garantia, a fronteira plataforma × cliente fechando só na emissão, e o confirmar alcançável em 375px sem ter rolado até o painel. Todos com decisão do usuário registrada.

17. Revisão da implementação

Preenchido pelo /spec implement, rubrica .claude/rubrics/implementacao.md. As revisões de fase (api, web, e2e) vão nas primeiras linhas; a da branch inteira, na última.

Rodada Escopo Critical Important Minor Veredicto
1 apps/api 1 3 6 reprovada
2 apps/api 0 0 6 aprovada
1 apps/web 3 6 6 reprovada
2 apps/web 0 3 6 reprovada
3 apps/web 0 1 7 reprovada
4 apps/web 1 1 5 reprovada
5 apps/web 0 0 4 aprovada
1 apps/e2e 0 3 5 reprovada
2 apps/e2e 0 1 10 reprovada
3 apps/e2e 0 1 5 reprovada — corrigida; 4ª rodada dispensada pelo usuário
1 branch 0 1 4 reprovada — corrigida

Resultado: as três fases fecharam; a revisão de branch reprovou por um Important só, já corrigido.

A fase apps/e2e fechou sem rodada aprovadora, e isso é decisão registrada. As três rodadas reprovaram por achado diferente a cada vez — nenhum Critical, e cada um corrigido antes da seguinte —, o que estourou o limite de três. O usuário dispensou a quarta e mandou seguir para o Passo 5. O que sustenta a fase, no lugar do carimbo: os três specs tocados rodaram verdes contra o dev-remote reconstruído (0039 com 4 testes, 0038 com 12, credit-analyses com 6), e a revisão de branch, que leu o e2e junto com os outros dois apps, não achou nada nele.

O Important da revisão de branch: a régua de meses da RN6 tem 22 testes do lado Python e zero nas duas cópias TypeScript — e a CA6b diz que é justamente ali que os dois lados têm chance de divergir. Trocar somarMeses por setMonth cru compilava, passava no preflight e passava no e2e em 28 dos 31 dias do mês: transbordo e grampeamento só divergem quando o dia de início é 29, 30 ou 31. Nos outros três, o modal derivaria uma data que a API recusa com 422.

Fechado com scripts/regua-de-meses.ts, pendurado no make preflight e no make lint: confere os seis casos da RN6 nas duas cópias, a propriedade que amarra derivar e validar, e a paridade entre elas em 155.977 comparações. O red-green foi feito — com setMonth cru no lugar, 3.926 divergências e saída 1.

Por que o apps/web custou cinco rodadas

Da rodada 2 em diante, os achados eram defeitos que a correção da rodada anterior criou — todos na mesma máquina de estados do diálogo: painel e rodapé discordando sobre por que o confirmar está cinza, saída de erro pertencente ao outro erro, "Calculando…" eterno.

A causa não é desatenção: no apps/web não havia como provar uma correção. Na API cada uma tem red-green — e por isso a fase fechou em duas rodadas. No web, tsc verde diz que compila, e a primeira leitura da correção era a do revisor, uma rodada inteira depois.

O que se fez na rodada 5, em vez de mais uma reescrita em lote:

  1. A leitura da tela saiu do componente para guaranteeDialogEstados.ts, função pura — o que a tela diz e se o confirmar acende, sem JSX no meio.
  2. apps/web/scripts/guarantee-dialog-estados.ts enumera os 124 estados alcançáveis e verifica os invariantes: a frase exibida é verdadeira, a saída pertence ao erro exibido, nunca emitir com erro na tela, nunca "Calculando…" sem chamada em curso, nunca erro obsoleto de insumos antigos.
  3. --mutantes é o red-green do verificador: reintroduz os cinco defeitos que os revisores acharam (rodadas 2, 3, 3, 4 e 4) e exige que os cinco fiquem vermelhos. Ficam.
  4. Está pendurado no make preflight e no make lint — verificador que depende de alguém lembrar não é rodado.

Defeitos aceitos:

  • GuaranteeCreateDialog.tsx — "Recarregar e ver" em vez de abrir a garantia existente. A 7.3 pede que o modal ofereça vê-la; o 409 GUARANTEE_EXISTS da API não carrega o id (guarantee_service.py:137-140), então o link direto não existe hoje. Custa um clique a mais num caminho raro (duas abas na mesma análise). Fica para quando o erro carregar o id.
  • Rodapé de 375px remete ao painel, não navega até ele. A exigência dura da 7.2 — motivo à vista, nunca cinza mudo — está cumprida; o que falta é o atalho.

18. Registro

O que mudou em relação ao planejado

  • A régua de meses virou módulo, nos três apps. O plano previa a conta duplicada em mesesEntre (diálogo) e prazoEmMeses (detalhe). Saiu um módulo por app — app/domain/periodo.py, shared/utils/periodo.ts e fixtures/periodo.ts —, e _months_between foi extinta: a API tinha duas réguas, e o simulate usava a antiga.
  • ForbiddenError ganhou code. Sem isso a tela não distinguia "seu perfil não tem a permissão" da recusa própria do SaaS admin (RN13), que tem todas as permissões.
  • Dois verificadores novos, e é a parte desta spec que sobrevive a ela: apps/web/scripts/guarantee-dialog-estados.ts (124 estados do diálogo, com --mutantes) e scripts/regua-de-meses.ts (155.977 comparações entre as duas cópias TypeScript da RN6). Os dois estão pendurados no make preflight e no make lint.
  • A fase apps/e2e fechou sem rodada aprovadora, por decisão do usuário — o porquê está na seção 17.

O que se aprendeu, e vale para o repositório

  • Campo de data do MUI v9 não aceita fill nem keyboard.type na raiz. É um role="group" de spans contenteditable, com o <input> oculto. O que funciona é pressSequentially na primeira seção; digitar por cima substitui, sem limpar antes. Medido — as duas primeiras tentativas perderam os dígitos em silêncio.
  • No apps/web, correção sem verificação custa uma rodada de portão por vez. As cinco rodadas da fase estão explicadas na seção 17; a regra que saiu delas está no CLAUDE.md e nos skills /spec e /fix.

Verificação final (Passo 5)

Contra o dev-remote reconstruído com este código:

  • pytest: 474 ✓ / 1 ✗. O vermelho é test_partner_agency_status_filter::test_alcanca_quem_esta_fora_da_primeira_pagina, intermitente catalogado no CLAUDE.md. Isolado 5×: 1 vermelho, 4 verdes — a assinatura de cara-ou-coroa do desempate por UUID. A branch não toca o arquivo nem o repositório que ele exercita.
  • e2e: 256 testes — 247 ✓, 7 ✗, 2 pulados. Nenhum dos sete é regressão desta branch, e cada um tem evidência:
  • list-pagination (2) — passam isolados; instabilidade de suíte.
  • isolamento-imobiliaria (3) — o inquilino do seed é o mais antigo dos 15 do escopo da imob, e o DataGrid virtualiza: a linha não chega ao DOM. Nenhum dos 15 foi criado por esta branch (os desta spec pertencem a outra imobiliária).
  • properties (1) — o autocomplete busca E2E com limit=50 e 65 imobiliárias casam; "Imobiliária E2E" não vem na página. Dívida registrada desde a spec 0038 (eram 61). A imobiliária criada por esta spec não casa com o termo.
  • credit-analysis-documents CA11 (1) — a rota é guardada por credit_analysis_document_requirements:update (App.tsx:367) e o ANALYST só tem list: ele vê "Você não tem permissão" onde o teste espera modo leitura. Contradição entre guarda e teste, anterior a esta branch — App.tsx não está no diff, e a mudança no seed é aditiva (guarantees:create para o ANALYST, conferido em /auth/me).
  • make preflight limpo (inclui os dois verificadores novos), tsc limpo, ruff sem apontamento nos arquivos novos da API.