Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
189 changes: 189 additions & 0 deletions docs/atros-v3/handoff-2026-08-12-baseline-gate-e-rls.md
Original file line number Diff line number Diff line change
@@ -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 = '<t>';
```

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 <TEMPORÁRIO> # 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.
Loading