From 15d2f84316a127c3ad46cec781b0d51f817f9eed Mon Sep 17 00:00:00 2001 From: lucas Date: Wed, 12 Aug 2026 01:59:52 -0300 Subject: [PATCH] =?UTF-8?q?docs:=20handoff=20de=202026-08-12=20=E2=80=94?= =?UTF-8?q?=20o=20banco=20ganhou=20marco=20zero,=20rede=20e=20RLS?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Entrada da próxima sessão sobre banco/infra. Registra o estado (main 95c05cc, 112/112 com RLS, 0 SECURITY DEFINER sem search_path, 3 migrations na pasta), o que os PRs #52/#53/#54 entregaram, e o fluxo novo de re-dump. Guarda as duas armadilhas que custaram caro hoje, porque as duas voltam: policy existe mesmo com RLS desligada e acorda ao ligar (foi assim que o DELETE de employees ampliou sem ninguém decidir), e o seed.sql usa INSERT e não COPY — contar por bloco COPY volta zero pra tudo, e zero parece resposta legítima. Fecha com os 5 itens abertos, cada um com a decisão que ele pede em vez de uma lista solta, e o que NÃO fazer — inclusive que a regra do Supabase Preview inverteu: vermelho agora é sinal real. Co-Authored-By: Claude Opus 5 --- .../handoff-2026-08-12-baseline-gate-e-rls.md | 189 ++++++++++++++++++ 1 file changed, 189 insertions(+) create mode 100644 docs/atros-v3/handoff-2026-08-12-baseline-gate-e-rls.md diff --git a/docs/atros-v3/handoff-2026-08-12-baseline-gate-e-rls.md b/docs/atros-v3/handoff-2026-08-12-baseline-gate-e-rls.md new file mode 100644 index 0000000..b6bcdc4 --- /dev/null +++ b/docs/atros-v3/handoff-2026-08-12-baseline-gate-e-rls.md @@ -0,0 +1,189 @@ +# Handoff — 2026-08-12: o banco ganhou marco zero, rede e RLS + +**Entrada da próxima sessão sobre banco/infra.** `main` = **`95c05cc`**, CI verde. +Working tree limpo. + +--- + +## 0. Onde o banco está agora + +``` +verdade do banco = supabase/migrations/00000000000000_baseline.sql + + as migrations posteriores a ela +``` + +A pasta `supabase/migrations/` tem **3 arquivos**: a baseline e as duas migrations +de RLS de hoje. A baseline **é** o dump da produção e **nunca se edita** — mudança +de schema é migration nova. + +| | | +|---|--:| +| Tabelas | 112 | +| Tabelas com RLS | **112 de 112** | +| Funções `SECURITY DEFINER` sem `search_path` | **0** | +| Policies | 360 | +| Snapshot | 🟢 autoritativo, dump de 2026-08-12 até `20260812010000` | + +As 161 migrations anteriores não se perderam: estão no histórico do git (commit +`1526705`) e em `db/migrations-arquivo/` (gitignored). + +--- + +## 1. O que foi feito + +### PR #52 — a baseline (`1526705` + `7a181a4`) + +`supabase/migrations/` **nunca** pôde ser reproduzida do zero: 45 das 112 tabelas +— `projects`, `clients` e `transactions` entre elas — nasceram fora do histórico, +pelo painel do Supabase, e o primeiro arquivo já abria com `ALTER TABLE projects`. +Um replay morria no statement 1 do arquivo 1. + +Era essa a causa do **Supabase Preview vermelho em todo commit**. Não era flaky, +era estrutural. O preço apareceu em 2026-08-10, quando a `20260810000000` falhou +no SQL Editor e passou batida. + +Provado em Postgres **17.6** limpo (versão exata da produção): 112 tabelas / 12 +views / 73 funções / 93 triggers / 348 policies / 107 com RLS — idêntico. + +> ⚠️ O diff textual produção × replay tem **64 linhas benignas** que vão reaparecer +> em toda comparação futura: `pg_net`/`pg_graphql` que o stack local instala +> sozinho, e 9 expressões que o Postgres re-normaliza ao reparsear +> (`= ANY ((ARRAY[a,b])::text[])` → `= ANY (ARRAY[a::text, b::text])`). +> **Não é drift** — drift é diferença de *objeto*, e essa contagem está zerada. + +O mesmo PR levou a faxina da raiz: prints, mockups, marketing, dataset de cliente +e uma cópia do repo do site saíram para `_local/` (gitignored, com `LEIA-ME.md`). +O que já era versionado e é citado por docs canônicos saiu por `git mv` para +`docs/referencias/`. + +### PR #53 — o gate (`501cf62`) + +`db/ESTRUTURA.md` é **gerado** de `db/snapshot/schema.sql` cruzado com `src/`. +Nada obrigava ele a acompanhar o dump. O CI agora roda +`gerar-estrutura.ts --check` e falha com exit 1 se divergir. Testado nos dois +sentidos antes de commitar. + +`tsx` virou devDependency pinada: não era declarado, e o `npx tsx` documentado +baixava da rede a cada execução. Gate pendurado em fetch não pinado falha à toa, +e gate que falha à toa ensina a ignorar vermelho. + +### PR #54 — RLS e `search_path` (`95c05cc`) + +5 das 112 tabelas tinham **RLS desligada com `GRANT ALL TO anon`** — e a chave +anônima vai no bundle do frontend. `employees` guarda `salario_bruto`, +benefícios e `cpf_mascara`. + +- `employees`, `company_ai_dossiers`, `consultant_memories` → padrão de + `ingestion_documents`: admin · staff ATR · cliente com acesso ao projeto +- `categorization_patterns` → catálogo global: logado lê, só a equipe escreve +- `meetings` → RLS **sem policy** (zero linhas, zero referências em `src/`) +- **39 funções** ganharam `search_path = 'public', 'pg_temp'`: as 29 sem nenhum, + mais **10 que tinham só `'public'`** e pareciam protegidas — sem `pg_temp` + explícito o Postgres procura o schema temporário primeiro + +**Primeira migration validada pela rede nova**: o Preview replayou baseline + +migrations do zero e passou em 11s. + +--- + +## 2. As duas armadilhas que custaram caro + +### Policy existe mesmo com RLS desligada + +`employees` **não era** tabela sem policy — tinha SEIS, dormentes. Policy existe +independentemente de `relrowsecurity`; ela só não é aplicada, e **acorda no +instante em que a RLS liga**. O F0 perguntou "a RLS está ligada?" e não perguntou +"que policies já existem?". + +Resultado: o `DROP POLICY IF EXISTS` substituiu 4 às cegas. Três sem efeito +prático, mas `employees_delete` era `is_admin()` puro — role IN ('owner','admin') +— e passou a deixar **consultor apagar linha de folha de pagamento**. Corrigido +pela `20260812010000`. + +**Quem achou foi o re-dump**: esperávamos +16 policies e vieram +12, e essa +diferença foi o fio da meada. Antes de mexer em RLS: + +```sql +SELECT tablename, policyname, cmd, qual FROM pg_policies WHERE tablename = ''; +``` + +Atenção especial ao DELETE — em 30 policies da casa ele é `is_admin()` puro. + +### `seed.sql` usa `INSERT`, não `COPY` + +Contei linhas procurando blocos `COPY ... \.` e recebi **0 para todas as tabelas**. +Usei esse zero para afirmar que as 5 tabelas estavam vazias — o argumento central +de "é barato agora". Três tinham dado (`categorization_patterns` ~33, +`company_ai_dossiers` ~5, `consultant_memories` ~4), e `project_members`, que +declarei vazia, tem 4 linhas. + +**Zero em TODAS as tabelas não é resultado, é sintoma de parser errado.** + +A conclusão sobreviveu, mas por evidência diferente da alegada: ninguém perdeu +acesso porque `profiles` tem 1 `owner` + 2 `admin` (cobertos por `is_admin`) e 3 +`cliente`, e `project_members` tem exatamente 3 linhas `role='cliente'` para eles. + +> Descoberta de contexto que vale reter: **`client_project_access` é uma VIEW +> sobre `project_members` filtrando `role='cliente'`**. Toda policy da casa que +> usa a view depende dessas 4 linhas. + +--- + +## 3. O fluxo, daqui pra frente + +Depois de aplicar migration no SQL Editor: + +```sh +npx supabase db dump --linked -f # nunca direto no destino: + # -f trunca pra 0 antes de falhar +# validar contagens de objeto contra o dump anterior, então mover +npx tsx scripts/db/gerar-estrutura.ts +git add db/snapshot/schema.sql db/ESTRUTURA.md +``` + +⚠️ Gere o `ESTRUTURA.md` **depois** de editar o `SNAPSHOT.md` — o cabeçalho do +`.md` lê a data de lá, e inverter a ordem faz o gate acusar (aconteceu hoje). + +--- + +## 4. O que está aberto + +Saída do cruzamento banco × código, nenhum tocado. **Cada um pede uma decisão +diferente** — a regra é o F0: mapear quem lê, no vivo, antes de decidir. + +| # | Item | Decisão | +|---|---|---| +| 1 | `pulse_checks` | `PulseCheckModal` não é importado por ninguém → **deletar o componente** | +| 2 | `system_notifications` | Bloco em `try/catch` mudo, stub assumido em comentário → **repontar** para `notifications`/`project_notifications` ou remover | +| 3 | `budgets` + `budget_items` | **Decisão de produto.** Feature órfã: a migration `20260114000000` está no arquivo e nunca aplicou, mas existe rota viva `/financeiro/orcamento`, 5 componentes e o módulo #9 no `module-service`. Reviver ou remover → candidato a `/norte` | +| 4 | 12 de 12 views sem `security_invoker` | **O de maior risco.** View `definer` pode estar sustentando acesso que a RLS da tabela embaixo negaria → análise view a view, nunca migration em bloco | +| 5 | 24 tabelas sem referência em `src/` | Ponto de partida de investigação, **não** lista de deleção — pode ser lida por RPC, script ou pelo painel | + +Ordem sugerida: 1 e 2 (rápidos, zero decisão) → 3 (`/norte`) → 4 (o caro). + +### Pontas soltas menores + +- `_local/atr-website-duplicata/` pode ser apagada: é duplicata byte a byte do + repo em `c:\Projetos\ATR-OS\atr-website`, mesmo commit `835d1c7`. +- `employees_cliente_insert` e `employees_cliente_update` seguem no banco, hoje + idênticas em efeito a `employees_insert`/`employees_update`. Policies + permissivas se somam por OR, então não muda comportamento — é cruft. +- O bloco **"Trilha atual" do `CLAUDE.md` parece defasado**: diz "Fases 0–4 + consolidadas / Próximo: Fase 5 (motor por evento)", mas o motor por evento já + entrou (`4fcf8fb`, e `docs/atros-v3/p2-motor-por-evento-desenho.md`). Conferir + no vivo antes de reescrever. + +--- + +## 5. O que NÃO fazer + +- **Não editar a baseline.** Mudança de schema é migration nova. +- **Não ignorar Supabase Preview vermelho.** A regra INVERTEU. Por meses o + veredito foi "crônico, não bloqueia merge"; isso morreu em 2026-08-11. Vermelho + agora quer dizer que a migration não replaya. +- **Não confiar em migration como prova.** Migration é intenção; dump é + realidade. Banco só fecha no re-dump, não no commit — foi o re-dump que achou + o `employees_delete` ampliado. +- **Não dumpar direto no destino.** `-f` trunca o arquivo pra zero linhas antes + de falhar. +- **Não misturar frentes.** Os 5 itens abertos são 5 trabalhos separados.