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.
- 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.
- A Parte II diz como se usa o que vai existir. Revisada na seção 8 (jornada × viewports).
- 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.
- 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.
- 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. - Na tela da análise, clica em "Emitir garantia", no topo — o botão do ícone de "+".
- 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.
- 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.
- 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.
- Aqui a jornada quebra, de dois jeitos:
- 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.
- 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.
- 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.
- 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:
- O analista aprova a análise — ou abre uma já aprovada — e clica em "Emitir garantia". Ele mesmo, sem passar o bastão.
- O modal abre mostrando sobre o que é a garantia e quanto ela vai custar. Ele lê sem sair dali.
- Informa quando a locação começa. O fim da vigência aparece sozinho, calculado com o prazo que a análise aprovou.
- Escreve uma nota, se quiser.
- 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.icondo 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-textcontinua 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
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 oDatedo JavaScript faz por padrão — a cópia do web não pode usarsetMonthcru.
Sobre ela:
- O último dia coberto por
kmeses a partir deiní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
ktal quefim ≤ ú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ãok < 1nunca 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_betweenna API,mesesEntreno diálogo de emissão eprazoEmMesesno 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 osimulatesó 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_betweennem 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_valueeexit_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_monthsderivado 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. Semcredit_analysis_id, ele exige sóguarantees:read; com a análise, exigeguarantees: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 oANALYSTsatisfaz 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_scopenão protege contra oADMIN_GLOBAL, e isso é por desenho.tenant_scopedevolveNonepara ele — "é o único perfil que opera cross-tenant" —, e o repositório trataNonecomo "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(exigeguarantees: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 oAGENCY_USERtem): eles derivam de um valor que o próprio chamador informou, com os mesmos multiplicadores — e a imobiliária já os lê hoje, portenant_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 usaADMIN_GLOBALcomo 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—GuaranteeCreateperdeguaranteed_value(RN2);GuaranteeSimulateResponseganhalmi_valueeexit_costs_value, os doisDecimal | None(RN9, RN11).GuaranteeResponseganhaexit_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—createpassa 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 porresolve_in_scope(10.3).simulatepassa a calcular e devolver LMI e custos de saída (RN9, RN10).db/rbac_seed.py—guarantees:createentra noANALYST(RN12).- Um teste da spec 0038 é revogado, não consertado.
test_analista_e_visualizador_nao_escrevemafirma que oANALYSTtoma 403 ao emitir — era a CA12 daquela spec, e a RN12 a revoga. O teste passa a afirmar o que a CA14 desta define: oANALYSTemite, 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
/shippassou a rodá-lo em todo deploy. PLATFORM_ADMIN_CANNOT_EMITexige um 403 com código próprio. HojeForbiddenErrorfixacode="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.mdda API. Hoje o decorator doPOST /guaranteesdocumentaguaranteed_valuecomo campo do corpo, e a descrição dosimulatenã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
VIEWERe doAGENCY_USER, o negativo doADMIN_GLOBAL(CA17a), e o positivo doANALYSTemitindo.
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 aofinancial_rental_packageda análise — sem que o cliente informe valor nenhum. - [ ] CA2.
POST /guaranteescomguaranteed_valueno 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_monthsque 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) e31/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_dateigual astart_date→ 422INVALID_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/simulatecom análise devolvelmi_valueeexit_costs_value. - [ ] CA10. Os dois batem, número a número, com o que o
POST /guaranteesgrava 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
simulatedevolve o valor novo — não o congelado na análise — e a emissão grava o mesmo.
Autorização
- [ ] CA13.
ANALYSTemite garantia com sucesso (novo). - [ ] CA14.
ANALYSTrecebe 403 ao registrar assinatura, ao encerrar e ao editar notas. - [ ] CA15.
VIEWERrecebe 403 ao emitir. - [ ] CA16.
AGENCY_USERrecebe 403 ao emitir e ao simular comcredit_analysis_id. - [ ] CA17. Análise de outra garantidora → 404 ao emitir, nunca 403.
- [ ] CA17a.
ADMIN_GLOBALemitindo, com uma análise aprovada de qualquer garantidora → 403PLATFORM_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_GLOBALchamando osimulatecom a análise de qualquer garantidora → 422SETTINGS_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
settingsbuscadas 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_GLOBALcom umcredit_analysis_idinexistente → 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.
apps/api— RN1 a RN11 no schema e no serviço,resolve_in_scopenocreate, RN12 no seed de RBAC, testes por RN com os negativos de papel e o positivo doANALYST.apps/web— o diálogo reescrito (Parte II), o ícone nos cinco lugares, o cliente da API.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
ANALYSTpassa 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_GLOBALcontinua 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:
- 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. apps/web/scripts/guarantee-dialog-estados.tsenumera 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.--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.- Está pendurado no
make preflighte nomake 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 409GUARANTEE_EXISTSda 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) eprazoEmMeses(detalhe). Saiu um módulo por app —app/domain/periodo.py,shared/utils/periodo.tsefixtures/periodo.ts—, e_months_betweenfoi extinta: a API tinha duas réguas, e osimulateusava a antiga. ForbiddenErrorganhoucode. 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) escripts/regua-de-meses.ts(155.977 comparações entre as duas cópias TypeScript da RN6). Os dois estão pendurados nomake preflighte nomake lint. - A fase
apps/e2efechou 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
fillnemkeyboard.typena raiz. É umrole="group"de spanscontenteditable, com o<input>oculto. O que funciona épressSequentiallyna 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á noCLAUDE.mde nos skills/spece/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 noCLAUDE.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 daimob, 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 buscaE2Ecomlimit=50e 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-documentsCA11 (1) — a rota é guardada porcredit_analysis_document_requirements:update(App.tsx:367) e oANALYSTsó temlist: 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.tsxnão está no diff, e a mudança no seed é aditiva (guarantees:createpara oANALYST, conferido em/auth/me).make preflightlimpo (inclui os dois verificadores novos),tsclimpo, ruff sem apontamento nos arquivos novos da API.