Files
melgarafael d74d4457e3 Merge remote-tracking branch 'origin/main' into feat/jev-onda-3
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.
2026-09-30 11:34:09 -03:00

43 KiB
Raw Permalink Blame History

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á em docs/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.json por tests/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 em proxy.ts (Next 16 renomeou middleware.ts → proxy.ts; ele injeta X-Request-Id e x-pathname e 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 em supabase/migrations/; o que o self-host aplica é supabase/baseline.sql.
  • Auth — Supabase Auth + @supabase/ssr, cookie SameSite=Strict. Sempre getUser() no server; nunca getSession(). MFA TOTP é opcional e ligado por quem administra (duas políticas que somam: plataforma e organização), regra pura em lib/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_changes para inbox/kanban, broadcast para sinais leves). Storage — bucket privado whatsapp-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:

  1. Zod valida todo input externo (body, query, path).
  2. Guard canônico: requireRole() de lib/auth/require-role.ts, requirePlatformAdmin, ou secret/HMAC. Nunca reimplemente a comparação de rank na mão. Handler mutante de app/api/v1 declara ainda requireSupportWrite( (lib/impersonate/support.ts) antes do efeito: barra escrita em support_readonly e não substitui RBAC/MFA.
  3. organization_id resolvido de fonte confiável (cookie/JWT/webhook secret/path token) — nunca do body.
  4. Query: RLS pelo client de sessão, ou filtro manual de organization_id quando usa service role.
  5. audit() (fire-and-forget) se houve mutação.
  6. Responda com ok() / fail() de lib/api/wrappers.ts — nunca monte Response na 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 o install.sh/update.sh do 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 em CLAUDE.md.
  • supabase/migrations/*.sql já aplicadas — nunca edite. Corrija com migration nova.
  • lib/supabase/admin.ts — service role bypassa RLS. Boa parte dos handlers de app/api/** o usa — reconte em vez de citar: 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.
  • 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êiner app as labels de roteamento. Todo up -d leva 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 (gate pnpm lint:channels); tela nova sem porta declarada em lib/navigation/registry.ts (gate tests/unit/navegacao-completude.test.ts); getSession() no server; segredo em query string; throw cru 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 fixam node-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 lint foi 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 em tailwind.config.js.
  • Sentry — coleta restrita (dataCollection) + scrub num ponto só, lib/sentry/privacidade.ts, provado pelo envelope do SDK em privacidade.sdk.test.ts; tunnelRoute: "/monitoring" evita ad-blocker.
  • Packaging (não-negociável; lei em docs/doctrine/packaging.md) — nenhum serviço de docker-compose.prod.yml constrói na máquina do cliente: todo serviço declara image: de imagem publicada, e build: existe só ao lado, como escape. Serviço build:-only é pulado por docker compose pull e 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; latest significa topo da main, 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 -d leva 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 -f recria o contêiner sem labels: o domínio inteiro responde 404 com o contêiner healthy (o healthcheck é um probe TCP interno). Runbook: docs/runbooks/deploy.md.
  • Env vars — nova variável entra em .env.example e em lib/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 de wc -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:unit tests/unit/** + todo *.test.ts(x) ao lado do código. Timeout 15s por teste
    Invariantes de banco (Docker) pnpm test:db Isolamento cross-tenant/RLS, RBAC e governança contra Postgres efêmero
    E2E (Playwright + axe-core) pnpm test:e2e Jornadas reais contra app buildado e o banco do baseline.sql
    Kit self-host (bash) pnpm test:shell scripts do kit, install.sh, update.sh — o único gate do kit
    Jornadas de canal pnpm test:journeys tests/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 de 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.
  • Arquivos de invariante de banco em tests/invariants/ — 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. Quantos: git ls-files 'tests/invariants/*.test.ts' | wc -l.
  • Specs Playwright em tests/e2e/, quase todas no CI (via e2e.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 o e2e verde não prova tela. As que ficam de fora estão declaradas em FORA_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 de CLAUDE.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 o e2e é check obrigatório — um PR que quebre o e2e nã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ôs vps-fresh-onboarding.spec.ts no 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 em scripts/pr-alcanca-o-e2e.sh): o que pula as partes sai com e2e verde 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.ts cobre login, signup, recuperação de senha e aceite de convite (contando por IP e por identificador hasheado); checkRateLimit cobre 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 o docs/threat-model.md ainda 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.ts declara as duas variáveis do Upstash como required() 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]/approve e admin/tenants) e, desde este commit, uma reutilizável em lib/api/idempotency.ts, aplicada em message-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 409 idempotency_in_progress. Isso vale para quem usa comIdempotencia — hoje só message-templates; as outras rotas mantêm o recibo delas.

  • .env.example está completo — medido em 2026-08-14: das 45 chaves de lib/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.ts cai em "dev-fallback" como secret HMAC se nenhum secret existir (inalcançável em produção, porque INTERNAL_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 -l contra git ls-files 'app/api/**/route.ts' | wc -l. Não há gate automático para o filtro de organization_id. Escrevendo handler novo, o filtro é responsabilidade sua.

  • Detalhes e prioridade: docs/harness-audit.md, docs/current-state.md e docs/threat-model.md.

Regras de segurança

  • Sempre getUser() no backend. Nunca getSession() (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 beforeSend que 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.yml constrói na máquina do cliente. Todo serviço declara image: de uma imagem publicada; build: só existe ao lado, como escape. Serviço build:-only é pulado por docker compose pull e imune a up -d sem --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 latest significa topo da main, não última release — quem quer a última release usa stable.
  • 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.