monify.app
Mony · Mapa Mental · O que a IA consegue hoje
versão v1.2.239+ · 📈 Lote 60.123 (Fluxo: empty-state + CTA de ativação): ATIVAÇÃO do user novo (última lacuna que o explorer apontou): o Fluxo de Caixa mostrava chart zerado + meses vazios pra quem não tinha o que projetar (frio, sem direção). Agora um bloco acolhedor + CTA pro +Novo, ADAPTATIVO: user novo (0 lançamentos) → "🔮 Veja o futuro do seu dinheiro" + "Adicionar 1º lançamento"; tem-dados-sem-recorrência → "Falta uma recorrência pra projetar" + "Adicionar recorrência". Condição = nenhum mês FUTURO tem income/expense. Sem sinal futuro, esconde chart/seletor flat mas MANTÉM o card do mês atual (contexto + botões de fatura). Revisão adversarial: sólido, 0 ALTA; 1 MÉDIA (redundância empty-state + chart chapado) corrigida. 4 tests novos. 3768 verdes. Frontend-only: src/App.tsx. Base: ⏳ Lote 60.122 (Fluxo: aviso de fatura pendente no saldo): polimento do cash-basis (fecha a MÉDIA da revisão do 60.121): no mês corrente, a fatura NÃO paga não entra no saldo (cash-basis) → o acumulado do Fluxo fica otimista. Agora `FluxoMonth.pendingFaturas` soma as faturas pendentes que ficaram fora do expense e o Fluxo mostra um aviso sutil âmbar ("⏳ saldo não desconta R$X de fatura(s) a pagar"). INFORMACIONAL — não muda income/expense/net/cumulative. 1 test novo. 3764 verdes. Base: 🩹 Lote 60.121 (Cash-basis de fatura — só conta como saída quando paga): CASH-BASIS de fatura (pedido do dono) — a compra de cartão só reduz o SALDO depois que a fatura é marcada PAGA; até lá é compromisso pendente (aparece badgeada, mas não conta). Cash-basis PURO: fatura vencida não paga continua pendente (não vira dívida). Helper isTxRealized + realizedTxs pré-filtram nas fronteiras de saldo (Dashboard/Lançamentos/Mony/snapshot/GVA/reserva) → todas herdam sem mudar assinatura; Fluxo Ponto B (fatura só soma no mês corrente/passado se paga, futuro projeta). LISTA mostra tudo (badge do 60.118); NÚMEROS cash-basis; maior-gasto/categoria seguem comportamentais (a compra é gasto quando feita). Revisão adversarial: 1 ALTA (cats vs expense incoerente → cats cash-basis) + 2 MÉDIA (snapshot congelado → comportamental estável) corrigidas. 8 files canônicos atualizados. 3763 verdes (era 3756). src/App.tsx. Base: 🩹 Lote 60.120 (Fluxo: despesa recorrente editável): a DESPESA recorrente no Fluxo (Nextel/Aluguel, kind txReal) não ganhava ✏️/⊘ — só a ENTRADA (kind salary). Agora o gate canEdit inclui txReal + os handlers do modal operam qualquer série recorrente por (monthKey && recurring), entrada ou saída. Fecha o follow-up do dono (continuo sem editar recorrente). Base: 🩹 Lote 60.119 (Bug 2+3: recorrência no mês atual — pendente + confirmar + editar): bug PROD do dono — salário recorrente com ORIGEM em mês anterior SUMIA do mês atual (R$0 entradas), embora projetasse no futuro (o mês corrente do computeFluxoProjection só olhava txs realizadas). Agora sintetiza a recorrência do mês atual como PENDENTE ("a confirmar", azul/itálico): CONTA na projeção (igual à provisão) mas só vira lançamento real quando o user confirma "✓ Já recebi/paguei" (materializeRecurring cria tx linkada via fromRecurringId = dedup; nada criado antes). Dashboard/Lançamentos seguem sem a recorrência até confirmar (modelo do dono). Bug 3: gate canEdit inclui pendingRecurring → ✏️/⊘ no mês atual (editar valor/pular/encerrar a série), inclusive despesa. Sanitizador backend preserva fromRecurringId (paridade). Revisão adversarial: 1 ALTA (botões mortos da despesa txReal no EditFluxoEventModal) + 2 MÉDIA (double-count via impactMonth no dedup; duplo-clique no confirmar) corrigidas. 13 tests novos. 3756 verdes (era 3739). src/App.tsx + api/user/txs.ts. Base: 🩹 Lote 60.118 (Bug 1: selo de fatura paga em Lançamentos): a fatura de cartão em Lançamentos não sinalizava se estava paga (user achava que "já saiu"). Decisão (com o dono): NÃO esconder (contabilidade canônica intacta) — só marcar "⏳ não paga · pague no Fluxo" (âmbar, só no mês atual e só com fluxoBeta) / "✓ paga" (verde). Chave de fatura paga single-sourced no helper faturaPaidKey (4 sites) → sem drift marcar↔ler. Revisão adversarial: 2 ALTA (selo só com fluxoBeta senão beco; "não paga" só no mês corrente — mês passado não afirma) corrigidas. 4 tests novos. Base: 🚀 Lote 60.117 (Onboarding: welcome proativo do 1º load): welcome PROATIVO no 1º load fecha a lacuna login→1ª ação (o onboarding era 100% reativo — só via a lâmpada da Mony). Modal ÚNICO (LS) pro user NOVO (sem lançamentos), na home, quando nenhum outro modal está aberto: 2 CTAs acionáveis (adicionar 1º lançamento → Lançamentos c/ form; conhecer a Mony → chat fresco c/ os chips do 60.116) + link do tour + explorar sozinho. shouldShowWelcome puro/testado (tabela-verdade). Revisão adversarial: 1 ALTA (não disparar atrás do LoginSplash — senão gravava "visto" sem o user ver) + 2 MÉDIA (não empilhar em modais de 1º load fora do gate; reset-data não re-mostra) + 1 BAIXA (foco no 1º CTA) corrigidas. 8 tests novos. 3739 verdes (era 3731). Frontend-only: src/App.tsx. Base: ✨ Lote 60.116 (Mony: chips de perguntas iniciais — fim da paralisia do campo vazio): os chips tocáveis eliminam a paralisia do campo vazio do user NOVO (a Mony abria com "pergunte o que quiser" + campo vazio). SÓ na saudação padrão (chat fresco): 3 chips que já submetem, adaptativos — <3 lançamentos → onboarding (Como funciona o app?, O que você faz?, Como economizo?) / com dados → análise (Como tô financeiramente?, Qual meu maior gasto?, Como economizo mais?). O click chama send(chip). Contrato blindado: cada chip roteia pra um handler determinístico bom (teste roda localReply em todos → zero fallback; um chip que leva a "não entendi" é pior que não ter chip). Não aparecem no re-engajamento (60.114) nem no histórico restaurado (60.115). Revisão adversarial (1 agente): 0 ALTA; 2 MÉDIA aplicadas — gate na saudação PADRÃO (antes reapareciam no re-engajamento e queimavam quota do free) + tap target 48px (regra Lighthouse). 4 tests novos. 3731 verdes (era 3727). Zero regressão. Frontend-only: src/App.tsx. Base: 💬 Lote 60.115 (Mony: histórico de mensagens persistido — a conversa do dia sobrevive): o chat da Mony deixa de ser efêmero — a conversa do MESMO DIA sobrevive ao recarregar. Complemento do 60.114: dentro do dia você reabre e a conversa está lá; novo dia → chat fresco com o re-engajamento. Local-first: só no seu device (localStorage, per-user, custo zero), NÃO vai pro backend → sem PII de conversa no servidor. persistableHistory(msgs, cap=40) puro: filtra "prem" (upsell), aplica cap, e — CRISIS-AWARE — descarta QUALQUER troca em que a Mony ofereceu o hotline do CVV (isCrisisReply = CVV + 188): a resposta dura do guard de crise E as soft de desabafo emocional. Por cautela — não re-exibe uma troca de sofrimento+hotline ao reabrir (desabafo financeiro não cita 188 → persiste). No GVA, o initializer RESTAURA a conversa do dia (day === hoje + tem msg do user; re-filtrada no restore como defesa contra LS adulterado); novo dia → cai no re-engajamento do 60.114. Writer persiste a cada msg. LGPD: LS_MONY_MSGS_KEY + LASTSEEN limpos nos 3 pontos de logout/reset + no email-resync. Revisão adversarial (1 agente): SÓLIDO nos 2 eixos safety-critical — a resposta de crise real tem CVV+188 → excluída; "CVV" só aparece no hotline (sem falso-positivo de "CVV do cartão"); o GVA DESMONTA no logout (early-return !loggedIn) → o writer NÃO re-grava state stale (evita o bug do 60.106); 4 pontos de clear cobrem o vazamento entre users. 1 MÉDIA (comentário impreciso corrigido) + 1 BAIXA (re-filter no restore) aplicadas. 6 tests novos. 3727 verdes (era 3721). Zero regressão. Frontend-only: src/App.tsx. Base: 🤝 Lote 60.114 (Mony: continuidade cross-sessão — a Mony retoma o fio ao voltar): quando o user volta ao chat (1ª vez de um novo dia), a Mony abre RETOMANDO o fio mais relevante da memória comportamental em vez da saudação genérica — fecha o arco (a memória que ela já tem vira CONVERSA). buildReengagementGreeting(commitments, txs, currentMonth, now) puro: prioridade — compromisso ATIVO do mês PASSADO (fechado, mensurável) → status real via resolveCommitment (✅ celebra / 👀 acolhe+renovar); compromisso do mês CORRENTE → "rolando esse mês"; categoria-problema crônica; null → saudação padrão (que ainda APRESENTA a Mony, certo pra user novo). Na abertura do chat (GVA), a saudação inicial vira contextual SE !autoAsk (não veio de card/CTA) E é a 1ª abertura do dia (LS_MONY_LASTSEEN_KEY) E há gancho. Determinística, custo zero, zero LLM. Escopo: re-engajamento na abertura — o histórico de mensagens NÃO é persistido (feature maior, fora do escopo). Revisão adversarial (1 agente): SÓLIDO, zero ALTA — usa só commitVerbo (tipo+categoria), NUNCA rawText (sem vazamento de texto cru/autolesão); "guardar" honesto; ✅ só no mês fechado (≠ bug 60.107); LGPD ok (commitments limpos no logout). 1 MÉDIA aplicada: anti-nag — o compromisso do mês passado só retoma nos primeiros 10 dias do mês (resultado fresco), depois a Mony segue em frente. 9 tests novos. 3721 verdes (era 3712). Zero regressão. Frontend-only: src/App.tsx. Base: 🔗 Lote 60.113 (Fix: chat-context descartava 10 tópicos de continuidade — irmão do 60.112): sweep proativo de paridade backend↔frontend (motivado pelo bug do recurring) achou e corrigiu 1 irmão. O sanitizador sanitizeCtx (api/user/chat-context.ts) tinha um allowlist de lastTopic CONGELADO em 7 valores (era ~Lote 21), enquanto o frontend persiste 16 (extractReplyContext, src/App.tsx ~2752-2904). Os 10 tópicos de CONTINUIDADE dos Lotes 60.91+ (ver_onde_cortar, calc_corte_cat, meta_da_sobra, divida_plano, emo_medo_acao, recuperacao_inicio, wow_salario_aluguel, apostas_redirect, emo_ferrado_top, sugestoes_focus) caíam fora. Mesma classe do txs/recurring: sanitizeCtx roda no POST e no GET (fail-silent) → o lastTopic sumia no round-trip. Na mesma sessão funcionava (o ref guarda o ctx), mas no reload / 2º device dentro de 20 min, o "Quero"/"Sim" após um CTA da Mony caía no menu genérico — o beco que o Lote 60.91 corrigiu, vazando pelo backend. Fix: o allowlist espelha a union ChatContext.lastTopic do frontend (17 valores) + comentário pra manter em sincronia. O sweep confirmou os demais endpoints SÓLIDOS (goals/history/paid-faturas/ceiling/profile + numéricos/tom do chat-context). 4 tests funcionais novos. 3712 verdes (era 3708). Zero regressão. Backend: api/user/chat-context.ts. Base: 🐛 Lote 60.112 (Fix PROD: saída recorrente parava de projetar no Fluxo de Caixa): bug reportado pelo dono — ao marcar uma SAÍDA como "🔁 se repete todo mês", ela não aparecia no Fluxo dos próximos meses. Rastreado ponta a ponta: o FRONTEND estava todo certo (toggle aparece pra saída, saveTx persiste recurring desde o 60.29, computeFluxoProjection projeta saída recorrente nos meses futuros — repro isolado confirmou). A causa era o BACKEND: o sanitizador sanitizeTxNewFields (api/user/txs.ts ~L100) gateava recurring em t.type === "income" SÓ — pra saída caía no else e DELETAVA o campo. Ao sincronizar, o Redis ficava sem recurring; no reload a tx voltava sem ele → parava de projetar. Causa raiz: o frontend foi liberado pra recorrência em saída no Lote 60.29, mas o sanitizador do backend nunca foi atualizado (regra legada "recorrente = salário = income"). Fix: aceitar (income || expense) — o resto da limpeza (freq/until/skipped/overrides) já é agnóstico de tipo. Nota: saídas recorrentes marcadas ANTES do fix já tiveram o campo descartado no servidor — precisam ser re-marcadas. 5 tests funcionais novos (lote60-112, importando sanitizeTxNewFields) + asserção do lote60-19 atualizada. 3708 verdes (era 3703). Zero regressão. Backend: api/user/txs.ts (1 linha de guard). Base: 📊 Lote 60.111 (Mony: Raio-X de saúde financeira enriquecido com a timeline): o "como tô financeiramente?" deixa de ser foto e vira filme. O handler de saúde financeira (Raio-X com status 🔴/🟡/🟠/🟢, do 60.28/29) ganha a dimensão TEMPORAL, conectando a memória comportamental (lotes 102–110) à pergunta que o usuário mais faz. (1) Tendência (meses fechados): poupança subindo/caindo (ambos positivos) ou rombo diminuindo/aumentando/saiu-do-vermelho (se houve vermelho). (2) Seu padrão: a categoria-problema crônica (top-3 recorrente). Reuso puro de computeMonthSnapshots + computeProblemCategory. PROGRESSIVE ENHANCEMENT: as 2 linhas só aparecem com ≥2 meses fechados — usuário novo mantém o raio-X de mês único (zero regressão). Revisão adversarial (1 agente): bloco sólido (sem crash/NaN, income=0 não mascara déficit como no 60.103, crise protegida). 3 achados de clareza/tom corrigidos: (A1 ALTA) "📈 melhorando" sob "🔴 Déficit" soava tom-surdo → rótulo explícito "(meses fechados)" + reconcilia ("mas este mês apertou"); (M1 MÉDIA) categoria crônica == maior gasto do mês → funde em "E não é só este mês"; (B1 BAIXA) mês que saiu do vermelho era "rombo diminuindo" → "saiu do vermelho 🎉". 9 tests novos. 3703 verdes (era 3694). Zero regressão. Frontend-only: src/App.tsx. Base: 🧪 Lote 60.110 (Mony: harness de cobertura expandido — 57 vertentes + 1 gap calibrado): a pedido do dono, auditoria da cobertura conversacional. As frentes novas (102–108: Consultora plano/simulação, evolução cross-mês, recaída, compromissos, categoria-problema) que viviam só nos testes dedicados de cada lote foram incorporadas ao corpus central do harness (lote60-92), que estava congelado em 60.94 — agora protegidas contra beco-sem-saída junto com o resto. +6 categorias, ~35 frases → 57 vertentes. A incorporação ACHOU 1 GAP REAL (o ponto do harness): "evoluí ou piorei?" (forma sem "mês passado") caía no fallback → o regex do handler de evolução (~3275) ganhou a alternativa (evoluí|melhorei|piorei) ou (…). Guardas de crise/EMO intactas (suíte de crise verde). Auditoria medida: 27/27 frases canônicas das frentes novas respondem deterministicamente (0 fallback); corpus completo sem beco. Não incorporadas (justificado): compra (já é categoria compra), déficit (já é fc5-risco-negativo), não-re-explicar (precisa do estado taught — coberto no teste lote60-109). 7 tests. 3694 verdes (era 3687). Zero regressão. Frontend: src/App.tsx (1 linha). Base: 📚 Lote 60.109 (Mony: perfil — não re-explicar conceito já ensinado): tijolo 2 de perfil/personalização. A Mony lembra o que JÁ te ensinou e para de repetir a aula do zero. A 2ª vez que você pergunta "o que é reserva de emergência?", "qual a regra dos 4%?", "como dividir meu salário?", ela dá um RELEMBRETE curto ("a gente já viu — resumão: …") em vez da explicação completa, e oferece a aula inteira de novo ("repete com 'de novo'"). 1ª vez = aula completa (zero regressão). Estado per-user (localStorage, custo zero, zero LLM), espelha o padrão de compromissos (60.106): persiste + re-sync no email + 3 limpezas (logout/reset/app-killed). GRAVAÇÃO ROBUSTA: localReply ganhou param taught; intercept cedo dá o relembrete só se o conceito está em taught E não é pedido "de novo" E não é crise. O send() marca "ensinado" SÓ quando a resposta REAL contém a ASSINATURA da aula completa (replyExplainsConcept) — dupla-trava (detect do q + assinatura na reply) nunca marca um conceito que não foi de fato dado. Revisão adversarial (1 agente): arquitetura sólida (gravação, escape hatch, regressão, crise/LGPD). 3 ALTA de amplitude do detect (colisão de roteamento): \bfire\b solto pegava "fire emblem/tv"; "distribuição ideal" roubava o handler de distribuição de categorias; "% ideal" solto → fix: os detects passam a ESPELHAR exatamente os regexes dos handlers reais (o relembrete só dispara onde a aula completa também dispararia). Crise vence (CVV 188), só grava chave+timestamp (nunca o texto do user). 12 tests novos. 3687 verdes (era 3675). Zero regressão. Frontend-only: src/App.tsx. Base: 🪞 Lote 60.108 (Mony: perfil — categoria-problema, a Mony conhece seu padrão): tijolo 1 de perfil/personalização. A Mony deixa de só saber "seu maior gasto DESTE mês" e passa a conhecer seu PADRÃO — a categoria variável que mais REAPARECE no seu top ao longo dos meses fechados. computeProblemCategory(txs) puro: últimos 6 meses fechados, presença no top-3 variável + total por categoria (reusa computeMonthSnapshots + calcMonthStats); EXCLUI fixas (aluguel/financiamento é compromisso, não escorregão). Handler reativo ("qual minha categoria-problema?", "onde eu sempre escorrego?", "qual meu maior gasto recorrente?") no cluster CEDO — exige CRONICIDADE (sem isso cai no handler de mês). Read-only/derivado: zero PII nova, zero LLM, custo zero. Revisão adversarial (1 agente): 2 ALTA — (1) ramo "meu vício de…" era dead code (auto-bloqueado pelo guard !EMO_DISTRESS_RE que contém "vício") → removido (EMO é dono da palavra); (2) overclaim — limiar fixo ≥2 numa janela de 6 declarava "vilão" com 33% de presença + desempate elegia categoria pequena onipresente sobre gasto grande → agora gate de recorrência ≥ METADE dos meses + entre crônicos vence o maior dreno (total). + MÉDIA (avg "nos meses em que aparece"; tom "mais reaparece" não "escorrega/vilão") + BAIXA (caso "tudo fixo"). Crise/EMO vencem mesmo com palavras de gasto. 16 tests novos. 3675 verdes (era 3659). Zero regressão. Frontend-only: src/App.tsx. Base: 🏆 Lote 60.107 (Mony: card proativo de compromisso — celebra/cutuca sozinho): fecha o loop do 60.106 + entrega o reforço positivo proativo. Em vez de esperar "o que eu prometi?", um card no Dashboard. Trigger 8 em computeProactiveInsights (reusa resolveCommitment, contabilidade canônica); o Dashboard lê os compromissos do localStorage (fonte única do chat) e passa pro trigger. MÊS FECHADO (mesAlvo === mês anterior, mensurável de verdade): resultado definitivo — ✅ celebra ("Você cumpriu! 🎉", só pra cortar/parar que são MEDIDOS pelo gasto; "guardar" usa título honesto "deu folga" pois ✅ = teve folga, não confirma que separou) / 👀 acolhe sem cobrança ("escorregar acontece — quer renovar?"). MÊS CORRENTE: só check-in gentil (janela dia 12–24), NUNCA "cumpriu". Revisão adversarial (1 agente): 2 ALTA — (1) "cortar" celebrava cedo demais no mês PARCIAL (parcial < mês cheio anterior por definição → ✅ falso já no dia 1); (2) jitter celebrei→cobrei (✅ virava 👀 ao gastar mais). Fix: celebração definitiva SÓ no mês fechado (cheio vs cheio); corrente só check-in. + MÉDIA (nag diário → janela 12–24) + BAIXA (guardar priority 1 < cortar/parar). Resto sólido (honestidade do guardar, ranking warn>success, reatividade do LS, autolesão não chega ao card — compromissos já passaram pelos guards de crise na entrada). 10 tests novos. 3659 verdes (era 3649). Zero regressão. Frontend-only: src/App.tsx. Base: 📝 Lote 60.106 (Mony: memória de compromissos — "vou cortar/guardar" → "o que eu prometi?"): a Mony lembra do que você se comprometeu. 100% determinística, localStorage, CUSTO ZERO, zero LLM/Haiku (pedido do dono). (1) detectCommitment(q) puro: "vou (tentar)? cortar/reduzir | guardar/poupar/economizar | parar de gastar/comprar/pedir | prometo" — só 1ª pessoa futura, com ÂNCORA FINANCEIRA obrigatória; bloqueia pergunta/hipotético/negação/3ª pessoa/passado. (2) buildCommitmentAck reconhece na hora SEM LLM (curto-circuita o Haiku). "o que eu prometi?" lista com status ao vivo pela contabilidade canônica (✅/👀/📝). (3) resolveCommitment HONESTO: "guardar" nunca afirma "guardou X" (saldo ≠ dinheiro separado) — diz "o mês fechou positivo, deu folga". Per-user (limpo no logout/reset/app-killed + re-sync ao trocar de email). SEGURANÇA DE VIDA (3 rodadas adversariais): o detector roda no send() ANTES do localReply, então autolesão NUNCA vira "📝 anotei" — defesa em 3 camadas (detectLifeCrisis + isSelfHarmCut POSIÇÃO-INDEPENDENTE + cortarFin) + o guard de crise MOVIDO PRO TOPO do localReply (precede plano/sim/todos — antes "cortar gastos e meu pulso" caía no plano) + detectLifeCrisis ampliado (prometo me matar, corte+corpo com lookahead de culinária, reflexivo presente "me corto", "cortar minha vida"). Bônus: guard no topo melhora a precedência de crise pro app inteiro. 68 tests novos. 3649 verdes (era 3581). Zero regressão. Frontend-only: src/App.tsx. Base: 📣 Lote 60.105 (Mony: coaching proativo — a recaída aparece sozinha): a Mony deixa de esperar você perguntar. Fecha o ciclo da timeline (60.102-104): em vez de só responder "tô recaindo?", ela LEVANTA a recaída sozinha como um card no Dashboard. (1) Novo trigger no computeProactiveInsights (sistema de insights existente): chama computeMonthSnapshots + detectRelapse (reuso do 60.104, já blindado) — card teaser gentil (warn 🔁) cuja ação "Me mostra" abre a Mony com "tô recaindo?" → a consulta completa (buildConsultaRecaida). (2) Cooldown NATURAL sem state novo: id mensal (relapse_) + o dismiss persistido que já existe → some no mês se dispensado, reaparece mês que vem só se ainda persistir. Usuário novo (<3 meses) ou melhorando → nenhum card. Revisão adversarial (1 agente): 1 ALTA — o ranking mostrava 1 card e ordenava por MAGNITUDE CRUA (unidades incomparáveis entre triggers): recaída podia afogar o alerta time-sensitive "dias até o limite", OU ser afogada pelo trigger de fixas. Fix: ProactiveInsight ganhou priority (urgência, não R$) → sort vira severidade → prioridade → magnitude (3=time-sensitive, 2=comportamental, 1=padrão, 0=estrutural). Resto sólido (cooldown, coerência card↔consulta, tom, sem NaN). 6 tests novos. 3581 verdes (era 3575). Zero regressão. Frontend-only: src/App.tsx. Base: 🔁 Lote 60.104 (Mony: detecção de recaída — "tô recaindo?"): o diferencial competitivo — a Mony percebe quando você VOLTA a escorregar. Consumidor avançado da timeline. Recaída = vinha BEM e escorregou de forma SUSTENTADA (2+ meses consecutivos), não um perrengue isolado. (1) detectRelapse(snaps) puro: VERMELHO (já fechou positivo e os 2 últimos negativos) ou POUPANÇA (baseline saudável e os 2 últimos bem abaixo, AMBOS ainda positivos, sem recuperar). Baseline = MEDIANA dos meses anteriores (imune a 1 mês de bônus/13º — max criava recaída fantasma). Exclui one-time, crash isolado e crônico. ≥3 meses fechados. (2) buildConsultaRecaida: tom de PERGUNTA, sem cobrança/culpa, sem score ("não é bronca: aconteceu algo ou passou no automático?"). (3) Handler com 3 camadas de segurança: detectLifeCrisis (reuso, crise→CVV) + EMO_DISTRESS_RE + RELAPSE_EMO_RE (recaída de VÍCIO/relacionamento/emoção — "recaí no cigarro/jogo/com meu ex" vai pro EMO, nunca vira resposta financeira). Revisão adversarial (1 agente): 3 ALTA corrigidos (ambiguidade emocional/vício, crash isolado, baseline por max). Limiares conservadores (preferem deixar passar a falsamente alarmar). 23 tests novos. 3575 verdes (era 3552). Zero regressão. Frontend-only: src/App.tsx. Base: 🛟 Lote 60.103 (Mony "como evoluí?" cross-mês + correção CRÍTICA do guard de crise): FEATURE — a Mony compara os 2 meses FECHADOS mais recentes (buildConsultaEvolucao, puro, via computeMonthSnapshots, sem backend novo). Sinal-chave = taxa de poupança (% da renda, métrica real, NUNCA score). Tom honesto: melhora encoraja 📈, queda observa+pergunta (nunca veredito), déficit fala de SALDO (saiu do vermelho 🎉 / reduziu o rombo / rombo aumentou — nunca "guardou da renda"); trata queda de renda e %↓ com R$↑. Os 5 handlers "Lote 24" reconciliados (apontam pra "como evoluí?"). SEGURANÇA (descoberto na revisão adversarial) — o guard de crise era PRÉ-EXISTENTEMENTE incompleto: só tinha matar/suicidar, SEM "morrer", SEM autolesão, SEM ideação passiva nem métodos — em PROD "queria morrer" NÃO acionava o CVV. Criada detectLifeCrisis (detecção CENTRALIZADA e idiom-aware, fonte única do guard + da exclusão do handler de evolução): pega suicídio, MÉTODOS (enforcar/tiro/overdose/pular), AUTOLESÃO (exige "me"), ideação passiva ("não quero existir", "dormir e não acordar"), desaparecimento e cognição suicida; e NÃO dispara nos dramas financeiros ("morrer DE FOME", "CORTAR gastos", "SUMIR COM a dívida", "cansei de viver NO VERMELHO", "me afogar EM DÍVIDAS"). Processo (ultracode): 6 rodadas adversariais (1 agente + workflow de 4 + 3 focadas) — cada uma pegou o que a anterior não viu (déficit income=0, advérbio interposto, morte/autolesão/métodos ausentes, morfologia, falso-positivos financeiros). 55 tests novos (evolucao + crise, ambos contratos). 3552 verdes (era 3499). Zero regressão. Frontend-only: src/App.tsx. Base: 🧬 Lote 60.102 (Persistência da timeline financeira · fundação temporal): a Mony deixa de ser cega ao passado — fundação pra comparação cross-mês, DETECÇÃO DE RECAÍDA e ciclos. (1) Tx.createdAt? = timestamp do lançamento (≠ date/evento), ADITIVO, carimbado em toda tx nova (saveTx + addQuickTx), sanitizado fail-silent no backend — txs antigas intactas. (2) computeMonthSnapshots (puro): resumo CANÔNICO (calcMonthStats por mês de impacto) de cada mês FECHADO com movimento ainda não gravado; backfilla do histórico existente + CONGELA (skip-if-exists, pois txs podem ser editadas retroativamente); exclui pending e o mês corrente. (3) Novo endpoint api/user/history.ts — hash user::history, GET/POST com auth sessionId + rate limit, POST FROZEN (não sobrescreve mês gravado), cap 24 meses, sanitize server-side (balance recomputado, valores <1e9, topVal≥0, cap 100 chaves). (4) Frontend: carrega no login + sincroniza SEM polling novo (converge em 1 POST por mês novo). Revisão adversarial (1 agente): ZERO ALTA — segura pra PROD (aditiva, retrocompatível, converge, frozen, sem vazamento); 4 endurecimentos aplicados. 4 tests novos. 3425 verdes (era 3421). Zero regressão. Frontend: src/App.tsx; Backend: api/user/history.ts (novo) + txs.ts. Base: 🔮 Lote 60.101 (Mony: A Consultora — Onda 4 · simulação de mudança recorrente): a Consultora simula "e se". Novo 4º cérebro determinístico buildConsultaSimulacao(change, opts) — "e se eu guardar/ganhar/cortar/gastar R$ X por mês (por N meses)?". Projeta o impacto acumulado (delta×H) sobre a trajetória do saldo CANÔNICO (baseFim = previousBalance + cumulative), mostra o custo anual de um gasto novo e alerta se derruba algum mês no vermelho. Preenche um gap real: os handlers eram "adiar X" (evento) e "gastar X em Y" (one-time) — faltava a mudança recorrente ao longo do tempo. Handler com sinal recorrente (por mês / a mais / por N meses); horizonte 1-12 meses; aceita R$/mil e % da renda. Reusa Consulta/renderConsulta (🎯 + 🗺️) + computeFluxoProjection. Revisão adversarial (1 agente) pegou 2 ALTA + 1 MÉDIA: (ALTA) off-by-one — computeFluxoProjection(txs,H) devolve H elementos, então proj[H] era undefined e a projeção era descartada (testes unitários não viram pq o fixture tinha H+1) → handler pede H+1 + cérebro indexa o último mês; (ALTA) "~N semanas" do delta isolado contradizia o simFim combinado → cruzamento sobre a projeção REAL; (MÉDIA) "20% por mês" virava R$ 20 → agora 20% da renda. Segurança: regex ancorada não toca o guard de crise (teste cobre). 17 tests novos. 3421 verdes (era 3404). Zero regressão. Frontend: src/App.tsx. Base: 🗺️ Lote 60.100 (Mony: A Consultora — Onda 3 · plano de ação multi-passo): depois de aconselhar compra (Onda 1) e mês negativo (Onda 2), a Consultora vira PLANO. Novo 3º cérebro determinístico buildConsultaPlano(goal, opts) — 4 metas: sair do vermelho / juntar pra X / cortar gastos / quitar dívida — passos em SEMANAS, raciocínio pelo saldo canônico. Acha as alavancas (maiores categorias variáveis, fora as fixas), calcula quanto cada corte de ~20% libera, soma a sobra do mês e projeta o prazo (gap ÷ ritmo). Fecha os 4 CTAs que compra/déficit já prometiam e caíam em fallback. Consulta ganhou meta + passos[]; renderConsulta rende 🎯 meta + 🗺️ plano numerado (retrocompatível). Revisão adversarial (1 agente): 0 ALTA de segurança; 6 achados corrigidos — "como quito minha dívida?" sem valor cai no playbook rico existente (bola de neve/Desenrola), plano só intercepta com valor explícito; regexes catch-all ancoradas; parse de milhar; tom honesto ("variáveis" não "discricionárias", "se mantido", "maior juro" como heurística). 18 tests novos. 3404 verdes (era 3386). Zero regressão. Frontend: src/App.tsx. Base: 🔭 Lote 60.99 (Mony: sweep proativo de auto-contradições — 5 coerências): a pedido do dono, caça PROATIVA de pares de handlers que divergem pra mesma entrada (em vez de pegar um a um). Workflow de sweep (5 mapeadores + cruzamento) → 39 candidatos → triagem (maioria já-corrigida nos 60.85-98 ou falso-positivo) → 5 reais: (1) "quanto gastei no cartão?" (FC12) rotulava "esse mês: R$ X" (parecia mexer no saldo) → agora "compras feitas em [mês]" + "compra entra na fatura, não no saldo do mês"; (2) "quantos dias aguento sem renda?" somava só meses positivos → inflava o fôlego de quem tem dívida → agora signed + clamp 0; (3) insight "N dias até o limite" disparava pelo mês isolado → alarmista com reserva → agora só dispara se a reserva não cobrir; (4) "causa principal" = fatura aponta "como dividi meus gastos?" (distingue agregado de categoria); (5) "liberdade mensal" (renda − fixas) ≠ "quanto sobra" (renda − tudo). Revisão adversarial (1 agente): 0 ALTA, 2 MÉDIA resolvidas. FC2 era falso-positivo; PROD grava despesa negativa (sinal-por-tipo no-op seguro). 7 tests novos. 3386 verdes (era 3379). Zero regressão. Frontend: src/App.tsx. Base: 🔧 Lote 60.98 (Mony: "e se eu adiar X?" para de se contradizer com o gasto — bug do dono): "e se eu adiar valenmodas?" dizia "não achei" enquanto "ValenModas" (consulta de gasto) achava o item (categoria, R$ 12.757,60, maior gasto). Causa: o handler de adiar/manter só varria eventos top-level da projeção (cartão vira "Fatura {cardName}", itens "Para Cartão") — uma categoria nunca casava. Fix: novo helper findSpendingByName reconcilia (acha por categoria/cartão/nome em grupo coeso, exato antes de substring) e, em vez de "não achei", aplica a regra canônica (Lote 60.96): cartão = fatura = compromisso ("adiar não some o gasto, atrasar vira juros → renegociar/cortar compras novas"); gasto comum já feito → "já saiu, não dá pra adiar o passado". Idem no "manter" + regex aceita "e se eu manter". Processo (ultracode): workflow de diagnóstico (3 mapeadores + síntese) + revisão adversarial (1 agente) — zero ALTA, 2 MÉDIA corrigidas (over-match de termo genérico; onCard em gasto misto). 10 tests novos. 3379 verdes (era 3369). Zero regressão. Frontend: src/App.tsx. Base: 🔧 Lote 60.97 (Mony: 3 incoerências de render/coerência — bug do dono em PROD + auditoria): (1) itálico quebrado — 11 respostas usavam _"frase"_ (itálico markdown) que o renderizador da Mony (só entende **negrito**) mostrava como underscore literal na tela → removidos, viram aspas; (2) FC5 vs realizado — "vou ficar negativo?" dizia "nenhum mês fecha negativo" (projeção, que conta provisões/entradas previstas) enquanto "estou negativo?" dizia "Sim, déficit -R$ X" (realizado) → FC5 agora reconcilia com o balance canônico; (3) FC4 (achado por agente auditor) — "como vou estar daqui N meses?" reportava cumulative SEM somar previousBalance → saldo futuro que não batia com a aba Fluxo quando há reserva; agora saldo canônico (cumulative + previousBalance), mesma regra dos Lotes 60.87/95/96. Auditoria adversarial (1 agente) varreu o resto: markdown não-suportado limpo; demais usos projeção-vs-realizado (FC16, buildConsulta*, FC3, FC8, adiar) todos corretos — FC4 era o único restante. 8 tests novos (lote60-97). 3369 verdes (era 3361). Zero regressão. Frontend: src/App.tsx. Base: 🩺 Lote 60.96 (Mony: A Consultora — Onda 2 · mês negativo + compra genérica viram conselho): estende o formato Diagnóstico→Porquê→Opções→Recomendação pra mais 2 frentes, reusando renderConsulta. (1) Mês negativo — novo cérebro buildConsultaDeficit: diagnóstico (mês fecha em −R$ X) → porquê (findMonthMainCause: causa + % do déficit) → opções de ajuste reais (simulateMonthWithoutEvent nos 3 maiores eventos) → recomendação. Plugado em FC3-negativo e FC5 ("vou ficar negativo?"). (2) Compra genérica — handler 6502 reconhece "comprar" (corrige "vale a pena comprar X de Y" que caía em "/mês") e responde no formato Consultora. Revisão adversarial (1 agente) pegou 2 bugs ALTA: (a) o cérebro de déficit ignorava a reserva → alarmista; agora usa previousBalance + cumulative e fala "atenção, não emergência" quando a reserva cobre; (b) "Adiar/cortar" era aplicado a fatura/financiamento (atraso = juros) → rótulo por tipo: discricionário → "Cortar/adiar" (✅ se vira o mês); compromisso (parcela/fatura) → "Renegociar" (nunca ✅, "não atrase"). + guards (mês corrente já-gasto; "comprar" não reclassifica plano de saúde/assinatura). Processo (ultracode): workflow de mapeamento (2 agentes) + revisão adversarial (1 agente) + verificação contra código real. 11 tests novos. 3361 verdes (era 3350). Zero regressão. Frontend: src/App.tsx. Base: 💡 Lote 60.95 (Mony: A Consultora — Onda 1 · decisão de compra vira Diagnóstico→Porquê→Opções→Recomendação): 1º passo do plano "IA Consultora" — a Mony deixa de só responder e passa a ACONSELHAR. Arquitetura nova determinística (zero LLM): separa ANÁLISE (dados → objeto Consulta) de RENDER (renderConsulta). Lote A (fundação): tipos Consulta/ConsultaOpcao + renderConsulta + buildConsultaCompra (cérebro de compra: à vista/parcelar/esperar com impacto calculado sobre computeFluxoProjection + findMonthMainCause, recomendação por score). Lote B (integração): o handler J2 ("posso/consigo/dá comprar [carro/tv/notebook/celular/...] de R$ Y") agora responde no formato Consultora sobre o saldo CANÔNICO (totalAvailable = reserva + mês) + projeção real. Revisão adversarial (1 agente) pegou e corrigimos 2 bugs ALTA: (a) parcelar era recomendado a quem já está no vermelho → novo gate noVermelho manda sair do vermelho primeiro; (b) "esperar" usava cumulative SEM previousBalance → número falso que contradizia o saldo canônico → agora previousBalance + cumulative. + gate de prudência (parcela ≤ 30% da renda) + dica acionável (desconto à vista/sem juros). Processo (ultracode): workflow de mapeamento (2 agentes) + revisão adversarial (1 agente) + verificação contra código real. 18 tests da Consultora. 3350 verdes (era 3332). Zero regressão. Frontend: src/App.tsx. Base: 🎓 Lote 60.94 (Mony: 3ª onda — backlog conversacional ZERADO · investimento/juros/FIRE): fecha o backlog do harness (60.92/93). As 7 últimas vertentes calibradas, verificadas contra o código real + guard onde havia risco: (1) cálculo de rendimento SEM verbo (calcMatch aceita "rendimento de 50 mil a 0.5% ao mês por 12 meses"; gate P>0&&i>0&&n>0 evita falso-positivo); (2) FIRE conceitual ("qual a regra dos 4%?" → 4% = 25× gasto anual, só sem valores; com valores segue no fireMatch); (3) FIRE inverso ("quanto de patrimônio pra viver dos juros?"); (4) simetria de juros com SÍMBOLOS ("+20% depois -20%" → não volta ao mesmo, 100×1,2×0,8=96; subiuPrimeiro reescrito robusto pra palavras E símbolos); (5) projeção de patrimônio ("simule meu crescimento em 8 anos" pede 3 inputs + exemplo, após o easter-egg de 100 anos); (6+7) FC3 ("qual meu gasto previsto?" → próximo mês; "quanto vou gastar em julho?" → parsing de mês nomeado no Fluxo de 6 meses, mês fora do horizonte = mensagem útil). KNOWN_GAPS agora VAZIO — todas as ~270 vertentes do corpus respondem. Processo (ultracode): verificação contra código real + revisão adversarial. 7 tests de qualidade novos (incl. prova matemática da simetria). 3332 verdes (era 3325). Zero regressão. Frontend: src/App.tsx. Base: 🎯 Lote 60.93 (Mony: 2ª onda de calibração conversacional — 24 vertentes): continuação do harness (60.92). Um workflow de 5 agentes Explore investigou os ~30 gaps do KNOWN_GAPS; fixes verificados contra o código real (alguns agentes erraram linha/regex) e aplicados com guard de contexto onde havia risco. 24 vertentes calibradas: Fluxo de Caixa beta ("que dia cai o pagamento?", "qual dia recebo?", "como estou em 6 meses?", "projeção de 12 meses?", "tem risco de mês negativo?", "qual a sobra média?", "quanto fica de sobra?", "quanto entra de salário recorrente?", "qual minha renda total mensal?", "que cartões eu uso?", "principal causa do mês?", "causa do déficit qual é?"); emocional ("tô desesperado com dinheiro", "tô afundado em conta", "ansioso com as finanças", "isso me dá ansiedade", "não sei o que fazer com as contas" — crise PURA sem contexto financeiro ainda vai pro CVV 188); saldo ("tô negativo?", "tô devendo?", "como dividi meus gastos?" → breakdown por categoria, "vou fechar no positivo?"); meta-conversa ("e aí mony", "recomeça", "o que você consegue fazer?" → menu de capacidades). Backlog restante (3ª onda): cálculo de investimento sem verbo, FIRE conceitual, simetria de juros com símbolos, FC3. Processo (ultracode): workflow de investigação (5 agentes) + verificação contra código real + revisão adversarial. 8 tests de qualidade novos. 3325 verdes (era 3317). Zero regressão nos 124 arquivos existentes. Frontend: src/App.tsx. Base: 🧪 Lote 60.92 (Mony: harness de cobertura conversacional + 1ª onda de calibração): pedido do dono — "crie agentes que testem todas as vertentes da Mony (pergunta → resposta → continuação) pra ter um sistema completo de expandir o público". Entregue um harness multi-turno DETERMINÍSTICO: como o localReply é determinístico, simula a conversa sem custo de LLM (localReply → extractReplyContext → afirmação → localReply). O corpus (categorias × frases reais de usuário — gírias, typos, formas curtas/longas) foi enumerado por um workflow de 5 agentes Explore. Detecta 3 modos de falha: beco sem saída (fallback/menu), continuidade quebrada (proposta "Quer X?" + "Quero" trava), inconsistência. Estrutura honesta: anti-regressão no que funciona + KNOWN_GAPS (backlog visível das ~30 vertentes que ainda caem em fallback) → calibração em ondas. 1ª onda — 11 vertentes calibradas: "quanto sobra?"/"quanto me resta?", dívida ("tô endividado"/"como saio das dívidas?"/"tenho X de dívida"), "como economizo mais?", juros compostos conceitual (sem números → "juros que rendem juros"), taxa de poupança ("quanto estou poupando?"/"quantos % economizando?"). Guard de negação (iOS-safe, sem lookbehind) impede falso-positivo de plano de dívida em "não tenho dívida". Processo (ultracode): workflow de enumeração (5 agentes) + checagem adversarial (pegou o falso-positivo de negação + o mis-route de poupança). 69 tests novos. 3317 verdes (era 3248). Frontend: src/App.tsx. Base: 🧠 Lote 60.91 (Mony: continuidade conversacional — afirmação após proposta da própria Mony): bug do dono — a Mony PROPÕE follow-ups ("Quer 3 formas práticas de cortar em ValenModas?") mas quando o user diz "Quero"/"Sim"/"Pode" ela NÃO continua, cai no menu genérico ("Pode contar comigo!"). Causa (workflow de mapeamento, 3 agentes): o reconhecimento de afirmação (isShortAffirm) já é amplo; o elo que quebra é o extractReplyContext — só "lembra" o tópico se reconhecer a frase exata, e várias CTAs novas não eram capturadas → sem lastTopic → "Quero" caía no menu. Fix: (1) extractReplyContext captura a FAMÍLIA de CTAs de corte ("Quer N formas/alavancas de cortar em X?", "Quer ver onde cortar?") → ver_onde_cortar; + "Quer que eu calcule quanto sobra se cortar N%?" → calc_corte_cat; + "Quer transformar essa sobra em meta?" → meta_da_sobra. (2) handlers de afirmação novos: calc_corte_cat (calcula 15% da maior categoria + novo saldo, vermelho), meta_da_sobra (reserva/meta/investimento), saude_financeira e wow_salario_aluguel (eram órfãos — lastTopic setado sem handler → menu). (3) isCompoundAffirm: reconhece afirmações compostas ("quero sim", "pode sim", "sim quero", "com certeza", "por favor", "quero muito") que escapavam. Processo (ultracode): workflow de mapeamento (3 agentes) + checagem adversarial (removeu 1 regex de captura frouxo/redundante; confirmou isCompoundAffirm sem falso-positivo; achou os órfãos). 33 tests novos (captura das CTAs, 21 variantes de afirmação, cada elo da cadeia, fluxo ponta-a-ponta, anti-regressão). 3248 verdes (era 3215). Frontend: src/App.tsx. Base: 🎨 Lote 60.90 (Mony: déficit SEMPRE em vermelho nos handlers de saldo + "sangramento" determinístico): 2 pontos do dono (screenshots). (1) "essas perguntas caem em fallback/LLM?" — descoberta: "saldo", "explique meu saldo" (o KPI do header dispara isso), "por que meu saldo é esse?" e "onde estou desperdiçando?" JÁ eram determinísticas (handler do localReply, NÃO o LLM — o "Olha, vou ser honesta contigo..." é handler real). Só "sangramento" caía no LLM → novo handler determinístico logo antes do fallback. (2) "saldo incorreto" — o VALOR está certo (confirmado); o problema era o déficit não vir em vermelho. Causa raiz: fmt() usa Math.abs → passar um balance negativo direto pro fmt mostrava o déficit como POSITIVO (−R$ 1.320 virava "R$ 1.320,00"). Fix: handlers de saldo/déficit passaram a usar fmtSigned (→ "-R$" → vermelho): saldo (5838), saudação (3911), limite diário (5164), impacto de compra (5225), fim de mês (5204), % poupança (4302), visão do mês (4614), cenários (3895/3906). Processo (ultracode): checagem adversarial (zero duplicação de handler + achou os déficits-sem-sinal secundários). 16 tests novos. 3215 verdes (era 3199). Frontend: src/App.tsx. Base: 🎨 Lote 60.89 (Mony: negativos SEM sinal de menos também viram vermelhos): continuação do 60.88 (screenshots do dono). Depois do 60.87/60.88 os números da Mony ficaram consistentes (saldo −1.320,20 em toda resposta), MAS o fallback LLM (Claude Haiku) escrevia a perda SEM o sinal de menos ("saídas passaram das entradas em R$ 1.320,20", "Você tá perdendo R$ 1.320,20 por mês" — em negrito virava VERDE, pior caso pra perda). O detector do 60.88 só pegava -R$/−R$/-%/"R$ X no vermelho". Fix: (1) buildSystemPrompt (api/pulse/llm-fallback.ts) instrui o LLM a SEMPRE marcar negativos com "−" colado no R$; (2) normalizeMonyNeg (rede de segurança no renderizador) injeta "−" quando há contexto claro de perda (perdendo/perda de/rombo de/déficit de/negativo em/no negativo de/estourou em/estouro de), tolerando ** entre o contexto e o R$; MONY_NEG_TOKEN também pega "R$ X negativo". Guard de NEGAÇÃO via offset (sem/evitar/impediu) pra "evitar perda de R$ X" NÃO ficar vermelho. Tudo iOS-safe (só replace + lookahead, NUNCA lookbehind — quebraria iOS Safari < 16.4). Processo (ultracode): checagem adversarial (pegou o falso-positivo de negação + 3 coberturas faltantes). 11 tests novos. 3199 verdes (era 3188). Frontend: src/App.tsx; Backend: api/pulse/llm-fallback.ts. Base: 🎨 Lote 60.88 (Mony: valores negativos em VERMELHO — render central + 2 deferidos do 60.87): pedido do dono "o que for negativo, traga na cor vermelha na Mony". O render das bolhas só colorava valores DENTRO de **bold** e checava o sinal só no INÍCIO do trecho — então "Diferença: -R$ 5.307,40" (minus no meio da frase) saía VERDE. Fix: colorização CENTRALIZADA (MONY_NEG_TOKEN + splitMonyNegatives pura/exportada + renderMonyText) — qualquer negativo (−R$/-R$ X, -X%, ou "R$ X no vermelho") vira VERMELHO em QUALQUER posição (bold ou texto normal), inclusive quando o "-" vem antes do ** (normaliza "-**" → "**-"); **bold** sem sinal segue lime; cobre handlers do localReply E o fallback LLM de uma vez. Bloqueia falso-positivo de range ("10-15%") via matchAll + checagem do char anterior — SEM lookbehind (que quebraria iOS Safari < 16.4 no parse do regex; só lookahead, universal). + 2 itens deferidos do 60.87: buildAlerts conta o MÊS CORRENTE por impacto (alerta "guardando R$ X este mês" usava o acumulado da vida toda); handler de viagem usa totalAvailable (reserva + mês) em vez de só o saldo do mês. Processo (ultracode): revisão adversarial (3 lentes — pegou o range + o risco iOS do lookbehind). 16 tests novos + testes 60.27.1/60.27.2 migrados pro render central. 3188 verdes (era 3172). Frontend: src/App.tsx. Base: 🩹 Lote 60.87 (Mony: fonte única de saldo — handlers + snapshot do fallback LLM): bug PROD de credibilidade — a Mony se CONTRADIZIA entre as próprias respostas no mesmo mês. Header −1.320,20 (correto), mas "qual meu maior gasto?" dizia "positivo +5.079,80, sem déficit" e o fallback LLM (Claude Haiku) dizia "−5.307,40 / saídas 39.584,17". Causa (auditoria por workflow de 4 agentes): cada camada contava diferente — (a) o handler "causa principal" decidia déficit/positivo por currentFluxo.net (projeção do Fluxo, que INCLUI provisões pending; o user tinha provisão de entrada pending ~6.400: −1.320,20 + 6.400 = +5.079,80); (b) buildPulseSnapshot (alimenta o Haiku) usava calcStats(txs) = acumulado da vida TODA (saídas 39.584,17 ≠ 35.596,97 do mês). Fix: tudo pela MESMA fonte canônica do header (calcMonthStats(filterTxsByImpactMonth) + calcReserve, por mês de impacto) — (1) o gate "positivo vs déficit" do handler usa o saldo REALIZADO (balance), nunca a projeção; (2) buildPulseSnapshot usa o mês corrente e agora envia previousBalance + totalAvailable pro LLM (aviso anti-alarmista: mês negativo MAS reserva cobre — não trate como emergência); backend api/pulse/llm-fallback.ts (Snapshot + buildSnapshotText) atualizado; (3) as 3 chamadas de computeFluxoProjection no localReply passam previousBalance + paidFaturas. Deferido (lote próprio): buildAlerts por mês + handler de viagem usar totalAvailable. Processo (ultracode): workflow de auditoria (4 agentes) + revisão adversarial (3 lentes — correção ✅, regressão ✅, completude). 9 tests novos = 3172 verdes (era 3163). Frontend: src/App.tsx; Backend: api/pulse/llm-fallback.ts. Base: 🩹 Lote 60.86 (Radar/insights/gráfico/Mony por mês de fatura — consistência total): continuação do 60.85 — 3 absurdos pegos pelo user em PROD. O 60.85 unificou Dashboard/Lançamentos/Fluxo pro mês da fatura, mas o Radar do Dia e os insights/alertas ficaram contando por DATA DA COMPRA → divergiam da Home. Sintomas (Junho, saldo Home −1.320,20): (1) Radar mostrava "saldo negativo em R$ 4.795,20" (= −1.320,20 − 3.475 das compras Junho-data/Julho-fatura); (2) projeção "−R$ 560,64" confusa (menos negativa que o saldo — a fatura sumia do mês, ficava escondida no "arrastado"); (3) alerta "1 dia até entrar no limite" estando JÁ em déficit (o sobraThis do insight era positivo por data da compra). Causa (workflow de diagnóstico, 4 agentes): computeRadarOfDay (t.date.startsWith) e computeProactiveInsights (ymOf(t.date)) não foram migrados no 60.85. Fix (impact-month via txImpactMonth/filterTxsByImpactMonth + exclui pending, igual Dashboard): Radar (thisMonth + prevMonthTxs por impacto → saldo/projeção batem com a Home e a projeção volta a ser coerente), insights (todos os triggers por impacto; o Trigger 5 "dias até o limite" não dispara quando já em vermelho, pois sobraThis vira negativo), deriveChart (gráfico de 8 meses do Dashboard), estimateFixedExpenses/estimateSurvivalDays (respostas da Mony) e computeMicroReaction (toast ao lançar). Não tocados (justificado por revisão adversarial): buildAlerts (usa calcStats sobre toda a história, sem atribuição de mês — não pode contradizer o saldo) e detectRecurringTxs (já ignora cartão desde o 60.84). Processo (ultracode): workflow diagnóstico (4 agentes) + revisão adversarial (3 lentes — correção ✅, regressão ✅, completude pegou 6 candidatos → 4 corrigidos, 2 falso-positivos). 13 tests novos = 3163 verdes (era 3150). Backend INTACTO. Frontend: src/App.tsx. Base: 🩹 Lote 60.85 (contabilidade de cartão unificada nas 3 telas — saldo consistente): bug PROD CRÍTICO de credibilidade — um user com 2 cartões (faturas pagas) viu 3 saldos diferentes pro MESMO mês: Home déficit −2.023,21, Lançamentos +1.451,79, Fluxo +19.684,54. Causa (workflow de diagnóstico de 5 agentes): cada tela contava cartão diferente — Home por DATA DA COMPRA, Lançamentos por MÊS DA FATURA, Fluxo PULAVA fatura paga (sumia + inflava saldo). Lançamentos − Fluxo = 18.232,75 = exatamente as 2 faturas pagas. Fix (decisão do dono — mês da fatura canônico + fatura paga conta + selo): (1) helper puro txImpactMonth(t) = fonte única do mês de impacto (cartão→fatura; impactMonth manual; senão data); filterTxsByImpactMonth + calcReserve delegam pra ele; (2) Dashboard, Mony, Lançamentos e o handler de mês trocaram filterTxsCurrentMonthfilterTxsByImpactMonth (saldo conta cartão pela fatura em toda tela); (3) computeFluxoProjection: fatura paga não some mais — aparece "✓ PAGA" e continua contando; (4) calcUpcomingFaturas exclui as pagas (Home + Mony). Resultado: 3 telas com o MESMO saldo. Sem cartão = idêntico. Processo (ultracode): workflow diagnóstico (5 agentes) + revisão adversarial (3 lentes — sem dupla-contagem, telas reconciliam; pegou 1 HIGH na Mony → corrigido). 14 tests novos = 3150 verdes (era 3136) + testes dos Lotes 60.3/60.4/60.31/60.52/60.53/60-saldo atualizados. Backend INTACTO. Frontend: src/App.tsx. Base: 🎯 Lote 60.84 (seletor de vencimento: obrigatório sem pré-seleção + vale pra compra única): 2 pedidos do user — (1) ao marcar parcelado no cartão o vencimento não deve vir preenchido, e sim ser escolha obrigatória; (2) compra ÚNICA no cartão também deve deixar escolher o vencimento direto. Fix: (a) condição do seletor passou de `cardOn && installmentsOn` pra só `cardOn` → vale pra compra única E parcelada, label condicional "parcela N/M" ou "compra"; (b) sem pré-seleção — ao ligar o cartão (toggle) o form.date é limpo, nenhum chip marcado, idem ao marcar parcelado com cartão já ligado; (c) obrigatório — guard no saveTx bloqueia se cartão ON e vencimento não escolhido; (d) removidos o label "da compra" + a dica antiga do Lote 60.44; (e) detectRecurringTxs agora EXCLUI tx de cartão (com dia-01, 3+ compras de mesmo nome/valor virariam falso-positivo de "recorrente ~dia 1" — bug pego por revisão adversarial em 3 lentes). Processo: workflow de mapeamento (5 agentes) + revisão adversarial (3 lentes). 16 tests novos = 3136 verdes (era 3127) + testes dos Lotes 60.39/60.44/60.80/60.83 atualizados. Backend INTACTO. Frontend: src/App.tsx. Base: 🎯 Lote 60.83 (cartão+parcelado — seletor de VENCIMENTO direto): pedido do user no cartão Uniclass ("fechamento só na tela sem ação por trás, e ao lançar perguntar se a parcela 9 vence 15/06 ou 15/07, e a partir daí ajustar a próxima — pois hoje só entra no 15/06 se eu colocar a data da compra 01/06"). O modelo era de trás pra frente: o user sabia o vencimento, mas tinha que codificar isso numa data cujo dia (vs fechamento) jogasse a parcela na fatura certa. Fix: pra cartão+parcelado, o campo Data vira um SELETOR DE VENCIMENTO (chips: mês atual + 2 à frente, ex 15/06 · 15/07 · 15/08, + o selecionado se editar fora da janela). O user escolhe direto em qual fatura a parcela atual cai; a próxima encadeia sozinha na dica ("parcela 9/10 → 15/06 · parcela 10/10 → 15/07"). Fechamento fica informativo. Sem tocar no motor: ao escolher o mês mk, grava tx.date = "<mk>-01" (dia 1 cai sempre nessa fatura, 1 ≤ closingDay) → calcFaturaMonthKey mapeia certo e o display (Lote 60.82) mostra o vencimento, nunca o "01". Casos sem cartão / cartão sem parcelado seguem com o input de data. Novo helper puro faturaCandidates(today, count). 14 tests novos = 3127 verdes (era 3113, +14) + asserção de estrutura do 60.44 atualizada. Backend INTACTO. Frontend: src/App.tsx. Base: 🩹 Lote 60.82 (cartão — data exibida vira o VENCIMENTO da fatura, não a âncora): observação fina do user no cartão Uniclass ("o cartão vence todo dia 15, do jeito que está, está incorreto") — a tx "Para Cartão 9/10 · Uniclass" mostrava 01/06 (a data-âncora da parcela que ele digitou pra escolher a fatura), mas pra cartão a data que importa é o vencimento (15), quando o dinheiro sai. Causa (meio-caminho do Lote 60.52): o 60.52 já agrupava o cartão pelo MÊS da fatura, mas seguia exibindo o DIA da compra/âncora. Fix: helper puro txDisplayDayMonth(date, card) — pra cartão devolve o dueDay no mês da fatura (calcFaturaMonthKey(date, closingDay)); sem cartão, data própria. Aplicado em: Lançamentos (item + subitem de grupo), Recentes do Dashboard (Lote 60.54) e linha de cada parcela dentro da fatura no Fluxo de Caixa. Resultado: "Para Cartão 9/10 · Uniclass" passa de 01/06 → 15/06 (fatura Junho). Campo de ENTRADA segue sendo data (o dia decide a fatura); só o EXIBIDO virou o vencimento. Sem cartão = data digitada preservada. 14 tests novos = 3113 verdes (era 3099, +14) + asserção do 60.22 atualizada. Backend INTACTO. Frontend: src/App.tsx. Base: 🩹 Lote 60.81 (Fluxo de Caixa — parcela ATUAL de cartão editável na fatura): bug PROD pego pelo user no cartão Uniclass ("deveria conseguir editar e não consigo") — a parcela atual de um cartão parcelado (ex: "Para cartão (9/10)" dentro da Fatura Uniclass) não tinha os botões ✏️/⊘, enquanto a parcela atual de parcelas SEM cartão (Carro 25/48, MP Dessa 21/24) sempre teve. Causa: o Lote 60.22.7 só dava txId+parcelaIdx pra parcela FUTURA (parcelaIdx > current) no cartão, assumindo "a atual já é tx real do mês, edita via Lançamentos" — premissa que quebra no cartão+parcelado, onde a parcela atual pode cair numa fatura FUTURA (compra 15/06 parcela 9/10 fechamento 08 → fatura Julho), longe do mês corrente. Fix: removido o gate isFutureParcela — toda parcela de cartão (inclusive a atual) recebe txId+parcelaIdx, expondo os mesmos handlers de edit/skip já provados (espelha as parcelas não-cartão). Tipo do pushItem/faturasPorCartao ampliado. Compra única no cartão continua sem botões. 8 tests novos = 3099 verdes (era 3091, +8) + asserções do 60.22.7 atualizadas. Backend INTACTO. Frontend: src/App.tsx. Base: 🩹 Lote 60.80 (cartão parcelado — a Data ancora a parcela ATUAL): confusão PROD pega pelo user no cartão Uniclass — lançou compra parcelada 9/10 pondo a data da COMPRA original (10/09/2025) estando na parcela 9, fechamento 08 / vencimento 15, pedindo "contar a partir de junho", mas o app jogou tudo em Outubro/2025. Causa raiz: a projeção ancora a parcela ATUAL na tx.date via parcelaDate(date, pIdx - current), então leu "parcela 9 caiu em 10/09/2025" → dia 10 > fechamento 08 → fatura Out/2025. Somado a: impactMonth ("contar a partir de junho") é IGNORADO em cartão (precedência card > impactMonth > date) + Lançamentos não espalha parcelas. Fix (Opção B turbinada, sem migração de dados, backend intacto): (1) label da Data com parcelado ativo vira "parcela {current}/{total}" em vez de "da compra"/"1ª parcela" — precedência nova provisioned > installments > card; (2) dica dinâmica ao vivo mostra em qual fatura (cartão) ou mês (sem cartão) a parcela atual + as próximas 2 caem, reusando parcelaDate + calcFaturaMonthKey + monthLabelFromKey — user vê "parcela 9/10 → Outubro 2025" e corrige a data; (3) gate !installmentsOn na dica de cartão antiga pra não duplicar. Não fizemos (decisão consciente, lote dedicado): mudar a semântica do campo date (breaking pra parcelas em PROD) nem impactMonth valer pra cartão. 21 tests novos = 3091 verdes (era 3070, +21) + asserções dos Lotes 60.39/60.44 atualizadas. Backend INTACTO. Frontend: src/App.tsx. Base: 📅 Lote 60.79 (Lançamentos: sort por date DESC + filtro chips por período): Bug reportado pelo user via screenshot PROD — lista de Lançamentos vinha ordenada por `createdAt` (último lançado primeiro), confuso quando lançava tx retroativa. Cenário real visível na screenshot: JD Joelma 02/06 lançada AGORA aparecia ACIMA de Smart TV 06/06. Fix: helper puro `sortTxsByDateDesc(txs)` ordena por `date` DESC, com `id` DESC como tiebreaker (preserva ordem de inserção entre tx de mesma data — última lançada primeiro DENTRO do mesmo dia). Bonus pedido pelo user na mesma screenshot: filtro por data dentro do mês via chips toggle "Mês inteiro · Hoje · Ontem · Últimos 7d" abaixo do tab de tipo (Todos/Entradas/Saídas), dentro do bloco "📅 Período". Helper puro `applyDateFilter(txs, range, today)` testável isolado, AND com filtros existentes de tipo + categoria. Default "all" = comportamento atual preservado. Visível só fora da aba Metas (filter !== "goals"). Sort aplicado APÓS todos os filtros, antes do agrupamento de cartões. Cada toggle reseta usersPage(0) anti bug de paginação stale. 24 tests novos = 3070 verdes (era 3046, +24). Backend INTACTO. Frontend: src/App.tsx. Base: 📣 Lote 60.78 (sobre.html — labels leigos nas 6 badges de auditoria): User notou que os labels técnicos das badges ("Security Headers", "HTTP Observatory", "Website Grader", "PageSpeed Mobile") não eram entendíveis pelo usuário leigo. Sugestão dele: frase amigável em destaque + nome técnico embaixo. Trocas (badge-label leigo · badge-source técnico): "Security Headers" → Servidor blindado · "Snyk (Security Headers)"; "HTTP Observatory" → Conexão segura · "MDN/Mozilla (HTTP Observatory)"; "Website Grader" → Site bem feito · "HubSpot (Website Grader)"; "PageSpeed Mobile" → Rápido no celular · "Google (PageSpeed)"; "Acessibilidade" → Acessível pra todos · "WebAIM (WAVE)"; "Sem malware" → Sem vírus nem golpe · "Sucuri Sitecheck". Estratégia preserva credibilidade do nome técnico pra quem reconhece + comunica claramente pro leigo. Meta description também atualizada com linguagem leiga. Backend INTACTO. Frontend: só public/sobre.html. Base: 🏅 Lote 60.77 (sobre.html — 6 badges de auditoria atualizados): Lote 60.76 conquistou PageSpeed mobile **100/100/100/100** (Desempenho + Acessibilidade + Boas Práticas + SEO TODOS perfeitos no Moto G Power emulado 4G lento). A seção "Pode confiar" do /sobre.html ficou desatualizada citando "PageSpeed 94+" da era pré-otimização. Atualizações: (1) PageSpeed 94+ → 100 + label vira "PageSpeed Mobile" pra ser explícito; (2) Headline "4 auditorias independentes" → "6 auditorias independentes"; (3) 2 badges novos com auditorias rodadas na sessão de manutenção — WAVE AIM 9.9 (Acessibilidade · WebAIM) + Sucuri ✓ (Sem malware · 9 blacklists limpas: Google Safe Browsing + McAfee + ESET + PhishTank + Yandex + Opera + Sucuri Labs + outras); (4) meta description atualizada citando as novas auditorias. Badges originais preservados: Snyk A+ Security Headers, MDN/Mozilla A+ HTTP Observatory, HubSpot 100 Website Grader. Grid CSS mantido (1fr 1fr): 6 badges viram 3 linhas × 2 colunas naturalmente. 12 tests novos = 3046 verdes (era 3034, +12). Backend INTACTO. Frontend: só public/sobre.html. Base: 🎯 Lote 60.76 (skeleton estático LCP — PageSpeed 92 → 95+ esperado): Lote 60.75 fez code-split do AdminPanel (bundle -17%) mas PageSpeed mobile continuou 92. Diagnóstico real do PageSpeed v1.2.149 revelou *"Detalhamento da LCP — Atraso na renderização do elemento: 2810ms"* (TTFB 0ms = Vercel Edge perfeito, problema é só timing JS). Causa raiz: <main id="root"> ficava VAZIO no HTML estático; browser esperava o React bootar (~2.8s no Moto G Power emulado) antes de pintar QUALQUER coisa. Fix cirúrgico: replicar visualmente o conteúdo da tela de login (logo "monify.app" + "COPILOTO COMPORTAMENTAL" + <h1>Evita o colapso financeiro</h1> + tagline completa) DENTRO de <main id="root"> como HTML estático. CSS do skeleton inline no <style> critical com classes .lcp-skel-* usando CSS vars (--bg/--tx/--lime) — themes dark/light continuam funcionando. Browser pinta o LCP element IMEDIATAMENTE no parse do HTML; quando React monta, createRoot().render() substitui sem flash perceptível (visual idêntico). Resultado esperado: LCP delay 2810ms → ~300ms, PageSpeed mobile 92 → 95-97. 17 tests novos = 3034 verdes (era 3017, +17). Backend INTACTO. Frontend: index.html. Base: ⚡ Lote 60.75 (code-split AdminPanel — PageSpeed mobile 92 → ~95+): user reportou que PageSpeed mobile caiu de 95+ pra 92 com warning "245KB de JS não usado" no bundle inicial. Diagnóstico do build: bundle único `index-*.js` de 1.2MB (308KB gzip), warning explícito do Vite sobre chunks >500KB. Causa: AdminPanel (2940 linhas, painel super-admin usado por <1% do tráfego) vinha junto no bundle inicial. Fix cirúrgico: extrair AdminPanel pra `src/AdminPanel.tsx` separado + `React.lazy(() => import("./AdminPanel"))` + `` na rota /admin. Constantes/helpers/types (BG/LIME/TX/ENV/ADMIN_VERSION/ADMIN_API_URLS/fmt/localDateKey/formatActiveTime/deriveAdminChart/applyUsersFilters/AdminUser/etc) ganharam `export` pra serem importadas pelo arquivo extraído. Resultado medido: bundle inicial caiu de 1228KB → 1023KB (-205KB raw, 308KB → 291KB gzip, -17KB gzip), AdminPanel virou chunk lazy de 117KB (23KB gzip) que só baixa em /admin. Bundle inicial -17% pra 99% dos usuários. Tests anti-regressão dos Lotes 58/60.18/60.73/60.74 atualizados pra ler App.tsx + AdminPanel.tsx concatenados. 22 tests novos = 3017 verdes (era 2995, +22). Backend INTACTO. Base: 🔎 Lote 60.74 (filtros admin combináveis + chip Pagante + LGPD privacy.html): 3 melhorias em 1 lote pra fechar observabilidade do painel admin. (1) Filtros combináveis (AND entre dimensões) por coluna no painel: chips toggle Plano (Free/Pro/Juntos/🎁 Trial) + chips toggle Auth (🔑 Senha/G Google) + dropdown Cadastro (Todos/Hoje/Últimos 7d/Últimos 30d), além do search Nome+Email existente. Botão "✕ Limpar" RED aparece só quando algum filtro ativo. Cada toggle reseta usersPage(0) anti bug de paginação stale. Helper puro applyUsersFilters(users, filters) + hasActiveUsersFilters exportados em escopo de módulo pra teste isolado. AND entre dimensões, OR dentro da mesma dimensão (multi-select). (2) Chip LIME "💳 PIX" / "💳 Cartão" ao lado do plano pra identificar conversão paga real (paymentMethod ∈ {pix, card} e NÃO trial) — distingue visualmente conversão paga de cortesia Trial (amber). (3) public/privacy.html atualizada: linha explícita sobre "método de autenticação utilizado (e-mail e senha, login com Google)" na seção 3.1 + parágrafo novo explicando que no login social a Monify recebe do provedor apenas nome + e-mail verificado, NUNCA a senha do usuário no provedor. ADMIN_VERSION 1.5.0 → 1.6.0. 39 tests novos em lote60-74 = 2995 verdes (era 2956, +39). Backend INTACTO. Frontend: src/App.tsx. Base: 🛂 Lote 60.73 (coluna Auth no painel admin): após Lote 60.72 recuperar 2 users Google invisíveis em PROD, user pediu visibilidade de qual método de autenticação cada user usou (Senha tradicional vs Login com Google) pra suporte operacional + observabilidade. Análise LGPD: authProviders é metadado operacional, NÃO PII sensível Art. 5 II — base legal Art. 7 V (execução de contrato). Admin já vê dados mais sensíveis (CPF redacted, partnerEmail). Backend: api/admin/users.ts parseia authProviders defensivamente (array OR string JSON via Upstash auto-parse), inferência retroativa ["password"] quando passwordHash existe sem campo (users pré-60.60). Retorna em PROD e DEV. Frontend: coluna nova "Auth" entre "Plano" e (DEV-only Pref), chip cinza "🔑 Senha" + chip BLUE "G Google" lado a lado quando há múltiplos providers. Tooltips explicam cada chip. Empty state "—" pra legado raro. ADMIN_VERSION 1.4.0 → 1.5.0. 19 tests novos = 2956 verdes (era 2937, +19). Base: 🔧 Lote 60.72 (backfill admin retroativo): user reportou em PROD que mesmo após Lote 60.71 (que corrigiu o zadd faltante pra cadastros NOVOS) um usuário cadastrado via Google continuava INVISÍVEIS no painel admin — porque ele tinha sido criado ANTES do deploy de v1.2.145 e nunca foi indexado em users:by_created. Solução: novo endpoint admin POST /api/admin/backfill-users-index (GET=dry-run) que varre Redis via SCAN cursor-based (não bloqueia Upstash), filtra chaves do shape ^user:[^:]+@[^:]+$ (rejeita subkeys :txs/:push/:paid_faturas/:categories), lê createdAt do hash e adiciona no zset com esse score (preserva ordem cronológica real). Idempotente via zscore — não sobrescreve quem já está indexado, pode rodar várias vezes. Fallback Date.now() com marcador "fallback-now" se createdAt ausente/inválido. Audit log backfill-users-index. 13 tests novos = 2937 verdes (era 2924, +13). Base: 🩹 Lote 60.71 (2 bugs CRÍTICOS PROD): Bug 1 — usuários cadastrados via Login com Google estavam INVISÍVEIS pro painel admin pq socialAuth.ts esquecia zadd("users:by_created", ...) que /api/signup já fazia (linha 105). Fix: espelha lógica do signup tradicional no helper Google quando isNewUser=true, não-bloqueante. Bug 2 — convite Juntos via login Google não vinculava automaticamente (cenário real Tiago→Ligia em PROD v1.2.144): user precisou vincular manualmente via /api/admin/force-link. Causa: race condition do useEffect retry de invite com React 19 batching de setStates. Fix: chamar tryAcceptPendingInvite(newProfile) EXPLICITAMENTE dentro do callback Google após hidratar profile + setLoggedIn, sem depender de timing de useEffect. Idem em handleReactivateAccount (cobertura completa pra cenário raro de reativação + invite).
último sync: 2026-07-01
main: HEAD · **Lote 34 (LLM Monitor)**: novo painel admin `/api/admin/pulse-llm-monitor` agrega telemetria detalhada do Claude Haiku — custo USD **real** (tokens devolvidos pela Anthropic), latência, cache hit rate, taxa de sucesso, projeção mensal vs orçamento (US$ 18). Telemetria nova `pulse:llm:events:YYYY-MM-DD` registra cada chamada (sucesso/falha/cache) com tokens, latência, classificação de erro (timeout/rate_limit/anthropic_http/empty/auth/config). UI: 6 KPI cards coloridos por status (verde/âmbar/vermelho), alertas inteligentes ("top 5 queries dominam X% do custo → vira handler local pra economizar US$/mês"), gráfico de barras custo/dia, tabela top 20 perguntas ordenadas por custo com ação inline "✓ tratar" (reusa hash `pulse:fb_handled` compartilhado com Pulse Analytics) e botão 📋 copiar pra começar regex local, seção dedicada "❌ LLM também falhou" pra urgência alta, chips de breakdown de erros. Janela padrão = hoje (custo é decisão de agora, não de 7 dias atrás). **Lote 33 (Pulse híbrido — mantido)**: keyword first → Claude Haiku 4.5 quando bate fallback. Snapshot agregado, cache 15min, rate limit 10/min+100/dia, fail-open. 769 testes verdes. Zero regressão.
⚡ MONIFY PULSE
localReply + LLM fallback
camada 1 local (zero custo)
+ Claude Haiku como rede de segurança
v1.2.239+ · 3768 testes · 320 handlers · 🎯🎉💧🔒📝🔐👥💬🛡️♿🎨🩹💳🚨🔧🧹🛡️📨🎨🐛💚🔗📋🩹🔐⏱️🩹🔧🛂🔎⚡🎯🏅📣📅🩹🩹🩹🎯🎯🩹🩹🩹🎨🎨🎨🧠🧪🎯🎓💡🩺🔧🔧🔭🗺️🔮🧬🛟🔁📣📝🏆🪞📚🧪📊🐛🔗🤝💬 · LGPD ✅
CORE
Engine NLP híbrida
localReply rápido + LLM fallback
MEMÓRIA
ChatContext + continuity
KV · user:<email>:chatctx
MONETIZAÇÃO
Conceder/cancelar Pro grátis
cortesia controlada · cron expiração
PROATIVO
Insights + Radar + Push
determinístico · ranqueado · ritual de retorno
QA · TELEMETRIA
Arena + Analytics + LLM Monitor
1399 testes · 16.166 sim · custo Anthropic em tempo real
SEGURANÇA · LGPD
Hardening
defense-in-depth
PAGAMENTO
PIX QR inline + Webhook
/v1/payments direto · sem redirect
CÁLCULO
Engine financeira
fórmulas determinísticas · caps anti-absurdo