Seis consertos numa varredura que existe para matar exatamente esta classe.
**1. A régua não delimitava a seção (AGENTS.md).** A linha trocava o número pelo
comando — e o comando, `grep -c '^[0-9]\+\. \*\*' CLAUDE.md`, devolve 25 enquanto a
mesma linha afirma 16. Casa toda linha numerada em negrito do arquivo (anti-patterns,
packaging, higiene de branches, migrations) e perde os itens 1–10 do próprio DoD, que
não são negrito. Trocar número por comando só ajuda se o comando responder à pergunta:
`sed -n '/^## Definition of Done/,/^Um staff engineer/p' CLAUDE.md | grep -cE '^[0-9]+\. '`
devolve 16 nas três árvores em que medi.
**2. `EH_PLACEHOLDER` casava prefixo sem fronteira.** A alternativa `bar` casava o começo
de `barra-do-navegador` — `lib/branding/barra-do-navegador.ts` existe hoje e estava
invisível ao gate. Ancorado o fim do segmento. Provado: path morto começando com "bar"
passava verde antes, reprova agora.
**3. A isenção de citação vetava a LINHA por uma palavra.** `!l.includes("dizia")`
deixava passar `o \`imagens-ok\` ainda não é obrigatório, ao contrário do que a doc dizia`
— afirmação falsa e EM VIGOR, isenta porque a palavra vinha depois dela. Em prosa pt-br
"dizia" é comum; vetar a linha por contê-la é vetar quase qualquer parágrafo. A isenção
passa a olhar só o que vem ANTES da frase: quem cita põe a marca antes, quem afirma põe
a frase primeiro. Três casos medidos — afirmação em vigor reprova, citação legítima
passa, árvore limpa 4/4.
**4. `docs/index.md` não registrava `docs/audits/`.** Pasta nova, dois arquivos, nenhum
no índice — e o `CLAUDE.md:8` manda usar o índice antes de varrer `docs/`. Um deles não
era citado por nenhum outro `.md`: ilha, contra o invariante 1 do Sistema Vivo.
**5. A contagem do próprio índice.** Dizia 149 arquivos em 24 subpastas; a régua devolve
154 e 20 — e a régua das subpastas nem existia. Ambas escritas ao lado do número.
**6. Os 14 blocos "comando que mede" abriam com o path da minha máquina.** Num repo
open source a régua tem que ser copiável: `cd "$(git rev-parse --show-toplevel)"`.
Executei a da linha 516 depois da troca — devolve 154, batendo com o índice.
Mais a data do front-matter de `current-state.md` e `harness-audit.md`, que dizia
2026-07-27 para um commit de 2026-07-29 — o campo estruturado, sete linhas acima do
banner que o PR já tinha corrigido.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UdBExiJrE84JCWG16ZAgXN
242 KiB
Auditoria — afirmações de estado na documentação
Data: 2026-08-14 · Medido contra: origin/main @ 840917ed
Método: 11 agentes em paralelo, um por grupo de documento, cada um extraindo toda frase que afirma como o mundo está e medindo-a contra a fonte com um comando real. Depois dois passes adversariais: um tentando derrubar os vereditos "VERDADEIRA", outro caçando o que ninguém olhou.
Resultado: 393 afirmações medidas · 166 confirmadas · 227 com problema · 4 vereditos "VERDADEIRA" derrubados · 19 achados em documentos não cobertos.
Como usar este documento
Cada achado traz o comando que o mede. É o comando que vale, não o veredito. Este relatório é uma fotografia de 2026-08-14 e envelhece exatamente como o que ele critica — quem for tratar um achado roda o comando de novo antes de confiar na linha.
A triagem é por tipo de documento, não por gravidade:
| Tipo | O que fazer |
|---|---|
Autoridade — doutrina, ADR, runbooks, CLAUDE.md, AGENTS.md, CONTRIBUTING.md, READMEs |
Consertar. E onde a afirmação puder virar comando, trocar em vez de corrigir: número corrigido envelhece de novo, rode isto para saber não envelhece. |
Retrato — docs/current-state.md, docs/harness-audit.md |
Não corrigir as contagens. Carimbar no topo o SHA e a data contra os quais os números conferem, e parar de manter. Documento honestamente datado nunca mente; documento atualizado uma vez volta a mentir. |
Registro — CHANGELOG.md |
Cada seção descreve uma versão e é histórica por natureza. Só se corrige o que afirma sobre aquela versão e é falso. |
Planejamento — docs/stories/, docs/handoffs/, docs/specs/ |
Deixar como está. Registro histórico pode apontar para o que nunca existiu; os 385 ponteiros mortos do repo vivem aí, e o gate os exclui de propósito. |
O gate tests/unit/documentacao-aponta-para-o-que-existe.test.ts impede a reincidência
das duas piores formas (ponteiro morto e nota de pendência sobrevivente) nos documentos
de autoridade. O resto é dívida que decai sozinha se ninguém alimentar.
AUTORIDADE — 152 achados
AGENTS.md — 22
L24 · FALSA · gravidade alta · sobre-o-codigo
WAHA Plus (engine NOWEB)
Mede com:
sed -n '110,152p' docker-compose.prod.yml
Deu:
image: ${WAHA_IMAGE:-devlikeapro/waha:latest-2026.7.2}
# Core grátis por padrão; troque para devlikeapro/waha-plus via WAHA_IMAGE se
# precisar de multi-número/retry/S3 (licença paga).
WHATSAPP_DEFAULT_ENGINE: ${WHATSAPP_DEFAULT_ENGINE:-NOWEB}
Sugestão: WAHA (engine NOWEB; o docker-compose.prod.yml entrega Core por padrão — devlikeapro/waha. Plus é opt-in por WAHA_IMAGE=devlikeapro/waha-plus, e é o que habilita multi-número/retry/S3). Confira: grep -n 'WAHA_IMAGE' docker-compose.prod.yml.
Vira teste: tests/unit: o default de WAHA_IMAGE em docker-compose.prod.yml e a palavra usada em AGENTS.md/CLAUDE.md concordam (Core vs Plus)
L26 · FALSA · gravidade alta · pendencia
o job
ciroda 22, mas operfainda builda em 20 — divergência comengines, registrada como bug
Mede com:
grep -rn "node-version" .github/workflows/
Deu:
.github/workflows/perf.yml:22: node-version: 22
.github/workflows/ci.yml:19: node-version: 22
.github/workflows/ci.yml:69: node-version: 22
.github/workflows/e2e.yml:164: node-version: 22
(perf.yml traz o comentário: "# 22 como o job `ci` e o `.nvmrc`. Buildar numa versão que o `engines` (>=22) não suporta esconde quebra ou inventa falha.")
Sugestão: Runtime: Node ≥22 (.nvmrc = 22; todos os jobs de CI fixam a mesma versão). Confira: grep -rn 'node-version' .github/workflows/.
Vira teste: tests/unit: todo node-version: em .github/workflows/ é igual ao conteúdo de .nvmrc
L28 · FALSA · gravidade alta · data-versao
Versão do produto: 1.0.0 (
CHANGELOG.md, SemVer — mudança que afeta quem roda VPS entra lá).
Mede com:
grep -nE '^## \[' CHANGELOG.md | head -6 ; git tag --list | tail -6 ; node -e "console.log(require('./package.json').version)"
Deu:
9:## [Não lançado]
11:## [1.3.0] — 2026-08-13
72:## [1.2.1] — 2026-08-12
117:## [1.2.0] — 2026-08-11
258:## [1.1.0] — 2026-07-30
280:## [1.0.0] — 2026-07-27
tags: v1.0.0 v1.1.0 v1.1.1-jmpo.1 v1.2.0 v1.2.1 v1.3.0
package.json version: 0.1.0
Sugestão: Versão do produto: a última lançada é o topo de CHANGELOG.md (grep -m2 -E '^## \[' CHANGELOG.md) e tem tag git correspondente (git tag --list 'v*' | tail -1). SemVer — mudança que afeta quem roda VPS entra lá. (package.json segue em 0.1.0 e não é a fonte da versão do produto.)
Vira teste: tests/unit: a versão do topo de CHANGELOG.md tem uma tag git v<versão> correspondente
L34 · FALSA · gravidade media · contagem
166 route handlers REST (versionado por path) — 169 contando
app/api/**
Mede com:
git ls-files 'app/api/v1/**/route.ts' | wc -l ; git ls-files 'app/api/**/route.ts' | wc -l ; git ls-tree -r 741c4ec8 --name-only | grep -cE '^app/api/v1/.*/route\.ts$'
Deu:
201
203
(no SHA 741c4ec8 que o doc cita noutro bloco: 200)
Sugestão: route handlers REST versionados por path — reconte com git ls-files 'app/api/v1/**/route.ts' | wc -l (e git ls-files 'app/api/**/route.ts' | wc -l para o total de app/api/**).
L54 · FRAGIL · gravidade baixa · sobre-o-codigo
pnpm test:unit # vitest — EXCLUI tests/invariants e tests/e2e
Mede com:
sed -n '25,33p' vitest.config.ts
Deu:
exclude: [
"**/node_modules/**", ".next", "dist", ".claude/**",
"tests/e2e/**", "tests/invariants/**", "tests/journeys/**",
]
Sugestão: pnpm test:unit # vitest — EXCLUI tests/invariants, tests/e2e e tests/journeys (lista viva em vitest.config.ts → exclude)
L57 · FALSA · gravidade media · sobre-o-codigo
pnpm gov:verify # typecheck + lint + test:unit ← verificação única atual
Mede com:
node -e "console.log(require('./package.json').scripts['gov:verify'])"
Deu:
pnpm typecheck && pnpm lint && pnpm lint:channels && pnpm test:unit
Sugestão: pnpm gov:verify # typecheck + lint + lint:channels + test:unit ← verificação única atual # (confira o encadeamento real: node -e \"console.log(require('./package.json').scripts['gov:verify'])\")
Vira teste: tests/unit: a lista de passos descrita em AGENTS.md/CLAUDE.md para gov:verify bate com o script em package.json
L64 · FALSA · gravidade alta · sobre-o-codigo
.github/workflows/ci.yml:verify= typecheck + lint + test:unit
Mede com:
sed -n '9,58p' .github/workflows/ci.yml
Deu:
Steps do job `verify`: Typecheck (pnpm typecheck) · Lint (pnpm lint) · Channel provider leak (pnpm lint:channels) · Unit tests (pnpm test:unit) · Kit self-host (bash) (pnpm test:shell) — cinco passos, não três.
Sugestão: .github/workflows/ci.yml: verify = typecheck + lint + lint:channels + test:unit + test:shell (cinco passos — pnpm lint sozinho não cobre lint:channels nem o kit self-host). Reconfira com awk '/^ verify:/,/^ invariants:/' .github/workflows/ci.yml | grep -A1 'name:'.
Vira teste: tests/unit: os passos run: do job verify de ci.yml estão todos citados no bloco de CI de AGENTS.md
L67 · FALSA · gravidade alta · contagem
roda 45 das 46 specs Playwright contra um Supabase local de verdade
Mede com:
ls tests/e2e/*.spec.ts | wc -l ; awk '/^ SPECS_PARTE_1:/{f=1;next} /^ SPECS_PARTE_2:/{f=0} f' .github/workflows/e2e.yml | grep -coE '[a-z0-9-]+\.spec\.ts' ; awk '/^ SPECS_PARTE_2:/{f=1;next} /^ # Cada item aqui/{f=0} f' .github/workflows/e2e.yml | grep -coE '[a-z0-9-]+\.spec\.ts'
Deu:
disco = 48
SPECS_PARTE_1 = 25
SPECS_PARTE_2 = 22
FORA_DO_CI = 1
rodam = 47, soma = 48 (bate com o disco)
Sugestão: roda todas as specs Playwright menos as declaradas em FORA_DO_CI contra um Supabase local de verdade com o baseline.sql aplicado. O número não vai escrito aqui de propósito — ele já apodreceu quatro vezes. Meça: ls tests/e2e/*.spec.ts | wc -l (disco) e echo $SPECS_PARTE_1 $SPECS_PARTE_2 | wc -w nas listas de .github/workflows/e2e.yml.
Vira teste: tests/unit: SPECS_PARTE_1 + SPECS_PARTE_2 + FORA_DO_CI == ls tests/e2e/*.spec.ts (o repo já tem tests/unit/e2e-cobertura-completa.test.ts para a soma; falta o gate sobre o NÚMERO citado nos docs)
L69 · FRAGIL · gravidade media · ativo-obrigatorio
É check obrigatório desde 2026-08-08.
Mede com:
gh api repos/melgarafael/DeskcommCRM/branches/main/protection --jq '.required_status_checks.contexts|join(", ")' ; sed -n '10,17p' .github/workflows/e2e.yml
Deu:
verify, build-and-size, invariants, e2e, imagens-ok
(mas o cabeçalho do PRÓPRIO e2e.yml ainda afirma o contrário: "# NÃO-BLOQUEANTE por ausência, não por mordaça: `e2e` não está na lista de checks obrigatórios da branch protection, então falhar aqui não segura merge.")
Sugestão: É check obrigatório — confirme antes de citar: gh api repos/melgarafael/DeskcommCRM/branches/main/protection --jq '.required_status_checks.contexts'. (A data de ativação não é auditável pelo repo; e o cabeçalho de .github/workflows/e2e.yml ainda diz que o e2e NÃO é obrigatório — esse comentário está podre e contradiz a branch protection.)
Vira teste: tests/unit: nenhum comentário em .github/workflows/ afirma que um check está fora dos obrigatórios sem bater com a lista da branch protection (ou, mais simples: o texto 'não está na lista de checks obrigatórios' não aparece em e2e.yml)
L114 · FALSA · gravidade media · contagem
lib/supabase/admin.ts— service role bypassa RLS. 89 rotas o usam
Mede com:
grep -rl "createAdminClient" app/api --include='route.ts' | wc -l ; git ls-files 'app/api/**/route.ts' | wc -l
Deu:
119
203
Sugestão: lib/supabase/admin.ts — service role bypassa RLS. Boa parte dos handlers de app/api/** o usa (reconte: grep -rl createAdminClient app/api --include=route.ts | wc -l contra git ls-files 'app/api/**/route.ts' | wc -l); toda query precisa filtrar organization_id manualmente, resolvido de fonte confiável (cookie/JWT/webhook secret/path token), nunca do body.
L130 · FALSA · gravidade baixa · contagem
lib/database.types.ts(6.1k linhas — gerado do schema Supabase)
Mede com:
wc -l < lib/database.types.ts
Deu:
7023
Sugestão: lib/database.types.ts (gerado do schema Supabase — wc -l lib/database.types.ts se quiser o tamanho de hoje)
L131 · FRAGIL · gravidade baixa · ponteiro
graphify-out/(grafo de conhecimento; regenerado por/graphify .)
Mede com:
test -d graphify-out && echo existe || echo ausente ; grep -n graphify .gitignore
Deu:
ausente
100:# graphify (análise local de knowledge graph)
101:graphify-out/
Sugestão: graphify-out/ (grafo de conhecimento local; ignorado pelo git e ausente num clone fresco — só existe depois de rodar /graphify .)
L153 · FRAGIL · gravidade media · contagem
257 arquivos de teste unitário em
tests/unit/... O repo tem 491 arquivos*.test.ts(x)no total
Mede com:
git ls-files 'tests/unit/*.test.ts' 'tests/unit/*.test.tsx' | wc -l ; git ls-files '*.test.ts' '*.test.tsx' | wc -l ; git ls-tree -r 741c4ec8 --name-only | grep -cE '^tests/unit/.*\.test\.tsx?$' ; git rev-list --count 741c4ec8..HEAD
Deu:
HEAD (840917ed): 264 e 511
741c4ec8 (o SHA declarado): 257 e 491
commits desde o SHA declarado: 45
Sugestão: Arquivos de teste unitário em tests/unit/ e no repo inteiro — conte, não cite: git ls-files 'tests/unit/*.test.ts' 'tests/unit/*.test.tsx' | wc -l e git ls-files '*.test.ts' '*.test.tsx' | wc -l. A diferença vive junto ao código, fora de tests/, e também roda em test:unit.
L154 · FRAGIL · gravidade baixa · contagem
102 arquivos de invariante de banco em
tests/invariants/
Mede com:
git ls-files 'tests/invariants/*.test.ts' | wc -l ; git ls-tree -r 741c4ec8 --name-only | grep -cE '^tests/invariants/.*\.test\.ts$'
Deu:
HEAD: 103
741c4ec8: 102
Sugestão: Arquivos de invariante de banco em tests/invariants/ (git ls-files 'tests/invariants/*.test.ts' | wc -l) — RLS/isolamento cross-tenant, RBAC, governança (G1–G6). Excluídos do test:unit de propósito; rodam via pnpm test:db e no job invariants do CI.
L157 · FALSA · gravidade alta · contagem
46 specs Playwright em
tests/e2e/(ls tests/e2e/*.spec.ts | wc -l). 45 rodam no CI
Mede com:
ls tests/e2e/*.spec.ts | wc -l ; awk '/^ FORA_DO_CI:/{f=1;next} /^ steps:/{f=0} f' .github/workflows/e2e.yml | grep -oE '[a-z0-9-]+\.spec\.ts'
Deu:
48 no disco; 47 nas listas de execução; FORA_DO_CI = vps-fresh-onboarding.spec.ts (1)
Sugestão: Specs Playwright em tests/e2e/ — reconte antes de citar (ls tests/e2e/*.spec.ts | wc -l), e o quanto disso roda no CI sai de echo $SPECS_PARTE_1 $SPECS_PARTE_2 | wc -w em .github/workflows/e2e.yml (check obrigatório). A única de fora está declarada em FORA_DO_CI — hoje vps-fresh-onboarding, por dependência de serviço externo (WAHA/Redis/Resend/Nuvemshop).
Vira teste: tests/unit: nenhum número literal de specs e2e sobrevive em AGENTS.md/CLAUDE.md sem bater com ls tests/e2e/*.spec.ts | wc -l
L159 · FRAGIL · gravidade media · ponteiro
Ver issue #63.
Mede com:
gh issue view 63 --repo melgarafael/DeskcommCRM --json number,title,state
Deu:
{"number":63,"state":"CLOSED","title":"E2E: 3 das 33 specs fora do CI (inclui a vps-fresh-onboarding, P0) + e2e ainda não é gate"}
Sugestão: A exclusão e o motivo medido vivem no bloco FORA_DO_CI de .github/workflows/e2e.yml — a issue #63, que originou a discussão, está fechada e seu título descreve um estado que já não vale ("3 das 33 specs", "e2e ainda não é gate").
L161 · FRAGIL · gravidade media · data-versao
Limitações conhecidas (estado em 2026-07-29, contra
origin/main@789dfa6)
Mede com:
git log --oneline -1 789dfa6 ; git rev-list --count 789dfa6..HEAD
Deu:
789dfa6f fix(audit): nenhuma acao via MCP era auditada, e o silencio escondia isso
(o cabeçalho declara 2026-07-29, mas o próprio bloco traz uma correção datada de 2026-08-14 na linha 166 — o SHA declarado não descreve o conteúdo do bloco)
Sugestão: ## Limitações conhecidas > Cada item abaixo carrega o comando que o mede. Um item sem comando é suspeito de estar podre — meça antes de repassar. A régua de datar o bloco inteiro num SHA foi abandonada: os itens envelhecem em ritmos diferentes e o cabeçalho passava a mentir por todos eles.
L163 · FALSA · gravidade media · contagem
1 das 46 specs E2E segue fora do CI (
vps-fresh-onboarding), e oe2eé check obrigatório desde 2026-08-08
Mede com:
ls tests/e2e/*.spec.ts | wc -l ; gh api repos/melgarafael/DeskcommCRM/branches/main/protection --jq '.required_status_checks.contexts|join(", ")'
Deu:
48
verify, build-and-size, invariants, e2e, imagens-ok
Sugestão: Exatamente 1 spec E2E segue fora do CI (vps-fresh-onboarding — a lista viva é FORA_DO_CI em .github/workflows/e2e.yml), e o e2e é check obrigatório. Ou seja: um PR que quebre o e2e não entra — mas a jornada de instalação fresca, que é o produto que se vende, continua sem gate. Se você mexeu nela, a prova é sua.
L168 · FALSA · gravidade alta · pendencia
Rate limit HTTP existe em 2 pontos do código (webhook de captação e dispatcher de IA); login, signup, aceite de convite, crons e MCP estão sem. Não há lockout por conta no login.
Mede com:
grep -rn 'auth/rate-limit' app/ lib/ proxy.ts --include='*.ts' --include='*.tsx' | grep -v '\.test\.' ; grep -n 'contaBloqueadaPorFalhas\|registrarFalhaDeLogin' app/actions/auth/signInWithPassword.ts ; sed -n '136,142p' lib/auth/rate-limit.ts
Deu:
app/team/accept-invite/[token]/page.tsx:14:import { authRateLimited, AUTH_LIMITS }
app/actions/auth/signUp.ts:14:import { authRateLimited, AUTH_LIMITS }
app/actions/auth/requestPasswordReset.ts:8:import { authRateLimited, AUTH_LIMITS }
app/actions/auth/signInWithPassword.ts:13: contaBloqueadaPorFalhas / 14: registrarFalhaDeLogin / 58: (await authRateLimited("login", null, AUTH_LIMITS.login)) || (await contaBloqueadaPorFalhas(...)) / 78: await registrarFalhaDeLogin(...)
AUTH_LIMITS = { login: {ip: loginIpLimit(), id: 5, windowSec: 300}, signup: {ip:20, windowSec:3600}, reset: {ip:30, id:3, windowSec:3600}, invite_accept: {ip:60, windowSec:3600} }
(além disso: app/api/v1/webhooks/in/[token]/
Sugestão: Rate limit HTTP cobre hoje o webhook de captação, o dispatcher de IA e as quatro portas de auth — login, signup, recuperação de senha e aceite de convite (AUTH_LIMITS em lib/auth/rate-limit.ts). Há lockout por conta no login: 5 senhas erradas na mesma conta em 5 min trancam a janela, inclusive contra ataque distribuído por muitos IPs (contaBloqueadaPorFalhas / registrarFalhaDeLogin, chamados em app/actions/auth/signInWithPassword.ts). Continuam sem limitador: crons e MCP. Reconfira quem importa o módulo: grep -rn 'auth/rate-limit' app/ lib/ --include='*.ts*' | grep -v '\.test\.'.
Vira teste: tests/unit: signInWithPassword chama contaBloqueadaPorFalhas antes do provedor e registrarFalhaDeLogin depois de senha errada (guarda o comportamento, não o nome)
L170 · FRAGIL · gravidade media · sobre-o-codigo
Fallback do rate limit é em memória — sem Upstash configurado o limite é por processo.
Mede com:
grep -n 'UPSTASH' lib/env.ts ; sed -n '44,62p' lib/ai/dispatcher/rate-limit.ts
Deu:
lib/env.ts:83: UPSTASH_REDIS_REST_URL: required("UPSTASH_REDIS_REST_URL"),
lib/env.ts:84: UPSTASH_REDIS_REST_TOKEN: required("UPSTASH_REDIS_REST_TOKEN"),
comentário do próprio módulo: "A versão anterior dizia que as duas variáveis do Upstash são 'opcionais no kit self-host, então sem elas este é o caminho normal': falso, e medido — `lib/env.ts:83-84` as declara `required()`, o app não sobe sem elas. O que de fato leva para cá é Redis INALCANÇÁVEL com a variável presente"
Sugestão: Fallback do rate limit é em memória — mas o gatilho não é "sem Upstash configurado": lib/env.ts declara as duas variáveis do Upstash como required() e o app não sobe sem elas. O que cai para a memória é Redis inalcançável com a variável presente (contêiner srh parado, rede caída, URL errada) — e aí o limite passa a ser por processo.
L172 · FALSA · gravidade alta · pendencia
6 vars de
lib/env.tsfaltam no.env.example, incluindo 3 secrets.
Mede com:
comm -23 <(grep -oE '^\s{2}[A-Z][A-Z0-9_]+:' lib/env.ts | tr -d ' :' | sort -u) <(grep -oE '^[A-Z][A-Z0-9_]+' .env.example | sort -u)
Deu:
NODE_ENV
(45 chaves em lib/env.ts; 54 no .env.example; única ausente é NODE_ENV, que não é secret nem var de configuração do operador. Spot-check dos secrets: AI_CRED_AES_KEY:40, CPF_ENCRYPTION_KEY:33, LGPD_SIGNING_KEY:132, IMPERSONATE_COOKIE_SECRET:124, NUVEMSHOP_OAUTH_ENCRYPTION_KEY:35, WAHA_BYO_ENCRYPTION_KEY:37, INTERNAL_SECRET:18, INTERNAL_CRON_SECRET:20, WAHA_HMAC_SECRET:50 — todos presentes)
Sugestão: Toda var de lib/env.ts está no .env.example exceto NODE_ENV (que o runtime define, não o operador). Se você adicionar env var, adicione nos dois lugares (item 9 do DoD) — e confira com comm -23 <(grep -oE '^\s{2}[A-Z][A-Z0-9_]+:' lib/env.ts | tr -d ' :' | sort -u) <(grep -oE '^[A-Z][A-Z0-9_]+' .env.example | sort -u).
Vira teste: tests/unit: toda chave do schema de lib/env.ts aparece em .env.example (allowlist explícita para NODE_ENV) — gate que substitui a contagem em prosa
L176 · FALSA · gravidade media · contagem
89 dos 169 handlers de
app/api/**usam service role — sem gate automático para o filtro deorganization_id
Mede com:
grep -rl "createAdminClient" app/api --include='route.ts' | wc -l ; git ls-files 'app/api/**/route.ts' | wc -l
Deu:
119
203
Sugestão: Boa parte dos handlers de app/api/** usa service role — sem gate automático para o filtro de organization_id. Meça hoje: grep -rl createAdminClient app/api --include=route.ts | wc -l de git ls-files 'app/api/**/route.ts' | wc -l. Escrevendo handler novo, o filtro é responsabilidade sua.
CLAUDE.md — 14
L8 · FALSA · gravidade media · contagem
índice dos 149 docs
Mede com:
cd "$(git rev-parse --show-toplevel)" && git ls-files 'docs/**/*.md' | wc -l && git ls-files 'docs/*.md' 'docs/**/*.md' | sort -u | wc -l && sed -n '13p' docs/index.md
Deu:
Régua declarada pelo próprio índice (`git ls-files 'docs/**/*.md' | wc -l`): 151. Contando também os 10 `.md` da raiz de docs/ (`git ls-files 'docs/*.md' 'docs/**/*.md' | sort -u | wc -l`): 161. docs/index.md:13 ainda diz "Mapa dos **149** arquivos `.md` de `docs/`, espalhados por **24** subpastas" — e `find docs -mindepth 1 -type d | wc -l` devolve 30. O cabeçalho do índice declara `audited_against: origin/main @ 789dfa6 (v1.0.0, 2026-07-27)`.
Sugestão: - [docs/index.md](docs/index.md) — índice dos docs de docs/, com regra de precedência quando dois docs discordam. Use antes de sair varrendo docs/. O total citado lá dentro está congelado em 2026-07-27 — se precisar do número, conte: git ls-files 'docs/*.md' 'docs/**/*.md' | sort -u | wc -l.
Vira teste: tests/unit/doc-index.test.ts: extrair o número em negrito de docs/index.md:13 e comparar com a régua que a própria linha declara; reprovar na divergência.
L31 · FALSA · gravidade media · sobre-o-codigo
Rate limit: Upstash Redis sliding window
Mede com:
cd "$(git rev-parse --show-toplevel)" && sed -n '17,20p' lib/auth/rate-limit.ts && sed -n '4p' lib/ai/dispatcher/rate-limit.ts
Deu:
lib/auth/rate-limit.ts:17-19: "Janela FIXA (é o que `checkRateLimit` implementa: `INCR` + `EXPIRE`), então uma rajada na virada da janela passa em dobro." lib/ai/dispatcher/rate-limit.ts:4: "Fixed-window counter on Upstash Redis: `ai-runs:<org>:<window-start>` with …". Nenhuma implementação de sliding window na superfície de rate limit.
Sugestão: - Rate limit: contador de janela fixa em Upstash Redis (INCR + EXPIRE, lib/ai/dispatcher/rate-limit.ts), com degradação para contador em memória do processo quando Upstash não está configurado. Janela fixa deixa passar rajada na virada — é limite de abuso, não de precisão. Sliding window nunca foi implementado; onde os docs antigos dizem isso, eles copiaram o PRD.
L32 · FRAGIL · gravidade baixa · data-versao
strings tipo
"anthropic/claude-sonnet-4-6"
Mede com:
cd "$(git rev-parse --show-toplevel)" && sed -n '24,34p' lib/ai/gateway.ts
Deu:
lib/ai/gateway.ts:24-33 — `export type ModelId = "anthropic/claude-sonnet-5" | "anthropic/claude-opus-5" | "anthropic/claude-haiku-4-5" | "openai/text-embedding-3-small" | (string & {})`; `DEFAULT_BOT_MODEL = "anthropic/claude-sonnet-5"`. O ID citado no doc (`claude-sonnet-4-6`) não está mais na união canônica — sobrevive só em fixtures de teste (lib/followup/plano-de-tempo.test.ts) e no comentário do próprio gateway.ts:5.
Sugestão: - AI: Vercel AI Gateway (Anthropic primário; OpenAI backup pra embeddings). Os IDs canônicos vivem na união ModelId de lib/ai/gateway.ts — leia lá em vez de copiar um exemplo daqui; hoje o default de chat é DEFAULT_BOT_MODEL.
L49 · FALSA · gravidade alta · sobre-o-codigo
POSTs de criação na API aceitam header
Idempotency-Key: <uuid>(TTL 24h via Upstash)
Mede com:
cd "$(git rev-parse --show-toplevel)" && grep -rln 'export async function POST' app/api/v1/ | wc -l && grep -rln 'Idempotency-Key' app/api/v1/ && grep -rn 'redis|upstash' 'app/api/v1/lgpd/requests/[id]/approve/route.ts'
Deu:
95 arquivos de rota declaram POST em app/api/v1/. Exatamente 1 lê o header: app/api/v1/lgpd/requests/[id]/approve/route.ts (linhas 41-45). E ele NÃO usa Upstash — persiste na tabela Postgres `idempotency_keys` (linha 81: `.from("idempotency_keys")`). Grep por redis/upstash nesse arquivo: 0 ocorrências.
Sugestão: - Idempotência de POST não é geral: hoje só POST /api/v1/lgpd/requests/{id}/approve exige Idempotency-Key, e ele grava na tabela Postgres idempotency_keys (não em Upstash). Antes de assumir que sua rota nova herda isso, meça: grep -rln 'Idempotency-Key' app/api/v1/. Estender para os demais POSTs de criação é trabalho em aberto, não estado atual.
L56 · FALSA · gravidade alta · sobre-o-codigo
Paginação: cursor opaco base64+HMAC por default
Mede com:
cd "$(git rev-parse --show-toplevel)" && grep -rn 'base64url' app/api/v1/leads/_handler.ts app/api/v1/messages/_handler.ts && for f in app/api/v1/messages/_handler.ts app/api/v1/leads/_handler.ts app/api/v1/admin/inbox/conversations/route.ts; do printf '%s createHmac=' $f; grep -c createHmac $f; done && grep -rln createHmac app/api/v1/
Deu:
leads/_handler.ts:125 e messages/_handler.ts:143 decodificam com `JSON.parse(Buffer.from(raw,"base64url").toString("utf8"))` — JSON em base64url, sem assinatura. createHmac = 0 nos três handlers de cursor. Em toda a app/api/v1 só 3 arquivos usam createHmac, e os três são webhooks da Nuvemshop (store-redact, customer-data-request, customer-redact).
Sugestão: - Paginação: cursor opaco em base64url de um JSON — hoje sem HMAC. O cursor é adivinhável e forjável; trate-o como entrada não confiável e valide o payload decodificado antes de usar. Assinar o cursor é dívida aberta, não o estado do código: grep -rln createHmac app/api/v1/ devolve só os webhooks da Nuvemshop.
Vira teste: tests/unit/cursor-assinado.test.ts: montar um cursor com base64url(JSON) alterado à mão e afirmar que o handler o REJEITA. Hoje ele nasce vermelho — é a catraca que segura a correção.
L60 · FALSA · gravidade media · sobre-o-codigo
Rate limit headers:
X-RateLimit-*+Retry-Afterem 429
Mede com:
cd "$(git rev-parse --show-toplevel)" && grep -rin 'x-ratelimit' --include='*.ts' --include='*.tsx' . | grep -v node_modules && grep -rn 'Retry-After' --include='*.ts' lib/ app/ | grep -v test
Deu:
`X-RateLimit` não aparece em NENHUM arquivo .ts/.tsx do repo — só em CLAUDE.md:60, docs/prd, docs/specs, docs/business-rules e lib/api/README.md. `Retry-After` é emitido em exatamente 1 rota: app/api/v1/webhooks/in/[token]/route.ts:62 (`"Retry-After": "60"`); lib/api/client.ts:152 só o LÊ. Bônus: lib/api/README.md:25 aponta para um `rate-limit.ts` que não existe em lib/api/ (o diretório tem client, errors, handlers, recusa, types, wrappers).
Sugestão: - Rate limit: hoje o produto devolve Retry-After em uma rota (app/api/v1/webhooks/in/[token]) e nenhum header X-RateLimit-* — a família inteira só existe nos PRDs/specs. Se for implementar, faça no fail() de lib/api/wrappers.ts para valer em todas as rotas de uma vez; até lá não prometa o header a integrador. Confira: grep -rin 'x-ratelimit' --include='*.ts' .
Vira teste: tests/unit/wrappers.test.ts: afirmar que fail("rate_limited", …, { status: 429 }) responde com Retry-After e X-RateLimit-Limit. Nasce vermelho e vira a catraca.
L61 · FRAGIL · gravidade baixa · sobre-o-codigo
X-Request-Idem toda response (correlaciona com audit log)
Mede com:
cd "$(git rev-parse --show-toplevel)" && grep -rln 'NextResponse.json' app/api/v1/ && grep -rln 'from "@/lib/api/wrappers"' app/api/v1/ | wc -l && grep -n 'X-Request-Id' lib/api/wrappers.ts
Deu:
lib/api/wrappers.ts seta o header em 3 pontos (linhas 56, 81, 91) — ok()/fail(). 211 arquivos de app/api/v1 importam os wrappers. Mas 3 rotas respondem por fora deles: app/api/v1/health/route.ts, app/api/v1/webhooks/meta/[token]/route.ts, app/api/v1/onboarding/whatsapp/session/route.ts — essas saem sem X-Request-Id.
Sugestão: - X-Request-Id em toda response que passa por ok()/fail() (é o wrapper que o injeta). Três rotas respondem com NextResponse.json direto e escapam — health, webhooks/meta/[token] e onboarding/whatsapp/session. Rota nova usa o wrapper; se não usar, não é correlacionável.
Vira teste: tests/unit/rotas-usam-wrappers.test.ts: varrer app/api/v1/**/route.ts, reprovar NextResponse.json fora de uma allowlist nomeada (catraca que só encolhe).
L76 · FALSA · gravidade media · pendencia
Retenção 5 anos. Hot 90 dias, cold (S3) o resto
Mede com:
cd "$(git rev-parse --show-toplevel)" && grep -rin 's3' --include='*.ts' --include='*.sql' lib/ app/api/v1/cron/ supabase/baseline.sql | grep -i 'audit\|cold\|arquiv' && grep -rn 'Retencao 5 anos' supabase/baseline.sql
Deu:
Zero ocorrências de arquivamento em S3 ligado ao audit log. O único vestígio da política é um COMMENT: supabase/baseline.sql:1276 — `COMMENT ON TABLE "public"."api_audit_log" IS 'L-10: Append-only. Retencao 5 anos.'`. Nenhum cron/worker de expurgo ou de cold storage existe (nada em app/api/v1/cron/ toca retenção de audit).
Sugestão: - Retenção pretendida: 5 anos (registrada como COMMENT em api_audit_log). Nada disso é executado hoje: não há expurgo de 90 dias nem arquivamento cold em S3 — a tabela só cresce. Tratar como pendência declarada, não como comportamento; quem dimensionar disco de VPS precisa saber disso.
L92 · FRAGIL · gravidade media · sobre-o-codigo
STOP detection: regex
/STOP|PARAR|SAIR|UNSUBSCRIBE/ino inbound →is_blocked=trueautomaticamente
Mede com:
cd "$(git rev-parse --show-toplevel)" && grep -rn 'UNSUBSCRIBE' --include='*.ts' lib/ app/ workers/
Deu:
lib/channels/pos-entrada.ts:73 — `export const STOP_RX = /(?<![\p{L}\p{N}])(STOP|PARAR|SAIR|UNSUBSCRIBE)(?![\p{L}\p{N}])/iu;`. A regex real tem lookarounds de borda unicode que a do doc não tem: a do doc casa dentro de palavra ("PARARIA", "SAIRAM", "desistop") e bloquearia o contato por engano.
Sugestão: - STOP detection: STOP_RX em lib/channels/pos-entrada.ts — as palavras STOP/PARAR/SAIR/UNSUBSCRIBE com borda de palavra unicode ((?<![\p{L}\p{N}])…(?![\p{L}\p{N}])), senão "PARARIA" e "SAIRAM" bloqueiam o contato. Importe a constante; nunca redigite a regex.
Vira teste: tests/unit/pos-entrada.test.ts: afirmar que STOP_RX NÃO casa 'PARARIA'/'SAIRAM'/'desistop' e casa 'PARAR'/'stop'.
L250 · FRAGIL · gravidade baixa · sobre-o-codigo
pnpm typecheck # tsc --noEmit (estrito)
Mede com:
cd "$(git rev-parse --show-toplevel)" && python3 -c "import json;print(json.load(open('package.json'))['scripts']['typecheck'])" && ls tsconfig*.json
Deu:
typecheck = `tsc --noEmit -p tsconfig.typecheck.json`. Existem DOIS tsconfig: tsconfig.json (com `"strict": true`, linha 11) e tsconfig.typecheck.json — o gate roda contra o segundo, não contra o default.
Sugestão: pnpm typecheck # tsc --noEmit -p tsconfig.typecheck.json (projeto próprio, não o tsconfig.json default)
L253 · FALSA · gravidade media · contagem
pnpm test:db # Postgres efêmero + baseline install/update + 364 invariantes
Mede com:
cd "$(git rev-parse --show-toplevel)" && gh run view --job 94858059457 --log | grep -a 'Test Files\|Tests '
Deu:
Job `invariants` do run 31828419528 (ci.yml, main, HEAD 840917ed, 2026-08-14T18:26): "Test Files 103 passed (103)" e "Tests 760 passed | 1 expected fail | 1 skipped (762)". Controle local: `ls tests/invariants/*.ts | wc -l` = 107 arquivos.
Sugestão: pnpm test:db # Postgres efêmero + baseline install/update + a suíte de invariantes (conte: o job invariants imprime "Test Files"/"Tests" no fim do log)
L254 · FALSA · gravidade media · sobre-o-codigo
pnpm test:e2e # Playwright (requer dev server)
Mede com:
cd "$(git rev-parse --show-toplevel)" && sed -n '118,140p' playwright.config.ts
Deu:
playwright.config.ts:118-138 — `webServer: { command: \`pnpm exec next start --port ${PORT}\`, …, reuseExistingServer: false }`, com o comentário: "Produção (`next build` antes!): dev-server compila por rota (40-80s) e Turbopack dev quebra cookies() fora do request scope — inviável p/ e2e." Ou seja: a suíte SOBE o próprio servidor de produção e recusa reusar qualquer processo na porta. Contradiz o próprio CLAUDE.md:303, que manda usar `next build` + `next start`.
Sugestão: pnpm test:e2e # Playwright — exige pnpm build antes; ele sobe o próprio next start (nunca reusa servidor na porta)
L261 · FALSA · gravidade alta · ativo-obrigatorio
verify(ci.yml) — typecheck + lint + test:unit.
Mede com:
cd "$(git rev-parse --show-toplevel)" && grep -n ' - name:' .github/workflows/ci.yml
Deu:
verify tem 5 passos executáveis: 'Typecheck' (pnpm typecheck), 'Lint' (pnpm lint), 'Channel provider leak' (pnpm lint:channels, ci.yml:35), 'Unit tests' (pnpm test:unit, ci.yml:38), 'Kit self-host (bash)' (pnpm test:shell, ci.yml:50). A descrição do CLAUDE.md nomeia 3 de 5.
Sugestão: - verify (ci.yml) — cinco passos, não três: typecheck, lint, lint:channels, test:unit e test:shell (o kit self-host em bash). Confira a lista na fonte, que é onde ela muda: grep -n ' - name:' .github/workflows/ci.yml.
Vira teste: tests/unit/doutrina-ci.test.ts: ler .github/workflows/ci.yml, extrair os - name: do job verify, e reprovar quando um passo existir no workflow e não estiver citado no bloco 'Testes' do CLAUDE.md
L264 · FALSA · gravidade media · contagem
roda 45 das 46 specs Playwright (medido em 2026-08-14 @
741c4ec8; reconte antes de citar
Mede com:
cd "$(git rev-parse --show-toplevel)" && ls tests/e2e/*.spec.ts | wc -l && python3 -c "import yaml;e=yaml.safe_load(open('.github/workflows/e2e.yml'))['jobs']['e2e']['env'];print('RODOU',len(e['SPECS_PARTE_1'].split())+len(e['SPECS_PARTE_2'].split()),'FORA',len(e['FORA_DO_CI'].split()))"
Deu:
48 specs no disco. SPECS_PARTE_1 = 25, SPECS_PARTE_2 = 22 → RODOU 47; FORA_DO_CI = 1 (vps-fresh-onboarding.spec.ts). Ou seja: 47 de 48, não 45 de 46. Nenhuma spec fora das três listas (a soma fecha).
Sugestão: - e2e (e2e.yml) — sobe Supabase local, aplica o baseline.sql e roda todas as specs de tests/e2e/ menos as declaradas em FORA_DO_CI. Não cite o número aqui — ele já apodreceu quatro vezes. Meça: ls tests/e2e/*.spec.ts | wc -l para o disco, e o passo "Declarar o que este job ainda NÃO cobre" do próprio job, que CONTA em vez de afirmar. A exclusão de hoje é vps-fresh-onboarding (precisa de WAHA + Redis + Resend + Nuvemshop) — e ela é a P0 da doutrina de QA Visual, ou seja, e2e verde não prova a jornada de instalação fresca, que é o produto que se vende.
Vira teste: tests/unit/e2e-cobertura-completa.test.ts já garante a SOMA das três listas. Falta o complemento: reprovar quando o CLAUDE.md contiver a string 'das NN specs' — o número não deve existir na doutrina.
CONTRIBUTING.md — 11
L7 · FRAGIL · gravidade media · ponteiro
Identifique o epic de origem em [
docs/stories/epics/MASTER.md](docs/stories/epics/MASTER.md).
Mede com:
git log -1 --format='%h %ad %s' --date=short -- docs/stories/epics/ ; git log --since="2026-05-14" --oneline -- docs/stories/epics/ | wc -l ; git log -1 --format=%ad --date=short
Deu:
Último toque: `3eb57cf4 2026-05-06 feat(epic-13): UI Lista agentes + Credentials [wave 10]`. Commits nos últimos 3 meses: **0**. HEAD é de 2026-08-14 — o diretório está parado há 3 meses e 8 dias. O arquivo EXISTE (257 linhas), mas o processo que ele descreve morreu.
Sugestão: 3. Se a sua mudança nasce de uma issue, cite o número dela no PR. (docs/stories/epics/ é registro histórico da construção do MVP — último commit em 2026-05-06; não é mais o ponto de partida de contribuição.)
Vira teste: assertar que CONTRIBUTING.md não manda "identificar o epic de origem" se git log -1 --format=%ct -- docs/stories/epics/ for mais velho que N dias — ou, mais simples, marcar o diretório como histórico e proibir a instrução
L22 · FALSA · gravidade media · sobre-o-codigo
Conventional commits + escopo
EPIC-XX
Mede com:
git log -60 --format=%s | grep -oE '^[a-z]+\(([^)]+)\)' | sed -E 's/^[a-z]+\(//;s/\)$//' | sort | uniq -c | sort -rn | head -12
Deu:
15 marca / 12 onboarding / 3 kit / 2 runbook / 2 ci / 2 auth / 1 vocabulário / 1 u6c / 1 u6b / 1 p4 / 1 merge / 1 J1.21 — **zero** commits com escopo `EPIC-XX` nos últimos 60.
Sugestão: Conventional commits. O escopo é o assunto real da mudança — a área (marca, onboarding, kit, ci, auth) ou o número da issue (#184). Exemplos do que está na main: fix(#184): o gate de idempotência deixa de ser decorativo feat(packaging): o worker e o scheduler viram imagem publicada docs(runbook): deploy com os dois arquivos de compose Confira o padrão vigente antes de copiar daqui: git log -30 --format=%s.
L30 · FALSA · gravidade baixa · sobre-o-codigo
O assunto deve ser imperativo e ≤72 chars.
Mede com:
git log -60 --format=%s | awk 'length($0)>72' | wc -l
Deu:
35 (de 60 commits, 58% estouram os 72 chars — inclusive os da própria `main` recente)
Sugestão: Mensagens em PT-BR são aceitas e o assunto deve ser imperativo. Não há teto de caracteres imposto — a main tem assuntos longos de propósito, porque a frase que explica a mudança vale mais que a coluna 72. Se quiser conferir a prática atual: git log -30 --format=%s | awk '{print length($0)}' | sort -n | tail -1.
L34 · FRAGIL · gravidade media · ponteiro
O
epic-executorconsome o frontmatter (epic_id,priority,depends_on,status) e executa wave-by-wave com validação E2E continuous.
Mede com:
ls .claude/skills/ ; test -e .claude/skills/epic-executor && echo existe || echo AUSENTE_NO_REPO ; grep -rn 'epic_id\|priority\|depends_on' ~/.claude/skills/epic-executor/scripts/*.py | head -5
Deu:
`.claude/skills/` do repo contém apenas `DeskcommCRM` e `sistema-vivo` → `AUSENTE_NO_REPO`. A skill só existe em `~/.claude/skills/epic-executor/` (instalação pessoal do mantenedor); os campos são de fato lidos lá (`init_epic_state.py:29,54,87,144`). Frontmatter: 15/15 arquivos `EPIC-*.md` têm `epic_id`, `priority`, `depends_on` e `status`.
Sugestão: Remover a seção ### epic-executor. Ela descreve uma ferramenta que não está no repositório (.claude/skills/ só traz DeskcommCRM e sistema-vivo) e um fluxo parado desde 2026-05-06 — quem vem de fora não tem como executá-la e vai gastar tempo procurando. Se ficar, precisa dizer em uma linha: "ferramenta interna do mantenedor, não distribuída no clone".
Vira teste: para cada ferramenta citada como parte do fluxo em CONTRIBUTING.md, assertar que ela existe no repositório (.claude/skills/<nome> ou package.json scripts)
L38 · FRAGIL · gravidade baixa · pendencia
Ao finalizar um epic: 1. Atualizar frontmatter
status: pending → completed (partial: ...)... 3. Atualizar a row correspondente emdocs/stories/epics/MASTER.md.
Mede com:
git log --since="2026-05-14" --oneline -- docs/stories/epics/ | wc -l
Deu:
0 — nenhum commit atualizou frontmatter ou MASTER.md em 3 meses, apesar de a `main` ter recebido dezenas de features nesse período (packaging, marca própria, onboarding, MFA opcional).
Sugestão: Remover junto com a seção ### epic-executor (mesmo processo, mesma morte). Se houver intenção de reviver, o passo é uma issue com dono — não um parágrafo num doc que ninguém executa.
L50 · FRAGIL · gravidade media · ativo-obrigatorio
O que o CI reprova sozinho — rode antes de abrir o PR e não terá surpresa:
Mede com:
gh api repos/melgarafael/DeskcommCRM/branches/main/protection --jq '.required_status_checks.contexts|join(", ")' ; grep -n 'run: pnpm' .github/workflows/ci.yml .github/workflows/perf.yml
Deu:
Obrigatórios: verify, build-and-size, invariants, e2e, imagens-ok. O bloco local cobre `verify` (typecheck, lint, lint:channels, test:unit, test:shell), `build-and-size` (pnpm build) e `invariants` (pnpm test:db) — **3 dos 5**. `e2e` e `imagens-ok` não são exercitados por nenhum comando desse bloco.
Sugestão: O que você consegue reproduzir na sua máquina — cobre 3 dos 5 checks obrigatórios (verify, build-and-size, invariants): bash pnpm typecheck && pnpm lint && pnpm lint:channels && pnpm test:unit && pnpm test:shell && pnpm build pnpm test:db # precisa de Docker; sobe um Postgres limpo e aplica o baseline Os outros dois — e2e e imagens-ok — só rodam no CI e podem te surpreender no PR: o e2e sobe Supabase local, e o imagens-ok constrói as três imagens Docker (o next build de dentro da imagem não enxerga tests/, então ele pega coisa que o pnpm build daqui não pega).
L69 · FALSA · gravidade media · ativo-obrigatorio
Nenhum job de CI confere isso
Mede com:
pnpm vitest run tests/unit/manifest-x-migrations.test.ts 2>&1 | tail -5 ; grep -n 'run: pnpm test:unit' .github/workflows/ci.yml
Deu:
Test Files 1 passed (1) / Tests 6 passed (6) — e `.github/workflows/ci.yml:38: run: pnpm test:unit` dentro do job `verify` (required check). O arquivo guarda os DOIS sentidos: linha do MANIFEST sem arquivo e arquivo sem linha no MANIFEST.
Sugestão: O CI confere DUAS das três pernas: tests/unit/manifest-x-migrations.test.ts (roda no verify) reprova migration sem linha no MANIFEST e linha no MANIFEST sem arquivo. A perna que nenhum job confere é o baseline: migration sem apêndice em supabase/baseline.sql passa verde e simplesmente não chega em quem instalou numa VPS. Confira com pnpm vitest run tests/unit/manifest-x-migrations.test.ts
Vira teste: grep em CONTRIBUTING.md por "Nenhum job de CI confere" cruzado com a existência de tests/unit/manifest-x-migrations.test.ts — se o teste existe, a frase é falsa
L72 · FALSA · gravidade alta · pendencia
O CI reprova serviço
build:-only e instalação em tag móvel (imagem quebrada ainda não bloqueia merge)
Mede com:
gh api repos/melgarafael/DeskcommCRM/branches/main/protection --jq '.required_status_checks.contexts|join(", ")' ; sed -n '70,79p' CONTRIBUTING.md
Deu:
verify, build-and-size, invariants, e2e, imagens-ok
(e, 5 linhas abaixo, o próprio CONTRIBUTING.md já lista imagens-ok como obrigatório: "Obrigatórios: `verify`, `invariants` (isolamento RLS), `build-and-size`, `e2e` e `imagens-ok`.")
Sugestão: O CI reprova serviço build:-only, instalação em tag móvel e imagem quebrada (imagens-ok é required check da main);
Vira teste: o mesmo teste textual do achado da linha 390 — a frase foi replantada no irmão, e o arquivo se contradiz cinco linhas abaixo
L72 · FALSA · gravidade alta · pendencia
O CI reprova serviço
build:-only e instalação em tag móvel (imagem quebrada ainda não bloqueia merge)
Mede com:
gh api repos/melgarafael/DeskcommCRM/branches/main/protection --jq '.required_status_checks.contexts|join(", ")'
Deu:
verify, build-and-size, invariants, e2e, imagens-ok
Sugestão: O CI reprova serviço build:-only, instalação em tag móvel e imagem que não constrói — as três coisas bloqueiam merge. Confira a lista viva com gh api repos/melgarafael/DeskcommCRM/branches/main/protection --jq '.required_status_checks.contexts';
Vira teste: expect(contexts).toContain('imagens-ok') — assertar que o parêntese "ainda não bloqueia" não sobrevive à ativação do check; ou um teste de doc que proíba a string "ainda não bloqueia merge" em CONTRIBUTING.md enquanto imagens-ok estiver na branch protection
L82 · FRAGIL · gravidade baixa · ponteiro
O
imagens-ok(em.github/workflows/publish-image.yml) constrói as três imagens que o self-hoster instala
Mede com:
grep -rn 'imagens-ok' .github/workflows/ ; sed -n '160,180p' .github/workflows/publish-image.yml ; sed -n '58,90p' .github/workflows/publish-image.yml | grep 'name:\|dockerfile:'
Deu:
`.github/workflows/publish-image.yml:160: imagens-ok:` — existe e o path está certo. Mas o job é fachada: `needs: [build-and-push]`, `permissions: {}`, e o único step roda `[ "${{ needs.build-and-push.result }}" = "success" ]`. Quem constrói é `build-and-push`, matriz de 3: deskcommcrm/Dockerfile, deskcomm-worker/Dockerfile.worker, deskcomm-scheduler/Dockerfile.scheduler.
Sugestão: O publish-image.yml constrói as três imagens que o self-hoster instala (deskcommcrm, deskcomm-worker, deskcomm-scheduler); o check imagens-ok é o job de fachada que reprova quando qualquer uma delas falha. Roda em PR e bloqueia desde 2026-08-13.
Vira teste: assertar que o job imagens-ok existe em .github/workflows/publish-image.yml e que sua lista needs cobre o job da matriz de 3 imagens
L139 · FALSA · gravidade media · ponteiro
Veja [
README.md](README.md) §Como rodar local.
Mede com:
grep -n 'Como rodar local' README.md ; grep -n '^#' README.md
Deu:
grep 'Como rodar local' → sem saída (0 ocorrências). Seções reais do README: `## ⚡ Instalar na sua VPS`, `## 🔄 Atualizar`, `## 🧑💻 Desenvolvimento (só pra contribuir com o código)` (linha 265), `## 🧪 Testes` (321).
Sugestão: Veja [README.md](README.md) §🧑💻 Desenvolvimento — inclui a receita de schema (aplique o baseline.sql, não as migrations/).
Vira teste: para cada link markdown com âncora §<texto> num doc, assertar que <texto> aparece como heading no arquivo apontado
README.en.md — 14
L123 · FALSA · gravidade alta · ativo-obrigatorio
have Google Authenticator or Authy at hand — the first admin login requires MFA
Mede com:
npx tsx <chama exigeCadastroDeMfa({role:'admin',isPlatformAdmin:true,plataformaExige:false,empresaExige:false})> ; grep -n 'mfa_required' scripts/bootstrap-owner.ts
Deu:
dono do install.sh exige MFA? false
scripts/bootstrap-owner.ts:215: mfa_required: false,
Sugestão: (pt-br: mesma correção do README.md, traduzida.) Texto pronto: "Open https://<your-domain> (the padlock takes ~1 min to appear) and sign in as the admin. Two-factor authentication is not required by default — install.sh creates the owner with mfa_required = false, on purpose: a full-screen blocker right after onboarding was where fresh installs died. Want to enforce it? Turn it on under Settings › Security (the rule lives in lib/auth/politica-mfa.ts). During onboarding, scan the QR code with your WhatsApp number."
Vira teste: o mesmo teste do achado do README.md, varrendo os três arquivos
L167 · FALSA · gravidade media · data-versao
The target is the latest published release (
v1.2.3), not the tip ofmain
Mede com:
git tag -l 'v*' --sort=-v:refname | head -1 ; grep -n '1.3.0' CHANGELOG.md
Deu:
v1.3.0 ; CHANGELOG.md:11 "## [1.3.0] — 2026-08-13" ; nenhuma tag nem seção v1.2.3 existe
Sugestão: (pt-br: apagar o número inventado.) Texto pronto: "The target is the latest published release — the newest tag, the same one git tag -l 'v*' --sort=-v:refname | head -1 returns — not the tip of main."
L174 · FRAGIL · gravidade baixa · sobre-o-codigo
If you see
⚠ avisos que não são os esperados, that one is worth keeping.
Mede com:
grep -rn 'avisos que' hostgator-setup-kit/ ; sed -n '144,151p' hostgator-setup-kit/update.sh
Deu:
ZERO ocorrências de 'avisos que'. A string real é update.sh:146 "⚠ Apareceram avisos no banco que NÃO são os esperados:"
Sugestão: (pt-br: citar a string literal do script.) Texto pronto: "If you see ⚠ Apareceram avisos no banco que NÃO são os esperados:, that one is worth keeping."
L234 · FALSA · gravidade media · sobre-o-codigo
| CRM | Kanban (where each deal sits in the funnel) · Contacts · Pipelines (stages, business vocabulary and loss reasons) |
Mede com:
grep -oE 'label: "[^"]+"' lib/navigation/registry.ts ; sed -n '166,210p' lib/navigation/registry.ts
Deu:
grupo crm: "Funis" (/app/kanban), "Contatos", "Etapas do funil" (/app/settings/tenant/pipelines). Comentário no registry: "ERA Kanban ... o produto tinha CINCO vocabulários para a mesma coisa"; e "Pipeline é palavra de quem construiu o sistema; funil de vendas é palavra de quem vende".
Sugestão: (pt-br: o produto baniu "Kanban"/"Pipeline" da interface; o README ensina o vocabulário errado.) Texto pronto: "| CRM | Funis / Funnels (your sales funnels — click one to open its board) · Contacts · Etapas do funil / Funnel stages (each funnel's columns, business vocabulary and loss reasons) |"
Vira teste: tests/unit/readme-telas.test.ts cobrindo os três arquivos
L238 · FALSA · gravidade baixa · contagem
| Organization | Team · Support distribution · Organization · LGPD · API Tokens · Security ... |
Mede com:
grep -oE 'label: "[^"]+"' lib/navigation/registry.ts | sed 's/label: //'
Deu:
... "Equipe" "Distribuição de atendimento" "Organização" "Marca" "Billing" "LGPD" "API Tokens" => a tela "Marca" (/app/settings/marca) falta na tabela
Sugestão: (pt-br: acrescentar a tela Marca.) Texto pronto: "| Organization | Team · Support distribution · Organization · Marca (white-label: name, logo and accent of the installation) · LGPD · API Tokens · Security (MFA, recovery codes, sessions) · Profile, Notifications, Billing |"
L240 · FRAGIL · gravidade baixa · ativo-obrigatorio
Every screen has a door in the navigation — CI fails a screen that exists but can only be reached by typing its URL.
Mede com:
npx vitest run tests/unit/navegacao-completude.test.ts ; sed -n '19,46p' tests/unit/navegacao-completude.test.ts
Deu:
Tests 6 passed (6). ESCOPO declarado: "só app/app/** ... O admin de plataforma (app/admin/), o onboarding e as páginas públicas têm navegação própria". NAV_ALLOWLIST com 7 rotas isentas.
Sugestão: (pt-br: declarar o escopo e a allowlist.) Texto pronto: "Every app/app/** screen has a door in the navigation — or an entry in the allowlist of tests/unit/navegacao-completude.test.ts with a written justification. CI fails a new screen that has neither."
L308 · FALSA · gravidade alta · ativo-obrigatorio
Four checks are required to merge into
main— all verified in branch protection, not just on paper
Mede com:
gh api repos/melgarafael/DeskcommCRM/branches/main/protection --jq '.required_status_checks.contexts|join(", ")'
Deu:
verify, build-and-size, invariants, e2e, imagens-ok
Sugestão: (pt-br: trocar o numeral pela régua e acrescentar a linha do imagens-ok.) Texto pronto: "The required checks to merge into main are whatever branch protection says — that is the ruler, not this table: gh api repos/melgarafael/DeskcommCRM/branches/main/protection --jq '.required_status_checks.contexts|join(\", \")'" + nova linha da tabela: "| imagens-ok | fails when any of the three published Docker images (app, worker, scheduler) does not build — job imagens-ok in .github/workflows/publish-image.yml |"
Vira teste: tests/unit/readme-checks-obrigatorios.test.ts cobrindo os três arquivos
L313 · FALSA · gravidade media · contagem
runs 618 invariants across 98 files
Mede com:
TEST_DB_CONTAINER=fake npx vitest list --config vitest.db.config.ts | grep -c ' > ' ; ls tests/invariants/*.test.ts | wc -l
Deu:
761 ; 103
Sugestão: (pt-br: trocar o número pelo comando.) Texto pronto: "...and runs the tests/invariants/** suite — RBAC, assignment, scoping, routing, follow-up, webhooks and automations. For today's number: TEST_DB_CONTAINER=x npx vitest list --config vitest.db.config.ts | grep -c ' > '"
L315 · FALSA · gravidade media · contagem
runs 44 of the 45 Playwright specs through the frontend
Mede com:
ls tests/e2e/*.spec.ts | wc -l ; python3 <soma SPECS_PARTE_1+SPECS_PARTE_2+FORA_DO_CI>
Deu:
disco: 48 ; rodam 47 (25+22) ; fora 1 (vps-fresh-onboarding) ; nao listadas: []
Sugestão: (pt-br: tirar o par de números.) Texto pronto: "| e2e | boots a local Supabase, applies baseline.sql and runs every spec in tests/e2e/ but one through the frontend — the job itself fails if a spec on disk is in none of the three lists. Recount with ls tests/e2e/*.spec.ts | wc -l |"
L319 · FRAGIL · gravidade baixa · sobre-o-codigo
proves a user of org A sees zero rows of org B in
conversations,messages,contactsandcrm_leads. A control case first proves org B's rows actually exist
Mede com:
sed -n '200,258p' tests/invariants/rls-isolation.test.ts
Deu:
TABLES tem 12 entradas (conversations ... org_guardrail_layers), não 4. O caso "superuser sees both orgs (seed sanity)" roda DEPOIS do laço, não antes.
Sugestão: (pt-br: 12 tabelas e o controle vem depois.) Texto pronto: "...proves a user of org A sees zero rows of org B in every table of the TABLES list in tests/invariants/rls-isolation.test.ts — 12 today, from conversations to org_guardrail_layers. Each table carries a positive control (user A still reads their own org's rows), and the suite closes by checking both orgs' rows really exist — without that, the test would pass against an empty table."
L336 · FALSA · gravidade media · contagem
Index of all 157 documents, with a precedence rule
Mede com:
git ls-files 'docs/**/*.md' | wc -l ; find docs -name '*.md' | wc -l ; grep -n '149' docs/index.md
Deu:
151 ; 161 ; docs/index.md:13 "Mapa dos **149** arquivos .md de docs/ ... régua: git ls-files 'docs/**/*.md' | wc -l"
Sugestão: (pt-br: trocar o total pela régua.) Texto pronto: "| [docs/index.md](docs/index.md) | Index of the docs/ documents, with a precedence rule for when two docs disagree (the index itself carries the ruler: git ls-files 'docs/**/*.md' | wc -l) |"
L361 · FALSA · gravidade alta · ativo-obrigatorio
That line is the complete list of required gates on purpose
Mede com:
gh api repos/melgarafael/DeskcommCRM/branches/main/protection --jq '.required_status_checks.contexts|join(", ")' ; sed -n '356,358p' README.en.md
Deu:
protection: verify, build-and-size, invariants, e2e, imagens-ok
README.en.md:356: pnpm typecheck && pnpm lint && pnpm lint:channels && pnpm test:unit && pnpm test:shell && pnpm build
README.en.md:357: pnpm test:db
=> cobre verify + build-and-size + invariants; NÃO cobre e2e nem imagens-ok.
Sugestão: (pt-br: dizer que a linha é só o que roda local.) Texto pronto: "That line is everything you can run on your own machine, on purpose: running half of them and discovering the rest as a red surprise after hours of waiting is the worst first experience this repository knows how to deliver. It covers verify, build-and-size and invariants; the other two required checks (e2e and imagens-ok) only run in CI. The live list comes from gh api repos/melgarafael/DeskcommCRM/branches/main/protection --jq '.required_status_checks.contexts|join(\", \")'."
L379 · FRAGIL · gravidade baixa · pendencia
Foundation & platform — auth (MFA for admins), multi-tenancy with RLS + isolation test
Mede com:
npx tsx <exigeCadastroDeMfa com o estado do install.sh> ; grep -n mfa_required scripts/bootstrap-owner.ts
Deu:
dono do install.sh exige MFA? false ; bootstrap-owner.ts:215 mfa_required: false
Sugestão: (pt-br: MFA virou opcional.) Texto pronto: "Foundation & platform — auth (optional two-factor, enforceable per organization or platform-wide under Settings › Security), multi-tenancy with RLS + isolation test, 4-role RBAC, append-only audit log, tenant onboarding."
L422 · FRAGIL · gravidade media · sobre-o-codigo
no performance tracing and no session replay, both pinned to 0 on that path
Mede com:
cat instrumentation-client.ts
Deu:
tracesSampleRate: community ? 0 : 1 ; replaysSessionSampleRate: community ? 0 : 0.1 ; replaysOnErrorSampleRate: 1.0 <-- não depende de `community`
Sugestão: (pt-br: o replay DE ERRO continua ligado — omitir isso num parágrafo de privacidade é o pior lugar para omitir.) Texto pronto: "— no performance tracing and no session replay (both pinned to 0 on that path). Error replay stays on, deliberately: it only records when there is an error, it is what explains the stack trace, and it ships with maskAllText/blockAllMedia."
README.es.md — 14
L124 · FALSA · gravidade alta · ativo-obrigatorio
mano Google Authenticator o Authy — el primer inicio de sesión de admin exige MFA
Mede com:
npx tsx <chama exigeCadastroDeMfa({role:'admin',isPlatformAdmin:true,plataformaExige:false,empresaExige:false})> ; grep -n 'mfa_required' scripts/bootstrap-owner.ts
Deu:
dono do install.sh exige MFA? false
scripts/bootstrap-owner.ts:215: mfa_required: false,
Sugestão: (pt-br: mesma correção do README.md, traduzida.) Texto pronto: "Abre https://<tu-dominio> (el candado tarda ~1 min en aparecer) y entra con el admin. La verificación en dos pasos no se exige por defecto — el install.sh crea al dueño con mfa_required = false, a propósito: un bloqueador de pantalla completa justo después del onboarding era donde moría la instalación nueva. ¿Quieres exigirla? Actívala en Configuración › Seguridad (la regla vive en lib/auth/politica-mfa.ts). En el onboarding, escanea el código QR con el WhatsApp de tu número."
Vira teste: o mesmo teste do achado do README.md, varrendo os três arquivos
L167 · FALSA · gravidade media · data-versao
El objetivo es la última versión publicada (
v1.2.3), no la punta demain
Mede com:
git tag -l 'v*' --sort=-v:refname | head -1 ; grep -n '1.3.0' CHANGELOG.md
Deu:
v1.3.0 ; CHANGELOG.md:11 "## [1.3.0] — 2026-08-13" ; nenhuma tag nem seção v1.2.3 existe
Sugestão: (pt-br: apagar o número inventado.) Texto pronto: "El objetivo es la última versión publicada — la tag más nueva, la misma que devuelve git tag -l 'v*' --sort=-v:refname | head -1 —, no la punta de main."
L174 · FRAGIL · gravidade baixa · sobre-o-codigo
Si aparece
⚠ avisos que não são os esperados, ahí sí guarda el mensaje.
Mede com:
grep -rn 'avisos que' hostgator-setup-kit/ ; sed -n '144,151p' hostgator-setup-kit/update.sh
Deu:
ZERO ocorrências de 'avisos que'. A string real é update.sh:146 "⚠ Apareceram avisos no banco que NÃO são os esperados:"
Sugestão: (pt-br: citar a string literal do script.) Texto pronto: "Si aparece ⚠ Apareceram avisos no banco que NÃO são os esperados:, ahí sí guarda el mensaje."
L235 · FALSA · gravidade media · sobre-o-codigo
| CRM | Kanban (dónde está cada negocio en el embudo) · Contactos · Embudos (etapas, vocabulario del negocio y motivos de pérdida) |
Mede com:
grep -oE 'label: "[^"]+"' lib/navigation/registry.ts ; sed -n '166,210p' lib/navigation/registry.ts
Deu:
grupo crm: "Funis" (/app/kanban), "Contatos", "Etapas do funil" (/app/settings/tenant/pipelines). Comentário: "ERA Kanban ... o nome saiu da interface"
Sugestão: (pt-br: o produto baniu "Kanban" da interface e separou "Funis" de "Etapas do funil".) Texto pronto: "| CRM | Funis / Embudos (tus embudos de venta — haz clic en uno para abrir su tablero) · Contactos · Etapas do funil / Etapas del embudo (las columnas de cada embudo, el vocabulario del negocio y los motivos de pérdida) |"
Vira teste: tests/unit/readme-telas.test.ts cobrindo os três arquivos
L239 · FALSA · gravidade baixa · contagem
| Organización | Equipo · Distribución de atención · Organización · LGPD · API Tokens · Seguridad ... |
Mede com:
grep -oE 'label: "[^"]+"' lib/navigation/registry.ts | sed 's/label: //'
Deu:
... "Equipe" "Distribuição de atendimento" "Organização" "Marca" "Billing" "LGPD" "API Tokens" => a tela "Marca" (/app/settings/marca) falta na tabela
Sugestão: (pt-br: acrescentar a tela Marca.) Texto pronto: "| Organización | Equipo · Distribución de atención · Organización · Marca (white-label: nombre, logo y color de la instalación) · LGPD · API Tokens · Seguridad (MFA, códigos de recuperación, sesiones) · Perfil, Notificaciones, Billing |"
L241 · FRAGIL · gravidade baixa · ativo-obrigatorio
Toda pantalla tiene puerta en la navegación — el CI reprueba una pantalla que existe pero a la que solo se llega escribiendo la URL.
Mede com:
npx vitest run tests/unit/navegacao-completude.test.ts ; sed -n '19,46p' tests/unit/navegacao-completude.test.ts
Deu:
Tests 6 passed (6). ESCOPO: "só app/app/**"; NAV_ALLOWLIST com 7 rotas isentas.
Sugestão: (pt-br: declarar o escopo e a allowlist.) Texto pronto: "Toda pantalla de app/app/** tiene puerta en la navegación — o una entrada en la allowlist de tests/unit/navegacao-completude.test.ts con la justificación escrita. El CI reprueba una pantalla nueva que no tenga ni una cosa ni la otra."
L309 · FALSA · gravidade alta · ativo-obrigatorio
Cuatro checks son obligatorios para mergear en
main— todos verificados en la branch protection, no solo en el papel
Mede com:
gh api repos/melgarafael/DeskcommCRM/branches/main/protection --jq '.required_status_checks.contexts|join(", ")'
Deu:
verify, build-and-size, invariants, e2e, imagens-ok
Sugestão: (pt-br: trocar o numeral pela régua e acrescentar a linha do imagens-ok.) Texto pronto: "Los checks obligatorios para mergear en main son los de la branch protection — esa es la regla, no esta tabla: gh api repos/melgarafael/DeskcommCRM/branches/main/protection --jq '.required_status_checks.contexts|join(\", \")'" + nueva fila: "| imagens-ok | reprueba cuando alguna de las tres imágenes Docker publicadas (app, worker, scheduler) no construye — job imagens-ok en .github/workflows/publish-image.yml |"
Vira teste: tests/unit/readme-checks-obrigatorios.test.ts cobrindo os três arquivos
L314 · FALSA · gravidade media · contagem
corre 618 invariantes en 98 archivos
Mede com:
TEST_DB_CONTAINER=fake npx vitest list --config vitest.db.config.ts | grep -c ' > ' ; ls tests/invariants/*.test.ts | wc -l
Deu:
761 ; 103
Sugestão: (pt-br: trocar o número pelo comando.) Texto pronto: "...y corre la suite de tests/invariants/** — RBAC, asignación, alcance, enrutamiento, follow-up, webhooks y automatizaciones. Para el número del día: TEST_DB_CONTAINER=x npx vitest list --config vitest.db.config.ts | grep -c ' > '"
L316 · FALSA · gravidade media · contagem
corre 44 de las 45 specs de Playwright por el frontend
Mede com:
ls tests/e2e/*.spec.ts | wc -l ; python3 <soma SPECS_PARTE_1+SPECS_PARTE_2+FORA_DO_CI>
Deu:
disco: 48 ; rodam 47 (25+22) ; fora 1 (vps-fresh-onboarding) ; nao listadas: []
Sugestão: (pt-br: tirar o par de números.) Texto pronto: "| e2e | levanta un Supabase local, aplica baseline.sql y corre todas las specs de tests/e2e/ menos una por el frontend — el propio job reprueba si una spec del disco no está en ninguna de las tres listas. Recuenta con ls tests/e2e/*.spec.ts | wc -l |"
L320 · FRAGIL · gravidade baixa · sobre-o-codigo
prueba que un usuario de la org A ve cero filas de la org B en
conversations,messages,contactsycrm_leads. Antes, un caso de control prueba que las filas de la org B realmente existen
Mede com:
sed -n '200,258p' tests/invariants/rls-isolation.test.ts
Deu:
TABLES tem 12 entradas (conversations ... org_guardrail_layers), não 4. O caso "superuser sees both orgs (seed sanity)" roda DEPOIS do laço, não antes.
Sugestão: (pt-br: 12 tabelas e o controle vem depois.) Texto pronto: "...prueba que un usuario de la org A ve cero filas de la org B en cada tabla de la lista TABLES de tests/invariants/rls-isolation.test.ts — hoy 12, de conversations a org_guardrail_layers. Cada tabla lleva su control positivo (el usuario A sigue leyendo las filas de su propia org), y la suite cierra comprobando que las filas de ambas orgs realmente existen — sin eso, el test pasaría con la tabla vacía."
L337 · FALSA · gravidade media · contagem
Índice de los 157 documentos, con regla de precedencia
Mede com:
git ls-files 'docs/**/*.md' | wc -l ; find docs -name '*.md' | wc -l ; grep -n '149' docs/index.md
Deu:
151 ; 161 ; docs/index.md:13 "Mapa dos **149** arquivos .md de docs/ ... régua: git ls-files 'docs/**/*.md' | wc -l"
Sugestão: (pt-br: trocar o total pela régua.) Texto pronto: "| [docs/index.md](docs/index.md) | Índice de los documentos de docs/, con regla de precedencia cuando dos docs discrepan (el propio índice trae la regla: git ls-files 'docs/**/*.md' | wc -l) |"
L362 · FALSA · gravidade alta · ativo-obrigatorio
Esa línea es la lista completa de los gates obligatorios, a propósito
Mede com:
gh api repos/melgarafael/DeskcommCRM/branches/main/protection --jq '.required_status_checks.contexts|join(", ")' ; sed -n '357,358p' README.es.md
Deu:
protection: verify, build-and-size, invariants, e2e, imagens-ok
README.es.md:357: pnpm typecheck && pnpm lint && pnpm lint:channels && pnpm test:unit && pnpm test:shell && pnpm build
README.es.md:358: pnpm test:db
=> cobre verify + build-and-size + invariants; NÃO cobre e2e nem imagens-ok.
Sugestão: (pt-br: dizer que a linha é só o que roda local.) Texto pronto: "Esa línea es todo lo que puedes correr en tu propia máquina, a propósito: correr solo la mitad y descubrir el resto como sorpresa roja después de horas de espera es la peor primera experiencia que este repositorio sabe entregar. Cubre verify, build-and-size e invariants; los otros dos checks obligatorios (e2e e imagens-ok) solo corren en el CI. La lista viva sale de gh api repos/melgarafael/DeskcommCRM/branches/main/protection --jq '.required_status_checks.contexts|join(\", \")'."
L380 · FRAGIL · gravidade baixa · pendencia
Fundación y plataforma — auth (MFA para admin), multi-tenancy con RLS + test de aislamiento
Mede com:
npx tsx <exigeCadastroDeMfa com o estado do install.sh> ; grep -n mfa_required scripts/bootstrap-owner.ts
Deu:
dono do install.sh exige MFA? false ; bootstrap-owner.ts:215 mfa_required: false
Sugestão: (pt-br: MFA virou opcional.) Texto pronto: "Fundación y plataforma — auth (verificación en dos pasos opcional, exigible por organización o por la plataforma en Configuración › Seguridad), multi-tenancy con RLS + test de aislamiento, RBAC de 4 roles, audit log append-only, onboarding de tenant."
L423 · FRAGIL · gravidade media · sobre-o-codigo
sin rastreo de performance y sin replay de sesión, que quedan en 0 en ese camino
Mede com:
cat instrumentation-client.ts
Deu:
tracesSampleRate: community ? 0 : 1 ; replaysSessionSampleRate: community ? 0 : 0.1 ; replaysOnErrorSampleRate: 1.0 <-- não depende de `community`
Sugestão: (pt-br: o replay DE ERRO continua ligado.) Texto pronto: "— sin rastreo de performance y sin replay de sesión (los dos en 0 en ese camino). El replay de error sigue activo, a propósito: solo graba cuando hay un error, es lo que explica el stack trace, y ya sube con maskAllText/blockAllMedia."
README.md — 15
L121 · FALSA · gravidade alta · ativo-obrigatorio
o primeiro login de admin exige MFA
Mede com:
cat lib/auth/politica-mfa.ts; sed -n '195,225p' scripts/bootstrap-owner.ts; npx tsx <script que chama exigeCadastroDeMfa({role:'admin', isPlatformAdmin:true, plataformaExige:false, empresaExige:empresaExigeMfa(null)})>
Deu:
dono do install.sh exige MFA? false
scripts/bootstrap-owner.ts:215: mfa_required: false, (comentario acima: "`mfa_required: false` EXPLICITO, contra o default `true` da coluna ... deixa-la em `true` significaria ... TODA instalacao nova voltaria a receber o bloqueador de tela cheia")
lib/auth/politica-mfa.ts:67 exigeCadastroDeMfa: `if (p.isPlatformAdmin && p.plataformaExige === true) return true; if (p.role === "admin" && p.empresaExige) return true; return false;`
lib/auth/server.ts:176 requiresMfa() consulta essa politica.
Sugestão: Abra https://<seu-domínio> (o cadeado leva ~1 min pra aparecer) e entre com o admin. A verificação em duas etapas não é exigida por padrão — o install.sh cria o dono com mfa_required = false, de propósito: um bloqueador de tela cheia logo depois do onboarding é onde a instalação nova morria. Quer exigi-la? Ligue em Configurações › Segurança (a regra vive em lib/auth/politica-mfa.ts). No onboarding, escaneie o QR code com o WhatsApp do seu número.
Vira teste: tests/unit/readme-x-politica-mfa.test.ts: expect(readFileSync('README.md','utf8')).not.toMatch(/exige MFA/) enquanto exigeCadastroDeMfa({role:'admin',isPlatformAdmin:true,plataformaExige:false,empresaExige:false}) === false
L164 · FALSA · gravidade media · data-versao
O alvo é a última versão publicada (
v1.2.3), não o topo damain
Mede com:
git tag -l 'v*' --sort=-v:refname | head -1 ; gh release list --limit 3 ; grep -n "TARGET_TAG=\"\$(git tag" hostgator-setup-kit/update.sh
Deu:
v1.3.0
v1.3.0 — o worker que nunca era atualizado Latest v1.3.0 2026-08-13T19:58:03Z
v1.2.1 — correções de segurança v1.2.1 2026-08-12
hostgator-setup-kit/update.sh:39: [ -n "$TARGET_TAG" ] || TARGET_TAG="$(git tag -l 'v*' --sort=-v:refname | head -1)"
=> o mecanismo ("tag mais nova") está CERTO; o número v1.2.3 nunca existiu — não há tag nem seção no CHANGELOG.
Sugestão: O alvo é a última versão publicada — a tag mais nova, a mesma que git tag -l 'v*' --sort=-v:refname | head -1 devolve —, não o topo da main: atualizar leva sempre a uma versão marcada e descrita no [CHANGELOG.md](CHANGELOG.md), nunca a um commit não testado.
Vira teste: tests/unit/readme-ponteiros.test.ts: toda string v<x>.<y>.<z> citada nos READMEs existe em git tag -l
L171 · FRAGIL · gravidade baixa · sobre-o-codigo
Se aparecer
⚠ avisos que não são os esperados, aí sim guarde a mensagem.
Mede com:
grep -rn 'avisos que' hostgator-setup-kit/ ; sed -n '144,151p' hostgator-setup-kit/update.sh
Deu:
grep 'avisos que' em hostgator-setup-kit/: ZERO ocorrências.
update.sh:146: c_ylw "⚠ Apareceram avisos no banco que NÃO são os esperados:"
update.sh:149: c_grn "✓ banco atualizado (e conversas reorganizadas, se havia bagunça)."
Sugestão: O script filtra esse ruído e mostra ✓ banco atualizado. Se aparecer ⚠ Apareceram avisos no banco que NÃO são os esperados:, aí sim guarde a mensagem.
Vira teste: tests/shell/update-guard.test.sh: toda string entre crases nos READMEs que se apresente como saída do update.sh existe literalmente em hostgator-setup-kit/update.sh
L233 · FALSA · gravidade media · sobre-o-codigo
| CRM | Kanban (onde cada negócio está no funil) · Contatos · Funis (etapas, vocabulário do negócio e motivos de perda) |
Mede com:
grep -oE 'label: "[^"]+"' lib/navigation/registry.ts ; sed -n '166,210p' lib/navigation/registry.ts
Deu:
labels do grupo crm: "Funis" (href /app/kanban) e "Etapas do funil" (href /app/settings/tenant/pipelines) e "Contatos".
registry.ts:168 comentário: "⚠️ ERA \"Kanban\", e a URL continua sendo. O nome saiu da interface porque o produto tinha CINCO vocabulários para a mesma coisa ... Ficou \"Funis\" porque é o que esta tela É"
registry.ts:196 comentário: "⚠️ ERA \"Funis\" ... lá se ABRE o funil, aqui se CONFIGURA o que ele significa"
Sugestão: | CRM | Funis (seus funis de venda — clique num pra abrir o quadro de clientes) · Contatos · Etapas do funil (as colunas de cada funil, o vocabulário do negócio e os motivos de perda) |
Vira teste: tests/unit/readme-telas.test.ts: todo label de NAV_DESTINATIONS com sidebar: true aparece na tabela de telas dos três READMEs, e nenhum rótulo banido ("Kanban", "Pipeline") aparece
L237 · FALSA · gravidade baixa · contagem
| Organização | Equipe · Distribuição de atendimento · Organização · LGPD · API Tokens · Segurança ... |
Mede com:
grep -oE 'label: "[^"]+"' lib/navigation/registry.ts | sed 's/label: //'
Deu:
... "Equipe" "Distribuição de atendimento" "Organização" "Marca" "Billing" "LGPD" "API Tokens"
=> a tela **Marca** (href /app/settings/marca) está no registro de navegação e falta na tabela do README.
Sugestão: | Organização | Equipe · Distribuição de atendimento · Organização · Marca (white-label: nome, logo e cor da instalação) · LGPD · API Tokens · Segurança (MFA, códigos de recuperação, sessões) · Perfil, Notificações, Billing |
Vira teste: mesmo teste do achado anterior: todo label de NAV_DESTINATIONS aparece na tabela de telas
L239 · FRAGIL · gravidade baixa · ativo-obrigatorio
Toda tela tem porta na navegação — o CI reprova tela que existe mas em que só se chega digitando a URL.
Mede com:
npx vitest run tests/unit/navegacao-completude.test.ts ; sed -n '19,46p' tests/unit/navegacao-completude.test.ts
Deu:
Test Files 1 passed (1) / Tests 6 passed (6)
MAS: "ESCOPO: só `app/app/**` — a navegação do tenant. O admin de plataforma (`app/admin/`), o onboarding e as páginas públicas têm navegação própria."
E `const NAV_ALLOWLIST` tem 7 rotas isentas (/app, /app/ai/agents/new, /app/team/invite, /app/settings/tenant/whatsapp, /app/settings/canal-oficial, /app/settings/templates, /app/settings/atualizacao).
Sugestão: Toda tela de app/app/** tem porta na navegação — ou uma entrada na allowlist de tests/unit/navegacao-completude.test.ts com a justificativa escrita. O CI reprova tela nova que não tenha nem uma coisa nem outra.
L307 · FALSA · gravidade baixa · contagem
│ └── api/v1/ # API REST canônica (196 route handlers)
Mede com:
find app/api/v1 -name 'route.ts' | wc -l
Deu:
201
Sugestão: │ └── api/v1/ # API REST canônica (find app/api/v1 -name route.ts | wc -l)
L331 · FALSA · gravidade alta · ativo-obrigatorio
Quatro checks são obrigatórios pra mergear na
main— todos verificados na branch protection, não só no papel
Mede com:
gh api repos/melgarafael/DeskcommCRM/branches/main/protection --jq '.required_status_checks.contexts|join(", ")'
Deu:
verify, build-and-size, invariants, e2e, imagens-ok
Sugestão: Os checks obrigatórios pra mergear na main são os da branch protection — e a régua é ela, não esta tabela: console $ gh api repos/melgarafael/DeskcommCRM/branches/main/protection --jq '.required_status_checks.contexts|join(", ")' verify, build-and-size, invariants, e2e, imagens-ok (e acrescentar a linha que falta na tabela) | imagens-ok | reprova quando qualquer uma das três imagens Docker publicadas (app, worker, scheduler) não constrói — job imagens-ok em .github/workflows/publish-image.yml |
Vira teste: tests/unit/readme-checks-obrigatorios.test.ts: para cada check citado na tabela do README, expect(jobsDeWorkflows()).toContain(check); e o README não pode declarar um numeral ("Quatro"/"Four"/"Cuatro") de checks
L336 · FALSA · gravidade media · contagem
roda 618 invariantes em 98 arquivos
Mede com:
TEST_DB_CONTAINER=fake npx vitest list --config vitest.db.config.ts 2>&1 | grep -c ' > ' ; TEST_DB_CONTAINER=fake npx vitest list --config vitest.db.config.ts 2>&1 | grep ' > ' | cut -d' ' -f1 | sort -u | wc -l ; ls tests/invariants/*.test.ts | wc -l
Deu:
761
103
103
Sugestão: | invariants | sobe um Postgres limpo, aplica o baseline.sql em modo install (ON_ERROR_STOP=1) e depois em modo update (provando idempotência), e roda a suíte de tests/invariants/** — RBAC, atribuição, escopo, roteamento, follow-up, webhooks e automações. Para o número do dia: TEST_DB_CONTAINER=x npx vitest list --config vitest.db.config.ts \| grep -c ' > ' |
Vira teste: tests/unit/readme-sem-contagem-podre.test.ts: o README não pode conter um literal de contagem de invariantes que divirja de vitest list --config vitest.db.config.ts
L338 · FALSA · gravidade media · contagem
roda 44 das 45 specs Playwright pelo frontend
Mede com:
ls tests/e2e/*.spec.ts | wc -l ; python3 -c "conta SPECS_PARTE_1 + SPECS_PARTE_2 + FORA_DO_CI de .github/workflows/e2e.yml"
Deu:
disco: 48
parte1 25 parte2 22 rodam 47 fora 1 ['vps-fresh-onboarding.spec.ts']
nao listadas: []
Sugestão: | e2e | sobe Supabase local, aplica o baseline.sql e roda todas as specs de tests/e2e/ menos uma pelo frontend — o próprio job reprova se alguma spec do disco não estiver em nenhuma das três listas. Reconte com ls tests/e2e/*.spec.ts \| wc -l |
Vira teste: o gate já existe dentro de e2e.yml (compara NO_DISCO com as três listas); o teste novo é: o README não pode citar um par de números de spec — ou, se citar, ele bate com ls tests/e2e/*.spec.ts | wc -l
L342 · FRAGIL · gravidade baixa · sobre-o-codigo
prova que um usuário da org A enxerga zero linhas da org B em
conversations,messages,contactsecrm_leads. Antes disso, um caso de controle prova que as linhas da org B realmente existem
Mede com:
sed -n '200,258p' tests/invariants/rls-isolation.test.ts
Deu:
const TABLES = [conversations, messages, contacts, crm_leads, org_memory_versions, org_memory_entries, skill_activations, ai_routers, ai_router_decisions, knowledge_searches, contact_field_proposals, org_guardrail_layers] => 12 tabelas, não 4.
O caso "superuser sees both orgs (seed sanity...)" está DEPOIS do laço, não antes; o controle por tabela é `user of org A still reads their own org rows in ${table} (positive control)`.
Sugestão: Entre os invariantes está o teste de isolamento RLS: cria 2 organizações, simula os claims JWT pelo mesmo caminho auth.uid() / fn_user_org_ids() que as policies de produção usam, e prova que um usuário da org A enxerga zero linhas da org B em cada tabela da lista TABLES de tests/invariants/rls-isolation.test.ts — hoje 12, de conversations a org_guardrail_layers. Cada tabela leva junto um controle positivo (o usuário A continua lendo as linhas da própria org) e a suíte fecha conferindo que as linhas das duas orgs realmente existem — sem isso, o teste passaria com a tabela vazia.
L360 · FALSA · gravidade media · contagem
Índice dos 157 documentos, com regra de precedência
Mede com:
git ls-files 'docs/**/*.md' | wc -l ; find docs -name '*.md' | wc -l ; grep -n '149' docs/index.md
Deu:
151 (régua declarada dentro do próprio docs/index.md)
161 (todos os .md de docs/, incluindo os 10 da raiz)
docs/index.md:13:Mapa dos **149** arquivos `.md` de `docs/` ... régua: `git ls-files 'docs/**/*.md' | wc -l`
=> 157 não é nenhum dos três números; o próprio índice diz 149 e a régua dele devolve 151.
Sugestão: | [docs/index.md](docs/index.md) | Índice dos documentos de docs/, com regra de precedência quando dois docs discordam (o próprio índice traz a régua: git ls-files 'docs/**/*.md' \| wc -l) |
Vira teste: tests/unit/readme-ponteiros.test.ts: nenhum dos READMEs cita um total de docs; ou, se citar, ele é igual a git ls-files('docs/**/*.md').length
L386 · FALSA · gravidade alta · ativo-obrigatorio
Essa linha é a lista completa dos gates obrigatórios, de propósito
Mede com:
gh api repos/melgarafael/DeskcommCRM/branches/main/protection --jq '.required_status_checks.contexts|join(", ")'; grep -n 'pnpm typecheck && pnpm lint' README.md
Deu:
protection: verify, build-and-size, invariants, e2e, imagens-ok
README.md:380: pnpm typecheck && pnpm lint && pnpm lint:channels && pnpm test:unit && pnpm test:shell && pnpm build
README.md:381: pnpm test:db
=> a linha cobre `verify` + `build-and-size` + `invariants`. NAO cobre `e2e` nem `imagens-ok`, que sao obrigatorios.
Sugestão: Essa linha é tudo o que dá pra rodar na sua máquina, de propósito: rodar só metade e descobrir o resto como surpresa vermelha depois de horas de espera é a pior primeira experiência que este repositório sabe entregar. Ela cobre verify, build-and-size e invariants — os outros dois checks obrigatórios (e2e e imagens-ok) só rodam no CI. A lista viva sai de gh api repos/melgarafael/DeskcommCRM/branches/main/protection --jq '.required_status_checks.contexts|join(", ")'.
Vira teste: tests/unit/readme-checks-obrigatorios.test.ts: a frase "lista completa dos gates obrigatórios" só pode existir se o comando local cobrir TODOS os contexts da branch protection
L406 · FRAGIL · gravidade baixa · pendencia
Fundação & plataforma — auth (MFA pra admin), multi-tenancy com RLS + teste de isolamento
Mede com:
npx tsx <chama exigeCadastroDeMfa com o estado do install.sh> ; grep -n 'mfa_required' scripts/bootstrap-owner.ts
Deu:
dono do install.sh exige MFA? false
scripts/bootstrap-owner.ts:215: mfa_required: false,
lib/auth/politica-mfa.ts: "o cadastro passa a ser OPCIONAL e ligado numa tela de Configurações"
Sugestão: Fundação & plataforma — auth (verificação em duas etapas opcional, exigível por organização ou pela plataforma em Configurações › Segurança), multi-tenancy com RLS + teste de isolamento, RBAC 4 papéis, audit log append-only, onboarding de tenant.
L465 · FRAGIL · gravidade media · sobre-o-codigo
sem rastreamento de performance e sem replay de sessão, que ficam em 0 nesse caminho
Mede com:
cat instrumentation-client.ts ; grep -n 'tracesSampleRate' sentry.server.config.ts sentry.edge.config.ts
Deu:
instrumentation-client.ts:30 tracesSampleRate: community ? 0 : 1,
instrumentation-client.ts:33 replaysSessionSampleRate: community ? 0 : 0.1,
instrumentation-client.ts:34 replaysOnErrorSampleRate: 1.0, <-- NÃO depende de `community`
sentry.server.config.ts:16 tracesSampleRate: community ? 0 : 1,
Comentário no próprio arquivo: "O replay DE ERRO continua, porque é o que explica o stack trace".
Sugestão: — sem rastreamento de performance e sem replay de sessão (os dois em 0 nesse caminho). O replay de erro continua ligado, de propósito: ele só grava quando há erro, é o que explica o stack trace, e já sobe com maskAllText/blockAllMedia.
Vira teste: tests/unit/sentry-comunidade.test.ts: com DSN da comunidade, tracesSampleRate === 0 && replaysSessionSampleRate === 0 && replaysOnErrorSampleRate === 1
SECURITY.md — 4
L5 · FALSA · gravidade media · sobre-o-codigo
O DeskcommCRM é distribuído em rolling release a partir da branch
main.
Mede com:
grep -n 'TARGET_TAG="$(git tag' hostgator-setup-kit/update.sh ; gh release list --limit 6 ; sed -n '163,164p' README.md
Deu:
update.sh:39 → `[ -n "$TARGET_TAG" ] || TARGET_TAG="$(git tag -l 'v*' --sort=-v:refname | head -1)"`. Releases: v1.3.0 (2026-08-13), v1.2.1, v1.2.0, v1.1.0, v1.0.0. README:163 → "**O alvo é a última versão publicada** (`v1.2.3`), não o topo da `main`".
Sugestão: O DeskcommCRM é distribuído por versões marcadas (v1.x.y), publicadas como release e descritas no [CHANGELOG.md](CHANGELOG.md). O update.sh sempre aponta para a última versão publicada — nunca para o topo da main. Correções de segurança entram na próxima versão; mantenha sua instalação atualizada (bash hostgator-setup-kit/update.sh em self-host). Para saber qual é a última: gh release list --limit 1.
Vira teste: assertar que SECURITY.md não afirma "a partir da branch main" enquanto hostgator-setup-kit/update.sh resolver o alvo por git tag -l 'v*'
L9 · FRAGIL · gravidade media · ativo-obrigatorio
|
main(mais recente) | ✅ |
Mede com:
gh release list --limit 6 ; grep -n 'TARGET_TAG=' hostgator-setup-kit/update.sh | head -3
Deu:
5 releases publicadas (v1.0.0 … v1.3.0, a última em 2026-08-13); o update.sh faz `git checkout "$TARGET_TAG"` da tag mais recente. Nenhuma instalação de cliente roda a `main`.
Sugestão: | Versão | Suportada | | --- | --- | | Última release publicada (v1.x.y mais recente) | ✅ | | Releases anteriores | ❌ | Qual é a última hoje: gh release list --limit 1 (ou a página de Releases).
L20 · NAO_VERIFICAVEL · gravidade baixa · ativo-obrigatorio
Confirmação de recebimento em até 7 dias. Avaliação e resposta em até 30 dias ... Crédito no advisory publicado
Mede com:
gh api repos/melgarafael/DeskcommCRM/security-advisories --jq '.[] | "\(.ghsa_id) criado=\(.created_at) estado=\(.state)"'
Deu:
GHSA-cwqh-j5j7-prrm criado=2026-08-12 estado=triage; GHSA-87qq-3c7m-p22c criado=2026-08-12 estado=triage; GHSA-qmw2-qf85-57vf criado=2026-08-04 estado=triage. Três abertos, **zero publicados** — o mais velho tem 10 dias (dentro dos 30, então o SLA não está violado). O canal de confirmação (comentário no advisory) não é visível pela API.
L29 · FRAGIL · gravidade baixa · sobre-o-codigo
Bypass de autenticação/RBAC (roles
viewer/agent/manager/admin, super-admin)
Mede com:
grep -n 'export type Role\|PAPEIS_HUMANOS' lib/auth/types.ts
Deu:
lib/auth/types.ts:20 → `export type Role = "viewer" | "agent" | "ai_operator" | "manager" | "admin";` (5 papéis). lib/auth/types.ts:30 → `PAPEIS_HUMANOS = ["viewer", "agent", "manager", "admin"]`. O `ai_operator` existe no tipo e não está listado no escopo.
Sugestão: Bypass de autenticação/RBAC (papéis do tenant — os humanos viewer/agent/manager/admin e o ai_operator do agente — e o super-admin de plataforma)
Vira teste: assertar que todo membro de Role em lib/auth/types.ts aparece citado em SECURITY.md §Escopo
docs/DEPLOY-CHECKLIST.md — 4
L8 · FRAGIL · gravidade baixa · sobre-o-codigo
Supabase cloud provisionado pelo
install.sh
Mede com:
grep -n 'SUPABASE_ACCESS_TOKEN\|Criando o projeto Supabase' hostgator-setup-kit/install.sh | head
Deu:
913:# Com SUPABASE_ACCESS_TOKEN no ambiente e as credenciais ainda vazias, o
916:if [ -z "${NEXT_PUBLIC_SUPABASE_URL:-}" ] && [ -n "${SUPABASE_ACCESS_TOKEN:-}" ]; then
917: step "Criando o projeto Supabase automaticamente"
919: || die "Não consegui criar o projeto Supabase. Crie no painel e rode de novo sem SUPABASE_ACCESS_TOKEN."
Sugestão: | A. VPS self-host | o produto — o que o cliente compra e o que a doutrina rege | Supabase cloud: o install.sh provisiona sozinho quando há SUPABASE_ACCESS_TOKEN no ambiente (L916); sem o token, o projeto é criado no painel e o instalador só pede as credenciais |
L46 · FALSA · gravidade alta · pendencia
Depois da release,
stableeX.Y.Zsão o mesmo digest nas três imagens.
Mede com:
for i in deskcommcrm deskcomm-worker deskcomm-scheduler; do for t in 1.3.0 stable; do echo -n "$i:$t "; docker buildx imagetools inspect ghcr.io/melgarafael/$i:$t --format '{{.Manifest.Digest}}'; done; done
Deu:
os três pares divergem (fc10b029 vs c4bc70b6; 81e5af56 vs 3fe292ca; 4396263b vs a0d5c3ad). O texto narra a divergência como episódio de 19:53→19:58 e o commit 648df229 removeu o gatilho `release:` do workflow — mas NADA reparou a 1.3.0 no registry: a condição de `stable` no metadata-action exige `event_name=='push' && ref_type=='tag'`, então o rebuild das 19:58 moveu `1.3.0` e não moveu `stable`.
Sugestão: - [ ] Depois da release, stable e X.Y.Z são o mesmo digest nas três imagens. Nesta ordem, e não antes: na v1.3.0 a conferência rodou antes do gh release create, passou, e o próprio release republicou a versão em cima — verde às 19:53, divergente às 19:58. O gatilho foi removido, mas a divergência da 1.3.0 NUNCA foi reparada no registry: stable segue no build das 19:53 e 1.3.0 no das 19:58. Não instrua ninguém a usar stable como "a 1.3.0" sem rodar o laço do item 10 da doutrina primeiro.
L63 · FALSA · gravidade media · sobre-o-codigo
A versão anterior está anotada (é uma linha do
.env:APP_IMAGE)
Mede com:
grep -rn 'APP_IMAGE\|WORKER_IMAGE\|SCHEDULER_IMAGE' hostgator-setup-kit/update.sh
Deu:
190:# antigo grava só `APP_IMAGE`, e o worker fica seguindo um canal móvel.
194:export APP_IMAGE="${IMG_APP}:${VERSAO_ALVO}"
195:export WORKER_IMAGE="${IMG_WORKER}:${VERSAO_ALVO}"
196:export SCHEDULER_IMAGE="${IMG_SCHEDULER}:${VERSAO_ALVO}"
Sugestão: - [ ] A versão anterior está anotada — são três linhas do .env: APP_IMAGE, WORKER_IMAGE e SCHEDULER_IMAGE (update.sh grava as três, L194-196). Anotar só a primeira é o defeito que o pin das três veio consertar: o rollback voltaria o app e deixaria worker e scheduler na versão nova
Vira teste: assert: toda variável *_IMAGE que hostgator-setup-kit/update.sh exporta aparece no bloco Rollback de docs/DEPLOY-CHECKLIST.md
L77 · FRAGIL · gravidade baixa · sobre-o-codigo
o pipeline da Vercel faz deploy a cada push na
maine não aplica migration nenhuma
Mede com:
ls vercel.json ; grep -rn 'supabase db push\|db push\|migration' .github/workflows/*.yml
Deu:
ls: vercel.json: No such file or directory
.github/workflows/e2e.yml:181: - name: Subir Supabase local (sem a cadeia de migrations)
.github/workflows/e2e.yml:183: mv supabase/migrations /tmp/migrations-off
(nenhum workflow aplica migration em banco remoto)
Sugestão: - [ ] Migrations aplicadas no banco da Vercel — supabase/migrations/, e confira: nenhum artefato do repo aplica migration (ls vercel.json → ausente; grep -rn 'db push' .github/workflows/ → vazio), e o gatilho de deploy é configuração da Vercel, fora do repo. Em 2026-08-04 a produção rodou código à frente do banco e o inbox devolveu 500 (42703)
docs/adr/0001-packaging-e-distribuicao.md — 7
L24 · FRAGIL · gravidade media · contagem
ela existe desde 2026-07-02, com 258 execuções do workflow de publicação e 4 releases
Mede com:
gh run list --workflow=publish-image.yml --limit 500 --json conclusion --jq 'length' ; gh release list --limit 50 --json tagName --jq 'length'
Deu:
285
5
Sugestão: A investigação do repositório mostrou o contrário: ela existe desde 2026-07-02 (commit 7edd153f, que criou .github/workflows/publish-image.yml), com centenas de execuções verdes e releases publicadas. Os números apodrecem a cada push, então a régua vai junto: gh run list --workflow=publish-image.yml --limit 500 --json conclusion --jq 'length' e gh release list --limit 50 --json tagName --jq 'length'. O que faltava não era o pipeline — era a regra que decide o que entra nele, e um gate que a exerça.
L34 · FALSA · gravidade media · sobre-o-codigo
é o namespace que o CI já publica (
IMAGE_NAME: ${{ github.repository }})
Mede com:
grep -rn 'IMAGE_NAME' .github/ ; grep -n 'images:' .github/workflows/publish-image.yml
Deu:
(grep IMAGE_NAME: zero linhas)
103: images: ${{ env.REGISTRY }}/${{ github.repository_owner }}/${{ matrix.name }}
Sugestão: Escolhido porque é o namespace que o CI já publica (images: ${{ env.REGISTRY }}/${{ github.repository_owner }}/${{ matrix.name }}, em .github/workflows/publish-image.yml), que o compose já consome, e — decisivo — que está gravado no .env de cada cliente instalado, por install.sh e por update.sh, como string literal.
L46 · FALSA · gravidade media · sobre-o-codigo
os testes que casam a string (
tests/shell/update-guard.test.sh,hostgator-setup-kit/test-validators.sh,tests/unit/packaging-artefato-do-cliente.test.ts)
Mede com:
grep -c 'melgarafael' tests/unit/packaging-artefato-do-cliente.test.ts ; grep -rln 'ghcr.io/melgarafael' --exclude-dir=node_modules . | grep -E 'tests/|hostgator-setup-kit/test'
Deu:
0
tests/shell/update-guard.test.sh
hostgator-setup-kit/test-validators.sh
Sugestão: ... os testes que casam a string (tests/shell/update-guard.test.sh, hostgator-setup-kit/test-validators.sh) e os docs — mais o .env de cada instalação viva, que é a parte que nenhum commit alcança. (tests/unit/packaging-artefato-do-cliente.test.ts entra no custo por outro motivo: ele guarda a forma do compose e os gatilhos do workflow, não a string do namespace.) Régua para reconferir antes de citar este parágrafo: grep -rln "ghcr.io/melgarafael" --exclude-dir=node_modules .
Vira teste: tests/unit/packaging-artefato-do-cliente.test.ts: it("o ADR só lista como guarda-namespace arquivos que contêm a string") — para cada path citado na §D1 como teste que 'casa a string', exigir grep('ghcr.io/melgarafael') > 0
L126 · FALSA · gravidade alta · pendencia
Ainda não ganhamos: o gate da imagem só reprova merge depois que
imagens-okentrar na branch protection ... Enquanto isso, o job roda em PR e informa, mas não bloqueia.
Mede com:
gh api repos/melgarafael/DeskcommCRM/branches/main/protection --jq '.required_status_checks.contexts|join(", ")'
Deu:
verify, build-and-size, invariants, e2e, imagens-ok
Sugestão: Ganhamos também o gate: imagens-ok é status check obrigatório da main — merge com imagem quebrada não passa. Régua, porque esta linha já ficou um dia inteiro afirmando o contrário depois de o check já bloquear: console $ gh api repos/melgarafael/DeskcommCRM/branches/main/protection --jq '.required_status_checks.contexts|join(", ")' verify, build-and-size, invariants, e2e, imagens-ok Em PR a imagem é construída e descartada: o gate é o build, não a publicação (push: ${{ github.event_name != 'pull_request' }}).
Vira teste: tests/unit/packaging-artefato-do-cliente.test.ts: it("o ADR não afirma que imagens-ok é não-bloqueante") — ler docs/adr/0001-packaging-e-distribuicao.md e reprovar se casar /imagens-ok[^\n]*(não bloqueia|ainda não|entrar na branch protection)/i, já que o job existe em .github/workflows/publish-image.yml
L144 · FRAGIL · gravidade baixa · contagem
10 tags publicadas e públicas;
tags/listanônimo responde, manifest delatest= 200
Mede com:
TOKEN=$(curl -s 'https://ghcr.io/token?scope=repository:melgarafael/deskcommcrm:pull&service=ghcr.io' | python3 -c 'import sys,json;print(json.load(sys.stdin)["token"])'); curl -s -H "Authorization: Bearer $TOKEN" https://ghcr.io/v2/melgarafael/deskcommcrm/tags/list | python3 -c 'import sys,json;t=json.load(sys.stdin)["tags"];print(len(t),sorted(t))' ; curl -s -o /dev/null -w '%{http_code}\n' -H "Authorization: Bearer $TOKEN" https://ghcr.io/v2/melgarafael/deskcommcrm/manifests/latest
Deu:
13 ['1.0', '1.0.0', '1.1', '1.1.0', '1.2', '1.2.0', '1.2.1', '1.3', '1.3.0', 'latest', 'main', 'quebrada-teste', 'stable']
200
Sugestão: tags publicadas e públicas — tags/list anônimo responde e o manifest de latest volta 200 (o número cresce a cada release; conte no registry, não aqui). Provável causa do erro: gh api users/…/packages devolve 403 sem escopo read:packages — instrumento cego lido como ausência
L146 · FRAGIL · gravidade baixa · contagem
existe desde 2026-07-02; 258 runs, 252 verdes
Mede com:
git log --diff-filter=A --format='%h %ad' --date=short -- .github/workflows/publish-image.yml ; gh run list --workflow=publish-image.yml --limit 500 --json conclusion --jq 'length, ([.[]|select(.conclusion=="success")]|length)'
Deu:
7edd153f 2026-07-02
285
278
Sugestão: existe desde 2026-07-02 (7edd153f criou o workflow); a esmagadora maioria dos runs é verde — reconte com gh run list --workflow=publish-image.yml --limit 500 --json conclusion --jq 'length, ([.[]|select(.conclusion=="success")]|length)' antes de citar um número
L150 · FALSA · gravidade media · ponteiro
o repo já documentava esse modo de falha em
_common.sh:294-299
Mede com:
sed -n '294,299p' hostgator-setup-kit/_common.sh ; grep -n 'up -d` FALHA' hostgator-setup-kit/_common.sh
Deu:
294: val="${val//\"'\\''\"/\"'\"}"
295: ;;
296: esac
297: printf -v "$key" '%s' "$val"
298: export "${key?}"
299: done < "$file"
---
483:# referência, o `up -d` FALHA e o contêiner não sobe, mesmo com a imagem já no
Sugestão: ... O gatilho real é up -d — e o repo já documentava esse modo de falha no comentário de gravar_imagens, em hostgator-setup-kit/_common.sh. Número de linha apodrece a cada edição do script; a régua é grep -n 'up -d\ FALHA' hostgator-setup-kit/_common.sh`.
Vira teste: tests/unit/docs-ponteiros.test.ts: it("ponteiro arquivo:linha do ADR aponta para o assunto certo") — para cada <arquivo>:<n>-<m> citado, exigir que o trecho contenha um termo do contexto ('up -d'), senão reprovar
docs/doctrine/packaging.md — 10
L42 · FRAGIL · gravidade baixa · contagem
| Exemplos |
deskcommcrm,deskcomm-worker|
Mede com:
grep -n -A12 'matrix:' .github/workflows/publish-image.yml | grep 'name:'
Deu:
- name: deskcommcrm
- name: deskcomm-worker
- name: deskcomm-scheduler
Sugestão: | Exemplos | deskcommcrm, deskcomm-worker, deskcomm-scheduler |
Vira teste: tests/unit/packaging-artefato-do-cliente.test.ts: toda name: da matriz de publish-image.yml aparece citada em docs/doctrine/packaging.md — uma quarta imagem, um dia, entra no gate e some da doutrina
L51 · FRAGIL · gravidade media · sobre-o-codigo
wahaesrh— que tocam WhatsApp e rate limit — estão pinadas de verdade (tag exata e digest)
Mede com:
grep -n 'image:' docker-compose.prod.yml
Deu:
132: image: ${WAHA_IMAGE:-devlikeapro/waha:latest-2026.7.2}
153: image: redis:7-alpine
175: image: hiett/serverless-redis-http@sha256:5b0bb9239fce53abf87b2018a7a0deb9ec7bd900c5360738fe5fbeeb426f9150
212: image: caddy:2-alpine
Sugestão: waha e srh — que tocam WhatsApp e rate limit — estão pinadas de verdade: o WAHA por tag exata (devlikeapro/waha:latest-2026.7.2) e o srh por digest. Nenhuma das duas anda sozinha dentro de um major. Confira: grep -n 'image:' docker-compose.prod.yml.
L133 · FRAGIL · gravidade baixa · data-versao
gravam no
.envdo cliente uma tag de versão (1.2.1), nuncalatest,mainoustable
Mede com:
ghcr_status deskcomm-worker 1.2.1 ; ghcr_status deskcomm-scheduler 1.2.1
Deu:
deskcomm-worker:1.2.1 -> 404
deskcomm-scheduler:1.2.1 -> 404
Sugestão: Trocar o exemplo por uma versão que exista nas três imagens: "gravam no .env do cliente uma tag de versão (1.3.0), nunca latest, main ou stable" — e o mesmo na primeira linha da tabela de canais (linha 254). Usar 1.2.1 como exemplo do estado desejado contradiz o parágrafo 13 linhas abaixo, que diz que deskcomm-worker:1.2.1 nunca vai existir: nenhuma instalação pode nascer pinada nesse número.
L136 · FALSA · gravidade alta · contagem
Duas exceções, ambas deliberadas e ambas com aviso na tela
Mede com:
grep -n 'VERSAO_ALVO=\|trio_publicado' hostgator-setup-kit/install.sh | sed -n '1,12p' ; sed -n '1036,1060p' hostgator-setup-kit/install.sh
Deu:
1024: VERSAO_ALVO="$(ultima_versao_publicada …)"
1037: if [ -n "$VERSAO_ALVO" ] && trio_publicado "$VERSAO_ALVO"; then # pina a versão
1039: elif trio_publicado "stable"; then
1042: VERSAO_ALVO="stable" <-- 3º caminho, grava TAG MÓVEL no .env
1045: elif [ -n "$VERSAO_ALVO" ]; then # constrói worker/scheduler na VPS
1056: VERSAO_ALVO="latest"
Sugestão: Três exceções, todas deliberadas e todas com aviso na tela — porque falhar fechado aqui seria recusar instalar por não conseguir resolver um número: 1. A versão mais recente ainda não tem as três imagens publicadas (a tag do git nasce minutos antes delas): cai no canal stable e avisa. É o caminho mais provável nos primeiros minutos de uma release. 2. Sem rede ou sem tag no remoto, cai em latest e avisa. Trocar previsibilidade por disponibilidade é o negócio errado numa instalação que já começou. 3. Quem preenche o .env à mão a partir de .env.hostgator.example recebe stable — o piso seguro para quem não vai rodar a entrevista. --yes com o template preserva esse valor. A cascata inteira está em grep -n 'VERSAO_ALVO=' hostgator-setup-kit/install.sh — leia lá antes de confiar nesta lista.
Vira teste: tests/shell: para cada atribuição de VERSAO_ALVO a uma tag móvel em install.sh, a doutrina precisa nomear a exceção — assert que o conjunto {stable, latest} extraído de grep -oE 'VERSAO_ALVO="(stable|latest)"' install.sh está integralmente citado no bloco de exceções do invariante 3
L230 · FALSA · gravidade alta · pendencia
Vale a partir da próxima release. Nenhuma imagem já publicada carrega
APP_VERSION… Todo o parque instalado hoje respondedesconhecido
Mede com:
# config blob das imagens no GHCR (docker daemon fora do ar; li o registry direto)
python3 scratchpad/ghcr_cfg.py deskcommcrm 1.2.1 ; python3 scratchpad/ghcr_cfg.py deskcommcrm 1.3.0 ; python3 scratchpad/ghcr_cfg.py deskcomm-worker 1.3.0 ; gh release list --limit 2
Deu:
deskcommcrm:1.2.1 APP_VERSION env: AUSENTE
deskcommcrm:1.3.0 APP_VERSION env: ['APP_VERSION=1.3.0']
deskcomm-worker:1.3.0 APP_VERSION env: ['APP_VERSION=1.3.0']
deskcommcrm:stable APP_VERSION=1.3.0 | deskcommcrm:latest APP_VERSION=840917e
gh release list: v1.3.0 … Latest … 2026-08-13T19:58:03Z
Sugestão: > Onde já vale. Toda imagem publicada a partir da v1.3.0 carrega APP_VERSION — medido em 2026-08-14 direto no registry: deskcommcrm:1.3.0 e deskcomm-worker:1.3.0 trazem APP_VERSION=1.3.0; a 1.2.1 não traz nenhuma. Quem ainda roda imagem anterior à 1.3.0 responde desconhecido, que é a resposta honesta e o motivo de o fallback não ser mais um número plausível. Para saber o que a SUA imagem carrega: docker run --rm ghcr.io/melgarafael/deskcommcrm:<tag> node -e 'console.log(process.env.APP_VERSION)'.
L234 · FALSA · gravidade media · ponteiro
O item 9 do checklist de release reprova contra a 1.2.1 de propósito.
Mede com:
grep -n '^\[ \] [0-9]' docs/doctrine/packaging.md | sed -n '8,10p'
Deu:
[ ] 8. A imagem reporta a versão certa: docker run … node -e 'console.log(process.env.APP_VERSION)' → X.Y.Z
[ ] 9. `gh release create vX.Y.Z` com as notas do CHANGELOG
[ ] 10. SÓ AGORA: `stable` e X.Y.Z são o MESMO digest
Sugestão: O item 8 do checklist de release reprova contra a 1.2.1 de propósito.
Vira teste: tests/unit: parsear os itens [ ] N. do checklist e assertar que o item citado no invariante 7 é o que contém APP_VERSION — ponteiro por número quebra em silêncio quando alguém insere um item no meio
L256 · FRAGIL · gravidade media · data-versao
|
stable| implementador validando antes de atualizar clientes | sim |always| a última release publicada |
Mede com:
for i in deskcommcrm deskcomm-worker deskcomm-scheduler; do for t in 1.3.0 stable; do python3 scratchpad/ghcr_cfg.py $i $t | head -1; done; done
Deu:
deskcommcrm:1.3.0 top-digest=sha256:fc10b029… | deskcommcrm:stable top-digest=sha256:c4bc70b6…
deskcomm-worker:1.3.0 sha256:81e5af56… | deskcomm-worker:stable sha256:3fe292ca…
deskcomm-scheduler:1.3.0 sha256:4396263b… | deskcomm-scheduler:stable sha256:a0d5c3ad…
(os dois lados carregam revision=9bd59e9 e APP_VERSION=1.3.0 — builds distintos do mesmo commit)
Sugestão: Acrescentar à nota do item 10 (depois de "O gatilho foi removido…"): A divergência da v1.3.0 continua viva no registry. Medido em 2026-08-14, stable e 1.3.0 são digests DIFERENTES nas três imagens — mesmo revision (9bd59e9) e mesmo APP_VERSION (1.3.0), builds distintos. O conserto foi no gatilho, não no que já foi publicado: rodar o item 10 contra a 1.3.0 hoje reprova, e é o esperado. A primeira release em que o item 10 deve bater é a próxima.
L350 · FRAGIL · gravidade baixa · data-versao
Apagar tags de branch dos três pacotes —
docs-doutrina-packaginge qualquer outra que tenha nascido de umworkflow_dispatchde ensaio.
Mede com:
ghcr_status() { … }; for i in deskcommcrm deskcomm-worker deskcomm-scheduler; do for t in docs-doutrina-packaging quebrada-teste; do echo "$i:$t -> $(ghcr_status $i $t)"; done; done
Deu:
deskcommcrm:docs-doutrina-packaging -> 404
deskcommcrm:quebrada-teste -> 200
deskcomm-worker:docs-doutrina-packaging -> 404 | quebrada-teste -> 404
deskcomm-scheduler:docs-doutrina-packaging -> 404 | quebrada-teste -> 404
Sugestão: Apagar tags de branch dos três pacotes — qualquer uma nascida de um workflow_dispatch de ensaio. Tag de branch é artefato de trabalho: se ficar, vira canal órfão que alguém pina por engano achando que é release, e ela nunca mais se move. Para listar o que existe: ghcr_status <imagem> <tag> acima, ou gh api /user/packages/container/<imagem>/versions --jq '.[].metadata.container.tags'. O registry ainda carrega quebrada-teste (só em deskcommcrm) como lembrete; a docs-doutrina-packaging já foi apagada — 404 nos três em 2026-08-14.
L367 · FRAGIL · gravidade baixa · data-versao
EXIGE ESCOPO QUE O TOKEN PADRÃO DO
ghNÃO TEM. Medido no corte da 1.3.0: comgist, read:org, repo, workflowa API devolve 403
Mede com:
gh auth status 2>&1 | grep -i 'scopes'
Deu:
- Token scopes: 'delete:packages', 'gist', 'read:org', 'read:packages', 'repo', 'workflow'
Sugestão: EXIGE ESCOPO QUE O TOKEN PADRÃO DO gh NÃO TEM — no corte da 1.3.0, com gist, read:org, repo, workflow, a API devolveu 403 tanto para listar quanto para apagar versão de pacote. Confira o SEU token antes de chegar aqui: gh auth status | grep -i scopes precisa mostrar read:packages e delete:packages; se faltar, gh auth refresh -h github.com -s read:packages,delete:packages.
L390 · FALSA · gravidade alta · pendencia
imagem quebrada reprova — assim que o check entrar na branch protection (ver invariante 2)
Mede com:
gh api repos/melgarafael/DeskcommCRM/branches/main/protection --jq '.required_status_checks.contexts|join(", ")'
Deu:
verify, build-and-size, invariants, e2e, imagens-ok
Sugestão: | CI (mecânico) | imagens-ok em publish-image.yml | imagem quebrada reprova o merge — é required check da main. Confira na fonte antes de confiar nesta linha: gh api repos/melgarafael/DeskcommCRM/branches/main/protection --jq '.required_status_checks.contexts' |
Vira teste: tests/unit/packaging-artefato-do-cliente.test.ts: o texto de docs/doctrine/packaging.md e de CONTRIBUTING.md não pode conter /assim que o check entrar|ainda não bloqueia merge|ainda não é obrigatório/ — congela a frase de pendência que já apodreceu duas vezes neste mesmo arquivo
docs/index.md — 10
L5 · FALSA · gravidade media · data-versao
last_updated: 2026-07-29
Mede com:
git log -1 --format='%h %ad %s' --date=short -- docs/index.md
Deu:
64ff6f3e 2026-08-13 docs(incidente): runbook de remediação do worker congelado — declarado NÃO ENSAIADO
Sugestão: last_updated: 2026-08-13 # régua: git log -1 --format=%ad --date=short -- docs/index.md
Vira teste: tests/unit/docs-ponteiros.test.ts: it("last_updated do índice bate com o último commit que o tocou") — comparar o campo com git log -1 --format=%ad --date=short -- docs/index.md
L8 · FALSA · gravidade media · data-versao
audited_against: origin/main @
789dfa6(v1.0.0, 2026-07-27)
Mede com:
git rev-list -n1 v1.0.0 ; git log -1 --format='%h %ad' --date=short 789dfa6
Deu:
3ae07056055b8a8a3ab00e53d641e954e10f3480
789dfa6f 2026-07-29
Sugestão: audited_against: origin/main @ 789dfa6 (2026-07-29 — dois dias e alguns commits DEPOIS da tag v1.0.0, que é 3ae07056 de 2026-07-27)
Vira teste: tests/unit/docs-ponteiros.test.ts: it("o front-matter do índice não rotula o SHA auditado com uma tag que não é dele") — se audited_against citar vX.Y.Z, exigir git rev-list -n1 vX.Y.Z == o SHA citado
L13 · FALSA · gravidade media · contagem
Mapa dos 149 arquivos
.mddedocs/, espalhados por 24 subpastas — régua:git ls-files 'docs/**/*.md' | wc -l.
Mede com:
git ls-files 'docs/**/*.md' | wc -l ; git ls-files -- docs/ | grep -c '\.md$' ; git ls-files 'docs/**/*.md' | xargs -n1 dirname | sort -u | wc -l
Deu:
151
161
24
Sugestão: Mapa dos arquivos .md de docs/. O número apodrece a cada doc novo, então ele não mora aqui — conte: git ls-files 'docs/**/*.md' | wc -l (subpastas com doc: git ls-files 'docs/**/*.md' | xargs -n1 dirname | sort -u | wc -l). Existe porque a documentação cresceu sem ponto de entrada: sem este índice, humano e agente não acham o que já foi decidido e reescrevem por cima.
Vira teste: tests/unit/docs-ponteiros.test.ts: it("o índice não fixa a contagem de docs") — reprovar se a linha 13 de docs/index.md contiver um literal numérico de 2+ dígitos antes de 'arquivos .md'
L113 · FALSA · gravidade alta · pendencia
impacto medido e as duas rotas de remediação. Ainda não ensaiado
Mede com:
sed -n '1,12p' docs/runbooks/remediar-worker-congelado.md; grep -c '\[ENSAIADO\]' docs/runbooks/remediar-worker-congelado.md
Deu:
> ## Estado do ensaio: **U6-b EXECUTADO em 2026-08-13, com uma ressalva que muda o procedimento**
> O ensaio rodou numa VPS real, com estado legado **reproduzido** (não simulado): clone no commit `ee520110` ... o worker migrou de imagem local (`2174fb4f`) para a publicada ... 69 → 73 tabelas
(marcadores [ENSAIADO] nas seções 5.0/A1/A3/A4/A5)
Sugestão: | [runbooks/remediar-worker-congelado.md](runbooks/remediar-worker-congelado.md) | Incidente: o worker congelado — diagnóstico (diagnostico.sh), impacto medido e as duas rotas de remediação. Ensaiado em VPS real (U6-b, 2026-08-13), com uma ressalva que muda o procedimento: a primeira execução do update.sh não troca o worker. Leia o cabeçalho do runbook antes de operar |
Vira teste: tests/unit/docs-ponteiros.test.ts: it("o índice não declara não-ensaiado um runbook que se declara ensaiado") — se docs/index.md casar /Ainda não ensaiado/ na linha que cita remediar-worker-congelado.md, reprovar quando o runbook contiver /EXECUTADO em \d{4}-\d{2}-\d{2}/
L113 · FALSA · gravidade media · pendencia
Incidente: o worker congelado — diagnóstico (
diagnostico.sh), impacto medido e as duas rotas de remediação. Ainda não ensaiado
Mede com:
grep -n 'remediar-worker-congelado' docs/index.md docs/testing/user-journey-map.md
Deu:
docs/index.md:113 … **Ainda não ensaiado**
docs/testing/user-journey-map.md:585 | U6 `[P0]` | … | **EXECUTADO 2026-08-13** (U6-b, U6-c e **aplicado em produção**) …
Sugestão: | [runbooks/remediar-worker-congelado.md](runbooks/remediar-worker-congelado.md) | Incidente: o worker congelado — diagnóstico (diagnostico.sh), impacto medido e as duas rotas de remediação. Ensaiado (U6-b/U6-c) e aplicado na produção do projeto em 2026-08-13; o que os ensaios não cobriram está no §6 |
L114 · FALSA · gravidade media · pendencia
Ativação da doutrina de packaging — os 3 passos que não cabem num PR (pacote público, check obrigatório, primeira release)
Mede com:
sed -n '1,13p' docs/runbooks/ativar-packaging.md
Deu:
> **CONCLUÍDO em 2026-08-14. Este runbook é histórico** — guardado porque descreve o procedimento e as armadilhas de cada passo, não porque haja algo a fazer.
> | 1–2. Pacotes públicos | feito | ... | 3. `imagens-ok` obrigatório | feito | ... | 4. Primeira release completa | feito | v1.3.0 | 5. Tag de ensaio apagada | feito |
Sugestão: | [runbooks/ativar-packaging.md](runbooks/ativar-packaging.md) | Ativação da doutrina de packaging — CONCLUÍDA em 2026-08-14. Histórico: guarda o procedimento e as armadilhas dos passos que não cabiam num PR (pacote público, imagens-ok obrigatório, primeira release completa, tag de ensaio apagada) |
L135 · FALSA · gravidade alta · contagem
Raiz (em voo):
HANDOFF.md(follow-up),HANDOFF-harness-evolution.md,HANDOFF-operacao-visivel.md
Mede com:
ls HANDOFF*.md
Deu:
HANDOFF-conversa-vira-lead.md HANDOFF-followup-vivo.md HANDOFF-fv-w1-fila.md HANDOFF-harness-evolution.md HANDOFF-ia-360.md HANDOFF-marca-propria.md HANDOFF-operacao-visivel.md HANDOFF-sistema-vivo-consertos.md HANDOFF-tres-papeis.md HANDOFF.md (10 arquivos; a lista do índice cita 3)
Sugestão: - Raiz (em voo): a lista não cabe aqui sem apodrecer — o sinal é o arquivo, não esta linha. Liste com ls HANDOFF*.md; tudo que aparecer está em voo, e o que foi encerrado migrou para [handoffs/](handoffs/).
Vira teste: tests/unit/docs-ponteiros.test.ts: it("o índice não enumera HANDOFF da raiz") — reprovar se docs/index.md listar por nome um subconjunto próprio de ls('HANDOFF*.md') na seção 8
L138 · FRAGIL · gravidade baixa · contagem
[
superpowers/](superpowers/) —plans/especs/datados por onda, maishandoffs/
Mede com:
ls docs/superpowers/
Deu:
handoffs plans reports specs
Sugestão: - [superpowers/](superpowers/) — plans/, specs/ e reports/ datados por onda, mais handoffs/
L146 · FALSA · gravidade media · ponteiro
graphify-out/— grafo do repositório (7310 nós, 17705 arestas, 538 comunidades na última geração).
Mede com:
ls graphify-out/ ; git ls-files | grep -c '^graphify-out/' ; grep -n graphify .gitignore
Deu:
ls: graphify-out/: No such file or directory
0
100:# graphify (análise local de knowledge graph)
101:graphify-out/
Sugestão: graphify-out/ — grafo do repositório, gerado localmente e fora do git (.gitignore:101): num clone novo o diretório não existe. Gere com /graphify . antes de consultar; nós, arestas e comunidades saem do GRAPH_REPORT.md daquela geração, não deste índice — que não tem como medi-los. GRAPH_REPORT.md traz god nodes, hyperedges e comunidades. Gerado — não editar.
Vira teste: tests/unit/docs-ponteiros.test.ts: it("ponteiro de doc não aponta para caminho gitignored sem dizer isso") — se docs/index.md citar um path que git check-ignore resolve, exigir a palavra 'gerado'/'fora do git' na mesma linha
L160 · FALSA · gravidade alta · pendencia
docs/architecture/contém só o diagrama do agent-turn; a doutrina ... pede que o "mapa vivo" da arquitetura reflita toda peça nova com ≥2 arestas — NÃO IDENTIFICADO se isso está sendo cumprido, e é a lacuna documental mais relevante que sobrou.
Mede com:
ls docs/architecture/ && ls docs/architecture/*.architecture.json | wc -l
Deu:
agent-turn.html agent-turn.workflow.json atualizacao-self-service.architecture.json crm-vivo.architecture.json escalacao-ciclo-humano.architecture.json followup-dossie.architecture.json gestao-funis.architecture.json ia-360-organizar.architecture.json ia-360-retencao.architecture.json indice-de-atrito.architecture.json marca-propria.architecture.json README.md
10
Sugestão: - docs/architecture/ deixou de ser lacuna: além do diagrama do agent-turn, reúne os mapas vivos *.architecture.json exigidos pelo item 13 do DoD, com README.md próprio. Confira a cobertura antes de afirmar que a doutrina está ou não sendo cumprida: ls docs/architecture/*.architecture.json | wc -l.
Vira teste: tests/unit/mapas-de-arquitetura.test.ts (já existe): acrescentar it("o índice não declara docs/architecture/ vazio") — reprovar se docs/index.md casar /architecture/` contém só/ enquanto houver >1 arquivo *.architecture.json
docs/runbooks/ativar-packaging.md — 9
L3 · FRAGIL · gravidade alta · pendencia
CONCLUÍDO em 2026-08-14. Este runbook é histórico — guardado porque descreve o procedimento e as armadilhas de cada passo, não porque haja algo a fazer.
Mede com:
docker buildx imagetools inspect ghcr.io/melgarafael/deskcommcrm:stable --format '{{.Manifest.Digest}}' ; docker buildx imagetools inspect ghcr.io/melgarafael/deskcommcrm:1.3.0 --format '{{.Manifest.Digest}}'
Deu:
stable → sha256:c4bc70b606c8e63f3fc9557ed31b1a211b58beaac3b67509eb0c8acbc0dbaead
1.3.0 → sha256:fc10b029e326792e9972bb712b7392f9c7ba7014c749f2ba5e59f6a3b7a7f0ad
(item 10 do §Checklist de release, docs/doctrine/packaging.md:341, reprova)
Sugestão: > Os cinco passos foram dados (o último em 2026-08-14). Este runbook é histórico — > guardado porque descreve o procedimento e as armadilhas de cada passo. > Ele não atesta que o estado continua valendo, e um item segue aberto: o stable > e o 1.3.0 estão em digests diferentes nos três pacotes (§4). Reconfira cada linha > com o comando da seção correspondente antes de confiar nela.
L10 · FRAGIL · gravidade alta · pendencia
- Primeira release completa | feito | v1.3.0;
1.3.0establenos três pacotes
Mede com:
for i in deskcommcrm deskcomm-worker deskcomm-scheduler; do for t in 1.3.0 stable; do echo -n "$i:$t "; docker buildx imagetools inspect ghcr.io/melgarafael/$i:$t --format '{{.Manifest.Digest}}'; done; done
Deu:
deskcommcrm:1.3.0 sha256:fc10b029e326… / deskcommcrm:stable sha256:c4bc70b606c8…
deskcomm-worker:1.3.0 sha256:81e5af567cc8… / deskcomm-worker:stable sha256:3fe292cad2bd…
deskcomm-scheduler:1.3.0 sha256:4396263ba807… / deskcomm-scheduler:stable sha256:a0d5c3ad2296…
(pares divergentes nas TRÊS; até as camadas amd64 diferem: 42d064f0… vs 198c494e…)
Sugestão: > | 4. Primeira release completa | parcial | v1.3.0 publicada nos três pacotes; o item 10 do checklist da doutrina (stable e 1.3.0 no MESMO digest) não bate — stable ainda aponta para o build das 19:53. Rode o laço do §Checklist de release antes de confiar no canal |
L11 · FALSA · gravidade media · ponteiro
| 5. Tag de ensaio apagada | feito |
docs-doutrina-packaging→ 404 nos três |
Mede com:
grep -n '^#\+ ' docs/runbooks/ativar-packaging.md
Deu:
52:## 1. Merge do PR na `main`
67:## 2. Tornar os dois pacotes novos PÚBLICOS
91:## 3. `imagens-ok` vira status check obrigatório
125:## 4. Primeira release completa
144:## 5. Ensaio numa VPS real
(a linha 5 da tabela NÃO é o §5 do corpo — "tag de ensaio apagada" é o item 11 do checklist da doutrina; o §5 do corpo, o ensaio na VPS, não tem linha nenhuma na tabela)
Sugestão: > | 5. Ensaio numa VPS real (§5) | feito | U6-c em 2026-08-13: /api/v1/health → 1.3.0 na instalação atualizada; evidência em [remediar-worker-congelado.md](remediar-worker-congelado.md) §6 | > | Extra: tag de ensaio apagada (item 11 da doutrina) | feito | docs-doutrina-packaging → 404 nos três |
Vira teste: assert: cada linha numerada da tabela de estado do runbook casa com um heading ## N. do próprio arquivo
L19 · FALSA · gravidade media · pendencia
Enquanto eles não forem dados, parte da doutrina é conselho, não gate — e o texto diz isso onde for o caso.
Mede com:
grep -rn 'Pendência de ativação' docs/doctrine/packaging.md CLAUDE.md CONTRIBUTING.md ; sed -n '112,116p' docs/doctrine/packaging.md
Deu:
(grep: nenhuma ocorrência)
112: > **Ativado.** `imagens-ok` **é** required check da `main`. Medido em 2026-08-14:
116: > verify, build-and-size, invariants, e2e, imagens-ok
Sugestão: Este documento existe porque a entrega da [doutrina de packaging](../doctrine/packaging.md) teve três passos que não podiam estar dentro do PR: dois dependiam de administração do repositório e um dependia de as imagens existirem. Os três foram dados, e o texto da doutrina já não carrega ressalva de pendência — o invariante 2 diz Ativado (confira com grep -n 'Ativado' docs/doctrine/packaging.md).
L119 · FRAGIL · gravidade baixa · pendencia
Feito isso, remova a "Pendência de ativação" do invariante 2 em [
../doctrine/packaging.md](../doctrine/packaging.md) e as ressalvas correspondentes emCLAUDE.mdeCONTRIBUTING.md.
Mede com:
grep -rn 'Pendência de ativação' docs/doctrine/packaging.md CLAUDE.md CONTRIBUTING.md AGENTS.md
Deu:
(nenhuma ocorrência nos quatro arquivos)
Sugestão: Isso já foi feito — grep -rn 'Pendência de ativação' docs/doctrine/packaging.md CLAUDE.md CONTRIBUTING.md não devolve nada, e o invariante 2 hoje abre com Ativado. O parágrafo fica como registro do porquê: deixar a ressalva de pé depois de a pendência ter sido resolvida transforma a doutrina em documento que subestima a si mesmo — o defeito espelhado do que ela veio consertar.
L145 · FALSA · gravidade media · ponteiro
O item 11 do checklist de release, e o único que o CI não exercita.
Mede com:
grep -n '^\[ \] 1[12]\.' docs/doctrine/packaging.md ; grep -n 'O item 12 é o único' docs/doctrine/packaging.md
Deu:
341:[ ] 10. SÓ AGORA: `stable` e X.Y.Z são o MESMO digest, nas três imagens:
(item 11) "Apagar tags de branch dos três pacotes — `docs-doutrina-packaging` …"
(item 12) "Ensaio de atualização numa instalação real (não fresca): update.sh …"
+ packaging.md: "O item 12 é o único que exige VPS."
Sugestão: O item 12 do checklist de release — o único que exige VPS, e o único que a suíte de CI não exercita. (O item 11 é outra coisa: apagar as tags de branch dos três pacotes.)
Vira teste: assert: todo ponteiro do tipo "item N do checklist de release" em docs/ casa com a linha [ ] N. de docs/doctrine/packaging.md
L159 · FALSA · gravidade alta · sobre-o-codigo
É o caso U6 de [
../testing/user-journey-map.md](../testing/user-journey-map.md), declarado como não coberto de propósito.
Mede com:
grep -n "U6" docs/testing/user-journey-map.md | head -10
Deu:
585:| U6 `[P0]` | **Ensaio de atualização numa VPS real…** | **EXECUTADO 2026-08-13** (U6-b, U6-c e **aplicado em produção**) … |
587:U6 deixou de ser buraco em 2026-08-13, e o ensaio pagou o próprio custo
Sugestão: É o caso U6 de [../testing/user-journey-map.md](../testing/user-journey-map.md), executado em 2026-08-13 (U6-b, U6-c e aplicado em produção), com uma ressalva que só o ensaio podia revelar: a 1ª execução do update.sh não conserta o worker enquanto o canal stable não existir; a 2ª conserta. Evidência e limites em [remediar-worker-congelado.md](remediar-worker-congelado.md) §6. Continua fora da cobertura do CI — o que o mapa registra é o ensaio feito à mão, não um gate.
L164 · FALSA · gravidade alta · pendencia
Enquanto os passos 1–4 não acontecem
Mede com:
gh api repos/melgarafael/DeskcommCRM/branches/main/protection --jq '.required_status_checks.contexts|join(", ")' ; ghcr_status deskcommcrm stable
Deu:
verify, build-and-size, invariants, e2e, imagens-ok
deskcommcrm latest=200 stable=200 1.3.0=200 (idem worker e scheduler)
Sugestão: ## Como era enquanto os passos 1–4 não tinham acontecido (histórico — eles aconteceram em 2026-08-14) Nada quebrava, e isso era por construção. Nenhuma linha da tabela abaixo descreve o estado de hoje: os quatro passos foram dados. Ela fica como registro do desenho — para o estado atual, rode os comandos de cada seção.
L173 · FALSA · gravidade alta · ativo-obrigatorio
| PR de contribuidor |
imagens-okroda e informa; não bloqueia até o passo 3 |
Mede com:
gh api repos/melgarafael/DeskcommCRM/branches/main/protection --jq '.required_status_checks.contexts|join(", ")'
Deu:
verify, build-and-size, invariants, e2e, imagens-ok
Sugestão: | PR de contribuidor | imagens-ok rodava e informava; não bloqueava. Desde 2026-08-14 bloqueia — é required check da main (gh api repos/melgarafael/DeskcommCRM/branches/main/protection --jq '.required_status_checks.contexts\|join(", ")') |
docs/runbooks/deploy.md — 7
L12 · FALSA · gravidade media · ponteiro
cd /var/www/crm
Mede com:
grep -n "REPO_DIR=\|git clone\|PROJECT_DIR=" hostgator-setup-kit/install.sh ; grep -rn "/var/www" hostgator-setup-kit/*.sh ; grep -rn "^cd " docs/runbooks/*.md
Deu:
install.sh:22: REPO_DIR="${REPO_DIR:-deskcommcrm}"
install.sh:728: git clone --depth 1 "$REPO_URL" "$REPO_DIR" → cd "$REPO_DIR" ; PROJECT_DIR="$(pwd)"
/var/www no kit: só diagnostico.sh:46, como ÚLTIMO palpite de uma lista de 6 candidatos.
Runbook irmão: docs/runbooks/remediar-worker-congelado.md:81,101,110,218 usam `cd /caminho/do/projeto`.
Sugestão: bash cd /caminho/do/projeto # o install.sh clona em ./deskcommcrm — normalmente /root/deskcommcrm. # Não sabe onde está? find /root /opt /home -maxdepth 4 -name docker-compose.prod.yml docker compose -f docker-compose.prod.yml -f docker-compose.traefik.yml --env-file .env up -d app
L25 · FALSA · gravidade alta · ponteiro
associa o contêiner à rede que o Traefik enxerga (
TRAEFIK_DOCKER_NETWORK)
Mede com:
grep -rn "TRAEFIK_DOCKER_NETWORK" . --exclude-dir=node_modules --exclude-dir=.git ; grep -rln "TRAEFIK_NETWORK" . --exclude-dir=node_modules --exclude-dir=.git
Deu:
TRAEFIK_DOCKER_NETWORK -> 1 ocorrência em TODO o repo: docs/runbooks/deploy.md:25 (a própria frase).
TRAEFIK_NETWORK -> .env.hostgator.example, docker-compose.traefik.yml, hostgator-setup-kit/install.sh, _common.sh, test-validators.sh.
docker-compose.traefik.yml: traefik.docker.network: "${TRAEFIK_NETWORK:-traefik}" e networks.proxy.name: "${TRAEFIK_NETWORK:-traefik}".
.env.hostgator.example:75: #TRAEFIK_NETWORK=traefik
Sugestão: - associa o contêiner à rede onde o Traefik o encontra — TRAEFIK_NETWORK no .env, que o install.sh descobre e grava. O nome da variável está no próprio override, não confie nesta linha: grep -n TRAEFIK docker-compose.traefik.yml;
Vira teste: extrair de docs/runbooks/deploy.md todo token [A-Z][A-Z0-9_]{3,} entre crases e assertar que cada um aparece em docker-compose*.yml, .env.example ou .env.hostgator.example — expect(orfaos).toEqual([])
L46 · NAO_VERIFICAVEL · gravidade baixa · sobre-o-codigo
medido: com um -f ou com os dois, devolve o MESMO contêiner
Mede com:
docker version --format '{{.Server.Version}}'
Deu:
Sem resposta — o comando estourou 120s e foi para background; o daemon Docker não está de pé nesta máquina. `docker compose config` (que não precisa do daemon) roda, mas `ps -q` precisa. Afirmação NÃO reproduzida aqui.
Sugestão: O caminho que não depende da afirmação: use os dois -f também no inspect e apague a justificativa. Um runbook não precisa defender uma economia que não economiza nada. bash docker inspect "$(docker compose -f docker-compose.prod.yml -f docker-compose.traefik.yml ps -q app)" \ --format '{{.Config.Labels}}' | grep -o 'traefik.enable:[^ ]*' # esperado: traefik.enable:true (vazio = roteamento quebrado)
L54 · FRAGIL · gravidade baixa · sobre-o-codigo
esperado: 307 (redireciona pro login)
Mede com:
cat lib/auth/public-paths.ts ; cat app/page.tsx ; sed -n '60,90p' proxy.ts
Deu:
lib/auth/public-paths.ts: PUBLIC_PATHS começa com /^\/$/ → a raiz É pública, o proxy a deixa passar sem checar sessão.
app/page.tsx: export default function HomePage() { redirect("/app"); }
proxy.ts:83-86: só depois, em /app, é que o não-autenticado vira NextResponse.redirect(/login?next=...).
app/app/layout.tsx:28: if (!user) redirect("/login");
Sugestão: # esperado: 307 — o primeiro salto vai para /app (app/page.tsx), porque a raiz é # caminho público (lib/auth/public-paths.ts). Quem segue os redirects # (curl -L) termina no /login. # 404 = labels perdidas, refaça o deploy com os dois -f
L63 · FALSA · gravidade alta · sobre-o-codigo
commit → push → PR → merge na main → CI publica imagem → VPS puxa
Mede com:
grep -n "TARGET_TAG" hostgator-setup-kit/update.sh | head -3 ; grep -n "stable" docker-compose.prod.yml ; grep -rn "gh release create" docs/ .github/
Deu:
update.sh:39: [ -n "$TARGET_TAG" ] || TARGET_TAG="$(git tag -l 'v*' --sort=-v:refname | head -1)"
docker-compose.prod.yml:35 image: ${APP_IMAGE:-ghcr.io/melgarafael/deskcommcrm:stable} (idem worker:92 e scheduler:196)
docs/DEPLOY-CHECKLIST.md:45: - [ ] `gh release create vX.Y.Z` com as notas do CHANGELOG
docs/doctrine/packaging.md:340: [ ] 9. `gh release create vX.Y.Z`
Medido no GHCR: deskcommcrm:latest revision=840917ed (topo da main) vs deskcommcrm:stable version=1.3.0 revision=9bd59e9.
Sugestão: commit → push → PR → merge na main → CI publica `latest` → `gh release create vX.Y.Z` → CI publica a versão + `stable` → VPS puxa O merge na main NÃO chega a instalação nenhuma. Ele publica latest, e ninguém consome latest: o docker-compose.prod.yml tem :stable como default nas três imagens, e o update.sh alveja a maior tag v* (git tag -l 'v*' --sort=-v:refname | head -1). Sem cortar a release, a correção fica parada no registry. O checklist da release está em [../DEPLOY-CHECKLIST.md](../DEPLOY-CHECKLIST.md).
L72 · FRAGIL · gravidade media · contagem
publica três imagens —
deskcommcrm,deskcomm-workeredeskcomm-scheduler— sempre na mesma versão
Mede com:
grep -n "fail-fast\|push: \|- name: deskcomm" .github/workflows/publish-image.yml ; grep -n -A8 'if ! dc pull' hostgator-setup-kit/update.sh
Deu:
publish-image.yml: strategy.fail-fast: false ; matrix com deskcommcrm, deskcomm-worker, deskcomm-scheduler ; push: ${{ github.event_name != 'pull_request' }} em cada job da matriz.
update.sh:200: "if ! dc pull; then ... se um run de publicação quebrou ... o compose ainda tem build: ao lado do image: do worker e do scheduler"
GHCR (medido agora): as três respondem HTTP 200 em :latest e :stable.
Sugestão: publica três imagens — deskcommcrm, deskcomm-worker e deskcomm-scheduler — sob a mesma tag de versão. Sob a mesma tag, não necessariamente as três: a matriz roda com fail-fast: false e cada job empurra a sua, então uma versão pode existir para duas imagens e faltar na terceira (é exatamente o caso que o dc pull do update.sh trata). Antes de anunciar uma versão, confirme as três: for i in deskcommcrm deskcomm-worker deskcomm-scheduler; do docker buildx imagetools inspect ghcr.io/melgarafael/$i:<versão> --format '{{.Manifest.Digest}}'; done
L108 · FRAGIL · gravidade media · contagem
Requisitos: >= 4 GB de RAM ou swap (medido: ~4min num VPS de 3.8 GB com 4 GB de swap)
Mede com:
grep -rn "15-25min\|~4min" docker-compose.build.yml docs/
Deu:
docker-compose.build.yml:2: # Uso (avançado — requer VPS >=4GB RAM ou swap; build leva ~15-25min num VPS):
docs/runbooks/deploy.md:108: (medido: ~4min num VPS de 3.8 GB com 4 GB de swap)
docs/superpowers/specs/2026-07-02-hostgator-deploy-design.md:64: webpack 34min vs ~4min do turbopack ← o ~4min ali é do `pnpm build`, não da imagem
docs/adr/0001-packaging-e-distribuicao.md:148: "4–34 min" não existe no repo
Sugestão: Requisitos: >= 4 GB de RAM ou swap. Quanto demora, meça na hora (time docker compose ... build app) — os dois números que o repo carrega não medem a mesma coisa: o docker-compose.build.yml fala em ~15-25min para a imagem inteira, e os ~4min de docs/superpowers/specs/2026-07-02-hostgator-deploy-design.md são só o pnpm build com turbopack, dentro dela. E isto é o requisito deste caminho de exceção, não da operação normal.
docs/runbooks/remediar-worker-congelado.md — 11
L17 · FRAGIL · gravidade media · ativo-obrigatorio
A ressalva, e ela é o motivo de este aviso continuar aqui: a primeira execução do
update.shNÃO consertou o worker.
Mede com:
docker buildx imagetools inspect ghcr.io/melgarafael/deskcomm-worker:stable 2>&1 | awk '/^Digest:/{print $2; exit}'
Deu:
sha256:3fe292cad2bd8aa6ed0182e67a7bafd6e857e0fff14cedcb53694254dca0ef95 (o canal `stable` EXISTE — a precondição do U6-b não vale mais)
Sugestão: A ressalva, e ela mudou de motivo: com uma release publicada — o canal stable existe desde a v1.3.0 —, a primeira execução já troca a imagem do worker, mas a deixa sem pin, seguindo um canal móvel. Sem release publicada (o caso do U6-b), a primeira execução não troca nada. Nos dois casos a segunda execução é obrigatória: é ela que pina as três na mesma versão. Ver §5.0.
L54 · FRAGIL · gravidade baixa · contagem
Entre a data da imagem do worker e a versão instalada do app, 9 commits e 399 linhas em
workers/nunca chegaram.
Mede com:
git log --oneline --since=2026-07-31 --until=2026-08-13 -- workers/ | wc -l; git log --numstat --format='' --since=2026-07-31 --until=2026-08-13 -- workers/ | awk '{i+=$1;d+=$2} END{print i+d}'; git log --oneline ee520110..v1.2.1 -- workers/ | wc -l; git log --numstat --format='' ee520110..v1.2.1 -- workers/ | awk '{i+=$1;d+=$2} END{print i+d}'; git diff --shortstat ee520110 v1.2.1 -- workers/
Deu:
janela por data: 9 commits, 439 linhas (406+/33-)
ee520110..v1.2.1: 11 commits, 397 linhas (364+/33-)
diff líquido ee520110→v1.2.1: 4 files changed, 354 insertions(+), 23 deletions(-)
Sugestão: Entre a data da imagem do worker (31/07) e a versão instalada do app (1.2.1, de 12/08), os commits que tocaram workers/ nunca chegaram. O número depende do recorte — reconte em vez de repassar: bash git log --oneline --since=2026-07-31 --until=2026-08-13 -- workers/ | wc -l # 9 git log --numstat --format='' ee520110..v1.2.1 -- workers/ \ | awk '{i+=$1;d+=$2} END{print i+d}' # 397
L87 · FRAGIL · gravidade media · sobre-o-codigo
até a versão que conserta isto, ele lê
npm_package_version, que éundefinedsobCMD ["node","server.js"]. Toda instalação responde0.1.0— afetada ou não.
Mede com:
git show v1.2.1:app/api/v1/health/route.ts | grep -n 'npm_package_version'; git show v1.3.0:app/api/v1/health/route.ts | grep -n 'APP_VERSION'; grep -n 'version:' app/api/v1/health/route.ts
Deu:
v1.2.1:227 version: process.env.npm_package_version ?? "0.1.0",
v1.3.0:236 version: process.env.APP_VERSION || "desconhecido",
HEAD :236 version: process.env.APP_VERSION || "desconhecido",
Sugestão: Por que não basta olhar o /api/v1/health: numa instalação afetada (app ≤ 1.2.1) ele lê npm_package_version, que é undefined sob CMD ["node","server.js"] — então responde 0.1.0 esteja ela afetada ou não. Foi medido na produção. Desde a 1.3.0 o campo vem de APP_VERSION (injetada no build) e o fallback é desconhecido, nunca um número plausível — por isso ele serve como conferência depois da remediação (A4), mas nunca como diagnóstico antes.
L134 · FALSA · gravidade alta · sobre-o-codigo
backup do banco antes,
baseline.sqlidempotente, healthcheck no fim, e rollback automático do app se ele não voltar.
Mede com:
sed -n '250,270p' hostgator-setup-kit/update.sh; grep -n 'set_env_var .env APP_IMAGE "$PREV_IMAGE"' hostgator-setup-kit/agent.sh
Deu:
update.sh:264 c_ylw "⚠ Atualizei, mas o app não respondeu 'ok'. Veja os logs:"
update.sh:266 # Código de saída != 0: é o que o agent.sh usa pra saber que precisa voltar
update.sh:269 exit 1
agent.sh:299 set_env_var .env APP_IMAGE "$PREV_IMAGE"
Sugestão: A Rota A é mais longa e é o caminho que o update.sh já percorre em toda atualização: backup do banco antes, baseline.sql idempotente e healthcheck no fim. O rollback automático NÃO é do update.sh — executado à mão ele só sai com código 1 se o app não voltar; quem guarda a imagem anterior e reverte é o agent.sh (o caminho do botão da tela). Rodando o update.sh à mão, o rollback é o manual do A3. (O próprio §5, passo A3, já diz isto — este parágrafo é que discordava dele.)
L283 · FALSA · gravidade media · contagem
O
update.shmexe em exatamente três chaves do.env(APP_IMAGE,APP_PULL_POLICYe — desde esta versão — as do worker e do scheduler)
Mede com:
grep -n -A 13 '^gravar_imagens()' hostgator-setup-kit/_common.sh; grep -n 'ensure_encryption_key\|NUVEMSHOP_OAUTH_ENCRYPTION_KEY' hostgator-setup-kit/update.sh hostgator-setup-kit/_common.sh
Deu:
_common.sh:495-500 set_env_var … APP_IMAGE / APP_PULL_POLICY / WORKER_IMAGE / WORKER_PULL_POLICY / SCHEDULER_IMAGE / SCHEDULER_PULL_POLICY → 6 chaves
update.sh:274 ensure_encryption_key .env
_common.sh:638 printf '\nNUVEMSHOP_OAUTH_ENCRYPTION_KEY=%s\n' "$key" >> "$envfile" → 7ª chave possível
Sugestão: O update.sh reescreve seis chaves de imagem no .env — APP_IMAGE, APP_PULL_POLICY, WORKER_IMAGE, WORKER_PULL_POLICY, SCHEDULER_IMAGE, SCHEDULER_PULL_POLICY (todas em gravar_imagens, hostgator-setup-kit/_common.sh) — e acrescenta NUVEMSHOP_OAUTH_ENCRYPTION_KEY se ela faltar. Todo o resto fica intacto, inclusive o que o operador acrescentou à mão. Confira sem depender deste número: diff /root/.env.antes-remediacao .env.
Vira teste: tests/unit/packaging-artefato-do-cliente.test.ts: extrair as chaves que gravar_imagens grava em _common.sh e assertar expect(chaves).toEqual(['APP_IMAGE','APP_PULL_POLICY','WORKER_IMAGE','WORKER_PULL_POLICY','SCHEDULER_IMAGE','SCHEDULER_PULL_POLICY']) — quem acrescentar a quarta imagem reprova e vai atualizar o runbook.
L286 · FALSA · gravidade media · pendencia
O
diffdo A5 é o que prova isso, e é uma das coisas que o U6-b precisa confirmar.
Mede com:
sed -n '290,312p' docs/runbooks/remediar-worker-congelado.md; sed -n '372,376p' docs/runbooks/remediar-worker-congelado.md
Deu:
§6 (mesmo arquivo): "| customização do operador no `.env` | presente | **presente** | ✅ |"
P4 (mesmo arquivo): "**nenhuma** chave do `.env` sumiu (39 → 43: as 4 novas são as de imagem)"
Sugestão: O diff do A5 é o que prova isso, e já foi confirmado duas vezes: no U6-b a customização do operador sobreviveu ao ciclo (§6) e, na execução real, nenhuma chave do .env sumiu (39 → 43 — as 4 novas são as de imagem).
L345 · FRAGIL · gravidade baixa · data-versao
revision=9bd59e93,version=1.3.0— o digest bate com o da release
Mede com:
docker buildx imagetools inspect ghcr.io/melgarafael/deskcomm-worker:stable 2>&1 | awk '/^Digest:/{print $2;exit}'; docker buildx imagetools inspect ghcr.io/melgarafael/deskcomm-worker:1.3.0 2>&1 | awk '/^Digest:/{print $2;exit}'; git rev-parse v1.3.0^{commit}
Deu:
stable → sha256:3fe292cad2bd8aa6ed0182e67a7bafd6e857e0fff14cedcb53694254dca0ef95
1.3.0 → sha256:81e5af567cc8b72b6e0aa75295029b0c5b51d35d49a242827574a77b3c1b5cca
git → 9bd59e9361811ad1609969a9887fa46e84db343d (= label revision das DUAS imagens)
Sugestão: revision=9bd59e93, version=1.3.0 — a revision bate com o commit da tag v1.3.0 (git rev-parse v1.3.0^{commit} → 9bd59e9361811ad1…), que é o que prova identidade de conteúdo. O digest do índice de :stable e o de :1.3.0 são diferentes (pushes distintos do mesmo conteúdo), então comparar digest entre tags mede a coisa errada.
L419 · FALSA · gravidade alta · data-versao
As duas tags apontam para a mesma imagem. Aplicar o pin hoje não muda um byte do que roda
Mede com:
for t in latest latest-2026.7.2; do printf 'devlikeapro/waha:%-18s -> ' "$t"; docker buildx imagetools inspect devlikeapro/waha:$t 2>&1 | awk '/^Digest:/{print $2; exit}'; done; docker buildx imagetools inspect devlikeapro/waha:latest --format '{{json .Image}}' | head -c 60
Deu:
devlikeapro/waha:latest -> sha256:d52ad4f394d2e48eb92d58e0f04924ff6c7621a883d08ff64176479ecd77c9ca
devlikeapro/waha:latest-2026.7.2 -> sha256:65e593e30bb702f891550b9da5d65e9e0eff8a926f5451fac6a582db84d3a323
{ "created": "2026-08-14T12:14:01.705395726Z", ...
Sugestão: Ainda não consertado — e a janela FECHOU. O upstream publicou uma :latest nova em 2026-08-14T12:14Z, e as duas tags deixaram de apontar para a mesma imagem. Meça antes de decidir, em vez de confiar neste parágrafo: bash for t in latest latest-2026.7.2; do printf 'devlikeapro/waha:%s -> ' "$t" docker buildx imagetools inspect devlikeapro/waha:$t | awk '/^Digest:/{print $2; exit}' done Enquanto os digests forem iguais, aplicar o pin é só um restart do WAHA — a string muda, o config-hash muda, o conteúdo não. Quando forem diferentes — como estão agora —, o próximo update.sh de qualquer instalação legada TROCA a versão do WhatsApp sem ninguém pedir, porque dc pull sem argumento inclui o waha.
L435 · FALSA · gravidade alta · data-versao
Um restart do WhatsApp, não uma troca de versão.
Mede com:
docker buildx imagetools inspect devlikeapro/waha:latest 2>&1 | awk '/^Digest:/{print $2; exit}'
Deu:
sha256:d52ad4f394d2e48eb92d58e0f04924ff6c7621a883d08ff64176479ecd77c9ca (≠ latest-2026.7.2 = sha256:65e593e30bb7…)
Sugestão: O custo real de aplicar o pin tem duas partes, e a segunda mudou de sinal. (1) Mudar a string da imagem muda o config-hash do serviço, então o up -d recria o contêiner mesmo com digest idêntico — isso continua valendo, e está medido abaixo. (2) Desde 2026-08-14 os digests não são mais idênticos: aplicar o pin numa instalação legada agora TROCA a versão do WAHA, não só reinicia. NÃO MEDIDO: se uma sessão pareada volta WORKING depois de um restart — e menos ainda depois de uma troca de versão.
L439 · FALSA · gravidade alta · ponteiro
o enquadramento e as opções estão na issue do resíduo.
Mede com:
gh issue list --state all --limit 100 --json number,title | grep -i 'waha\|resíduo\|residuo\|pin'; gh issue list --state all --search 'created:>=2026-08-12' --json number,title
Deu:
nenhuma issue com WAHA/resíduo/pin no título; as 5 issues criadas desde 2026-08-12 são #235-#239 (Tailwind 4, pdf-parse, Zod em webhooks, credencial de canal, AGENTS.md) — nenhuma sobre o resíduo do WAHA
Sugestão: Segue não consertado porque a decisão é de quem opera. Não existe issue aberta sobre isto — quem for decidir abre uma primeiro, com as duas opções (reescrever WAHA_IMAGE no .env legado × deixar como está) e o custo medido de cada uma, e substitui esta frase pelo número dela.
Vira teste: tests/unit/evidencia-citada.test.ts (mesmo molde): varrer docs/**/*.md por referências a issue sem número ("a issue do …", "na issue de …") e reprovar — ponteiro sem alvo nomeável não é ponteiro.
L460 · FRAGIL · gravidade media · sobre-o-codigo
Nada aqui roda sozinho. Não existe atualização automática nem compulsória: o agente de atualização da tela só age quando alguém clica em "Atualizar agora"
Mede com:
grep -n 'PIN_CORRIGIDO=\|update_requested' hostgator-setup-kit/agent.sh; grep -n -A 5 '^completar_pin_ausente()' hostgator-setup-kit/_common.sh
Deu:
agent.sh:180 PIN_CORRIGIDO="$(completar_pin_ausente .env)" || PIN_CORRIGIDO="" ← roda ANTES do heartbeat, sem clique
agent.sh:196 [ "$(json_field "$RESP" update_requested)" = "true" ] || exit 0 ← só o update.sh depende do clique
_common.sh:470 set_env_var "$envfile" "$chave" "${IMG_NS}/${repo}:${ver}"
Sugestão: Nada aqui atualiza sozinho: não existe atualização automática nem compulsória — o update.sh só roda quando alguém o executa, e o agente da tela só o dispara quando alguém clica em "Atualizar agora". A única escrita que o agente faz por conta própria é fechar a lacuna do pin (§5.0, item 2): se WORKER_IMAGE/SCHEDULER_IMAGE estiverem ausentes, ele grava a versão que o contêiner já roda. Valor que já existe no .env ele nunca toca.
RETRATO — 60 achados
docs/current-state.md — 40
L5 · FALSA · gravidade media · data-versao
last_updated: 2026-07-29
Mede com:
git log -1 --format='%ad' --date=short -- docs/current-state.md
Deu:
2026-08-14
Sugestão: last_updated: 2026-08-14 — e, já que o corpo carrega blocos datados de 2026-08-13 e 2026-08-14 enquanto o frontmatter dizia 2026-07-29, vale a nota: o frontmatter é prazo de validade, e um que mente contra o próprio corpo é pior que nenhum. Régua: git log -1 --format=%ad --date=short -- docs/current-state.md.
Vira teste: tests/unit: o last_updated do frontmatter de docs/current-state.md e docs/harness-audit.md não pode ser anterior à data do último commit que tocou o arquivo.
L8 · FALSA · gravidade media · data-versao
audited_against: origin/main @
789dfa6(v1.0.0, 2026-07-27)
Mede com:
git log --oneline -1; grep -m2 -E '^## \[[0-9]' CHANGELOG.md
Deu:
840917ed Merge pull request #251 from melgarafael/fix/canal-stable-divergente
## [1.3.0] — 2026-08-13
Sugestão: audited_against: origin/main @ 840917ed (v1.3.0, 2026-08-13) — e cada bloco que for remedido depois carrega o próprio SHA inline, como o §4.1 já faz. Um audited_against global num doc emendado em cinco datas diferentes descreve só a primeira.
L33 · FALSA · gravidade media · data-versao
Versão:
1.0.0, marcada em 2026-07-27 (CHANGELOG.md). Primeira release versionada
Mede com:
grep -nE '^## ' CHANGELOG.md | head -4
Deu:
9:## [Não lançado]
11:## [1.3.0] — 2026-08-13
72:## [1.2.1] — 2026-08-12
117:## [1.2.0] — 2026-08-11
Sugestão: Versão: a última release está no topo do CHANGELOG.md (grep -m2 -E '^## \[[0-9]' CHANGELOG.md). A primeira release versionada foi a 1.0.0, em 2026-07-27; o projeto vinha sendo desenvolvido publicamente desde abril de 2026 sem tags.
L38 · FALSA · gravidade media · contagem
| Arquivos TS/TSX em
app+lib+components+workers| 987 |
Mede com:
find app lib components workers -name '*.ts' -o -name '*.tsx' | wc -l
Deu:
1305
Sugestão: Trocar a coluna "Valor" por "Régua": find app lib components workers -name '*.ts' -o -name '*.tsx' \| wc -l. Um doc que diz como contar não apodrece; um que diz o total apodrece no próximo merge — esta tabela inteira envelheceu num intervalo de duas semanas.
L39 · FALSA · gravidade media · contagem
| Route handlers (
app/api/**/route.ts) | 169 |
Mede com:
find app/api -name 'route.ts' | wc -l
Deu:
203
Sugestão: | Route handlers | find app/api -name 'route.ts' \| wc -l |
L40 · FALSA · gravidade media · contagem
| Migrations em
supabase/migrations/| 81 arquivos, até0092_stage_names_acentos|
Mede com:
ls supabase/migrations/*.sql | wc -l; ls supabase/migrations/*.sql | tail -1
Deu:
147
supabase/migrations/20260814090000_0157_marca_por_organizacao.sql
Sugestão: | Migrations | ls supabase/migrations/*.sql \| wc -l; a última é ls supabase/migrations/*.sql \| tail -1 |
L41 · FALSA · gravidade media · contagem
| Testes unitários (
*.test.ts(x)) | 221 arquivos |
Mede com:
git ls-files | grep -cE '\.test\.(ts|tsx)$'
Deu:
511
Sugestão: | Testes (*.test.ts(x)) | git ls-files \| grep -cE '\\.test\\.(ts\|tsx)$' |
L42 · FALSA · gravidade media · contagem
| Invariantes de banco (
tests/invariants/) | 56 arquivos |
Mede com:
ls tests/invariants/*.test.ts | wc -l; ls tests/invariants/*.ts | wc -l
Deu:
103 (arquivos .test.ts)
107 (todos .ts)
Sugestão: | Invariantes de banco | ls tests/invariants/*.test.ts \| wc -l — e declare a régua junto, porque "arquivos" e "casos" divergem por uma ordem de grandeza (o CLAUDE.md cita casos) |
L43 · FALSA · gravidade media · contagem
| Specs E2E (
tests/e2e/) | 19 |
Mede com:
ls tests/e2e/*.spec.ts | wc -l
Deu:
48
Sugestão: | Specs E2E | ls tests/e2e/*.spec.ts \| wc -l — este número já apodreceu quatro vezes; nunca copie de outro doc |
L44 · FALSA · gravidade media · contagem
| Documentos
.mdemdocs/| 119 (em 23 subpastas) |
Mede com:
git ls-files docs | grep -c '\.md$'; find docs -mindepth 1 -maxdepth 1 -type d | wc -l
Deu:
161
20 subpastas de 1º nível (30 contando aninhadas)
Sugestão: | Documentos .md em docs/ | git ls-files docs \| grep -c '\.md$' — e declare a régua: docs/index.md usa git ls-files 'docs/**/*.md', que não conta os .md do topo de docs/ e devolve um número menor |
L45 · NAO_VERIFICAVEL · gravidade media · contagem
| Import cycles | 0 (graphify, medido em árvore anterior) |
Mede com:
ls -d graphify-out
Deu:
ls: graphify-out: No such file or directory — o artefato que produziu o número não está no repo, e o próprio texto admite "árvore anterior"
Sugestão: Remover a linha da tabela. O número veio de um graphify-out/ que não existe mais neste repo e de uma árvore que não é esta; "0 ciclos medido noutra árvore" não é dado sobre o HEAD. Se o invariante importa, ele vira gate (madge --circular num step do verify), não linha de tabela.
Vira teste: CI: npx madge --circular --extensions ts,tsx app lib components workers reprovando em ciclo novo — aí o doc pode dizer "zero, e o gate garante".
L47 · FALSA · gravidade baixa · contagem
|
: any/as any| 7 |
Mede com:
grep -rnE "(: any\b|as any\b)" app lib components workers --include='*.ts' --include='*.tsx' | wc -l
Deu:
12
Sugestão: | : any / as any | grep -rnE '(: any\\b\|as any\\b)' app lib components workers --include='*.ts' --include='*.tsx' \| wc -l |
L54 · FALSA · gravidade media · contagem
o apêndice idempotente de
baseline.sqlcobre atémigration 0092, que é a última emsupabase/migrations/
Mede com:
grep -oE 'migration [0-9]{4}' supabase/baseline.sql | sort -u | tail -3; ls supabase/migrations/*.sql | tail -1
Deu:
migration 0155 / migration 0156 / migration 0157
supabase/migrations/20260814090000_0157_marca_por_organizacao.sql
Sugestão: o apêndice idempotente de baseline.sql cobre até a última migration de supabase/migrations/ — o invariante segue de pé. Confira sem citar número: diff <(grep -oE 'migration [0-9]{4}' supabase/baseline.sql | sort -u | tail -1) <(ls supabase/migrations/*.sql | tail -1 | grep -oE '_[0-9]{4}_' | tr -d _ | sed 's/^/migration /').
Vira teste: tests/invariants: a maior migration de supabase/migrations/ tem apêndice correspondente no baseline.sql. É o invariante que o próprio parágrafo diz ser o mais fácil de quebrar.
L66 · FALSA · gravidade media · sobre-o-codigo
Fundação & plataforma — auth com MFA para admin, multi-tenancy com RLS + teste de isolamento
Mede com:
grep -n 'export function' lib/auth/politica-mfa.ts
Deu:
67:export function exigeCadastroDeMfa(p: PoliticaDeMfa): boolean;
81:export function empresaExigeMfa(settings: unknown): boolean — a política soma platform_admins.mfa_required e organizations.settings.security.mfa_required, ambas com padrão NÃO exigir
Sugestão: Fundação & plataforma — auth com MFA TOTP opcional e ligado por quem administra (duas políticas independentes que somam, ambas com padrão não-exigir; regra em lib/auth/politica-mfa.ts), multi-tenancy com RLS + teste de isolamento, RBAC de 4 papéis, audit log append-only, onboarding de tenant.
L97 · FALSA · gravidade media · pendencia
| Follow-up inteligente (
HANDOFF.md) | Ondas 1–7 ✅; Onda 8 em andamento […] | gatilhostage_change, flywheel, e o fechamento do checklist DoD/PRD da 8.3 |
Mede com:
ls tests/e2e/gatilho-de-etapa.spec.ts tests/e2e/gatilho-de-caso.spec.ts
Deu:
tests/e2e/gatilho-de-etapa.spec.ts
tests/e2e/gatilho-de-caso.spec.ts — ambas nas listas de execução do e2e.yml (linha 128)
Sugestão: O gatilho de mudança de etapa deixou de ser pendência: tests/e2e/gatilho-de-etapa.spec.ts existe e roda no e2e.yml. Reescreva a linha listando só o que ainda falta (flywheel e o fechamento do checklist da 8.3), ou — melhor — troque a coluna "o que falta" por ponteiro ao HANDOFF, porque tabela de progresso num doc separado do épico apodrece por construção.
L100 · NAO_VERIFICAVEL · gravidade media · pendencia
| Casos humanos […] | Wave 7 (prova E2E) relatada PARCIAL […] | A CONFIRMAR se fechou |
Sugestão: As quatro entradas "A CONFIRMAR" da tabela §3 (Casos humanos, Inbox multimodal ondas 4–6, Fase FG, credencial Anthropic/Google) estão pendentes desde 2026-07-29 e nenhuma tem régua declarada. Ou cada uma ganha um comando que a responde, ou saem da tabela e viram issue com dono — "A CONFIRMAR" sem instrumento é pendência que ninguém pode fechar, e é o estado mais podre que um doc de estado pode carregar.
L112 · FALSA · gravidade alta · pendencia
transbordo de layout a 390px em qualquer tela e não existe caminho de criação de funil (só de etapas).
Mede com:
ls tests/e2e/pipelines-gestao.spec.ts docs/architecture/gestao-funis.architecture.json
Deu:
tests/e2e/pipelines-gestao.spec.ts
docs/architecture/gestao-funis.architecture.json
Sugestão: O achado de criação de funil foi endereçado: a gestão de funis pela tela existe, com mapa vivo (docs/architecture/gestao-funis.architecture.json) e spec E2E (tests/e2e/pipelines-gestao.spec.ts) que roda no e2e.yml. Continua registrado e não endereçado apenas o transbordo de layout a 390px, que é bug de primeira impressão em mobile.
L147 · FALSA · gravidade alta · contagem
e2e.ymlroda 45 das 46 specs, e oe2eé check obrigatório na branch protection desde 2026-08-08 (junto comverify,build-and-size,invariantseimagens-ok— cinco)
Mede com:
ls tests/e2e/*.spec.ts | wc -l; awk '/SPECS_PARTE_1:/,/FORA_DO_CI:/' .github/workflows/e2e.yml | grep -v '^ *#' | grep -oE '[a-z0-9-]+\.spec\.ts' | wc -l; gh api repos/melgarafael/DeskcommCRM/branches/main/protection --jq '.required_status_checks.contexts|join(", ")'
Deu:
48
47
verify, build-and-size, invariants, e2e, imagens-ok
Sugestão: e2e.yml roda todas as specs menos uma, e o e2e é check obrigatório na branch protection (junto com verify, build-and-size, invariants e imagens-ok — cinco). A única fora é vps-fresh-onboarding. Não copie o número daqui: ls tests/e2e/*.spec.ts | wc -l contra o FORA_DO_CI de .github/workflows/e2e.yml; a soma é conferida por tests/unit/e2e-cobertura-completa.test.ts. Este número já apodreceu quatro vezes — a quarta foi esta linha, escrita em 2026-08-14 e errada em dois dias.
L163 · FALSA · gravidade media · contagem
aplica
baseline.sqlem modo install e update, e roda os 56 arquivos detests/invariants/
Mede com:
ls tests/invariants/*.test.ts | wc -l
Deu:
103
Sugestão: aplica baseline.sql em modo install e update, e roda todos os arquivos de tests/invariants/ (ls tests/invariants/*.test.ts | wc -l). Esse buraco está fechado.
L165 · FALSA · gravidade media · contagem
O que continua fora (2026-08-14): 1 das 46 specs Playwright.
Mede com:
ls tests/e2e/*.spec.ts | wc -l; grep -n 'FORA_DO_CI' -A2 .github/workflows/e2e.yml | tail -2
Deu:
48
FORA_DO_CI: >-
vps-fresh-onboarding.spec.ts
Sugestão: O que continua fora: uma spec Playwright — a vps-fresh-onboarding.spec.ts, a jornada que a doutrina de QA Visual classifica como o caminho mais crítico do produto, porque exige WAHA + Redis + Resend + Nuvemshop no runner. O total do disco muda toda semana; conte na hora (ls tests/e2e/*.spec.ts | wc -l).
L174 · FALSA · gravidade baixa · ponteiro
vitest.config.ts:12excluitests/invariants/**etests/e2e/**dotest:unit.
Mede com:
grep -n 'invariants\|tests/e2e\|tests/journeys\|exclude' vitest.config.ts
Deu:
25: exclude: [
30: "tests/e2e/**",
31: "tests/invariants/**",
32: "tests/journeys/**" — a linha 12 é comentário sobre testTimeout
Sugestão: O bloco exclude de vitest.config.ts tira tests/e2e/**, tests/invariants/** e tests/journeys/** do test:unit (grep -n 'exclude' -A6 vitest.config.ts). Número de linha em ponteiro envelhece a cada edição do arquivo — cite o símbolo, não a linha.
L179 · FALSA · gravidade media · sobre-o-codigo
gov:verify=typecheck && lint && test:unit. Omitetest:dbetest:e2e.
Mede com:
node -e "console.log(require('./package.json').scripts['gov:verify'])"; grep -n 'name: ' .github/workflows/ci.yml | head -8
Deu:
pnpm typecheck && pnpm lint && pnpm lint:channels && pnpm test:unit
— e o job `verify` do ci.yml roda CINCO passos: Typecheck, Lint, Channel provider leak, Unit tests, Kit self-host (bash / pnpm test:shell)
Sugestão: gov:verify = typecheck && lint && lint:channels && test:unit. Omite test:db, test:e2e e test:shell — este último é o único gate que exercita o update.sh do kit self-host, e o job verify do CI o roda. Ou seja: gov:verify verde é mais fraco que o próprio check verify. Um agente que trate gov:verify verde como "pronto" vai declarar concluída uma mudança de schema sem nunca ter testado RLS, e uma mudança de kit sem nunca ter rodado o instalador. Régua: node -e "console.log(require('./package.json').scripts['gov:verify'])".
Vira teste: tests/unit: assertar que todo passo run: pnpm <script> do job verify do ci.yml está contido em gov:verify — ou que o doc declara a diferença.
L187 · FALSA · gravidade alta · contagem
checkRateLimit(lib/ai/dispatcher/rate-limit.ts) é chamado em 2 lugares: o webhook público de captação e o dispatcher de IA.
Mede com:
grep -rn "checkRateLimit" app lib workers --include='*.ts' | grep -v '\.test\.' | grep -v '^lib/ai/dispatcher/rate-limit.ts'
Deu:
app/api/v1/webhooks/in/[token]/route.ts:58 · lib/auth/rate-limit.ts:89, :94, :165 · lib/ai/dispatcher/index.ts:298 — 3 módulos de produção, 5 chamadas
Sugestão: checkRateLimit (lib/ai/dispatcher/rate-limit.ts) é chamado por três módulos de produção: o webhook público de captação, o dispatcher de IA e lib/auth/rate-limit.ts (que o aplica a login, signup, recuperação de senha e aceite de convite). Reconte antes de citar: grep -rn "checkRateLimit" app lib workers --include='*.ts' | grep -v '\.test\.'.
L188 · FALSA · gravidade alta · ativo-obrigatorio
Sem proteção:
/login,/signup,/team/accept-invite/:token, os crons,/api/internal/*,/api/mcp, webhooks WAHA e Nuvemshop.
Mede com:
grep -rn "auth/rate-limit" app lib --include='*.ts' --include='*.tsx' | grep -v '\.test\.'
Deu:
app/team/accept-invite/[token]/page.tsx:14:import { authRateLimited, AUTH_LIMITS } from "@/lib/auth/rate-limit";
app/actions/auth/requestPasswordReset.ts:8:import { authRateLimited, AUTH_LIMITS }...
app/actions/auth/signUp.ts:14:import { authRateLimited, AUTH_LIMITS }...
app/actions/auth/signInWithPassword.ts:16:} from "@/lib/auth/rate-limit";
Sugestão: Sem proteção: os crons, /api/internal/*, /api/mcp, webhooks WAHA e Nuvemshop. Já protegidos por lib/auth/rate-limit.ts (que embrulha o checkRateLimit com limite por IP e por identidade): /login, /signup, /team/accept-invite/:token e o pedido de recuperação de senha. Régua: grep -rn "auth/rate-limit" app lib --include='*.ts' | grep -v '\.test\.'.
Vira teste: tests/unit: para cada rota da lista "sem proteção", assertar que o módulo NÃO importa lib/auth/rate-limit — e o inverso para as protegidas. Assim a lista não pode ficar mentindo sem reprovar.
L193 · FALSA · gravidade alta · sobre-o-codigo
4.4
node_modulesdeste checkout está incompleto 🟠 70 pacotes, semtypescript—pnpm typecheckfalha comMODULE_NOT_FOUND.
Mede com:
ls -la node_modules/typescript | head -1; node_modules/.bin/tsc --version
Deu:
node_modules/typescript -> .pnpm/typescript@6.0.3/node_modules/typescript
Version 6.0.3
Sugestão: Remover a seção 4.4. Ela descrevia o disco de uma sessão de auditoria, não o projeto: neste checkout node_modules/typescript resolve para 6.0.3 e node_modules/.bin/tsc --version responde. Estado de node_modules é propriedade da máquina de quem lê, não achado do repositório — se voltar a faltar, o conserto é pnpm install, não uma linha de doc.
L200 · FALSA · gravidade alta · pendencia
6 variáveis declaradas em
lib/env.tse ausentes do template — incluindo três secrets:IMPERSONATE_COOKIE_SECRET,INTERNAL_CRON_SECRET,LGPD_SIGNING_KEY
Mede com:
for v in IMPERSONATE_COOKIE_SECRET INTERNAL_CRON_SECRET LGPD_SIGNING_KEY LGPD_DPO_EMAIL LGPD_EXPORT_EXPIRES_HOURS NUVEMSHOP_ENABLED; do printf "%s env.ts=%s .env.example=%s\n" $v $(grep -c $v lib/env.ts) $(grep -c $v .env.example); done
Deu:
IMPERSONATE_COOKIE_SECRET env.ts=3 .env.example=1
INTERNAL_CRON_SECRET env.ts=1 .env.example=1
LGPD_SIGNING_KEY env.ts=1 .env.example=1
LGPD_DPO_EMAIL env.ts=1 .env.example=1
LGPD_EXPORT_EXPIRES_HOURS env.ts=1 .env.example=1
NUVEMSHOP_ENABLED env.ts=3 .env.example=1
Sugestão: ### 4.5 ✅ .env.example — reconciliado As 6 variáveis que faltavam (incluindo os três secrets IMPERSONATE_COOKIE_SECRET, INTERNAL_CRON_SECRET e LGPD_SIGNING_KEY) estão hoje no template. Fica registrado como fechado, não como pendência. Régua para reconferir: for v in $(grep -oE '[A-Z_]{6,}' lib/env.ts | sort -u); do grep -q "$v" .env.example || echo "ausente: $v"; done. Inverso, e ainda aberto: FLYWHEEL_* e WATCHDOG_* estão no template (comentados) e não em lib/env.ts — lidos direto de process.env, portanto sem validação Zod.
Vira teste: tests/unit: toda var referenciada em lib/env.ts aparece em .env.example. É a única forma de o item 9 do DoD parar de apodrecer.
L212 · FALSA · gravidade baixa · data-versao
Corrigidas nesta auditoria: dizia Next.js 15 (é 16.2)
Mede com:
node -e "console.log(require('./package.json').dependencies.next)"
Deu:
^16.3.0
Sugestão: Corrigidas nesta auditoria: dizia Next.js 15 (a versão real está em package.json → dependencies.next), "rate limit sliding window" (é fixed-window) e "Idempotency-Key para POSTs de criação" (existe em 1 rota). Citar a minor de um framework num doc de estado é garantir que ele erre no próximo pnpm up.
L225 · FALSA · gravidade baixa · contagem
a evidência vive em
evidence/(85, contando as subpastas),docs/evidence/(18) eloop/checkpoints/evidence/(13) — 116 no total.
Mede com:
git ls-files -- '*.png' | grep -v / | wc -l; git ls-files evidence | grep -c '\.png$'; git ls-files docs/evidence | grep -c '\.png$'; git ls-files loop/checkpoints/evidence | grep -c '\.png$'
Deu:
0 (PNGs na raiz)
239 (evidence/)
18 (docs/evidence/)
13 (loop/checkpoints/evidence/)
Sugestão: hoje há zero PNGs rastreados na raiz (git ls-files -- '*.png' | grep -v / | wc -l) — a evidência vive em evidence/, docs/evidence/ e loop/checkpoints/evidence/. Os totais crescem a cada sessão de QA; conte na hora em vez de citar.
Vira teste: tests/unit ou CI: git ls-files -- '*.png' | grep -v / tem que devolver vazio. A raiz limpa vira gate em vez de nota.
L228 · FALSA · gravidade media · contagem
Restam 3 na raiz (
HANDOFF.md,-harness-evolution,-operacao-visivel), o que é consistente com "épico vivo fica visível, épico encerrado é arquivado".
Mede com:
ls HANDOFF*.md
Deu:
HANDOFF-conversa-vira-lead.md HANDOFF-followup-vivo.md HANDOFF-fv-w1-fila.md HANDOFF-harness-evolution.md HANDOFF-ia-360.md HANDOFF-marca-propria.md HANDOFF-operacao-visivel.md HANDOFF-sistema-vivo-consertos.md HANDOFF-tres-papeis.md HANDOFF.md — 10 arquivos
Sugestão: Hoje são dez HANDOFFs na raiz (ls HANDOFF*.md | wc -l) contra dois arquivados em docs/handoffs/. A regra "épico vivo fica visível, épico encerrado é arquivado" deixou de ser descrição e virou intenção: a raiz acumula épicos encerrados (ia-360, marca-propria, tres-papeis, followup-vivo…) sem ninguém mover. É pendência, não conformidade.
L234 · FALSA · gravidade media · contagem
mas o repo já tem migrations até 0092. São 34 migrations de deriva.
Mede com:
grep -n '0058\|Migration seguinte' HANDOFF.md | head -2; ls supabase/migrations/*.sql | tail -1
Deu:
33:- **Migration seguinte livre:** 0058.
supabase/migrations/20260814090000_0157_marca_por_organizacao.sql
Sugestão: HANDOFF.md:33 ainda afirma "Migration seguinte livre: 0058" — e o repo já passou de 0157 (ls supabase/migrations/*.sql | tail -1). A deriva quase triplicou desde a medição anterior, o que reforça a regra em vez de enfraquecê-la: HANDOFF não é fonte da verdade de schema — supabase/migrations/ e baseline.sql são.
L244 · FALSA · gravidade media · contagem
89 dos 169 handlers usam
createAdminClient(service role, bypassa RLS).
Mede com:
grep -rl 'createAdminClient' app/api --include='route.ts' | wc -l; find app/api -name route.ts | wc -l
Deu:
119
203
Sugestão: Mais da metade dos handlers usa createAdminClient (service role, bypassa RLS) — régua: grep -rl 'createAdminClient' app/api --include='route.ts' | wc -l sobre find app/api -name route.ts | wc -l. A regra "filtre organization_id manualmente, nunca do body" não tem enforcement automático na escrita. Os invariantes cobrem isolamento a sério e rodam em CI (invariants é check obrigatório), o que mitiga muito; o que falta é o gate que impede um handler novo de nascer errado.
Vira teste: lint rule ou teste de diff: route.ts novo que importa createAdminClient e não referencia organization_id reprova. É o gate que o próprio parágrafo diz faltar.
L253 · FALSA · gravidade baixa · ponteiro
Dockerfile:55fazapk add --no-cache ffmpeg
Mede com:
grep -n 'ffmpeg' Dockerfile
Deu:
69: # ffmpeg: a derivação de vídeo (Onda 3.1) roda no processo do app — o cron
72: RUN apk add --no-cache ffmpeg
Sugestão: o Dockerfile faz apk add --no-cache ffmpeg (grep -n ffmpeg Dockerfile), com comentário explicando que a derivação de vídeo roda no processo do app via o cron event-log-drain. Registrado como fechado.
L258 · NAO_VERIFICAVEL · gravidade media · pendencia
Dependência de credencial de terceiro para provar IA: se Anthropic segue com credencial placeholder e Google com chave de gateway inválida […] A CONFIRMAR se ainda vale.
Sugestão: Estado de credencial vive fora do repo; nenhum comando do checkout responde. Substituir por ponteiro ao lugar onde a resposta mora (painel de provedores, coberto por tests/e2e/prova-painel-provedores.spec.ts) e uma data de última verificação — ou remover do doc e virar issue. Risco sem instrumento de medição não é risco rastreado, é lembrete.
L259 · FALSA · gravidade media · contagem
lib/agent-engine/agent/inbound-turn.tscom 1789 linhas — 2,4× o segundo maior arquivo de lógica (AgentForm.tsx, 746)
Mede com:
find app lib components workers -name '*.ts' -o -name '*.tsx' | xargs wc -l | sort -rn | head -5
Deu:
7023 lib/database.types.ts (gerado)
2449 lib/agent-engine/agent/inbound-turn.ts
1318 lib/followup/graph-schema.test.ts
960 app/app/ai/agents/[id]/_components/AgentForm.tsx
959 lib/agent-engine/guardrails/before-send.ts
Sugestão: lib/agent-engine/agent/inbound-turn.ts é o maior arquivo de lógica escrita à mão do repo (o único maior é lib/database.types.ts, gerado) e é o hot path do produto. Cresceu ~660 linhas desde a primeira medição desta auditoria — e o crescimento é o dado, não o total. Régua: find app lib components workers -name '*.ts' -o -name '*.tsx' | xargs wc -l | sort -rn | head -5.
L266 · FALSA · gravidade baixa · contagem
Os 14 crons do produto são agendados exclusivamente pelo
cronddo serviçoscheduler
Mede com:
ls app/api/v1/cron/ | wc -l; ls -d vercel.json
Deu:
16
ls: vercel.json: No such file or directory
Sugestão: Os crons do produto (ls app/api/v1/cron/ | wc -l) são agendados exclusivamente pelo crond do serviço scheduler do docker-compose.prod.yml — a lista mora em docker/scheduler/entrypoint.sh — e não existe vercel.json neste repo (ls vercel.json → ausente). O resto do parágrafo segue valendo.
Vira teste: tests/unit: todo diretório de app/api/v1/cron/ tem entrada em docker/scheduler/entrypoint.sh. Cron sem agendador é evento sem consumer.
L288 · FALSA · gravidade alta · pendencia
- As branch protection rules exigem os dois checks do CI verdes para merge? Isso decide se o gate de RLS é bloqueante ou decorativo.
Mede com:
gh api repos/melgarafael/DeskcommCRM/branches/main/protection --jq '.required_status_checks.contexts|join(", ")'
Deu:
verify, build-and-size, invariants, e2e, imagens-ok
Sugestão: Apagar a pergunta 7. Está respondida: são cinco checks obrigatórios (verify, build-and-size, invariants, e2e, imagens-ok), e o gate de RLS (invariants) é bloqueante. A régua fica no lugar da resposta: gh api repos/melgarafael/DeskcommCRM/branches/main/protection --jq '.required_status_checks.contexts|join(", ")'.
L295 · FALSA · gravidade alta · pendencia
Se
pnpm typecheck/lint/test:unitpassam hoje — onode_modulesdeste checkout está incompleto e a auditoria não instala dependências.
Mede com:
node_modules/.bin/tsc --version; gh run list --workflow=ci.yml --branch=main --limit 5 --json conclusion --jq '.[].conclusion'
Deu:
Version 6.0.3
success
success
success
success
success
Sugestão: Trocar por: "Se typecheck/lint/test:unit passam hoje — passam: as cinco últimas execuções do ci.yml na main concluíram success. Régua: gh run list --workflow=ci.yml --branch=main --limit 5 --json conclusion --jq '.[].conclusion'." A razão declarada (node_modules incompleto) não vale mais.
L296 · FALSA · gravidade media · pendencia
Se o job
invariantsdo CI está passando — sabemos que existe, não que está verde.
Mede com:
gh run list --workflow=ci.yml --branch=main --limit 5 --json conclusion,displayTitle --jq '.[]|"\(.conclusion) \(.displayTitle)"'
Deu:
success Merge pull request #251…
success Merge pull request #249…
success Merge pull request #250…
success Merge pull request #248…
success Merge pull request #247…
Sugestão: Remover do bloco "não pôde ser confirmado". O ci.yml (que carrega os jobs verify e invariants) está verde nas cinco últimas execuções da main, e é uma chamada só: gh run list --workflow=ci.yml --branch=main --limit 5 --json conclusion --jq '.[].conclusion'.
L301 · FALSA · gravidade media · contagem
Contei 221 arquivos de teste unitário e 56 de invariante, compatível com mais de mil casos, mas não valida número específico.
Mede com:
git ls-files | grep -cE '\.test\.(ts|tsx)$'; ls tests/invariants/*.test.ts | wc -l
Deu:
511
103
Sugestão: Os números de teste citados nos HANDOFFs são auto-relatados e não reconciliam entre si. Conte na hora, e declare a régua junto — arquivo e caso são coisas diferentes: git ls-files | grep -cE '\.test\.(ts|tsx)$' para arquivos, ls tests/invariants/*.test.ts | wc -l para invariantes.
L304 · FALSA · gravidade media · sobre-o-codigo
Se
docs/architecture/cumpre o "mapa vivo" exigido pelo item 13 do DoD (contém só o diagrama do agent-turn).
Mede com:
ls docs/architecture/
Deu:
agent-turn.html · agent-turn.workflow.json · atualizacao-self-service · crm-vivo · escalacao-ciclo-humano · followup-dossie · gestao-funis · ia-360-organizar · ia-360-retencao · indice-de-atrito · marca-propria (.architecture.json) · README.md
Sugestão: Remover do bloco "não pôde ser confirmado". docs/architecture/ tem hoje dez mapas vivos além do diagrama do agent-turn, um por épico (ls docs/architecture/) — o item 13 do DoD está sendo cumprido na prática. O que continua sem gate é a exigência de "≥2 arestas" para peça nova.
docs/harness-audit.md — 20
L5 · FALSA · gravidade media · data-versao
last_updated: 2026-07-29
Mede com:
git log -1 --format='%ad' --date=short -- docs/harness-audit.md
Deu:
2026-08-14
Sugestão: last_updated: 2026-08-14 e audited_against: origin/main @ 840917ed (v1.3.0). O corpo já tem bloco datado de 2026-08-14 ("Números recontados em 2026-08-14 @ 741c4ec8"), então o frontmatter contradiz o próprio texto.
L26 · FALSA · gravidade media · contagem
119 docs em
docs/, README de 302 linhas em 3 idiomas
Mede com:
git ls-files docs | grep -c '\.md$'; wc -l README.md; ls README*.md
Deu:
161
490 README.md
README.en.md README.es.md README.md
Sugestão: Documentação farta em docs/ (git ls-files docs | grep -c '\.md$'), README traduzido em 3 idiomas (ls README*.md), PRDs, specs, CHANGELOG.md. Sem número fixo: os dois já apodreceram.
L29 · FALSA · gravidade media · ativo-obrigatorio
| H3 — Verificável | ✅ |
lint+typecheck+test:unit+build; CI roda os 3 primeiros em PR |
Mede com:
grep -n 'name: ' .github/workflows/ci.yml
Deu:
Typecheck / Lint / Channel provider leak / Unit tests / Kit self-host (bash) — cinco passos, não três
Sugestão: | H3 — Verificável | ✅ | typecheck + lint + lint:channels + test:unit + test:shell no job verify; build no perf.yml. Todos rodam em PR e são check obrigatório |
L31 · FALSA · gravidade media · contagem
Falta: 1 das 46 specs E2E fora do CI (45 rodam via
e2e.yml, obrigatório desde 2026-08-08; a de fora évps-fresh-onboarding, que é justamente a P0)
Mede com:
ls tests/e2e/*.spec.ts | wc -l; awk '/SPECS_PARTE_1:/,/FORA_DO_CI:/' .github/workflows/e2e.yml | grep -v '^ *#' | grep -oE '[a-z0-9-]+\.spec\.ts' | wc -l
Deu:
48
47
Sugestão: Falta: uma spec E2E fora do CI (vps-fresh-onboarding, a P0, por dependência de WAHA/Redis/Resend/Nuvemshop); format:check fora do CI; e o comando único local (gov:verify) não cobre test:db/test:e2e/test:shell. Não cite o total do disco aqui — ele muda toda semana e o tests/unit/e2e-cobertura-completa.test.ts já garante a soma.
L39 · FALSA · gravidade alta · contagem
O que separa de H5 é estreito: 16 dos 19 E2E não rodam em CI (
e2e.ymlcobresmoke,autheerror-pagesdesde 2026-07-30)
Mede com:
grep -c 'spec.ts' <(awk '/SPECS_PARTE_1:/,/FORA_DO_CI:/' .github/workflows/e2e.yml | grep -v '^ *#'); ls tests/e2e/*.spec.ts | wc -l
Deu:
47 specs nas listas de execução; 48 no disco. `e2e.yml` cobre muito além de smoke/auth/error-pages.
Sugestão: O que separa de H5 é estreito: 1 spec E2E fora do CI — a vps-fresh-onboarding, que protege justamente a primeira impressão que a doutrina classifica como o caminho mais crítico do produto. E pnpm gov:verify, o comando único que um agente naturalmente usa como critério de pronto, não inclui test:db, test:e2e nem test:shell: o CI pega o que ele deixa passar, mas só depois do push.
L46 · FALSA · gravidade media · contagem
doutrina escrita e específica (
CLAUDE.md), Definition of Done de 13 itens, 56 arquivos de invariantes de banco
Mede com:
awk '/^## Definition of Done/,/^Um staff engineer/' CLAUDE.md | grep -cE '^[0-9]+\.'; ls tests/invariants/*.test.ts | wc -l
Deu:
15 (itens do DoD)
103 (arquivos de invariante)
Sugestão: doutrina escrita e específica (CLAUDE.md), Definition of Done longo e checável (awk '/^## Definition of Done/,/^Um staff engineer/' CLAUDE.md | grep -cE '^[0-9]+\.'), invariantes de banco às centenas (ls tests/invariants/*.test.ts | wc -l), gate de install+update do baseline.sql num Postgres descartável rodando em CI…
L59 · FALSA · gravidade baixa · contagem
302 linhas: o que é, quickstart de 5 min, stack, estrutura, testes, roadmap, suporte. Traduzido (EN/ES).
Mede com:
wc -l README.md
Deu:
490 README.md
Sugestão: | 1 | README útil | ✅ | O que é, quickstart de 5 min, stack, estrutura, testes, roadmap, suporte. Traduzido (EN/ES). Mais CHANGELOG.md com aviso de "⚠️ Requer atenção" por versão, voltado a quem roda VPS |
L62 · FRAGIL · gravidade baixa · contagem
pnpm-lock.yaml, e ambos os jobs do CI usam--frozen-lockfile
Mede com:
grep -rc 'frozen-lockfile' .github/workflows/*.yml
Deu:
ci.yml:2 · e2e.yml:1 · perf.yml:1 · publish-image.yml:0
Sugestão: | 4 | Lockfile | ✅ | pnpm-lock.yaml, e todo job que instala dependências usa --frozen-lockfile (grep -rc 'frozen-lockfile' .github/workflows/*.yml). "Ambos os jobs" descrevia um CI de dois jobs; hoje são cinco, em quatro workflows |
L63 · FALSA · gravidade alta · pendencia
Existe (+
.env.hostgator.example), mas 6 vars delib/env.tscontinuam ausentes, entre elas 3 secrets
Mede com:
grep -c IMPERSONATE_COOKIE_SECRET .env.example; grep -c INTERNAL_CRON_SECRET .env.example; grep -c LGPD_SIGNING_KEY .env.example
Deu:
1
1
1 — as três presentes (idem LGPD_DPO_EMAIL, LGPD_EXPORT_EXPIRES_HOURS, NUVEMSHOP_ENABLED)
Sugestão: | 5 | .env.example | ✅ | Existe (+ .env.hostgator.example) e cobre as vars de lib/env.ts, secrets incluídos. Régua: for v in $(grep -oE '[A-Z_]{6,}' lib/env.ts | sort -u); do grep -q "$v" .env.example || echo "ausente: $v"; done |
L68 · FRAGIL · gravidade baixa · sobre-o-codigo
pnpm typecheck(tsc --noEmit, TS 6 estrito), roda no CI
Mede com:
node -e "console.log(require('./package.json').scripts.typecheck)"; node_modules/.bin/tsc --version
Deu:
tsc --noEmit -p tsconfig.typecheck.json
Version 6.0.3
Sugestão: | 10 | Checagem de tipos | ✅ | pnpm typecheck = tsc --noEmit -p tsconfig.typecheck.json (TS 6 estrito), roda no CI. O -p importa: o escopo verificado é o desse tsconfig, não o da raiz |
L70 · FALSA · gravidade media · contagem
56 arquivos de invariantes em
tests/invariants/+tests/api/. Excluídos dotest:unitde propósito (vitest.config.ts:12)
Mede com:
ls tests/invariants/*.test.ts | wc -l; grep -n 'exclude' vitest.config.ts
Deu:
103
25: exclude: [ (não a linha 12)
Sugestão: | 12 | Testes de integração | ✅ | Invariantes em tests/invariants/ (ls tests/invariants/*.test.ts \| wc -l) + tests/api/. Excluídos do test:unit de propósito (bloco exclude do vitest.config.ts) e rodados pelo job invariants do CI via pnpm test:db |
L71 · FALSA · gravidade alta · contagem
20 specs Playwright. 10 rodam no CI (
e2e.yml, ainda não-obrigatório), incluindo o P0vps-webhook-outbound-ssrf; o P0vps-fresh-onboardingcontinua fora (issue #63)
Mede com:
ls tests/e2e/*.spec.ts | wc -l; awk '/SPECS_PARTE_1:/,/FORA_DO_CI:/' .github/workflows/e2e.yml | grep -v '^ *#' | grep -oE '[a-z0-9-]+\.spec\.ts' | wc -l; gh api repos/melgarafael/DeskcommCRM/branches/main/protection --jq '.required_status_checks.contexts|join(", ")'
Deu:
48 (no disco)
47 (nas listas de execução)
verify, build-and-size, invariants, e2e, imagens-ok
Sugestão: | 13 | Testes E2E | ⚠️ | Todas as specs do disco menos uma rodam no e2e.yml, que é check obrigatório. A única de fora é o P0 vps-fresh-onboarding (precisa de WAHA + Redis + Resend + Nuvemshop) — logo, e2e verde não prova a instalação fresca. Reconte antes de citar número: ls tests/e2e/*.spec.ts \| wc -l contra FORA_DO_CI em .github/workflows/e2e.yml; a soma já é conferida por tests/unit/e2e-cobertura-completa.test.ts |
Vira teste: Já existe: tests/unit/e2e-cobertura-completa.test.ts reprova spec no disco fora das três listas. O que falta é o doc parar de repetir o número à mão.
L72 · FALSA · gravidade media · sobre-o-codigo
pnpm gov:verify=typecheck && lint && test:unit. Omitetest:dbetest:e2e
Mede com:
node -e "console.log(require('./package.json').scripts['gov:verify'])"
Deu:
pnpm typecheck && pnpm lint && pnpm lint:channels && pnpm test:unit
Sugestão: | 14 | Comando único de verificação | ⚠️ | pnpm gov:verify = typecheck && lint && lint:channels && test:unit. Omite test:db, test:e2e e test:shell — verde localmente é mais fraco até que o check verify do CI, que roda os cinco passos. Régua: node -e "console.log(require('./package.json').scripts['gov:verify'])" |
L73 · FALSA · gravidade alta · ativo-obrigatorio
ci.ymltem 2 jobs:verify(typecheck + lint + test:unit) einvariants[…] Falta E2E eformat:check.perf.ymlfaz build + bundle size;publish-image.ymlpublica no GHCR
Mede com:
grep -n 'name: ' .github/workflows/ci.yml; for f in .github/workflows/*.yml; do echo "-- $f"; awk '/^jobs:/{j=1;next} j && /^ [a-zA-Z0-9_-]+:/{print $0}' $f; done
Deu:
verify roda: Typecheck, Lint, Channel provider leak, Unit tests, Kit self-host (bash)
ci.yml: verify, invariants | e2e.yml: e2e | perf.yml: build-and-size | publish-image.yml: build-and-push, imagens-ok
Sugestão: | 15 | CI executando verificações | ✅ | ci.yml tem 2 jobs: verify (typecheck + lint + lint:channels + test:unit + test:shell, o único gate que exercita o update.sh do kit) e invariants (pnpm test:db — isolamento RLS + invariantes de governança, job paralelo, timeout 20min). e2e.yml roda os Playwright, perf.yml faz build + bundle size, publish-image.yml publica no GHCR e carrega o job imagens-ok. Falta só format:check. Os cinco checks obrigatórios: gh api repos/melgarafael/DeskcommCRM/branches/main/protection --jq '.required_status_checks.contexts\|join(", ")' |
L75 · FALSA · gravidade media · ponteiro
ARCHITECTURE.md(1 página) +docs/specs/(16 docs com schema e payloads) +docs/architecture/agent-turn+graphify-out/
Mede com:
ls -d graphify-out; git ls-files docs/specs | grep -c '\.md$'; ls docs/architecture/ | wc -l
Deu:
ls: graphify-out: No such file or directory
19
12
Sugestão: | 17 | Documentação arquitetural | ✅ | ARCHITECTURE.md (99 linhas) + docs/specs/ (git ls-files docs/specs \| grep -c '\.md$') + docs/architecture/ com o diagrama do agent-turn e os mapas vivos por épico (ls docs/architecture/). graphify-out/ não existe neste repo — o ponteiro anterior apontava para nada |
Vira teste: tests/unit: todo path citado em docs/harness-audit.md e docs/current-state.md existe no disco. Ponteiro morto é a falha mais barata de pegar e a mais cara de ler.
L77 · FALSA · gravidade media · contagem
Definition of Done de 13 itens em
CLAUDE.md
Mede com:
awk '/^## Definition of Done/,/^Um staff engineer/' CLAUDE.md | grep -cE '^[0-9]+\.'
Deu:
15
Sugestão: | 19 | Critérios de conclusão de tarefa | ✅ | Definition of Done em CLAUDE.md (awk '/^## Definition of Done/,/^Um staff engineer/' CLAUDE.md \| grep -cE '^[0-9]+\\.'); docs/doctrine/sistema-vivo.md com o Living System Checklist; template de PR com o checklist |
L78 · FALSA · gravidade media · contagem
Dockerfile+Dockerfile.worker,baseline.sqlauto-curativo cobrindo até a migration 0092
Mede com:
ls Dockerfile*; grep -oE 'migration [0-9]{4}' supabase/baseline.sql | sort -u | tail -1
Deu:
Dockerfile Dockerfile.scheduler Dockerfile.worker
migration 0157
Sugestão: | 20 | Ambiente reproduzível | ✅ | docker-compose.yml (dev), .prod.yml, três Dockerfiles publicados (Dockerfile, Dockerfile.worker, Dockerfile.scheduler — nenhum serviço de produção constrói na máquina do cliente), baseline.sql auto-curativo cobrindo até a última migration, scripts/test-db.sh com Postgres efêmero pg17 rodando em CI. ⚠️ A receita de ambiente fresco tem armadilhas que só existem em doc |
L94 · FALSA · gravidade alta · pendencia
1. Adicionar os E2E ao CI (ou a um workflow nightly) 🔴 · custo: ~30 linhas Maior buraco restante. […]
vps-webhook-outbound-ssrf.spec.tsé a única prova automatizada do guard de SSRF.
Mede com:
grep -n 'vps-webhook-outbound-ssrf' .github/workflows/e2e.yml
Deu:
108: degradacao-silenciosa.spec.ts vps-webhook-outbound-ssrf.spec.ts
Sugestão: ### ✅ JÁ FEITO (parcial) — E2E no CI e2e.yml roda todas as specs menos uma e é check obrigatório na main; a vps-webhook-outbound-ssrf.spec.ts entrou. Continua aberto só o caso que exige infra externa: vps-fresh-onboarding.spec.ts (WAHA + Redis + Resend + Nuvemshop), que é a P0 da doutrina de QA Visual. Um workflow nightly com esses serviços é o que ainda falta.
L108 · FALSA · gravidade alta · pendencia
3. Completar
.env.example🟠 · custo: 6 linhas As 6 vars ausentes, com comentário sobre quais são obrigatórias. Os 3 secrets são o caso grave: quem instala não sabe que precisa gerá-los.
Mede com:
grep -cE 'IMPERSONATE_COOKIE_SECRET|INTERNAL_CRON_SECRET|LGPD_SIGNING_KEY|LGPD_DPO_EMAIL|LGPD_EXPORT_EXPIRES_HOURS|NUVEMSHOP_ENABLED' .env.example
Deu:
6 — todas as seis já estão no template
Sugestão: ### ✅ JÁ FEITO — .env.example reconciliado As 6 vars (3 delas secrets) entraram no template. Sai do plano de correção e vira registro de item fechado.
L133 · FALSA · gravidade alta · pendencia
Se as branch protection rules do GitHub exigem os dois checks verdes para merge — é config de repositório remoto, invisível no checkout. Isso decide se o gate de RLS é bloqueante ou apenas informativo, e é a pergunta mais importante em aberto sobre o harness.
Mede com:
gh api repos/melgarafael/DeskcommCRM/branches/main/protection --jq '.required_status_checks.contexts|join(", ")'
Deu:
verify, build-and-size, invariants, e2e, imagens-ok
Sugestão: Remover deste bloco e registrar como respondido: a branch protection da main exige cinco checks — verify, build-and-size, invariants, e2e, imagens-ok. O gate de RLS (invariants) é bloqueante. Não é invisível no checkout: gh api repos/melgarafael/DeskcommCRM/branches/main/protection --jq '.required_status_checks.contexts|join(", ")' responde em um comando, e é essa linha que deve ficar no doc no lugar da lista.
REGISTRO — 15 achados
CHANGELOG.md — 15
L9 · FALSA · gravidade alta · pendencia
Não lançado
Mede com:
git log --oneline v1.3.0..HEAD | wc -l ; git log --oneline v1.3.0..HEAD --no-merges -- app lib hostgator-setup-kit supabase workers | wc -l ; git diff --name-only --diff-filter=A v1.3.0 HEAD -- supabase/migrations/
Deu:
64 commits desde v1.3.0; 38 deles (sem merges) tocam app/lib/hostgator-setup-kit/supabase/workers; 3 migrations novas: 0155_marca_da_instalacao_no_banco.sql, 0156_quadro_do_onboarding.sql, 0157_marca_por_organizacao.sql. Entre os commits: e4963c6e 'feat(auth): a verificação em duas etapas vira escolha, não imposição', 11d87a11 'marca própria (whitelabel)', 6aabe750 'Pipeline e Kanban saem da interface', c2f88e83 'onboarding: sem chave de IA, o passo de treinar era um BECO'. `git tag --contains` de cada um devolve vazio.
Sugestão: ## Não lançado ### Adicionado - Marca própria. Nome, cor e logo da instalação e de cada organização passam a viver no banco, e alcançam também e-mail, PDF e o aplicativo autenticador. - O onboarding monta o funcionário: o quadro nasce pela IA, a chave é testada na hora, e o vocabulário do funil é escolhido no wizard. ### Alterado - A verificação em duas etapas vira escolha, não imposição. O cadastro deixa de ser obrigatório para administradores; quem exige agora é uma chave em Configurações › Segurança (na plataforma e na organização), e o padrão é não exigir. Quem JÁ tem o segundo fator continua provando-o na sessão. - "Pipeline" e "Kanban" saem da interface — na tela agora são "Funis" e "Etapas do funil". ### ⚠️ Requer atenção - 3 mudanças de banco (migrations 0155 a 0157). O update.sh aplica sozinho. > Esta seção não pode ficar vazia enquanto houver comm
Vira teste: tests/unit/changelog-nao-lancado.test.ts — dada a última tag T (git describe --tags --abbrev=0), se git log T..HEAD --no-merges -- app lib hostgator-setup-kit supabase workers tem ≥1 commit, então o bloco entre ## [Não lançado] e o próximo ## [ deve ter ≥1 linha não vazia. Sabotagem que reprova: apagar o corpo da seção com commits de produto pendentes.
L11 · FALSA · gravidade media · ponteiro
[1.3.0] — 2026-08-13
Mede com:
grep -n '^## ' CHANGELOG.md ; grep -n '^\[' CHANGELOG.md
Deu:
Headings: Não lançado, 1.3.0, 1.2.1, 1.2.0, 1.1.0, 1.0.0. Definições de link: Não lançado, 1.2.1, 1.2.0, 1.1.0, 1.0.0. **Falta `[1.3.0]:`** — o único heading de versão sem destino. No GitHub ele renderiza como texto literal `[1.3.0]`, enquanto os vizinhos viram link para o diff.
Sugestão: Acrescentar, entre a linha de [Não lançado] e a de [1.2.1]: [1.3.0]: https://github.com/melgarafael/DeskcommCRM/compare/v1.2.1...v1.3.0
Vira teste: tests/unit/changelog-links.test.ts — todo heading ## [X.Y.Z] tem uma linha [X.Y.Z]: https://… correspondente. Sabotagem que reprova: apagar a definição de [1.2.1].
L32 · FRAGIL · gravidade baixa · data-versao
o instalador grava o número da versão (ex.:
1.2.1)
Mede com:
git tag --list | sort -V | tail -3 ; grep -n 'ultima_versao_publicada' hostgator-setup-kit/install.sh
Deu:
Tags: v1.2.0, v1.2.1, v1.3.0. install.sh:1025 resolve a última versão publicada — hoje uma instalação nova gravaria `1.3.0`, não `1.2.1`. O exemplo já era o número anterior no dia em que a 1.3.0 saiu.
Sugestão: Agora o instalador grava o número da versão publicada mais recente no seu .env, e é essa versão que fica no seu servidor até você decidir atualizar. Para ver qual é a sua: grep APP_IMAGE .env.
L44 · FALSA · gravidade alta · sobre-o-codigo
O WhatsApp (WAHA) e o serviço de limites deixaram de acompanhar automaticamente qualquer versão nova publicada por terceiros. Passam a mudar só quando nós testamos e lançamos.
Mede com:
grep -n 'WAHA_IMAGE' hostgator-setup-kit/*.sh ; grep -nE 'image:|pull_policy' docker-compose.prod.yml ; sed -n '405,440p' docs/runbooks/remediar-worker-congelado.md
Deu:
`WAHA_IMAGE` só aparece em install.sh:1427 (`envq WAHA_IMAGE "${WAHA_IMAGE:-devlikeapro/waha:latest-2026.7.2}"`) e em test-validators.sh. NENHUMA ocorrência em update.sh, agent.sh ou _common.sh — nada reescreve um `.env` legado. O compose pina (`waha:latest-2026.7.2`, srh por digest sha256:5b0bb923…), mas o runbook, medido em 2026-08-14, diz: "o install gravava `WAHA_IMAGE=devlikeapro/waha` (sem tag, isto é `:latest`) por cima: o pin existia e não alcançava ninguém" e "**Ainda não consertado** … no dia em que o devlikeapro publicar, o próximo `update.sh` de qualquer instalação legada troca a versão do WhatsApp sem ninguém pedir".
Sugestão: - O WhatsApp (WAHA) e o serviço de limites passam a nascer numa versão fixa em instalação nova. Numa instalação que já existia, a linha WAHA_IMAGE do seu .env continua como estava e nenhum script a reescreve — o WhatsApp segue acompanhando o que o fornecedor publicar. Para ver em que pé está o seu: grep WAHA_IMAGE .env. Sem : na linha, é :latest, e o procedimento para fixar (com o custo medido: recria o contêiner do WhatsApp) está em docs/runbooks/remediar-worker-congelado.md.
Vira teste: tests/shell — o gate de packaging hoje só mede o .env de instalação FRESCA (test-validators.sh:1380). Um caso irmão que parte de um .env legado com WAHA_IMAGE=devlikeapro/waha, roda update.sh e afirma o estado da linha depois transforma esta frase em fato verificável em vez de promessa.
L51 · FALSA · gravidade alta · ativo-obrigatorio
A partir desta versão o agente de atualização corrige parte disso sozinho, em até 5 minutos, sem você fazer nada — ele fixa a versão que já está rodando.
Mede com:
git show v1.3.0:hostgator-setup-kit/agent.sh | grep -n 'completar_pin_ausente\|pin_incompleto' ; grep -n 'completar_pin_ausente' hostgator-setup-kit/_common.sh ; git log --oneline -S 'o agente de atualização corrige parte disso sozinho' -- CHANGELOG.md ; git tag --contains 81b3bd5d ; git diff v1.3.0 HEAD -- CHANGELOG.md
Deu:
Em v1.3.0 o agent.sh NÃO tem `completar_pin_ausente` nem `pin_incompleto` (grep vazio). No HEAD: _common.sh:448 `completar_pin_ausente()`. O parágrafo entrou em 81b3bd5d 'feat(kit): o agente completa o pin sozinho', e `git tag --contains 81b3bd5d` devolve VAZIO — o commit não está em release nenhuma. O `git diff v1.3.0 HEAD -- CHANGELOG.md` mostra que a ÚNICA alteração no arquivo desde a tag foi reescrever este bloco de uma seção já publicada.
Sugestão: Mover o parágrafo inteiro para ## [Não lançado] → ### Alterado, e devolver à seção 1.3.0 o texto que a tag realmente carrega: Se o seu servidor foi instalado antes desta versão, rode o update.sh DUAS vezes. Medido em ensaio numa VPS: a primeira execução traz o agente novo, mas deixa a versão dele "solta" — acompanhando o canal em vez de ficar fixa, como o resto do sistema. Isso faria o agente saltar sozinho para a versão seguinte num reinício futuro, enquanto o resto do servidor continuaria onde está. A segunda execução fixa tudo na mesma versão. E, em Não lançado: - O agente de atualização completa a versão que ficou solta sozinho, em até 5 minutos, sem você fazer nada. O que ele nunca faz é mexer numa configuração que você escreveu à mão: se você escolheu acompanhar um canal de propósito, ele respeita e só avisa.
Vira teste: tests/unit/changelog-secao-publicada-e-imutavel.test.ts — para cada tag vX.Y.Z, o bloco ## [X.Y.Z] no CHANGELOG do HEAD deve ser byte-idêntico ao do git show vX.Y.Z:CHANGELOG.md. Reprova qualquer reescrita retroativa de seção já publicada.
L69 · FRAGIL · gravidade media · pendencia
Fora isso, nada exige ação sua. Um
.envantigo continua funcionando
Mede com:
grep -n 'WAHA_IMAGE' hostgator-setup-kit/update.sh hostgator-setup-kit/agent.sh hostgator-setup-kit/_common.sh ; grep -n 'Ainda não consertado' docs/runbooks/remediar-worker-congelado.md
Deu:
grep em update.sh/agent.sh/_common.sh: nenhuma ocorrência de WAHA_IMAGE. O runbook traz "**Ainda não consertado — mas a razão que escrevi aqui estava errada, e a medição a derrubou.**" e conclui: "Segue não consertado porque a decisão é de quem opera e a janela ainda está aberta". Existe, portanto, um item que o operador legado precisa decidir e que esta frase manda ele parar de procurar.
Sugestão: Fora isso, o update.sh faz o resto sozinho: um .env antigo continua funcionando, as configurações novas têm valor padrão e o próprio script as acrescenta. Uma linha ele não toca de propósito: a do WhatsApp (WAHA_IMAGE). Se o seu .env a tem sem número de versão, o WhatsApp segue acompanhando o que o fornecedor publicar — o diagnostico.sh aponta isso, e o procedimento (com o custo: recria o contêiner do WhatsApp) está no runbook.
L239 · FRAGIL · gravidade baixa · ativo-obrigatorio
Todas fechadas, com uma varredura que reprova a próxima.
Mede com:
ls -la tests/invariants/hardening-definer-varredura.test.ts ; grep -ciE 'security definer' supabase/baseline.sql
Deu:
tests/invariants/hardening-definer-varredura.test.ts existe (8831 bytes) e está na suíte `invariants`, que é required check na `main`. O baseline tem 51 ocorrências de `security definer`. O "8 de 25" da mesma frase é medição histórica contra a árvore da 1.2.0 e não é reproduzível sem subir o Postgres daquele momento — a varredura, que é a parte que protege daqui pra frente, está de pé.
L254 · FALSA · gravidade media · sobre-o-codigo
seu servidor já roda este código (a instalação acompanha a
main)
Mede com:
sed -n '1025,1060p;1310,1340p' hostgator-setup-kit/install.sh
Deu:
install.sh:1025 `VERSAO_ALVO="$(ultima_versao_publicada "$REPO_URL")"`; 1330-1339 grava `APP_IMAGE`, `WORKER_IMAGE`, `SCHEDULER_IMAGE` na tag resolvida e `*_PULL_POLICY` derivada (`latest|main|stable → always`, versão fixa → `missing`). A instalação NÃO acompanha mais a `main` — foi exatamente o que a 1.3.0 corrigiu (linhas 28-32 do próprio arquivo).
Sugestão: Se você instalou entre 30/07 e 11/08, seu servidor já roda este código — naquela época a instalação nova nascia acompanhando a main. (A partir da 1.3.0 não nasce mais: o instalador grava o número da versão.) Esta tag existe para que a atualização pela tela e o update.sh voltem a ter um alvo publicado para comparar.
L288 · FALSA · gravidade media · sobre-o-codigo
Autenticação via Supabase Auth com MFA TOTP obrigatório para administradores.
Mede com:
sed -n '1,40p' lib/auth/politica-mfa.ts ; git tag --contains e4963c6e
Deu:
lib/auth/politica-mfa.ts (existe no HEAD, 3896 bytes): "A regra era uma linha sem opção: `isPlatformAdmin || role === \"admin\"` … o cadastro passa a ser OPCIONAL e ligado numa tela de Configurações". Duas políticas independentes que somam (`platform_admins.mfa_required`, `organizations.settings.security.mfa_required`), padrão não-exigir. `git tag --contains e4963c6e` (o commit que fez a troca) devolve VAZIO — a mudança não está em nenhuma release nem no CHANGELOG.
Sugestão: Não reescrever a linha da 1.0.0 — ela é registro correto daquela versão. O conserto é a entrada que falta em ## [Não lançado] → ### Alterado: - A verificação em duas etapas do administrador vira escolha, não imposição. Até aqui toda instalação forçava o dono a cadastrar TOTP logo depois do onboarding — um sétimo passo que o assistente nunca anunciou. Agora o cadastro é ligado por quem administra, em Configurações › Segurança, e o padrão é não exigir. Quem já tem o segundo fator continua provando-o ao entrar — desligar a exigição não desliga o fator de quem o ativou por vontade própria.
L311 · FALSA · gravidade media · contagem
Cadeia de 7 verificações antes de cada envio, em ordem fixa: descadastro, LGPD, anti-banimento, variação de texto, promessa determinística, promessa semântica e disclosure.
Mede com:
node -e "...conta nome: em CONFERENCIAS_DE_SAIDA..." ; npx vitest run tests/unit/seguranca-lista-casa-com-a-cadeia.test.ts ; git show v1.0.0:lib/agent-engine/guardrails/before-send.ts | head -20
Deu:
HEAD: 10 conferências, nesta ordem — stop, lgpd, pacing, messaging_window, spinning, promise, semantic_promise, case_promise, internal_vocabulary, disclosure. O teste que prova que a lista casa com a cadeia real passa: `Test Files 1 passed (1) / Tests 8 passed (8)`. Em v1.0.0 o cabeçalho de before-send.ts já descrevia OITO gates — os 7 citados mais "(6.5) case promise", que a frase omitiu. A tela não repete o número: `PainelDeSeguranca.tsx:142` imprime `{CONFERENCIAS_DE_SAIDA.length}`.
Sugestão: - Cadeia de verificações determinísticas antes de cada envio, em ordem fixa e versionada — a primeira que veta interrompe as demais. A lista viva (e o número) está em lib/ai/guardrails/lista-de-conferencia.ts e é o que a tela do agente mostra; um teste reprova quando a lista e a cadeia real divergem. Cada avaliação vira registro durável e auditável — inclusive as que barram o envio.
Vira teste: Não vale um teste novo: tests/unit/seguranca-lista-casa-com-a-cadeia.test.ts já guarda a cadeia. O conserto é a prosa parar de repetir o número que o código já publica.
L336 · FALSA · gravidade baixa · contagem
8 scripts de operação:
install,update,backup,restore,reset-password,reset-mfa,healthchecke o assistente de instalação em IA.
Mede com:
git ls-tree -r --name-only v1.0.0 hostgator-setup-kit/ ; git show v1.0.0:hostgator-setup-kit/README.md | sed -n '44,52p' ; ls hostgator-setup-kit/*.sh | wc -l
Deu:
v1.0.0 tinha 8 arquivos `.sh`, mas um deles é `_common.sh` (biblioteca compartilhada, não operação); o README da tag documenta exatamente **7** scripts. O "assistente de instalação em IA" é `hostgator-setup-kit/CLAUDE.md` — um markdown ("# Você é o assistente de instalação do DeskcommCRM"), não um script. Portanto: 7 scripts + 1 documento, contados como 8 scripts. No HEAD a pasta tem 14 `.sh` (entram agent, comecar, diagnostico, marca-emails, supabase-provision, test-validators).
Sugestão: - 7 scripts de operação — install, update, backup, restore, reset-password, reset-mfa e healthcheck — mais CLAUDE.md, o roteiro que faz uma IA conduzir a instalação de ponta a ponta para quem não programa.
Vira teste: tests/shell ou tests/unit/kit-readme-casa-com-a-pasta.test.ts — todo .sh executável da pasta hostgator-setup-kit/ que não comece com _ aparece na tabela do README do kit. Hoje 6 dos 14 estão fora dela, incluindo o diagnostico.sh que o próprio CHANGELOG manda o cliente rodar.
L337 · FALSA · gravidade alta · sobre-o-codigo
Imagem publicada em
ghcr.io/melgarafael/deskcommcrm— a VPS não compila nada.
Mede com:
git show v1.0.0:docker-compose.prod.yml | grep -nE '^ [a-z0-9_-]+:|image:|build:'
Deu:
Em v1.0.0: `app` (linha 19) tem `image: ${APP_IMAGE:-ghcr.io/melgarafael/deskcommcrm:latest}`; `worker` (linha 42) tem **`build:`** e NENHUM `image:`; `scheduler` (linha 112) é `alpine:3.20` com `apk add` no `command:`. Ou seja, na própria v1.0.0 a VPS compilava o worker. O mesmo arquivo admite isso na linha 22: "O worker … era compilado dentro do seu servidor no dia da instalação".
Sugestão: - Imagem publicada em ghcr.io/melgarafael/deskcommcrm — a VPS não compila o app. (O worker ainda era compilado no seu servidor nesta versão; isso só foi corrigido na 1.3.0.)
Vira teste: tests/unit/packaging-sem-build-only.test.ts (se ainda não existir com este alcance) — todo serviço de docker-compose.prod.yml declara image:; serviço com build: e sem image: reprova. Sabotagem que reprova: remover o image: do worker.
L341 · FALSA · gravidade media · ativo-obrigatorio
CI com dois portões obrigatórios:
verify(typecheck, lint, testes unitários) einvariants.
Mede com:
gh api repos/melgarafael/DeskcommCRM/branches/main/protection --jq '.required_status_checks.contexts|join(", ")'
Deu:
verify, build-and-size, invariants, e2e, imagens-ok
Sugestão: - CI com portões obrigatórios na main — na 1.0.0 eram dois, verify (typecheck, lint, testes unitários) e invariants. A lista em vigor hoje é sempre esta, medida na fonte: gh api repos/melgarafael/DeskcommCRM/branches/main/protection --jq '.required_status_checks.contexts|join(", ")'.
Vira teste: Não é caso de teste no CHANGELOG (proteção de branch não tem histórico consultável). O padrão certo é o que o CLAUDE.md já usa: publicar o comando, não o resultado.
L342 · FRAGIL · gravidade media · contagem
roda 364 testes de invariante em 56 arquivos
Mede com:
git ls-tree -r --name-only v1.0.0 tests/invariants/ | grep -c '\.test\.ts$' ; ls tests/invariants/*.test.ts | wc -l ; grep -rhoE '^\s*(it|test)(\.\w+)?\(' tests/invariants/ | wc -l
Deu:
v1.0.0: 56 arquivos (o número da frase confere na tag). HEAD: 103 arquivos, 682 casos `it(`/`test(` declarados. O número 364 é repassado como VIVO em CLAUDE.md:253 e em docs/growth/lp-plano.md:300 — nos dois lugares ele já está velho por um fator de quase 2.
Sugestão: - O portão invariants sobe um Postgres limpo, aplica o baseline.sql em modo install e update, e roda a suíte de invariantes — 56 arquivos nesta versão — incluindo o teste de isolamento entre organizações, que prova que um usuário de uma organização não enxerga nenhuma linha de outra. (Para o número de hoje: ls tests/invariants/*.test.ts | wc -l.)
L349 · FALSA · gravidade media · ponteiro
Mede com:
grep -n '^\[' CHANGELOG.md ; git tag --list | sort -V | tail -3
Deu:
Definições presentes: Não lançado (v1.2.1...HEAD), 1.2.1, 1.2.0, 1.1.0, 1.0.0. Tags existentes: v1.2.0, v1.2.1, **v1.3.0**. O comparador de "Não lançado" ainda parte da v1.2.1, então o link mostra a 1.3.0 inteira como se fosse trabalho não lançado.
Sugestão: Não lançado: https://github.com/melgarafael/DeskcommCRM/compare/v1.3.0...HEAD
Vira teste: tests/unit/changelog-links.test.ts — a definição de [Não lançado] deve comparar contra a MAIOR tag existente (git tag --sort=-v:refname | head -1). Sabotagem que reprova: publicar uma tag nova sem atualizar a linha.
Vereditos "VERDADEIRA" derrubados no passe adversarial
De 69 reexaminadas, 4 caíram. Duas eram frases escritas na mesma sessão da varredura.
docs/runbooks/remediar-worker-congelado.md:196 → FALSA
Desde a 1.3.0, o agente completa parte disso sozinho — em até 5 minutos. O
agent.sh(cron do host) … preenche a chave que faltou usando a versão que o contêiner já está rodando
Por que caiu: O varredor grepou a ÁRVORE DE TRABALHO (main @ 840917ed) — _common.sh:464/466 e cron_line — para provar uma afirmação que é ESCOPADA POR VERSÃO ("desde a 1.3.0"). O kit que roda na VPS do cliente é o da tag: update.sh faz git checkout "$TARGET_TAG" (update.sh:119) e só depois o cron chama o agent.sh do disco. Na tag v1.3.0 a função completar_pin_ausente NÃO EXISTE — nem em _common.sh, nem chamada no agent.sh. Ela entrou em 81b3bd5d (2026-08-14), um dia DEPOIS do commit da v1.3.0 (2026-08-13 16:52), e não está em tag nenhuma: v1.3.0 é a mais recente. Ou seja: nenhuma instalação existente hoje tem esse comportamento. Presença de string na main != comportamento na versão publicada. Pior: quem lê este runbook para atender um cliente afetado vai dizer a ele "espere 5 minutos que o pin se completa sozinho" — e nada acontece, para sempre.
git show v1.3.0:hostgator-setup-kit/_common.sh | grep -c completar_pin_ausente ; git show v1.3.0:hostgator-setup-kit/agent.sh | grep -c completar_pin_ausente ; grep -n completar_pin_ausente hostgator-setup-kit/agent.sh ; git log --oneline -S completar_pin_ausente -- hostgator-setup-kit/_common.sh ; git log -1 --format='%h %ad' --date=short 81b3bd5d ; git merge-base --is-ancestor 81b3bd5d v1.3.0 && echo 'SIM (está na v1.3.0)' || echo 'NAO — posterior a v1.3.0' ; git tag -l 'v*' --sort=-v:refname | head -1 ; git log -1 --format='%ad' --date=iso v1.3.0
$ git show v1.3.0:hostgator-setup-kit/_common.sh | grep -c completar_pin_ausente
0
$ git show v1.3.0:hostgator-setup-kit/agent.sh | grep -c completar_pin_ausente
0
$ grep -n completar_pin_ausente hostgator-setup-kit/agent.sh # HEAD 840917ed
180:PIN_CORRIGIDO="$(completar_pin_ausente .env)" || PIN_CORRIGIDO=""
$ git log --oneline -S completar_pin_ausente -- hostgator-setup-kit/_common.sh
81b3bd5d feat(kit): o agente completa o pin sozinho — só a lacuna, nunca a decisão
$ git log -1 --format='%h %ad' --date=short 81b3bd5d
81b3bd5d 2026-08-14
$ git merge-base --is-ancestor 81b3bd5d v1.3.0 && echo SIM || echo 'NAO — posterior a v1.3.0'
NAO — posterior a v1.3.0
$ git tag -l 'v*' --sort=-v:refname | head -1
v1.3.0
$ git log -1 --format='%ad' --date=iso v1.3.0
2026-08-13 16:52:52 -0300
(controle de que a sonda estava viva: o mesmo grep na main devolve a linha 180 do agent.sh, e `bash t
docs/runbooks/remediar-worker-congelado.md:22 → FRAGIL
Ainda não coberto: o app não subiu contra um Supabase real (o ensaio usou um Postgres em contêiner como
SUPABASE_DB_URL) … e a sessão do WhatsApp foi representada por um arquivo marcador no volume, não por um número pareado de verdade.
Por que caiu: O comando do varredor (grep -rln 'U6-b|U6-c|remediar-worker-congelado' no repo) é sonda cega para a categoria que importa: ele procurou um ENSAIO POSTERIOR em outros arquivos e concluiu "nenhum registro". A refutação está 340 linhas abaixo, DENTRO DO MESMO ARQUIVO: o §P4 ("a execução real, na produção do projeto", 2026-08-13) documenta o app rodando contra o Supabase de produção (/api/v1/health → 1.3.0, healthy, com supabase: ok) e uma sessão de WhatsApp pareada de verdade (volume com 48.607 arquivos, sessão noweb presente, waha: ok). O aviso de topo — que é a primeira coisa que qualquer leitor vê — declara não coberto exatamente o que o próprio documento depois mede. O §441 ("O que estes ensaios NÃO cobriram") repete a lista com escopo correto ("ensaios"), o que torna o de cima o único que mente por falta de escopo.
sed -n '361,385p' docs/runbooks/remediar-worker-congelado.md
### P4 — a execução real, na produção do projeto (2026-08-13)
Primeira aplicação em instalação de verdade, com dados de verdade. **A produção reproduziu o
U6-c com exatidão** …
| `/api/v1/health` | `0.1.0` | — | **`1.3.0`, healthy** |
…
**Nada se perdeu, medido item a item:** volume WAHA com **48.607 arquivos** antes e depois,
sessão `noweb` presente, 4 volumes intactos, **nenhuma** chave do `.env` sumiu (39 → 43 …).
De fora da VPS: `HTTP 307`, e o health com `supabase: ok`, `redis: ok`, `waha: ok`.
docs/runbooks/deploy.md:110 → NAO_VERIFICAVEL
A régua de operação (7 contêineres, ~150 MB por número de WhatsApp,
mem_limitsomando 2560m entre app+worker+waha)
Por que caiu: O comando mediu duas das três parcelas (7 serviços, 768+512+1280 = 2560m) e passou batido pela terceira — o ~150 MB por número de WhatsApp, que é justamente a que sustenta a recomendação comercial de 4 GB e o limiar de aviso do install.sh. Nenhum comando da varredura, e nenhum artefato do repo, mede consumo por sessão WAHA: o número só existe em prosa, em 7 lugares que se citam entre si, e a ocorrência mais antiga é herdada da síntese do curso WAHA (docs/research/reference-synthesis.md, "NOWEB … ~150MB"), não de uma medição deste projeto. É número que pega carona: contar de novo os contêineres não calibra a parcela que ninguém mediu.
grep -rn '150 MB\|150MB' --include='*.md' . | grep -v node_modules ; git log --format='%h %ad %s' --date=short -S '150 MB' -- docs/ hostgator-setup-kit/ CHANGELOG.md | tail -3
CHANGELOG.md:18 · docs/white-label.md:183 · docs/research/reference-synthesis.md:117 · docs/runbooks/waha-hostgator.md:20 · docs/runbooks/deploy.md:110 · docs/doctrine/packaging.md:413 · hostgator-setup-kit/README.md:93 → 7 ocorrências, TODAS prosa; zero teste, zero saída de `docker stats`, zero anexo de medição
$ git log -S '150 MB' … | tail -3
efe82e50 2026-08-03 feat(kit): porta de entrada para quem ainda não tem servidor
fb103fad 2026-07-28 fix(vps): a recomendação passa a ser 4 GB, e o kit para de dizer que 2 GB bastam
8cd723b8 2026-05-05 docs(waha): switch production hosting recommendation Hetzner → Hostgator ← origem: prosa herdada, não medição
docs/index.md:29 → NAO_VERIFICAVEL
todos os links relativos da tabela de entrada (README, VISION, ARCHITECTURE, AGENTS, CLAUDE, CONTRIBUTING, CHANGELOG, current-state e os 74 demais)
Por que caiu: A citação não existe no arquivo. docs/index.md:29 é uma linha de tabela ("| README.md | O que é, quickstart de 5 min… |") — a frase citada foi sintetizada pelo varredor, então a linha do relatório não é auditável por quem for reconferir: abre o arquivo, não acha a frase, e não sabe se a divergência é do doc ou do relatório. Pior, a aritmética da própria citação não fecha com a própria medição: 8 nomeados + "os 74 demais" = 82 alvos, e o repo tem 75 links relativos com repetição / 74 únicos. O que É verdade (e eu reproduzi) é a propriedade medida — nenhum link relativo quebrado; o que cai é a afirmação como está escrita e ancorada.
sed -n '29p' docs/index.md ; python3 -c "import re,os;s=open('docs/index.md').read();ls=[l for l in re.findall(r'\]\(([^)]+)\)',s) if not l.startswith(('http','mailto'))];print('totais:',len(ls),'unicos:',len(set(ls)));print('QUEBRADOS:',[l for l in set(ls) if not os.path.exists(os.path.normpath(os.path.join('docs',l.split('#')[0])))])"
$ sed -n '29p' docs/index.md
| [`README.md`](../README.md) | O que é, quickstart de 5 min, stack, roadmap. Também em [EN](../README.en.md) / [ES](../README.es.md) |
$ python3 …
totais: 75 unicos: 74
QUEBRADOS: []
Documentos que a varredura NÃO cobriu
- ARCHITECTURE.md — visão de arquitetura de 1 página, linkada por README/CLAUDE; densa em contagens e afirmações de gate. Nenhum varredor a leu. É o doc que um dev abre ANTES de codar.
- docs/threat-model.md — inventário da superfície de ataque; citado como autoridade por CLAUDE.md e ARCHITECTURE.md. O front-matter declara
audited_against: origin/main @ 789dfa6 (2026-07-27), mas o corpo fala em presente e 4 itens já foram fechados no código. - triagem/TRIAGEM.md — o procedimento de triagem de PR (o próprio workflow que rodou esta varredura). Lista os checks obrigatórios da branch protection. CLAUDE.md avisa que medir contra a régua errada é 'o modo de falha nº 1 do procedimento de triagem'.
- triagem/references/complemento-do-ci.md — anexo do TRIAGEM.md com 'o que os gates não provam'.
- docs/deploy-hostgator/README.md — guia de compra + instalação para leigo (P0 de primeira impressão). Nomeia plano de VPS e ensina um passo de MFA que não existe mais.
- docs/testing/user-journey-map.md — o mapa de jornadas que o DoD 12 manda atualizar; fonte da verdade do QA de produto.
- tests/invariants/README.md — contrato do harness de invariantes que todo contribuidor lê antes de adicionar um.
- tests/e2e/README.md e tests/unit/README.md — listas de 'suítes a criar' escritas antes da implementação.
- lib/waha/README.md, lib/ai/README.md, lib/api/README.md — READMEs de módulo marcados 'Placeholder'.
- supabase/migrations/MANIFEST.md — o ledger obrigatório do item 11 do Definition of Done.
- docs/deploy-selfhost/README.md — AUDITADO SEM ACHADO: os 10 paths e as 12 env vars que cita existem; o fluxo bate com update.sh.
- docs/ATUALIZANDO.md — AUDITADO SEM ACHADO: conferido contra hostgator-setup-kit/update.sh linha a linha; os 6 passos, o backup e os 3 scripts citados conferem.
- docs/architecture/README.md — AUDITADO SEM ACHADO: a tabela de 10 mapas bate com
ls docs/architecture/*.json(10). - docs/doctrine/sistema-vivo.md — AUDITADO SEM ACHADO: '10 gates before-send' bate com BEFORE_SEND_GATES (10 entradas).
- docs/white-label.md — AUDITADO SEM ACHADO: /admin/marca, /app/settings/marca, X-Deskcomm-Signature e 'o alarme de orçamento não tem cron' conferem.
- docs/SETUP.md, docs/runbooks/waha-hostgator.md, docs/runbooks/ai-credentials-rotation.md, VISION.md, docs/business-rules/, docs/doctrine/restricao-de-canal.md, docs/doctrine/separacao-fala-e-operacao.md — varridos por existência de path e por contagem; zero path morto, zero contagem. O runbook de rotação cita
scripts/rotate-ai-cred-aes-key.tsmas DECLARA que ele não existe — honesto, não é achado. - Fora do escopo literal do brief (não são raiz nem docs/) mas não cobertos e com afirmação de estado: .claude/agents/triagem-medidor.md, .claude/skills/sistema-vivo/SKILL.md, hostgator-setup-kit/README.md, hostgator-setup-kit/CLAUDE.md, plan/progress.md, tasks/todo.md.
- NÃO SÃO ACHADO, e é deliberado: docs/specs/* e docs/stories/epics/* citam 324 paths inexistentes (medido). São planos — afirmam futuro, não estado. Excluí-los foi decisão minha, não omissão.
Achados nesses documentos (19)
triagem/TRIAGEM.md:101 · FALSA · gravidade alta
Obrigatórios no merge:
verify,build-and-size,invariants.
gh api repos/melgarafael/DeskcommCRM/branches/main/protection --jq '{contexts:.required_status_checks.contexts, strict:.required_status_checks.strict, reviews:.required_pull_request_reviews}'
{"contexts":["verify","build-and-size","invariants","e2e","imagens-ok"],"reviews":null,"strict":false}
Sugestão: São CINCO: verify, build-and-size, invariants, e2e, imagens-ok. As outras duas afirmações da mesma seção (strict=false na linha 89, sem review humano obrigatório na linha 15) foram medidas e CONFEREM — só a lista está velha. É o pior lugar possível para esse erro: CLAUDE.md registra que medir contra a régua errada é 'o modo de falha nº 1 do procedimento de triagem', e este é o arquivo que define o procedimento. Um triador que leia esta linha declara 'passou os obrigatórios' tendo rodado 3 de 5 — e os dois que faltam (e2e, imagens-ok) são justamente os que cobrem o artefato que o self-hoster instala.
docs/deploy-hostgator/README.md:177 · FALSA · gravidade alta
- Segurança em 2 etapas (MFA): no primeiro login o CRM pede pra configurar um código de 6 dígitos. Tenha o app Google Authenticator ou Authy no celular, escaneie o QR e digite o código. (Isso protege a conta de admin.)
grep -n 'mfa_required' scripts/bootstrap-owner.ts; grep -rn 'exigeCadastroDeMfa' --include='*.ts' app lib | grep -v test
scripts/bootstrap-owner.ts:215: mfa_required: false,
(comentario na linha 201: "`mfa_required: false` EXPLICITO, contra o default `true` da coluna")
lib/auth/politica-mfa.ts:67:export function exigeCadastroDeMfa(p) -> if (p.isPlatformAdmin && p.plataformaExige === true) return true; if (p.role === "admin" && p.empresaExige) return true; return false;
app/actions/auth/politicaDeMfa.ts:120 e lib/auth/server.ts:205 (os dois call sites reais)
Sugestão: O install.sh chama bootstrap-owner.ts, que grava mfa_required: false explícito, e o gate agora LÊ essa coluna. O dono de uma instalação nova NÃO recebe o bloqueador. Este é o guia P0 de primeira impressão: o comprador leigo é instruído a baixar um autenticador e esperar um QR que nunca aparece — e quando não aparece, ele conclui que a instalação falhou. Reescrever como passo opcional apontando para Configurações › Segurança.
ARCHITECTURE.md:11 · FALSA · gravidade alta
- Auth (Supabase Auth +
@supabase/ssr): cookie SameSite=Strict, MFA TOTP forçado pra admin/super-admin. SempregetUser()no server.
sed -n '60,72p' lib/auth/politica-mfa.ts
export function exigeCadastroDeMfa(p: PoliticaDeMfa): boolean {
if (p.isPlatformAdmin && p.plataformaExige === true) return true;
if (p.role === "admin" && p.empresaExige) return true;
return false;
}
// empresaExigeMfa(): "AUSENTE e `false`, E ISSO E A DECISAO" — toda org existente chega sem a chave
Sugestão: CLAUDE.md já carrega a doutrina nova ('MFA TOTP é opcional e ligado por quem administra'); ARCHITECTURE.md, que é a porta de entrada linkada pelo README, ficou na regra antiga isPlatformAdmin || role === "admin". Trocar por: duas políticas independentes que SOMAM (platform_admins.mfa_required e organizations.settings.security.mfa_required), padrão de ambas NÃO exigir, e registrar a distinção cadastrar≠provar (mfaEmDivida() não consulta a política).
docs/threat-model.md:56 · FALSA · gravidade alta
checkRateLimitexiste (lib/ai/dispatcher/rate-limit.ts) e é chamado em 2 pontos do código:/api/v1/webhooks/in/:tokene o dispatcher de IA. Nada mais. / Não há nenhum limite de tentativa em:/login…/signup…/team/accept-invite/:token
grep -rn 'auth/rate-limit' --include='*.ts' --include='*.tsx' app lib | grep -v '\.test\.'; grep -rn 'checkRateLimit(' --include='*.ts' app lib | grep -v '\.test\.'
app/actions/auth/signUp.ts:14:import { authRateLimited, AUTH_LIMITS } from "@/lib/auth/rate-limit";
app/actions/auth/requestPasswordReset.ts:8:import { authRateLimited, AUTH_LIMITS } ...
app/actions/auth/signInWithPassword.ts:16:} from "@/lib/auth/rate-limit";
app/team/accept-invite/[token]/page.tsx:14:import { authRateLimited, AUTH_LIMITS } ...
--- call sites de checkRateLimit ---
app/api/v1/webhooks/in/[token]/route.ts:58
lib/auth/rate-limit.ts:89 (por IP)
lib/auth/rate-limit.ts:94 (por identificador)
lib/auth/rate-limit.ts:165 (login_fail por conta)
lib/ai/dispatcher/index.ts:298
Sugestão: T1 é o único risco vermelho do documento, e a 'Conclusão honesta' inteira se apoia nele ('O que falta é estreito e específico: limite de tentativa na frente dos guards'). lib/auth/rate-limit.ts foi escrito para fechar exatamente isto — o cabeçalho dele cita a issue #64 e nomeia login, signup, recuperação e aceite de convite. RESSALVA HONESTA: sobrevive a sub-afirmação 'não existe lockout por conta' — rodei a MESMA sonda do doc (grep -rn 'lockout|failed_attempts' app lib supabase workers) e ela devolve ZERO. Mas rate-limit.ts:165 conta falha de login POR CONTA (chave auth:login_fail:id:), então a sonda do doc é cega para o mecanismo que existe. Rebaixar T1 e reescrever a conclusão.
docs/threat-model.md:139 · FALSA · gravidade media
T5 — Secrets ausentes do
.env.example🟠 CONFIRMADO —IMPERSONATE_COOKIE_SECRET,INTERNAL_CRON_SECRET,LGPD_SIGNING_KEYestão emlib/env.tse não no template. O operador não sabe que precisa gerá-los.
grep -n 'IMPERSONATE_COOKIE_SECRET\|INTERNAL_CRON_SECRET\|LGPD_SIGNING_KEY' .env.example .env.hostgator.example
.env.example:20:INTERNAL_CRON_SECRET=
.env.example:124:IMPERSONATE_COOKIE_SECRET=
.env.example:132:LGPD_SIGNING_KEY=
.env.hostgator.example:129:INTERNAL_CRON_SECRET= # opcional; vazio = usa INTERNAL_SECRET
.env.hostgator.example:133:IMPERSONATE_COOKIE_SECRET= # openssl rand -hex 32 (>=32 chars)
.env.hostgator.example:134:LGPD_SIGNING_KEY= # openssl rand -hex 32
Sugestão: Os TRÊS estão nos DOIS templates, e o de produção ainda traz o comando de geração ao lado. T5 está fechado; sai do corpo e da tabela de prioridade (linha 195). Deixar risco resolvido marcado 🟠 CONFIRMADO gasta atenção no lugar errado e faz duvidar dos itens que ainda valem — T4 continua VERDADEIRO e merece a atenção que T5 rouba (medido: lib/auth/invite-token.ts:16 ainda tem o literal "dev-fallback", e INVITE_TOKEN_SECRET dá 0 ocorrências em lib/env.ts, .env.example E .env.hostgator.example).
docs/threat-model.md:153 · FALSA · gravidade media
Ressalva: esse E2E não roda no CI — e é a única prova automatizada do guard. … | T6 | Guard de SSRF existe; o E2E que o prova não roda no CI | 🟢 |
grep -n 'vps-webhook-outbound-ssrf' .github/workflows/e2e.yml; gh api repos/melgarafael/DeskcommCRM/branches/main/protection --jq '.required_status_checks.contexts|join(", ")'
.github/workflows/e2e.yml:108: degradacao-silenciosa.spec.ts vps-webhook-outbound-ssrf.spec.ts
verify, build-and-size, invariants, e2e, imagens-ok
Sugestão: A spec está na lista de EXECUÇÃO do e2e.yml (não na FORA_DO_CI, que contém exatamente um item: vps-fresh-onboarding.spec.ts), e o job e2e é check obrigatório. A ressalva inteira caiu — e hoje ela desencoraja confiar na única defesa que o próprio documento chama de 'a defesa mais bem feita do repo'. Repete na tabela de prioridade, linha 197.
docs/threat-model.md:91 · FALSA · gravidade media
createAdminClient(service role, bypassa RLS) é importado em 89 dos 169 route handlers deapp/api/**(dos quais 166 estão sob/api/v1/). … os 56 arquivos de invariante emtests/invariants/rodam no CI
find app/api -name route.ts | wc -l; grep -rl 'createAdminClient' app/api --include=route.ts | wc -l; find app/api/v1 -name route.ts | wc -l; ls tests/invariants/*.test.ts | wc -l
203 (route.ts em app/api/**)
119 (destes, importam createAdminClient)
201 (sob /api/v1/)
103 (arquivos *.test.ts em tests/invariants/)
Sugestão: Os quatro números envelheceram na mesma direção, então a LEITURA não muda de sinal — mas a magnitude sim: 30 handlers novos com service role entraram desde a auditoria, num documento que diz que a mitigação é 'revisão humana', sem lint rule. O '56 arquivos de invariante' reaparece na linha 202, dentro da 'Conclusão honesta' — é o número que sustenta o rebaixamento de T3 de 🔴 para 🟠. Recontar e recarimbar o audited_against do front-matter (hoje 789dfa6, 2026-07-27).
docs/threat-model.md:40 · FALSA · gravidade media
|
/api/v1/cron/*(9 rotas) |Bearer INTERNAL_CRON_SECRET\|INTERNAL_SECRET, fail-closed | ❌ | … Os 9 crons e/api/internal/*
find app/api/v1/cron -mindepth 1 -maxdepth 1 -type d | wc -l; grep -oE 'api/v1/cron/[a-z-]+' docker/scheduler/entrypoint.sh | sort -u | wc -l
16 (diretorios de rota em app/api/v1/cron)
16 (crons agendados em docker/scheduler/entrypoint.sh)
Sugestão: São 16, não 9 — e as duas medições independentes (disco e agendador) concordam, o que de quebra confirma que tests/unit/cron-routes-scheduled.test.ts está fazendo o trabalho dele. A contagem errada aparece duas vezes (tabela §1 e bullet de T1). O MESMO erro está em ARCHITECTURE.md:65 ('drenados pelos 10 endpoints em app/api/v1/cron/') — duas fontes, dois números, nenhum certo. Quem consertar um precisa grepar o outro.
ARCHITECTURE.md:37 · FALSA · gravidade media
Rota autenticada de tenant (
/api/v1/*, 166 handlers):
find app/api/v1 -name route.ts | wc -l; find app/api/v1 -name route.ts -not -path 'app/api/v1/cron/*' | wc -l; find app/api/v1 -name route.ts -not -path 'app/api/v1/cron/*' -not -path 'app/api/v1/webhooks/*' | wc -l; git log -1 --format='%h %ad %s' --date=short -S'166 handlers' -- ARCHITECTURE.md
201 (todos os route.ts sob /api/v1)
185 (excluindo cron)
176 (excluindo cron e webhooks — a regua mais generosa para o texto "rota autenticada de tenant")
9152ab27 2026-07-30 docs(auditoria): remede os numeros e declara a regua de cada um
Sugestão: Medi com três réguas porque o doc não declara a dele; até a mais generosa (176) fica 10 acima de 166. O número foi escrito em 2026-07-30 pelo commit 9152ab27, cujo título é literalmente 'remede os numeros e declara a regua de cada um' — ou seja, uma recontagem deliberada apodreceu em duas semanas. O conserto que dura é declarar a régua NO texto e citar o comando, como CLAUDE.md já faz para as specs e2e.
ARCHITECTURE.md:16 · FALSA · gravidade media
⚠️ Aplicado hoje em apenas 2 pontos (webhook de captação e dispatcher de IA) — o surface público de auth está sem.
grep -rn 'auth/rate-limit' --include='*.ts' --include='*.tsx' app | grep -v '\.test\.'
app/actions/auth/signUp.ts:14
app/actions/auth/requestPasswordReset.ts:8
app/actions/auth/signInWithPassword.ts:16
app/team/accept-invite/[token]/page.tsx:14
Sugestão: Mesma família do T1 do threat-model, propagada para o doc irmão. 'O surface público de auth está sem' é exatamente o que lib/auth/rate-limit.ts fechou, nos quatro pontos. É o caso 'onde mais eu fiz isto': quem consertar o threat-model precisa grepar 'rate limit' nos demais .md, senão a frase errada sobrevive aqui.
docs/testing/user-journey-map.md:28 · FALSA · gravidade media
Gate:
organizations.onboarded_at. MFA obrigatório pra admin logo após o wizard.
sed -n '65p;73p' docs/testing/user-journey-map.md | cut -c1-200
linha 65: | J1.33 | A verificacao em duas etapas deixa de ser imposta | ... ambos com padrao nao-exigir - **PASS** (`tests/e2e/mfa-opcional.spec.ts`, `lib/auth/politica-mfa.test.ts`) |
linha 73: > **Achado ABERTO (nao e regressao, e primeira impressao):** ... O MFA obrigatorio para `admin` e decisao de produto e esta correto; o que esta errado e ele aparecer como surpresa ...
Sugestão: O documento se contradiz internamente em três lugares: a linha 28 (cabeçalho da jornada J1) e a linha 73 ('Achado ABERTO … o MFA obrigatório para admin é decisão de produto e está correto') afirmam o oposto do que a linha 65 (J1.33, marcada PASS) documenta ter mudado. Quem lê o mapa de cima para baixo — que é como se lê um mapa de jornadas — encontra a versão velha primeiro e para ali. O 'Achado ABERTO' da linha 73 foi fechado pelo próprio J1.33 e precisa virar 'resolvido em', não seguir aberto.
docs/testing/user-journey-map.md:17 · FALSA · gravidade media
- Evidência: screenshot/trace em
.superpowers/evidence/vps-qa/.
grep -n 'superpowers' .gitignore; git ls-files .superpowers | wc -l; ls -d .superpowers/evidence/vps-qa
.gitignore:98:.superpowers/
.gitignore:106:.superpowers/evidence/
0 (arquivos rastreados sob .superpowers/)
ls: .superpowers/evidence/vps-qa: No such file or directory
Sugestão: O doc aponta a evidência de TODAS as jornadas para um diretório que o .gitignore exclui duas vezes, que tem zero arquivos rastreados e que não existe nem no disco desta árvore. Ponteiro morto se lê como prova: quem audita conclui 'a evidência existe, só não abri'. Mesmo problema nas linhas 198 e 214. A evidência versionada do projeto mora em evidence/ (239 PNGs rastreados) — é para lá que as três linhas devem apontar.
triagem/references/complemento-do-ci.md:178 · FALSA · gravidade alta
Falta um passo do mantenedor: incluir
build-and-pushna branch protection. Enquanto não estiver lá, o check informa mas não barra.
gh api repos/melgarafael/DeskcommCRM/branches/main/protection --jq '.required_status_checks.contexts|join(", ")'; sed -n '155,178p' .github/workflows/publish-image.yml
verify, build-and-size, invariants, e2e, imagens-ok
--- publish-image.yml ---
# Job de fachada: da UM nome estavel para a branch protection exigir.
imagens-ok:
needs: [build-and-push]
steps:
- name: Falhar se qualquer imagem nao construiu
run: [ "${{ needs.build-and-push.result }}" = "success" ]
Sugestão: A pendência foi resolvida por outro caminho: em vez de exigir build-and-push direto, criou-se o job de fachada imagens-ok, que depende dele, falha junto, e É check obrigatório. O nome literal 'build-and-push' de fato não está na protection — então quem confere pelo NOME conclui que a pendência continua. Mas a CONSEQUÊNCIA afirmada ('informa mas não barra') é falsa, e é a consequência que decide se um triador deixa passar um PR que quebra a imagem. Na mesma página, linha 171 diz 'os quatro obrigatórios' — são cinco.
docs/deploy-hostgator/README.md:49 · FRAGIL · gravidade media
Plano recomendado: VPS NVMe 4 (2 vCPU / 4 GB / 100 GB NVMe) — é exatamente o mínimo que o runbook de produção declara. A stack sobe num NVMe 2, mas opera no limite.
grep -rniE 'turing|cartesius|nvme 4|nvme 2' --include='*.md' --include='*.sh' . | grep -v node_modules; grep -niE 'vcpu|RAM|GB|mínimo|swap' docs/runbooks/deploy.md
hostgator-setup-kit/comecar.sh:125: - Plano VPS Turing (ou superior) - 2 vCPU e 4 GB de RAM
hostgator-setup-kit/comecar.sh:126: O plano Cartesius (1 vCPU / 2 GB) NAO da conta
hostgator-setup-kit/README.md:7 -> Turing
docs/runbooks/waha-hostgator.md:18 -> Turing
docs/specs/03-spec-whatsapp-waha.md:61 e :1940 -> Turing
docs/specs/08-spec-deploy-observability.md:80 e :1367 -> Turing
docs/prd/03-prd-whatsapp-waha.md:369 -> Turing
docs/presentation/{pitch-deck,HANDOFF}.md -> Turing
docs/deploy-hostgator/README.md:49 -> "VPS NVMe 4" <-- unica ocorrencia no repo
--- runbook ---
docs/runbook
Sugestão: 'VPS NVMe 4' aparece UMA vez no repo inteiro; o artefato executável (comecar.sh:125 — o script que este mesmo guia manda o comprador rodar) e outros 9 documentos nomeiam 'VPS Turing'. O comprador leigo é mandado a uma caixinha com nome que o resto do produto não conhece, e o script que ele roda em seguida lhe diz outro nome. FRAGIL e não FALSA porque não consigo decidir qual está certo sem o catálogo atual da HostGator — mas os dois não podem estar certos, e a régua natural é o script. A segunda metade da frase também não se sustenta: docs/runbooks/deploy.md:108 declara '>= 4 GB de RAM ou swap' e diz explicitamente que é requisito do CAMINHO DE EXCEÇÃO (build na VPS), 'não da operação'.
tests/invariants/README.md:35 · FALSA · gravidade media
O que a suíte prova hoje\n\n-
rls-isolation.test.ts— cria 2 orgs + 1 usuário em cada e prova que o usuário da org A lê 0 rows da org B …
ls tests/invariants/*.test.ts | wc -l
103
Sugestão: O cabeçalho promete o inventário do que a suíte prova e lista 1 de 103 arquivos. É o README que todo contribuidor abre antes de escrever invariante novo — a seção seguinte é literalmente 'Regras pra adicionar invariante novo' — então ele decide o escopo do test:db com base numa foto de quando a suíte tinha um arquivo. Trocar a lista fixa por 'rode ls tests/invariants/*.test.ts' mais os 3-4 invariantes estruturais que merecem destaque; listar 103 à mão só recria o problema daqui a um mês.
tests/e2e/README.md:5 · FALSA · gravidade media
Suítes a criar\n\n-
auth.spec.ts— login com MFA, refresh, logout\n-tenant-isolation.spec.ts— GATE OBRIGATÓRIO: cria 2 tenants e valida que A não vê dados de B em nenhum endpoint\n-lgpd.spec.ts…whatsapp-inbox.spec.ts…kanban.spec.ts…super-admin.spec.ts
ls tests/e2e/*.spec.ts | wc -l; for p in auth tenant-isolation lgpd whatsapp-inbox kanban super-admin; do printf '%-22s ' $p; [ -e tests/e2e/$p.spec.ts ] && echo OK || echo AUSENTE; done
48 (specs no disco)
auth OK
tenant-isolation AUSENTE
lgpd AUSENTE
whatsapp-inbox AUSENTE
kanban AUSENTE
super-admin AUSENTE
Sugestão: 48 specs existem; 5 das 6 'a criar' nunca foram criadas com esses nomes (a cobertura veio com outros). O pior item é tenant-isolation.spec.ts, marcado GATE OBRIGATÓRIO — um contribuidor lê isso e conclui que existe um gate e2e de isolamento que não existe: o isolamento é provado por tests/invariants/, no job invariants, não por Playwright. O doc também manda npm run test:e2e num repo padronizado em pnpm. tests/unit/README.md tem o mesmo defeito: medi os 7 arquivos de 'Foco' que ele lista e os 7 estão ausentes.
lib/waha/README.md:3 · FALSA · gravidade baixa
Placeholder. Cliente real virá da Spec 03 — WhatsApp via WAHA Plus.
ls lib/waha/*.ts | wc -l; ls lib/waha/; ls lib/ai/ | wc -l; for p in auth idempotency rate-limit pagination audit cors; do printf '%-24s ' lib/api/$p.ts; [ -e lib/api/$p.ts ] && echo OK || echo AUSENTE; done
10 (arquivos .ts em lib/waha/)
client.ts ingest.ts media-send.ts message-id.ts send.ts webhook-auth.ts (+4 .test.ts)
37 (entradas em lib/ai/: dispatcher, rag, guardrails, runtime, skills, agents, budget, ...)
lib/api/auth.ts AUSENTE
lib/api/idempotency.ts AUSENTE
lib/api/rate-limit.ts AUSENTE
lib/api/pagination.ts AUSENTE
lib/api/audit.ts AUSENTE
lib/api/cors.ts AUSENTE
Sugestão: lib/waha/ é o caminho de ingestão de WhatsApp em produção — ingest.ts é citado por CLAUDE.md e pelo threat-model como o guard HMAC da borda — e continua rotulado 'Placeholder'. Idem lib/ai/README.md:3 ('Placeholder. Implementação real virá da Spec 05') com 37 entradas no diretório. Gravidade baixa porque ninguém decide errado por causa disso, mas é ruído que ensina a ignorar rótulo. IMPORTANTE, no sentido oposto: lib/api/README.md:21 ('A adicionar (próximas specs)') parece o mesmo caso e NÃO é — medi os 6 arquivos listados e os 6 realmente não existem. Essa seção está certa e não deve ser 'corrigida' junto.
supabase/migrations/MANIFEST.md:63 · FALSA · gravidade media
|
20260428000000|0016_lgpd_emergency_scope| EPIC-08 wave 3: lgpd_requests.emergency (boolean, default false) + scope (text check 'contact'/'tenant', default 'contact') + partial index lgpd_requests_emergency_idx …
ls supabase/migrations/ | grep -c 0016_lgpd; find supabase -name '*lgpd_emergency*'; grep -in 'emergency' supabase/baseline.sql | head -2
0 (nenhum arquivo *_0016_lgpd_emergency_scope.sql)
(find: nenhum resultado)
baseline.sql:1626: "emergency" boolean DEFAULT false NOT NULL,
baseline.sql:2544:CREATE INDEX "lgpd_requests_emergency_idx" ON "public"."lgpd_requests" ...
Sugestão: O MANIFEST declara a 0016 na tabela 'Applied', mas o arquivo de migration não existe em lugar nenhum. O efeito ESTÁ no baseline.sql, então o self-hoster recebe — mas a doutrina do CLAUDE.md diz que 'quem clonou uma versão antiga do banco precisa conseguir atualizar aplicando as migrations em ordem', e por esse caminho a 0016 nunca chega. É o modo de falha do item 11 do DoD, invertido. Diffei os 147 arquivos contra as 145 linhas: este é o ÚNICO caso. Os outros dois desencontros são inofensivos e não devem ser 'consertados' — 0013_ai_faq_items e 0021_incidents estão no MANIFEST com a coluna de timestamp preenchida como (wave 5) / (wave 11) em vez do carimbo de 14 dígitos, e 00001_initial_schema.sql (só extensions) não tem linha.
docs/handoffs/HANDOFF-indice-de-atrito.md:571 · FALSA · gravidade baixa
Evidência: [caminho feliz](passo4-inbox-demandas.png) · [sob falha](passo4-inbox-demandas-falha.png).
python3 checklinks.py (resolve todo link markdown relativo de `git ls-files '*.md'` contra o diretorio do arquivo); find . -name passo4-inbox-demandas.png; sed -n '188p' tests/unit/evidencia-citada.test.ts
docs/handoffs/HANDOFF-indice-de-atrito.md:571,572,637,638,731,732,918 -> 7 links .png
resolvem para docs/handoffs/<nome>.png -> NAO EXISTEM
os arquivos existem em ./evidence/<nome>.png
--- guarda ---
tests/unit/evidencia-citada.test.ts:188: if (!limpa.includes("/")) return path.posix.join(base, limpa); // base = "evidence"
--- varredura completa: 18 links relativos quebrados no repo ---
Sugestão: Ponto cego do guarda, não ausência de guarda: tests/unit/evidencia-citada.test.ts resolve nome sem barra como evidence/ e por isso considera os 7 links válidos. Ele prova que o ARQUIVO existe, nunca que o LINK resolve. Renderizado no GitHub, cada um aponta para docs/handoffs/.png e dá 404 para quem clica em 'Evidência'. Total da varredura em todo o repo: 18 links relativos quebrados — 2 são falso positivo MEU (docs/handoffs/BRIEFING-crm-vivo.md:1293 e HANDOFF-crm-vivo.md:3019 usam a palavra 'caminho' como placeholder ao EXPLICAR sintaxe de link), 7 são estes, 8 são .md dentro de evidence/ que escrevem o caminho a partir da raiz do repo estando já em evidence/ (os alvos existem; a resolução é que erra), e 1 é ausência real: evidence/ia-360-w4/medicao-vazamento/RELATORIO.md:126 a