Destrava o #1747 (706 commits atras), a pedido do dono. - A migration da onda 3 sai de 0433 (20260926210000) para 0500 (20260930170000): estava fora de ordem, com a main ate a 0499. Arquivo, rotulos do baseline, linha do MANIFEST (depois da 0499), testes e comentarios. - A lista do CHECK de agent_inbox_items.kind na migration ganha os cinco kinds da proposta (0464/0466/0475): rodando depois delas, a lista antiga os apagaria. Igual a do baseline. - Conflitos: baseline partiu da main com o delta do PR reaplicado (kinds na lista unica; apendice antes da VARREDURA anon); InboxKind, rotulos e destino da Central ficam com os kinds do Jev e os da proposta; mapa vivo com as arestas da main (e121-e123) e as do Jev renumeradas e124-e129; selo das traducoes do white-label regravado sobre o original mesclado; e2e.yml com os dois comentarios. O caminho da migration dentro de tests/invariants/jev-aviso-fecha-sozinho-e-e-unico.test.ts muda no commit seguinte, separado: o guard freeze-invariants barra edicao de invariante dentro da resolucao.
43 KiB
Repository Guidelines
Contrato portável para qualquer agente de código (Codex, Cursor, OpenCode, Antigravity, Copilot). Este arquivo é o núcleo. A doutrina completa e não-negociável vive em
CLAUDE.md— leia-o antes de tocar em código. O mapa de toda a documentação está emdocs/index.md. Precedência quando dois documentos discordam:CLAUDE.md>docs/specs/>docs/prd/>HANDOFF-*.md>README.md.
Project Overview
Sistema operacional de vendas open source com agentes de IA nativos, multi-nicho (e-commerce,
clínicas, imobiliárias, infoprodutos, serviços), WhatsApp como canal primário via WAHA, CRM
inteiro exposto por MCP. Multi-tenant com RLS desde o dia 1; LGPD nativa. Monetização =
self-host em VPS, não assinatura. Posicionamento: VISION.md; estado real de
implementação: docs/current-state.md.
Consequência que muda como você trabalha: o produto é distribuído como código. Quem instala numa VPS é o usuário. Uma mudança que funciona na máquina do dev e quebra no clone fresco é bug de produto, não detalhe de ambiente. Nada que exija edição manual de arquivo na VPS entra.
Stack canônica (major; a versão exata é o package.json):
Next.js 16 (App Router, Turbopack) · React 19 · TypeScript 6 estrito · Tailwind 4 (config em CSS) ·
shadcn/ui (new-york) · Supabase (Postgres + Auth + Realtime + Storage) · Zod 4 · Vitest 4 ·
Playwright 1 · Sentry 11 · WAHA 2026.7.2 (engine NOWEB) · Upstash Redis · Vercel AI Gateway
(@ai-sdk/anthropic|openai|google).
As majors acima são verificadas contra o
package.jsonportests/unit/agents-md-versoes.test.ts— declare só a major; afirmar minor em prosa cria débito que nenhum gate cobre e trava bump do Dependabot. Um teste irmão,tests/unit/documentacao-aponta-para-o-que-existe.test.ts, reprova todo path citado aqui que não exista no disco.
Architecture & Data Flow
- App — Next.js 16 App Router: UI + Route Handlers no mesmo repo. Server Components por
default;
"use client"só com estado/evento/API de browser. Middleware de borda emproxy.ts(Next 16 renomeoumiddleware.ts→proxy.ts; ele injetaX-Request-Idex-pathnamee autentica a sessão antes da rota). - DB — Supabase Postgres. RLS em toda tabela tenant-aware via helper
(
fn_user_org_ids()/fn_user_role_in_org()), a mesma função SECURITY DEFINER que o RBAC de aplicação usa. Schema versionado emsupabase/migrations/; o que o self-host aplica ésupabase/baseline.sql. - Auth — Supabase Auth +
@supabase/ssr, cookieSameSite=Strict. SempregetUser()no server; nuncagetSession(). MFA TOTP é opcional e ligado por quem administra (duas políticas que somam: plataforma e organização), regra pura emlib/auth/politica-mfa.ts. - Filas — event sourcing leve:
event_log+ workers drenados por cron. Trigger Postgres nunca faz HTTP. - IA — Vercel AI Gateway (Anthropic primário; embeddings pela OpenAI ou pelo Google, escolha da organização), RAG por tenant, guardrails before-send.
- Tempo real — Supabase Realtime (
postgres_changespara inbox/kanban,broadcastpara sinais leves). Storage — bucket privadowhatsapp-media, URL assinada.
Fluxo de uma rota autenticada de tenant:
flowchart LR
A[request] --> B[proxy.ts: X-Request-Id, sessao]
B --> C[1. Zod valida input]
C --> D[2. guard: requireRole / requirePlatformAdmin / secret]
D --> E[3. organization_id de fonte confiavel]
E --> F[4. query: RLS ou filtro manual de org]
F --> G[5. audit se mutacao]
G --> H[6. ok / fail]
Superfícies não-cookie (cada uma com guard próprio, nunca o cookie de sessão):
app/api/v1/cron/ (Bearer INTERNAL_CRON_SECRET, fail-closed), app/api/internal/
(x-internal-secret), app/api/mcp/ (Bearer dsk_... contra api_tokens), app/api/v1/webhooks/
(HMAC + path token), e parte de app/api/v1/ — rotas que aceitam cookie OU bearer pelo helper
lib/api/auth-dual.ts. Esta última cresce rota por rota (decisão do dono em 17/09/2026: converter
o que cada integração precisar), então o inventário é um comando e não uma lista:
git grep -ln "auth-dual" -- app/api/v1. Rota com o helper também precisa de entrada em
lib/auth/public-paths.ts, senão o proxy.ts devolve 401 antes do handler. Inventário e superfície de ataque: docs/threat-model.md.
Turno do agente de IA: inbound WhatsApp → HMAC + idempotência → event_log → worker →
runAgentTurn (RAG + tools MCP) → guardrails → adapter WAHA → handoff humano se o gatilho
disparar. Entrada do turno em lib/agent-engine/agent/inbound-turn.ts; diagrama em
docs/architecture/agent-turn.html.
| Path | O quê |
|---|---|
app/api/v1/ |
Route handlers REST (versionado por path) — reconte, não cite: 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/**) |
app/api/internal/, app/api/mcp/, app/api/v1/cron/ |
superfícies não-cookie (secret/bearer próprio) |
app/app/ |
UI autenticada do tenant · app/admin/ UI de plataforma |
app/actions/ |
Server Actions (auth, onboarding, team, settings) |
lib/agent-engine/, lib/ai/ |
runtime do agente, guardrails, RAG, dispatcher |
lib/api/wrappers.ts |
ok() / fail() — use sempre, não monte Response na mão |
lib/auth/require-role.ts |
requireRole() — guard canônico de RBAC |
lib/supabase/{browser,server,admin}.ts |
clients canônicos |
workers/ |
workers de event_log + crons |
supabase/migrations/ |
schema versionado · supabase/baseline.sql = o que o self-host aplica |
proxy.ts |
middleware do Next 16 (auth de borda, X-Request-Id) |
Idempotência de worker: unique (organization_id, external_id) + captura de code === '23505'. |
|
Contrato completo em docs/specs/07-spec-events-workers.md. |
Key Directories
| Path | O quê |
|---|---|
app/api/ |
Route handlers REST, versionados por path. Conte quantos existem: git ls-files 'app/api/**/route.ts' | wc -l |
app/app/ |
UI autenticada do tenant |
app/admin/ |
UI de plataforma (platform admin) |
app/actions/ |
Server Actions (auth, onboarding, team, settings) |
lib/agent-engine/ |
Runtime do agente: turno inbound/outbound, playbooks, handoff, follow-up, memória da org |
lib/ai/ |
Modelos, custo, orçamento, RAG, dispatcher, catálogo de providers |
lib/channels/ |
Abstração de canal (invariante de restrição de canal) |
lib/api/ |
wrappers.ts (ok()/fail()) e errors.ts (catálogo de códigos) |
lib/auth/ |
server.ts (sessão/org), require-role.ts (requireRole), public-paths.ts (borda) |
lib/supabase/ |
Clients canônicos: browser.ts, server.ts, admin.ts (service role) |
lib/branding/ |
Marca própria (white-label) — resolve do banco, nunca do .env |
workers/ |
Workers de event_log + crons |
components/, hooks/ |
React compartilhado; convenções nos README de cada pasta |
supabase/migrations/ |
Schema versionado + MANIFEST.md; supabase/baseline.sql é o que o self-host aplica |
hostgator-setup-kit/ |
Kit de instalação/atualização da VPS (install.sh, update.sh, diagnostico.sh, healthcheck.sh) |
scripts/ |
CLIs de operação e QA — ver scripts/README.md |
tests/ |
unit/, invariants/, e2e/, shell/, journeys/, fixtures/ |
docs/ |
Doutrina, PRDs, specs, regras de negócio, runbooks, design system — entrada em docs/index.md |
.agents/skills/ |
Guias do assistente embutidos (espelho em .claude/skills/, regerado por pnpm skills:sync) |
Development Commands
pnpm install # deps (frozen-lockfile no CI)
pnpm dev # dev server
pnpm build # next build
pnpm lint # eslint
pnpm typecheck # tsc --noEmit -p tsconfig.typecheck.json (inclui tests/)
pnpm test:unit # vitest — EXCLUI tests/invariants, tests/e2e e tests/journeys (lista viva em vitest.config.ts → exclude)
pnpm test:db # invariantes de banco + gate do baseline (PRECISA de Docker)
pnpm test:e2e # Playwright (PRECISA de app rodando + banco semeado)
pnpm gov:verify # typecheck + lint + lint:channels + lint:role-rank + test:unit
# ← verificação única atual; o encadeamento real sai de:
# node -e "console.log(require('./package.json').scripts['gov:verify'])"
pnpm lint # eslint (flat config)
pnpm typecheck # tsc --noEmit -p tsconfig.typecheck.json
pnpm test:unit # vitest run — EXCLUI tests/e2e, tests/invariants, tests/journeys
pnpm test:db # invariantes de banco + gate do baseline (PRECISA de Docker)
pnpm test:e2e # Playwright (PRECISA de app buildado + .env.e2e)
pnpm test:shell # scripts do kit self-host (bash)
pnpm gov:verify # typecheck + lint + lint:channels + lint:role-rank + test:unit
Um teste só:
pnpm vitest run lib/foo/bar.test.ts # um arquivo unit
pnpm vitest run -t "nome do caso" # um caso pelo nome
pnpm test:db tests/invariants/x.test.ts # um invariante (o script repassa os args ao vitest)
pnpm playwright test tests/e2e/x.spec.ts # uma spec e2e
⚠️ pnpm gov:verify não cobre tudo. Ele omite test:db, test:e2e e test:shell.
Se a mudança toca schema/RLS/tabela tenant-aware, rode pnpm test:db. Se toca UI ou fluxo de
usuário, rode pnpm test:e2e com evidência visual. Se toca Dockerfile*, docker-compose* ou
hostgator-setup-kit/, rode pnpm test:shell — é o único gate que exercita o kit.
O que o CI cobre. .github/workflows/ci.yml: verify = os passos do job, na ordem —
typecheck, lint, lint:channels, test:unit e test:shell hoje, e pnpm lint sozinho não
cobre os dois últimos (liste em vez de acreditar nesta linha:
awk '/^ verify:/,/^ invariants-majors:/' .github/workflows/ci.yml | grep -A1 'name:');
invariants = fachada sobre a matriz invariants-majors, que roda pnpm test:db (isolamento
RLS + invariantes de governança) e pnpm test:db:update (atualizar banco COM dados) uma vez por
major de Postgres suportado. .github/workflows/perf.yml: build-and-size = pnpm build.
.github/workflows/e2e.yml roda as specs Playwright contra um Supabase local de verdade com
o baseline.sql aplicado — o mesmo banco que o self-hoster tem. É check obrigatório — a
data de ativação não é auditável pelo repositório, e a lista viva está logo abaixo, com o
comando ao lado. Não há número aqui de propósito: esta linha já afirmou uma contagem exata
de specs e "a única de fora", e as duas envelheceram — a suíte cresce toda semana e a lista de
exceções muda com ela. Quem fica de fora é o que a própria variável declara; leia, não confie:
O CI tem cinco checks obrigatórios na main: verify, build-and-size, invariants, e2e,
imagens-ok. Não confie nesta lista — reconte antes de citar:
gh api repos/melgarafael/DeskcommCRM/branches/main/protection \
--jq '.required_status_checks.contexts|join(", ")'
e2e roda as partes da sua matrix em paralelo (quantas:
git show origin/main:.github/workflows/e2e.yml | grep -E '^ +parte: \[' — esta linha dizia
"três" até o PR #983 acrescentar a quarta); as specs de fora estão declaradas, com motivo
escrito, em FORA_DO_CI dentro de .github/workflows/e2e.yml. Leia em vez de supor:
git show origin/main:.github/workflows/e2e.yml | grep -A4 'FORA_DO_CI:'
Embedded Assistant Guides
O repositório embute guias em .agents/skills/ (lidos por Codex, Cursor, OpenCode, Antigravity;
o Claude Code lê o espelho em .claude/skills/). Carregue o guia quando o pedido casar, mesmo que
a pessoa não saiba que ele existe — tests/unit/skills-embutidas.test.ts exige que este arquivo
cite cada um:
Os cinco são checks obrigatórios na branch protection da main — medido em 2026-08-14 @ 741c4ec8 (o comando exige permissão de admin no repositório: com token de contribuidor ele devolve 404, medido em 2026-09-13):
| Situação | Guia |
|---|---|
| Instalar, atualizar ou consertar a instalação numa VPS; domínio, Supabase, WhatsApp | deskcomm-instalar |
| Configurar o CRM para um cliente ou nicho: agentes, roteadores, follow-ups, base | deskcomm-cliente-novo |
| Desempenho, conversão, custo de IA, funil, relatório | deskcomm-metricas |
| O agente responde errado, passa tudo para humano, não usa a agenda; afinar o prompt | deskcomm-prompt |
Contribuir: corrigir bug, abrir ou atualizar PR, migration, conflito com a main |
deskcomm-contribuir |
| Criar extensão/plugin/módulo de nicho, ou transformar um PR de nicho em pacote | deskcomm-extensao |
| Escrever ou revisar código aqui | deskcomm-doutrina |
O gate de arquitetura de qualquer peça que atende pessoas é a skill sistema-vivo (lei em
docs/doctrine/sistema-vivo.md).
Code Conventions & Common Patterns
Receita de route handler — nesta ordem, sem atalho:
- Zod valida todo input externo (body, query, path).
- Guard canônico:
requireRole()delib/auth/require-role.ts,requirePlatformAdmin, ou secret/HMAC. Nunca reimplemente a comparação de rank na mão. Handler mutante deapp/api/v1declara aindarequireSupportWrite((lib/impersonate/support.ts) antes do efeito: barra escrita emsupport_readonlye não substitui RBAC/MFA. organization_idresolvido de fonte confiável (cookie/JWT/webhook secret/path token) — nunca do body.- Query: RLS pelo client de sessão, ou filtro manual de
organization_idquando usa service role. audit()(fire-and-forget) se houve mutação.- Responda com
ok()/fail()delib/api/wrappers.ts— nunca monteResponsena mão.
const authz = await requireRole("manager", { requestId });
if (!authz.ok) return authz.response;
Erros — fail(code, message, status) com código de lib/api/errors.ts. Nunca throw cru na
borda. Cada response leva X-Request-Id, correlacionado com o audit log.
Nomes e dados — arquivos e símbolos em PT-BR são a norma (mantenha o idioma do arquivo que
editar). JSON da API em snake_case; dinheiro em _cents + currency; datas ISO-8601 UTC;
UUID v4. Testes ao lado do código (lib/foo/bar.test.ts) ou em tests/.
Log — lib/logger.ts (estruturado, JSON). console.log é proibido em código merged —
no-console é warn no ESLint e o DoD cobre o resto. Nunca logue segredo, token, CPF, telefone
ou e-mail; o beforeSend do Sentry higieniza, mas não é a única camada.
Multi-tenancy — organization_id uuid not null references organizations(id) on delete cascade
em toda tabela tenant-aware. lib/supabase/admin.ts bypassa RLS: toda query com service role
filtra organization_id manualmente. Sem gate automático para isso — a responsabilidade é sua.
Migrations — mudança de schema sai sempre como tripla: migration versionada em
supabase/migrations/, apêndice idempotente em supabase/baseline.sql e linha em
supabase/migrations/MANIFEST.md. Nunca edite migration já aplicada; corrija com uma nova.
Função nova em public precisa de revoke execute ... from public, anon e grant — são
duas origens de EXECUTE.
⚠️ E não leia o baseline com grep no arquivo inteiro: ele é dump + apêndice, a mesma
função aparece várias vezes, e quem vale é a última. Pergunte ao banco depois de aplicar
(pg_get_functiondef) ou ancore no último create or replace (rfind, nunca find).
Contar no arquivo responde "o arquivo menciona", não "o banco faz".
Marca própria (white-label) — o produto é revendido e o nome não é seu. Nunca escreva
"Deskcomm"/"DeskcommCRM" em código que alcança o usuário: tests/unit/branding.test.ts varre
app|components|lib|workers|hooks e reprova (a allowlist só encolhe). A marca resolve do banco
(platform_branding, organizations.settings.branding); APP_NAME/APP_LOGO_URL/APP_ACCENT_HEX
no .env são semente e piso de rollback. Fora do DOM (e-mail, ícone, issuer do MFA) use
marcaDaSaida() de lib/branding/saida.ts. O resolvedor nunca lança — ele roda em
app/layout.tsx e um throw ali é 500 em todas as telas. O PDF de LGPD não leva marca: ele nomeia
o controlador (organizations.legal_name) e o DPO.
supabase/baseline.sql— é o que oinstall.sh/update.shdo self-host aplicam. Toda mudança de schema tem que aparecer aqui como apêndice idempotente, senão não chega em quem instalou. Ver doutrina de Migrations emCLAUDE.md.supabase/migrations/*.sqljá aplicadas — nunca edite. Corrija com migration nova.lib/supabase/admin.ts— service role bypassa RLS. Boa parte dos handlers deapp/api/**o usa — reconte em vez de citar:grep -rl createAdminClient app/api --include='route.ts' | wc -lcontragit ls-files 'app/api/**/route.ts' | wc -l. Toda query precisa filtrarorganization_idmanualmente, resolvido de fonte confiável (cookie/JWT/webhook secret/path token), nunca do body.lib/auth/public-paths.ts— adicionar path aqui remove a checagem de auth de borda. Só com guard próprio dentro da rota..env*— não abra, não copie valor, não logue. Só.env.exampleé template.docker-compose.traefik.yml— numa VPS que já tem proxy reverso próprio (Hostinger, Coolify, Dokploy…), é o único lugar que dá ao contêinerappas labels de roteamento. Todoup -dleva os dois arquivos de compose: Anti-patterns proibidos — string que deveria ser FK; duplicação sem source of truth declarado; feature nomeando um provider de canal (gatepnpm lint:channels); tela nova sem porta declarada emlib/navigation/registry.ts(gatetests/unit/navegacao-completude.test.ts);getSession()no server; segredo em query string;throwcru na borda da API.
Important Files
| Arquivo | Por quê |
|---|---|
proxy.ts |
Middleware de borda do Next 16: auth, X-Request-Id, impersonation |
lib/api/wrappers.ts |
ok() / fail() — formato de resposta e X-Request-Id |
lib/api/errors.ts |
Catálogo de códigos de erro |
lib/auth/require-role.ts |
requireRole() — guard canônico de RBAC |
lib/auth/server.ts |
loadAuthUser(), resolveActiveOrg() — sessão e org ativa |
lib/auth/public-paths.ts |
Allowlist de paths sem auth de borda (só com guard próprio dentro da rota) |
lib/supabase/admin.ts |
Service role — bypassa RLS |
lib/logger.ts, lib/env.ts |
Log estruturado; contrato de env vars validado por Zod |
lib/audit/index.ts |
audit() — trilha de auditoria |
lib/database.types.ts |
Gerado do schema — não edite à mão |
supabase/baseline.sql |
O que o install.sh/update.sh aplicam — toda mudança de schema entra aqui |
workers/agent-worker/main.ts |
Entry point do worker de agente |
docker-compose.prod.yml |
Topologia de produção (imagens publicadas) |
docker-compose.traefik.yml |
Labels de roteamento para VPS que já tem proxy próprio |
instrumentation.ts, sentry.*.config.ts |
Boot de observabilidade |
Runtime/Tooling Preferences
- Node ≥ 22 (
engines,.nvmrc= 22; os workflows fixamnode-version: 22). Gerenciador: pnpm 9.15.9 (packageManager). Não use npm/yarn. - TypeScript estrito via
tsconfig.typecheck.json;strict,noUncheckedIndexedAccess,isolatedModules, alias@/*→ raiz.pnpm typechecké a régua. - ESLint flat config (
eslint.config.mjs, ESLint 10):next/core-web-vitals,react-hooks,typescript-eslint.next lintfoi removido no Next 16 — o script chama o CLI. - Prettier com
prettier-plugin-tailwindcss; classes Tailwind em ordem canônica. - Tailwind 4 — configuração em CSS (
app/globals.css), não emtailwind.config.js. - Sentry — coleta restrita (
dataCollection) + scrub num ponto só,lib/sentry/privacidade.ts, provado pelo envelope do SDK emprivacidade.sdk.test.ts;tunnelRoute: "/monitoring"evita ad-blocker. - Packaging (não-negociável; lei em
docs/doctrine/packaging.md) — nenhum serviço dedocker-compose.prod.ymlconstrói na máquina do cliente: todo serviço declaraimage:de imagem publicada, ebuild:existe só ao lado, como escape. Serviçobuild:-only é pulado pordocker compose pulle nunca é atualizado. Publicação é ato do CI (.github/workflows/publish-image.yml), nunca da sua máquina. Instalação aponta para número de versão;latestsignifica topo damain, a última release éstable. Dependência upstream é referenciada com tag fixa, nunca republicada (WAHA é licenciado). Bump de versão não pode exigir edição manual de arquivo na VPS. - Deploy em VPS com proxy próprio — todo
up -dleva os dois arquivos de compose:docker compose -f docker-compose.prod.yml -f docker-compose.traefik.yml --env-file .env up -d app. Esquecer o segundo-frecria o contêiner sem labels: o domínio inteiro responde404com o contêinerhealthy(o healthcheck é um probe TCP interno). Runbook:docs/runbooks/deploy.md. - Env vars — nova variável entra em
.env.examplee emlib/env.ts. Nunca leia nem logue valor de.env*; só.env.exampleé template. Segredo/token só em header, nunca em query string. - Gerados — não edite —
lib/database.types.ts,graphify-out/,pnpm-lock.yaml,.next/.
Testing & QA
lib/database.types.ts(gerado do schema Supabase — o tamanho de hoje sai dewc -l lib/database.types.ts)graphify-out/(grafo de conhecimento local; ignorado pelo git e ausente num clone fresco — só existe depois de rodar/graphify .)pnpm-lock.yaml,tsconfig.tsbuildinfo,next-env.d.ts,.next/Camada Comando O que cobre Unit (vitest, jsdom) pnpm test:unittests/unit/**+ todo*.test.ts(x)ao lado do código. Timeout 15s por testeInvariantes de banco (Docker) pnpm test:dbIsolamento cross-tenant/RLS, RBAC e governança contra Postgres efêmero E2E (Playwright + axe-core) pnpm test:e2eJornadas reais contra app buildado e o banco do baseline.sqlKit self-host (bash) pnpm test:shellscriptsdo kit,install.sh,update.sh— o único gate do kitJornadas de canal pnpm test:journeystests/journeys/(config Playwright própria)
Convenções: teste ao lado do código (lib/foo/bar.test.ts) ou em tests/{unit,api,invariants,e2e}.
tests/e2e/** e tests/invariants/** são excluídos do vitest de propósito — não os mova para
dentro do include do unit. Fixtures em tests/fixtures/, helpers em tests/helpers/, setup global
em tests/setup/vitest.setup.ts. Determinismo é regra: teste que depende de ordem ou de rede
quebra a suíte inteira.
Locator de tela compartilhada é contrato da suíte, não detalhe do teste. getByRole("button", { name: "Entrar" }) casa por substring — e /entrar/i, que era a forma do login, também: um
segundo botão com essa palavra na mesma tela ("Entrar com Google") torna o locator ambíguo, e o
Playwright recusa clicar (strict mode violation) em vez de escolher. O alcance não fica na
tela: /login é a porta de quase toda spec. Medido em 2026-09-21, um botão a mais ali pôs as 5
partes do e2e vermelhas — 320 violações do mesmo erro em 306 casos, 140 specs citadas no log.
Quem acrescenta botão ou link numa tela já coberta assume os locators que já existem: ancore com
{ name: "Entrar", exact: true }, forma que a suíte já usa 282× para outros rótulos, e meça antes
de empurrar:
git grep -nE "name: *(\"Entrar\"|'Entrar'|/entrar)" -- tests scripts | grep -vE 'exact: *true'
O conserto é no locator, nunca no produto: esconder um botão real para agradar regex de teste troca um defeito de teste por um defeito de tela.
O .env.e2e é obrigatório e é recusado se apontar para Supabase que não seja 127.0.0.1/
localhost — a proteção existe porque sem ela a suíte rodaria contra produção (pnpm e2e:env
gera o arquivo).
QA visual com recursos reais (doutrina). O produto é self-host: a experiência de quem instala
numa VPS é o produto. Toda feature nova, ou fix de comportamento visível, deve ser provada
pela tela como um usuário leigo faria, em ambiente fresco estilo VPS, com evidência visual.
curl não conta como prova de UX. Quando o caminho passa por um agente de IA, o caso de aceite
mede o par (tela pelo agente + ferramenta chamada direto, com o mesmo texto cru) e só conta
quando os dois concordam: docs/doctrine/prova-em-par.md. Mapa de jornadas:
docs/testing/user-journey-map.md.
Cada linha abaixo traz o comando que a mede — rode o comando em vez de citar número. Este bloco já foi datado num SHA uma vez e não funcionou: os itens envelhecem em ritmos diferentes, e o cabeçalho passava a mentir por todos eles.
- Arquivos de teste em
tests/unit/(git ls-files 'tests/unit/*.test.ts' 'tests/unit/*.test.tsx' | wc -l). O total do repositório sai degit ls-files '*.test.ts' '*.test.tsx' | wc -l— a diferença vive junto ao código, fora detests/, e também roda emtest:unit. - Arquivos de invariante de banco em
tests/invariants/— RLS/isolamento cross-tenant, RBAC, governança (G1–G6). Excluídos dotest:unitde propósito; rodam viapnpm test:dbe no jobinvariantsdo CI. Quantos:git ls-files 'tests/invariants/*.test.ts' | wc -l. - Specs Playwright em
tests/e2e/, quase todas no CI (viae2e.yml, obrigatório), em todo PR que alcança o que elas medem — PR só de documentação/teste de outra suíte pula as partes (scripts/pr-alcanca-o-e2e.sh), e ali oe2everde não prova tela. As que ficam de fora estão declaradas emFORA_DO_CI, com o motivo escrito ao lado. Esta linha já afirmou "menos uma" depois de deixarem de ser uma — por isso não conta mais. A issue #63, que originou a discussão, está fechada e o título dela descreve um estado que já não vale. Quantas existem:ls tests/e2e/*.spec.ts | wc -l. Quantas ficam fora:git show origin/main:.github/workflows/e2e.yml | grep -A4 'FORA_DO_CI:'. Antes de declarar pronto, siga a Definition of Done deCLAUDE.md— não confie na memória, conte lá:
sed -n '/^## Definition of Done/,/^Um staff engineer/p' CLAUDE.md | grep -cE '^[0-9]+\. '
Em resumo: typecheck/lint zerados, testes relevantes verdes, RLS testada se tocou tabela
tenant-aware, audit() se houve mutação, Zod em todo input externo, migration + baseline +
MANIFEST de tripla se mudou schema, prova visual se mudou UI, pnpm test:shell se tocou packaging,
Living System Checklist respondido (lei em docs/doctrine/sistema-vivo.md) e mapa vivo em
docs/architecture/ atualizado para peça nova.
Limitações conhecidas
Cada item abaixo carrega o comando que o mede — item sem comando é suspeito de estar podre,
e a régua da casa é medir antes de repassar. Datar o bloco inteiro num SHA foi abandonado: os
itens envelhecem em ritmos diferentes, e o cabeçalho passava a mentir por todos eles.
(O SHA 789dfa6, que ficava aqui, ficou para trás — meça com
git rev-list --count 789dfa6..origin/main.)
-
As specs E2E fora do CI são exatamente as declaradas em
FORA_DO_CI, e oe2eé check obrigatório — um PR que quebre oe2enão entra. Quais estão de fora é pergunta de comando, não de leitura: esta linha já afirmou por semanas que a jornada de instalação fresca seguia sem gate, e em 2026-09-19 o #983 pôsvps-fresh-onboarding.spec.tsno CI.git show origin/main:.github/workflows/e2e.yml | python3 -c "import sys,re; y=sys.stdin.read(); print(sorted({s for _,c in re.findall(r'(FORA_DO_CI):\s*>-\n((?:[ ]{8,}.*\n)+)',y) for s in re.findall(r'[a-z0-9-]+\.spec\.ts',c)}))"Gate não substitui prova: se você mexeu numa jornada, a prova pela tela continua sendo sua (DoD 12). E o gate só vale em PR que alcança o
e2e(regra emscripts/pr-alcanca-o-e2e.sh): o que pula as partes sai come2everde sem ter provado tela nenhuma, a da instalação fresca inclusive. O número e a contagem que ficavam aqui eram de uma fotografia de agosto. -
Rate limit HTTP:
lib/auth/rate-limit.tscobre login, signup, recuperação de senha e aceite de convite (contando por IP e por identificador hasheado);checkRateLimitcobre o webhook de captação e o dispatcher de IA. Crons seguem sem. O MCP conta em dois pontos: a recusa de token, antes da autenticação (lib/mcp/auth.ts, #1449), e o teto de chamadas de token válido — por token, por organização e de escrita, Spec 11 §7 (lib/mcp/rate-limit.ts, #1446). Meça antes de agir:grep -rln 'authRateLimited\|checkRateLimit(' app lib --include='*.ts' --include='*.tsx'. Esta linha dizia "existe em 2 pontos; login e signup estão sem" — era o estado anterior à issue #64, e odocs/threat-model.mdainda carrega a versão velha, com nota de reauditoria. -
Fallback do rate limit é em memória — mas o gatilho não é "sem Upstash":
lib/env.tsdeclara as duas variáveis do Upstash comorequired()e o app não sobe sem elas (grep -n UPSTASH lib/env.ts). O que cai para a memória é Redis inalcançável com a variável presente, e aí o limite passa a ser por processo. -
Idempotency-Keyé lido por 4 rotas e o contrato promete nos POSTs de criação. Há duas implementações com recibo (lgpd/requests/[id]/approveeadmin/tenants) e, desde este commit, uma reutilizável emlib/api/idempotency.ts, aplicada emmessage-templates. Reconte antes de citar:grep -rln 'Idempotency-Key' app/api/v1 --include='route.ts'. No helper reutilizável, a corrida entre duas requisições simultâneas com a mesma chave está fechada (issue #778, migration 0321): a chave é reservada ANTES do efeito e quem perde recebe 409idempotency_in_progress. Isso vale para quem usacomIdempotencia— hoje sómessage-templates; as outras rotas mantêm o recibo delas. -
.env.exampleestá completo — medido em 2026-08-14: das 45 chaves delib/env.ts, a única ausente éNODE_ENV, que não é configuração do operador. Esta linha dizia que faltavam 6, "incluindo 3 secrets"; os três (IMPERSONATE_COOKIE_SECRET,INTERNAL_CRON_SECRET,LGPD_SIGNING_KEY) estão lá. Se você adicionar env var, adicione nos dois lugares (item 9 do DoD) — a regra continua valendo, o que caiu foi a dívida. -
lib/auth/invite-token.tscai em"dev-fallback"como secret HMAC se nenhum secret existir (inalcançável em produção, porqueINTERNAL_SECRETé obrigatório e derruba o boot). -
Boa parte dos handlers de
app/api/**usa service role — reconte:grep -rl createAdminClient app/api --include='route.ts' | wc -lcontragit ls-files 'app/api/**/route.ts' | wc -l. Não há gate automático para o filtro deorganization_id. Escrevendo handler novo, o filtro é responsabilidade sua. -
Detalhes e prioridade:
docs/harness-audit.md,docs/current-state.mdedocs/threat-model.md.
Regras de segurança
- Sempre
getUser()no backend. NuncagetSession()(confia no cookie sem revalidar). - API key/token nunca em query string — só header. Plaintext do bearer é mostrado uma vez; no banco só hash SHA256.
- HMAC de webhook com
crypto.timingSafeEqual. Fail-closed quando o secret falta. - Nunca logue segredo, token, CPF, telefone ou e-mail. Sentry tem
beforeSendque higieniza — não confie nele como única camada. - Não commite screenshot/dump com dado real de cliente.
- Descadastro (STOP): quem bloqueia é só a regra de
lib/opt-out/deteccao.ts, quando o próprio cliente manda o STOP (não há bloqueio à mão no produto —lib/channels/pos-entrada.tsé o único escritor); o Jev (lib/ai/decisao/pedidos.ts) só é perguntado onde ela disse não, e nunca bloqueia ninguém — no máximo abre um aviso na Central ("Avisar a equipe").
Packaging — se você tocou Dockerfile*, docker-compose*.yml ou hostgator-setup-kit/
Lei completa em docs/doctrine/packaging.md. O não-negociável:
- Nenhum serviço de
docker-compose.prod.ymlconstrói na máquina do cliente. Todo serviço declaraimage:de uma imagem publicada;build:só existe ao lado, como escape. Serviçobuild:-only é pulado pordocker compose pulle imune aup -dsem--build— ele não é só caro de instalar, ele nunca é atualizado. - Publicação é ato do CI, nunca da sua máquina: as imagens publicadas atendem linux/amd64 e linux/arm64.
- Instalação de cliente aponta para número de versão, nunca para tag móvel. Aqui
latestsignifica topo damain, não última release — quem quer a última release usastable. - Dependência upstream é referenciada com tag fixa, nunca republicada (WAHA é licenciado).
- Bump de versão não pode exigir que o operador da VPS edite arquivo à mão.
pnpm test:shell é o único gate que exercita o kit. Rode-o.
Extensões — se sua mudança muda comportamento
Lei completa em docs/doctrine/extensoes.md; contrato vigente em
docs/specs/extensoes-declarativas-v1.md. Declare o
destino (núcleo, extensão, ambos ou infraestrutura) respondendo: se nenhuma organização ativar
isto, a operação comum continua inteira? O não-negociável:
- O núcleo continua útil com zero extensões. Nenhuma jornada do núcleo depende de extensão ativa.
- Extensão pede capacidade nomeada e não importa código interno, não lê o banco e não recebe cliente Supabase, ambiente ou dados do CRM. Instalar não concede autoridade.
- A instância decide o pacote; a organização decide o uso. A plataforma não reativa decisão da organização.
- Toda operação é recibo idempotente com saída pela tela; toda troca de ponteiro exige a revisão que a tela viu. Remover é lógico e preserva dados e configuração.
- Não anuncie SDK, código isolado ou marketplace público, e não extraia do núcleo recurso já distribuído sem equivalência e migração.
- Módulo oficial com dados não põe tabela no baseline para todos
(ADR-0002): um banco só,
public, tabelas criadas por função provisionadora fixa quando o módulo é instalado na instância.
Critério de conclusão
Vale a Definition of Done em CLAUDE.md — conte lá em vez de confiar num número aqui (sed -n '/^## Definition of Done/,/^Um staff engineer/p' CLAUDE.md | grep -cE '^[0-9]+\. '; esta linha já disse 15 quando o DoD tinha 16). A régua tem que DELIMITAR a seção: a primeira versão desta linha oferecia grep -c '^[0-9]\+\. \*\*' CLAUDE.md, que devolve 25 — 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 o número pelo comando só ajuda se o comando responder à pergunta. Não declare pronto
sem: typecheck/lint zerados, testes relevantes verdes, RLS testada se tocou tabela
tenant-aware, migration + baseline + MANIFEST se mudou schema, prova visual se mudou UI, e a
regra de packaging acima se mudou o artefato que o self-hoster instala.
Guias do assistente (skills embutidas)
O repositório embute guias em .agents/skills/ — lidos por Codex, Cursor, OpenCode e
Antigravity; o Claude Code lê o espelho em .claude/skills/ (pnpm skills:sync regrava, e
tests/unit/skills-embutidas.test.ts reprova divergência). Carregue o guia quando o pedido
casar, mesmo que a pessoa não saiba que ele existe. Fora de um clone (ou num clone antigo),
bash scripts/instalar-guias.sh liga os guias nas pastas globais dos cinco CLIs:
| situação | guia |
|---|---|
| instalar, atualizar ou consertar a instalação numa VPS; domínio, Supabase, WhatsApp que não conecta | deskcomm-instalar |
| configurar o CRM para um cliente ou nicho: agentes, roteadores, follow-ups, base de conhecimento | deskcomm-cliente-novo |
| desempenho, conversão, custo de IA, funil, relatório | deskcomm-metricas |
| o agente responde errado, passa tudo para humano, não usa a agenda; melhorar o prompt | deskcomm-prompt |
contribuir: corrigir bug, abrir ou atualizar PR, migration, conflito com a main |
deskcomm-contribuir — que fica quieto quando bash .agents/skills/deskcomm-contribuir/scripts/quem-sou.sh responde mantenedor |
| escrever ou revisar código aqui | deskcomm-doutrina (as três regras que mais custam) e sistema-vivo (o gate de arquitetura) |
Os guias têm vitrine pública em deskcomm.com.br/guias (pt-BR,
en, es), escrita à mão no repositório deskcomm-site (conteudo/guias.ts). Guia criado,
renomeado ou com comando novo → o PR avisa que aquela página precisa acompanhar. Ela sai do mesmo
PR do deskcomm-site que a página de changelog; quem quiser saber se já está no ar usa o curl da
seção "A vitrine" em docs/doctrine/versionamento.md.
Regra final — não invente
Este repositório tem PRDs, specs, regras de negócio e doutrina escritos
(docs/prd/, docs/specs/, docs/business-rules/, docs/doctrine/).
Nunca invente regra de negócio, número, SLA ou comportamento de produto.
Se a regra não está escrita, diga que não está e pergunte — não preencha a lacuna com
suposição plausível. Ao documentar, marque o que é CONFIRMADO (provado por código) e o
que é INFERIDO.
Release — mudança de comportamento visível a quem opera uma VPS traz o fragmento em
.changes/ declarando o efeito no operador (nada_mudou / capacidade_nova / exige_acao),
nunca o número. O número é calculado a partir do conjunto; confira com pnpm release:conferir e
corte com pnpm release:cortar. Régua e porquê: docs/doctrine/versionamento.md.
Quem instalou lê o CHANGELOG.md antes de rodar update.sh — mudança que exige
ação manual aparece sob "⚠️ Requer atenção".
Toda versão publicada aparece também em deskcomm.com.br/changelog
(pt-BR, en, es): a LP lê o CHANGELOG.md da main, ninguém escreve release no site, e o último
passo do corte reprova se a versão não chegou lá. Mudar o cabeçalho ## [X.Y.Z] — AAAA-MM-DD quebra
essa página — ver "A vitrine" em docs/doctrine/versionamento.md. Enquanto as três páginas não
responderem 200, esse passo reprova TODO corte; o curl que mede isso abre aquela seção.
Regra final — não invente. Este repositório tem PRDs, specs, regras de negócio e doutrina escritos. Nunca invente regra de negócio, número, SLA ou comportamento de produto. Se a regra não está escrita, diga que não está e pergunte. Ao documentar, marque o que é CONFIRMADO (provado por código) e o que é INFERIDO.
This is NOT the Next.js you know
This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in node_modules/next/dist/docs/ (resolved from this file's directory; in monorepos the next package may not be visible from the repo root) before writing any code. Heed deprecation notices.
This block is written and re-added by next dev — verify at node_modules/next/dist/server/lib/generate-agent-files.js. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean.