chore(growth-fase-0): CI prova o isolamento RLS, e o README para de afirmar o que não era verdade
O README afirmava "Teste de isolamento RLS é gate obrigatório" e o CLAUDE.md
repetia "obrigatório no CI antes de merge". Não era verdade: ci.yml rodava só
typecheck + lint + test:unit, e vitest.config.ts exclui tests/invariants/**.
Os 364 testes de invariante — incluindo o de isolamento entre 2 tenants —
rodavam só localmente, sob demanda. Afirmação pública, checável em 30 segundos,
e falsa.
Duas saídas possíveis: enfraquecer a frase ou tornar a frase verdadeira. Como o
script já existia (scripts/test-db.sh) e o runner do GitHub já tem Docker, o
custo de torná-la verdadeira era menor que o custo reputacional de mantê-la.
- ci.yml: job `invariants` em paralelo ao `verify`, rodando `pnpm test:db`
(Postgres efêmero + baseline em modo install/update + tests/invariants/**).
Job separado de propósito: não atrasa o feedback rápido e vira check nomeado.
- README.{md,en,es} + CLAUDE.md: a frase agora descreve o que o teste realmente
faz, com número medido pelo executor (364 em 56 arquivos — não os 304 que um
grep de `it(` sugeria) e com o caso de controle que prova que as linhas da
org B existem antes de provar que a RLS as esconde.
- CLAUDE.md: registra que test:unit NÃO cobre os invariantes, para que rodar só
test:unit e concluir "está verde" deixe de ser um falso verde.
Junto, três correções de vitrine que a auditoria de crescimento levantou:
- docker-compose.prod.yml: default apontava para ghcr.io/deskcommcrm/deskcommcrm,
namespace que não existe no registry (a imagem pública é ghcr.io/melgarafael/
deskcommcrm). Quebrava quem seguisse docs/deploy-selfhost/ sem .env completo.
- raiz: de 64 para 42 itens versionados. 7 documentos de trabalho arquivados em
docs/handoffs/ e 11 screenshots em docs/evidence/inbox-multimodal/, via git mv.
Ficam na raiz HANDOFF.md, HANDOFF-operacao-visivel.md e HANDOFF-harness-
evolution.md: os dois últimos estão modificados em feat/operacao-visivel e
movê-los agora criaria conflito de rename/edit no merge.
- roadmap dos 3 READMEs: removido "Fase FG — agente Vendaval consome a governança
via ai_dispatch_mode=external", jargão interno redundante com "MCP público".
tests/unit/evidencia-citada.test.ts: arquivar handoff quebrou 5 casos porque o
guarda resolve referência de imagem contra a pasta do documento — as imagens
seguem em evidence/, então ele reprovava documento correto por um caminho que
ele mesmo inventou, o defeito que o próprio arquivo já condena. docs/handoffs/
passa a contar como raiz, e a entrada de LEGADO acompanhou o caminho novo.
Verificado no SHA 0ea9f4b: typecheck 0, lint 0 erros, test:unit 1035/1035,
test:db 364 passando em 56 arquivos ("test:db verde").
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KMEbgy5YWXZimXQAN7oRMm
@@ -29,3 +29,28 @@ jobs:
|
||||
|
||||
- name: Unit tests
|
||||
run: pnpm test:unit
|
||||
|
||||
# Gate de isolamento multi-tenant + invariantes de governança.
|
||||
# Job separado de propósito: roda em paralelo ao verify (não atrasa o feedback
|
||||
# rápido) e vira um check nomeado, que é o que a doutrina de RLS exige provar.
|
||||
# scripts/test-db.sh sobe pgvector/pgvector:pg17, aplica supabase/baseline.sql
|
||||
# em modo install (ON_ERROR_STOP=1) e update (idempotência), e roda
|
||||
# tests/invariants/** contra ele. Invocar via `pnpm test:db` é o que põe
|
||||
# node_modules/.bin no PATH que o script herda para chamar `vitest`.
|
||||
invariants:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- uses: pnpm/action-setup@v6
|
||||
|
||||
- uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: 20
|
||||
cache: pnpm
|
||||
|
||||
- run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: RLS isolation + governance invariants
|
||||
run: pnpm test:db
|
||||
|
||||
@@ -163,13 +163,21 @@ Ver `README.md` pra detalhes de setup.
|
||||
## Testes
|
||||
|
||||
```bash
|
||||
npm run typecheck # tsc --noEmit (estrito)
|
||||
npm run lint # eslint next/core-web-vitals
|
||||
npm run test:unit # Vitest
|
||||
npm run test:e2e # Playwright
|
||||
pnpm typecheck # tsc --noEmit (estrito)
|
||||
pnpm lint # eslint next/core-web-vitals
|
||||
pnpm test:unit # Vitest (NÃO inclui tests/invariants/** — ver abaixo)
|
||||
pnpm test:db # Postgres efêmero + baseline install/update + 364 invariantes
|
||||
pnpm test:e2e # Playwright (requer dev server)
|
||||
```
|
||||
|
||||
CI deve rodar todos antes de merge. Teste de isolamento RLS é gate obrigatório.
|
||||
**Os invariantes não estão no `test:unit`.** `vitest.config.ts` exclui `tests/invariants/**` de propósito: essa suíte precisa de um Postgres real e roda via `vitest.db.config.ts`, orquestrada por `scripts/test-db.sh`. Rodar só `pnpm test:unit` e concluir "está tudo verde" é um falso verde — o isolamento RLS não foi exercitado.
|
||||
|
||||
O CI (`.github/workflows/ci.yml`) tem dois jobs em paralelo, ambos obrigatórios antes de merge:
|
||||
|
||||
- **`verify`** — typecheck + lint + test:unit.
|
||||
- **`invariants`** — `pnpm test:db`: sobe `pgvector/pgvector:pg17`, aplica `supabase/baseline.sql` em modo install (`ON_ERROR_STOP=1`) e update (idempotência), e roda os 364 testes de invariante, incluindo o de isolamento RLS entre 2 organizações.
|
||||
|
||||
Ao mexer em schema, RLS, RBAC, atribuição, escopo, roteamento, follow-up, webhooks ou automações: rode `pnpm test:db` **localmente** antes de abrir PR. É o único caminho que exercita o `baseline.sql` que o self-hoster realmente aplica.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -115,11 +115,14 @@ Details: [`ARCHITECTURE.md`](ARCHITECTURE.md).
|
||||
```bash
|
||||
pnpm typecheck # tsc --noEmit (strict)
|
||||
pnpm lint # eslint next/core-web-vitals
|
||||
pnpm test:unit # Vitest
|
||||
pnpm test:unit # Vitest (does NOT include tests/invariants/**)
|
||||
pnpm test:db # ephemeral Postgres + baseline install/update + invariants
|
||||
pnpm test:e2e # Playwright (requires dev server)
|
||||
```
|
||||
|
||||
CI runs everything before merge. **The RLS isolation test is a mandatory gate** — it creates 2 tenants and verifies no leakage. The **governance invariants suite** (100+ tests) locks down RBAC, assignment, scoping and routing against regressions.
|
||||
CI runs `typecheck`, `lint` and `test:unit` on every PR. A second job — **`invariants`** — boots a clean Postgres, applies `supabase/baseline.sql` in install mode (`ON_ERROR_STOP=1`) and then in update mode (proving idempotency), and runs **364 invariant tests** across 56 files covering RBAC, assignment, visibility scoping, routing, follow-up, webhooks and automations.
|
||||
|
||||
Among them is the **RLS isolation test**: it creates 2 organizations, simulates JWT claims through the same `auth.uid()` / `fn_user_org_ids()` path production policies use, and proves a user of org A sees **zero rows** of org B in `conversations`, `messages`, `contacts` and `crm_leads`. A control case first proves org B's rows actually exist in the database — without it, the test would pass against an empty table.
|
||||
|
||||
---
|
||||
|
||||
@@ -176,8 +179,6 @@ For **security vulnerabilities**, **do NOT open a public issue** — use [privat
|
||||
|
||||
### 🔮 Next
|
||||
|
||||
- **Phase FG** — the Vendaval agent consumes governance via `ai_dispatch_mode=external` 🔜 *(awaiting owner prioritization)*
|
||||
|
||||
- **Public MCP** — CRM capabilities exposed to the agent ecosystem: plug in any agent and it operates Deskcomm.
|
||||
- **Self-improvement flywheel** — the resolved-conversation → knowledge → better-agent loop, measured and human-gated.
|
||||
- **Niche templates** — ready-made pipelines and vocabularies for clinics, real estate, info-products and services (e-commerce already shipped).
|
||||
|
||||
@@ -115,11 +115,14 @@ Detalles: [`ARCHITECTURE.md`](ARCHITECTURE.md).
|
||||
```bash
|
||||
pnpm typecheck # tsc --noEmit (estricto)
|
||||
pnpm lint # eslint next/core-web-vitals
|
||||
pnpm test:unit # Vitest
|
||||
pnpm test:unit # Vitest (NO incluye tests/invariants/**)
|
||||
pnpm test:db # Postgres efímero + baseline install/update + invariantes
|
||||
pnpm test:e2e # Playwright (requiere dev server)
|
||||
```
|
||||
|
||||
CI ejecuta todo antes del merge. **El test de aislamiento RLS es un gate obligatorio** — crea 2 tenants y verifica que no haya fugas. La suite de **invariantes de gobernanza** (100+ tests) bloquea regresiones de RBAC, asignación, alcance y enrutamiento.
|
||||
El CI ejecuta `typecheck`, `lint` y `test:unit` en cada PR. Un segundo job — **`invariants`** — levanta un Postgres limpio, aplica `supabase/baseline.sql` en modo install (`ON_ERROR_STOP=1`) y luego en modo update (probando idempotencia), y ejecuta **364 tests de invariante** repartidos en 56 archivos, cubriendo RBAC, asignación, alcance de visualización, enrutamiento, follow-up, webhooks y automatizaciones.
|
||||
|
||||
Entre ellos está el **test de aislamiento RLS**: crea 2 organizaciones, simula los claims JWT por el mismo camino `auth.uid()` / `fn_user_org_ids()` que usan las policies de producción, y prueba que un usuario de la org A ve **cero filas** de la org B en `conversations`, `messages`, `contacts` y `crm_leads`. Antes, un caso de control prueba que las filas de la org B realmente existen en la base — sin él, el test pasaría contra una tabla vacía.
|
||||
|
||||
---
|
||||
|
||||
@@ -176,8 +179,6 @@ Para **vulnerabilidades de seguridad**, **NO abras un issue público** — usa e
|
||||
|
||||
### 🔮 Próximo
|
||||
|
||||
- **Fase FG** — el agente Vendaval consume la gobernanza vía `ai_dispatch_mode=external` 🔜 *(esperando priorización del dueño)*
|
||||
|
||||
- **MCP público** — capacidades del CRM expuestas al ecosistema de agentes: conecta el agente que quieras y opera Deskcomm.
|
||||
- **Flywheel de auto-mejora** — el ciclo conversación resuelta → conocimiento → agente mejor, medido y con compuerta humana.
|
||||
- **Plantillas por nicho** — pipelines y vocabularios listos para clínicas, inmobiliarias, infoproductos y servicios (e-commerce ya entregado).
|
||||
|
||||
@@ -139,11 +139,14 @@ DeskcommCRM/
|
||||
```bash
|
||||
pnpm typecheck # tsc --noEmit (estrito)
|
||||
pnpm lint # eslint next/core-web-vitals
|
||||
pnpm test:unit # Vitest
|
||||
pnpm test:unit # Vitest (NÃO inclui tests/invariants/**)
|
||||
pnpm test:db # Postgres efêmero + baseline install/update + invariantes
|
||||
pnpm test:e2e # Playwright (requer dev server)
|
||||
```
|
||||
|
||||
CI roda todos antes de merge. **Teste de isolamento RLS é gate obrigatório** — cria 2 tenants e verifica não-vazamento. A suíte de **invariantes de governança** (100+ testes) trava regressões de RBAC, atribuição, escopo e roteamento.
|
||||
O CI roda `typecheck`, `lint` e `test:unit` em todo PR. Um segundo job — **`invariants`** — sobe um Postgres limpo, aplica o `supabase/baseline.sql` em modo install (`ON_ERROR_STOP=1`) e depois em modo update (provando idempotência), e roda **364 testes de invariante** distribuídos em 56 arquivos, cobrindo RBAC, atribuição, escopo de visualização, roteamento, follow-up, webhooks e automações.
|
||||
|
||||
Entre eles 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 `conversations`, `messages`, `contacts` e `crm_leads`. Antes disso, um caso de controle prova que as linhas da org B realmente existem no banco — sem ele, o teste passaria mesmo com a tabela vazia.
|
||||
|
||||
---
|
||||
|
||||
@@ -213,8 +216,6 @@ Pra **vulnerabilidades de segurança**, **NÃO abra issue pública** — use o [
|
||||
|
||||
### 🔮 Próximo
|
||||
|
||||
- **Fase FG** — agente Vendaval consome a governança via `ai_dispatch_mode=external` 🔜 *(aguardando priorização do dono)*
|
||||
|
||||
- **MCP público** — capabilities do CRM expostas pro ecossistema de agentes: plugue o agente que quiser e ele opera o Deskcomm.
|
||||
- **Flywheel de auto-aprimoramento** — o loop conversa resolvida → conhecimento → agente melhor, medido e com gate humano.
|
||||
- **Templates por nicho** — pipelines e vocabulários prontos pra clínica, imobiliária, infoproduto e serviços (e-commerce já entregue).
|
||||
|
||||
@@ -16,7 +16,7 @@ services:
|
||||
# Os NEXT_PUBLIC_* reais vêm em RUNTIME (env_file abaixo) — o browser lê via
|
||||
# <PublicEnvScript/> e o servidor via lib/env.ts. Não precisa buildar no VPS.
|
||||
# Build local (avançado): docker compose -f docker-compose.prod.yml -f docker-compose.build.yml build
|
||||
image: ${APP_IMAGE:-ghcr.io/deskcommcrm/deskcommcrm:latest}
|
||||
image: ${APP_IMAGE:-ghcr.io/melgarafael/deskcommcrm:latest}
|
||||
pull_policy: ${APP_PULL_POLICY:-always}
|
||||
restart: unless-stopped
|
||||
env_file: .env
|
||||
|
||||
|
Before Width: | Height: | Size: 164 KiB After Width: | Height: | Size: 164 KiB |
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 171 KiB After Width: | Height: | Size: 171 KiB |
|
Before Width: | Height: | Size: 173 KiB After Width: | Height: | Size: 173 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 175 KiB After Width: | Height: | Size: 175 KiB |
|
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 175 KiB After Width: | Height: | Size: 175 KiB |
|
Before Width: | Height: | Size: 178 KiB After Width: | Height: | Size: 178 KiB |
|
Before Width: | Height: | Size: 210 KiB After Width: | Height: | Size: 210 KiB |
|
Before Width: | Height: | Size: 173 KiB After Width: | Height: | Size: 173 KiB |
@@ -148,7 +148,7 @@ const DOCS = versionados("*.md").filter(
|
||||
* sozinha.
|
||||
*/
|
||||
const LEGADO = new Set([
|
||||
"HANDOFF-inbox-multimodal.md",
|
||||
"docs/handoffs/HANDOFF-inbox-multimodal.md",
|
||||
"HANDOFF-operacao-visivel.md",
|
||||
"HANDOFF.md",
|
||||
"docs/superpowers/plans/2026-07-21-onda0-fundacao-midia.md",
|
||||
@@ -177,7 +177,13 @@ function refsNormalizadas(doc: string): string[] {
|
||||
...new Set(
|
||||
refs.map((ref) => {
|
||||
const limpa = ref.replace(/^\.\//, "");
|
||||
const base = dir === "." ? "evidence" : dir;
|
||||
// `docs/handoffs/` conta como raiz: são handoffs ARQUIVADOS que nasceram
|
||||
// na raiz do repo e citam evidência sem prefixo, como todo doc de raiz.
|
||||
// Arquivar o documento não move as imagens — elas seguem em `evidence/`.
|
||||
// Sem este caso o guarda procuraria as imagens dentro de `docs/handoffs/`
|
||||
// e reprovaria documento correto por um caminho que ele mesmo inventou —
|
||||
// exatamente o defeito que o comentário acima já mandou não repetir.
|
||||
const base = dir === "." || dir === "docs/handoffs" ? "evidence" : dir;
|
||||
// Sem diretório → resolve contra a pasta do documento.
|
||||
if (!limpa.includes("/")) return path.posix.join(base, limpa);
|
||||
// Subpasta REAL de evidence/ → também resolve. Aceitar a referência sem
|
||||
|
||||