From 8c1ba031f2aa26e735f4bc992b5e603c6320a617 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 1 Aug 2026 06:32:15 +0000 Subject: [PATCH] =?UTF-8?q?C8=20=E2=80=94=20fecha=20F1=E2=80=93F2=20no=20p?= =?UTF-8?q?lano=20(Issue=20#37)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Freshness audit confirmou que perf de import/lista de apoiadores já estava entregue em main desde 2026-07-19; atualiza status do plano e CHANGELOG-AGENTS. Co-authored-by: Francisco Solla --- docs/CHANGELOG-AGENTS.md | 2 ++ docs/plans/escala-dry-pos-c6.md | 46 +++++++++++++++++++-------------- 2 files changed, 28 insertions(+), 20 deletions(-) diff --git a/docs/CHANGELOG-AGENTS.md b/docs/CHANGELOG-AGENTS.md index de7a8619..fb0efd42 100644 --- a/docs/CHANGELOG-AGENTS.md +++ b/docs/CHANGELOG-AGENTS.md @@ -4,6 +4,8 @@ Conteúdo movido do `AGENTS.md` em 2026-07-30 (fatiamento do paradigma de agente --- +**Recently resolved (2026-08-01):** **C8 F1–F2** (Issue #37) — freshness audit confirmou entrega em código desde 2026-07-19: locks advisory em batch (`acquireTextAdvisoryLocks`), bulk import com `.returning()` + leituras drizzle, overview derivando `total` de `totalDocs`, migration `pg_trgm`, guard `contactSearchQuery` (mín. 2 chars). Plano [`escala-dry-pos-c6.md`](docs/plans/escala-dry-pos-c6.md) fechado; follow-ups de filtros/forms permanecem no C9. + **Recently resolved (2026-08-01):** **Cutover residue cleanup** — removed dead `pnpm db:refresh:stage` / `scripts/refresh-stage.mjs` and `pnpm agent:promote` / `scripts/agent-promote.mjs`; closed `docs/CUTOVER-MAIN-ONLY.md`; scrubbed PR template `STAGE_DATABASE_URL` checklist item. Branch + Environment `stage` already deleted. **Recently resolved (2026-08-01):** **PRs de agente = Ready + auto-merge (audit incluso)** — removida a exceção do `engineering-audit` (artefatos também armam `gh pr merge --auto --merge`). Safety net GHA `agent-pr-ready-automerge.yml`: PRs `cursor/*` → `main` Draft→Ready + auto-merge. Rule always-on `agent-pr-workflow.mdc`. diff --git a/docs/plans/escala-dry-pos-c6.md b/docs/plans/escala-dry-pos-c6.md index 6ce6430e..e600de66 100644 --- a/docs/plans/escala-dry-pos-c6.md +++ b/docs/plans/escala-dry-pos-c6.md @@ -1,11 +1,13 @@ # Escala e DRY pós-C6 (apoiadores / import / listas) -Status: F4 (DRY de forms) **entregue** — twin praça 2026-07-21 + hardening 2026-07-23; **o resto do F4 (10 escadas hand-rolled escritas depois) fechou em 2026-07-25 no Pass 2 W4d** (`runCampaignFormAction`, exceções documentadas em comentário); **F3 (parsers das listas de entidade) fechou no Pass 2 W1-D2**; **F4b entregue 2026-07-27** (guard ampliado para `*FormActions.ts` + `taskActions.ts`; 6 irmãos migrados; `taskActions.ts` allowlisted); F1–F2 abertos com gatilho (volume real de import/base nominal pós-Onda 0) -Atualizado em: 2026-07-27 (F4b entregue — guard + migração dos irmãos) +Status: **F1–F2 entregues** (2026-07-19, código em `main`; plano fechado 2026-08-01 Issue #37); F4 (DRY de forms) **entregue** — twin praça 2026-07-21 + hardening 2026-07-23; **o resto do F4 (10 escadas hand-rolled escritas depois) fechou em 2026-07-25 no Pass 2 W4d** (`runCampaignFormAction`, exceções documentadas em comentário); **F3 (parsers das listas de entidade) fechou no Pass 2 W1-D2**; **F4b entregue 2026-07-27** (guard ampliado para `*FormActions.ts` + `taskActions.ts`; 6 irmãos migrados; `taskActions.ts` allowlisted); F3 (helpers DRY restantes — `CampaignListEmptyState`, `firstValue` local) com gatilho +Atualizado em: 2026-08-01 (F1–F2 fechados — Issue #37) Item do roadmap: [docs/roadmap.md](../roadmap.md) (Fill-ins abertos, item C8) Responsável: — -> **Revisão 2026-07-24:** a remodelagem M1 renomeou `plaza*`/`nucleus*` → `municipality*` (paths `pracas/` → `municipios/`), e o hardening 2026-07-23 (Fase 5) completou a **Fase 4**: os 4 ladders `*/nova` migraram para `runCampaignRedirectFormAction`, os demais `formActions` já usam `mapCampaignFormActionError` + `campaignFormFields` (única exceção: `planos/[slug]/lifecycleFormActions.ts`, que só retorna safe-messages fixas — sem ladder para migrar), e `CampaignInviteForm` já importa `fieldError`/`errorProps` de `campaignFormFields`. O split de access também entregou parte da F3: `getAdvisorMunicipalityIds` (`src/utilities/access/municipalities.ts`) é a variante sem `PayloadRequest`, com cache request-scoped por contexto, e `supporterListOverviewAggregate` recebe os IDs pré-carregados. Restam F1 (perf do import) e o restante de F2/F3 — verificar contra o código no PR. +> **Revisão 2026-07-24:** a remodelagem M1 renomeou `plaza*`/`nucleus*` → `municipality*` (paths `pracas/` → `municipios/`), e o hardening 2026-07-23 (Fase 5) completou a **Fase 4**: os 4 ladders `*/nova` migraram para `runCampaignRedirectFormAction`, os demais `formActions` já usam `mapCampaignFormActionError` + `campaignFormFields` (única exceção: `planos/[slug]/lifecycleFormActions.ts`, que só retorna safe-messages fixas — sem ladder para migrar), e `CampaignInviteForm` já importa `fieldError`/`errorProps` de `campaignFormFields`. O split de access também entregou parte da F3: `getAdvisorMunicipalityIds` (`src/utilities/access/municipalities.ts`) é a variante sem `PayloadRequest`, com cache request-scoped por contexto, e `supporterListOverviewAggregate` recebe os IDs pré-carregados. +> +> **Revisão 2026-08-01 (Issue #37 — freshness audit):** F1–F2 já estavam implementados desde 2026-07-19 (`1e23b27`) e sobreviveram ao Pass 3 (módulos em `src/utilities/supporter/`). As-built confirmado: locks em batch via `acquireTextAdvisoryLocks`, bulk com `.returning()` + leituras drizzle, overview derivando `total` de `totalDocs`, migration `pg_trgm`, `contactSearchQuery` nos filtros, `advisorMunicipalityIds` pré-carregados em `loadSupportersPageData`. Int specs: `campaignSupporter.int.spec.ts`, `postgresTransactionLocks.int.spec.ts`, `supporterListFilters.int.spec.ts`. Follow-ups de filtros/forms → C9 ([escala-dry-pos-c8.md](escala-dry-pos-c8.md)). ## Contexto @@ -47,9 +49,9 @@ Sem este item, a importação funciona em volume baixo, mas: (a) o gargalo de im ```mermaid flowchart TD - F1["Fase 1 — Perf import
locks em 1 RT · .returning · drizzle reads"] - F2["Fase 2 — Perf lista
1 COUNT · trgm · memo accessible nuclei"] - F3["Fase 3 — DRY helpers
drizzleBulk · getAdvisorMunicipalityIds ✓ · DrizzleTx assertion"] + F1["Fase 1 — Perf import ✓
locks em 1 RT · .returning · drizzle reads"] + F2["Fase 2 — Perf lista ✓
totalDocs · trgm · advisorMunicipalityIds"] + F3["Fase 3 — DRY helpers
drizzleBulk ✓ · getAdvisorMunicipalityIds ✓ · DrizzleTx assertion ✓"] F4["Fase 4 — DRY forms ✓
(entregue 2026-07-21 + 2026-07-23)"] C6["C6 apoiadores ✓"] --> F1 @@ -58,18 +60,22 @@ flowchart TD F3 --> F4 ``` -### Fase 1 — Perf do caminho quente de import +### Fase 1 — Perf do caminho quente de import — **ENTREGUE** (2026-07-19) -- **`acquireContactPhoneLocks`** (`src/utilities/contactPhoneInvariant.ts` / `postgresTransactionLocks.ts`): trocar o loop `for (const key of sortedKeys) await database.execute(...)` por uma query única `SELECT pg_advisory_xact_lock(hashtextextended(k, 0)) FROM unnest(ARRAY[...]) AS k` (ordenar para evitar deadlock). Contrato `acquireContactPhoneLocks(payload, req, phones[])` inalterado — callers (bulk, single-create, hook) não mudam. -- **`bulkInsertSupporterImport`** (`src/utilities/supporterImportBulk.ts`): `tx.insert(contactTable).values(...).returning({ id: true, phone: true })` em vez do re-`payload.find` por chunk para recuperar IDs (elimina ~10 round-trips no cap de 5k). Widen o tipo `DrizzleTx` local para tipar `.returning()`. -- Substituir os dois `payload.find` de existência (contacts por phone, supporters por contact_id) por `SELECT ... FROM contact WHERE phone = ANY($)` / `SELECT contact_id FROM supporter WHERE contact_id = ANY($) AND municipality_id IS NULL` na mesma sessão drizzle da txn. +**As-built:** -### Fase 2 — Perf de leitura da lista +- **`acquireContactPhoneLocks`** → `acquireTextAdvisoryLocks` em `src/utilities/postgresTransactionLocks.ts`: uma query `SELECT pg_advisory_xact_lock(hashtextextended(k, 0)) FROM unnest(ARRAY[...])` por batch (chaves deduplicadas e ordenadas). Contrato inalterado; pinado em `tests/int/postgresTransactionLocks.int.spec.ts`. +- **`bulkInsertSupporterImport`** (`src/utilities/supporter/supporterImportBulk.ts`): `.returning()` para IDs de contacts novos; leituras de existência via SQL drizzle (`phone IN (...)` / `contact_id IN (...) AND municipality_id IS NULL`) — zero `payload.find` na txn. `assertDrizzleColumns` no init. +- Import CSV end-to-end: `tests/int/campaignSupporter.int.spec.ts` (preview→confirm, token HMAC). -- **`loadSupporterListOverviewData`** (`src/utilities/supporterPageData.ts`): derivar `total` do `result.totalDocs` da lista (já computado por Payload); o overview roda só os dois `COUNT(*) FILTER` (certo+tende, indeciso). Elimina um COUNT filtrado completo por navegação. -- **`computeSupporterListOverviewAggregate`** (`src/utilities/supporterListOverviewAggregate.ts`): ~~memoizar o lookup de municípios acessíveis por request~~ — **largamente absorvido** (2026-07-23): `getAccessibleMunicipalityIds` tem cache request-scoped por contexto e o aggregate recebe `advisorMunicipalityIds` pré-carregados; conferir no PR se lista e overview compartilham de fato uma única query `municipality`. -- **Migration** `add_contact_trgm_index`: `CREATE EXTENSION IF NOT EXISTS pg_trgm` + `CREATE INDEX ... USING gin (name gin_trgm_ops)` e `(city gin_trgm_ops)` em `contact`. Torna `ILIKE '%q%'` sargable. Única migration do item. -- Avaliar restringir `q` a mínimo 2–3 chars no cliente (reduz seq scans inúteis) — opcional, sem migration. +### Fase 2 — Perf de leitura da lista — **ENTREGUE** (2026-07-19) + +**As-built:** + +- **`loadSupportersPageData`** (`src/utilities/supporter/supporterPageData.ts`): overview recebe `result.totalDocs`; aggregate roda só os dois `COUNT(*) FILTER` (certo+tende, indeciso). Retorna `null` quando `total <= 0`. +- **`computeSupporterListOverviewAggregate`** (`src/utilities/supporter/supporterListOverviewAggregate.ts`): `advisorMunicipalityIds` pré-carregados uma vez por render (advisor); filtros unificados via `supporterListFilters.ts` / `supporterListSqlFilters.ts` (entregue no C9). +- **Migration** `20260719_020000_add_contact_trgm_index`: extensão `pg_trgm` + GIN em `contact.name` e `contact.city`. +- **`contactSearchQuery`**: `isContactSearchQueryReady` (mín. 2 chars) em `buildSupporterSearchTerms` — lista e overview ignoram `q` de 1 caractere. Pinado em `tests/int/supporterListFilters.int.spec.ts` e `campaignSupporter.int.spec.ts`. ### Fase 3 — DRY de helpers @@ -108,7 +114,7 @@ Appetite: **~2h eng, fill-in.** O `/simplify` do C13 mostrou que a invariante do - `actions/password.ts` / `profile.ts` / `leaderSupporter.ts` exportam `*FormAction` com ladder hand-rolled fora do filtro `*FormActions.ts` — gatilho: próximo toque nesses arquivos **ou** decisão de expandir o guard além de route-level FormActions. - `convite/[token]/formActions.ts` login half ainda hand-rolled (password-confirm + redirect) — gatilho: próximo toque no convite; autofill já usa o wrapper. -**Revisão 2026-07-27:** F4b fechada; F1–F2 seguem com gatilho de volume. +**Revisão 2026-07-27:** F4b fechada; F1–F2 seguiam com gatilho de volume — **fechadas 2026-08-01** (Issue #37; código já em `main` desde 2026-07-19). **Migration:** Fases 1, 3, 4, 4b sem schema. Fase 2: `pnpm migrate:create add_contact_trgm_index` (extensão + 2 GIN indexes). Sem Consent novo. @@ -132,10 +138,10 @@ Appetite: **~2h eng, fill-in.** O `/simplify` do C13 mostrou que a invariante do - [escala-dry-pos-b2.md](escala-dry-pos-b2.md) — precedente B5 (pós-B2 simplify) - [escala-dry-pos-c3.md](escala-dry-pos-c3.md) — precedente C7 (pós-C3 simplify) - Review `/simplify` 2026-07-19 (quality / reuse / performance) — origem das fases -- `src/utilities/supporterImportBulk.ts` — bulk insert (locks, .returning, drizzle reads) -- `src/utilities/postgresTransactionLocks.ts` — `acquireContactPhoneLocks` (loop por telefone) -- `src/utilities/supporterListOverviewAggregate.ts` — aggregate SQL + `resolveAccessConstraint` -- `src/utilities/supporterPageData.ts` — `loadSupporterListOverviewData` (duplo COUNT) +- `src/utilities/supporter/supporterImportBulk.ts` — bulk insert (locks, .returning, drizzle reads) +- `src/utilities/postgresTransactionLocks.ts` — `acquireTextAdvisoryLocks` (batch advisory locks) +- `src/utilities/supporter/supporterListOverviewAggregate.ts` — aggregate SQL + `resolveAccessConstraint` +- `src/utilities/supporter/supporterPageData.ts` — `loadSupportersPageData` (overview derivado de `totalDocs`) - `src/utilities/electionResultsImport.ts` — `chunk`/`requireTable`/`INSERT_CHUNK_SIZE` duplicados - `src/utilities/access/municipalities.ts` — `getAccessibleMunicipalityIds` / `getAdvisorMunicipalityIds` - `src/utilities/campaignFormActionError.ts` / `campaignFormFields.ts` — helpers adotados por 2 forms