mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-14 17:43:04 +00:00
Merge pull request #7 from fsolla/cursor/p4-audit-artifacts-78a7
Pass 4 audit artifacts: plan, ledger reconcile, delivery plan, GUARDRAILS.md
This commit is contained in:
commit
77fb5384d4
6 changed files with 312 additions and 12 deletions
|
|
@ -4,6 +4,10 @@ Conteúdo movido do `AGENTS.md` em 2026-07-30 (fatiamento do paradigma de agente
|
|||
|
||||
---
|
||||
|
||||
**Recently resolved (2026-07-31):** **Pass 4 — auditoria de engenharia autônoma + guardrails das misses** — primeira execução em modo autônomo (Cursor Cloud) da skill engineering-audit, com precheck solitário (pool desligado) e primeiro harvest de `kind:agent-miss` (escopo OPS2: nasce `docs/GUARDRAILS.md`). **P0:** os 4 globals públicos (`site-settings`/`home`/`metadata`/`privacy-policy`) aceitavam qualquer autenticado no `update` — JWT de campanha (liderança inclusa) editava o site público via `/api/globals/*`; lockdown `payloadAdminOnly` + pins + guarda ([PR #91](https://github.com/fsolla/teqo/pull/91); editores perdem escrita nos globals de config deliberadamente). **P1:** `seed-posts.mjs` chamava `dieWithLabel` sem importar ([PR #92](https://github.com/fsolla/teqo/pull/92), com guarda de presença de import + `DAY_MS` single-source + ESLint lib→utilities sem type-import + guarda de `overrideAccess` omitido). **Misses:** #73 (deadlock `municipality_rels` — classe reproduzida: 7 violações int + 11 deep links e2e; migração para o alocador com teste de classe reescrito classe-agnóstico + guarda de 3 regras, [PR #93](https://github.com/fsolla/teqo/pull/93)); #52 (Drawer por célula em 3 listas de chips → `CampaignListSheetProvider` + invariante dev + guarda, [PR #94](https://github.com/fsolla/teqo/pull/94)); #53 (portão de readiness do autenticador virtual) + #54 (guarda de goto não assentado + convenção TESTING.md) em [PR #95](https://github.com/fsolla/teqo/pull/95); #83 fechada (guarda `agent:ship` já viva). **Registrado, não retry-fixed:** spec de chips de `campaignLeaderships` falha deterministicamente em dev na árvore limpa (nenhum POST dispara; prod 48/48 verde) — causa raiz via debug workflow (P4-L). Artefatos: seção Pass 4 no plano de qualidade, 15 linhas novas no ledger (2 stale fechadas: P3-A/P3-B), plano [`entrega-engenharia-p4.md`](docs/plans/entrega-engenharia-p4.md) com ondas P4-A→P4-L + mapa de guardas + defer+gatilho.
|
||||
|
||||
**Recently resolved (2026-07-31):** **CI build skip para diffs fora da árvore buildável** — `scripts/build-affected.mjs` + `classifyBuildScope` (núcleo em `test-affected-core.mjs`) decidem `run|none`; job `build-scope` barato no `ci-pr.yml` e o job `build` (Postgres + `pnpm build`) só roda em `mode=run`. Docs/tests/tooling/workflows pulam o build; `src/`, `public/` e configs do Next (`next.config`/`tsconfig`/`tailwind`/`postcss`/`components.json`/lockfile) forçam. `gate:ci` espelha. Push em `main` (`ci.yml`) continua sempre full.
|
||||
|
||||
**Recently resolved (2026-07-31):** **Consolidação de Issues (project-status)** — merge **B68+** (#39) → **O0+** (#35) (mesmo `model: kimi-k3-low`); **não** merge D3/D4/D5 (modelos mistos kimi vs composer); **não** merge B53–B55 (já `done`); fatia **B79** → chassis (#15) + **B91** busca no drawer (#76, `depends: [B79]`). Planos: `escala-dry-pos-onda0.md`, `busca-global-bottom-drawer.md`, `chassis-bottom-drawer-acoes-rapidas.md`.
|
||||
|
||||
## Known Gaps (as of 2026-07-24 — resolve before relying on this file for onboarding new devs)
|
||||
|
|
|
|||
43
docs/GUARDRAILS.md
Normal file
43
docs/GUARDRAILS.md
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
# Guardrails
|
||||
|
||||
Ledger dos guardrails determinísticos colhidos de `kind:agent-miss` (fluxo `pnpm agent:file-miss` → harvest no engineering-audit → guarda programática) e dos endurecimentos de guarda existentes. Uma linha por guarda: origem, classe na escada de determinismo, mecanismo, status.
|
||||
|
||||
**Classes (escada do Pass 3 D6 / skill engineering-audit passo 4b):** 1 tipo (estado ruim irrepresentável) · 2 ESLint · 3 spec de convenção · 4 análise estática em CI (knip/madge) · 5 pin comportamental · 6 doc/convenção (judgment-only declarado — não fingir que doc é guarda).
|
||||
|
||||
## Ativos
|
||||
|
||||
| Guarda | Origem | Classe | Mecanismo | Status |
|
||||
| ---------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------- | --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------- |
|
||||
| `agent:ship` exige stamp de /simplify casado com HEAD; PR Ready + auto-merge + watch até MERGED | [#83](https://github.com/fsolla/teqo/issues/83) | 3 (script + hooks) | `scripts/agent-ship.mjs`, `.cursor/hooks/guard-pr-create.mjs` (bloqueia `gh pr create` manual), `.cursor/hooks/pr-merge-watchdog.mjs` (re-engata com PR aberto) | vivo desde 2026-07-31 (PR #89); miss fechada no Pass 4 |
|
||||
| Alocador de municípios em specs — sem slug fixo + mutação; sem `municipio=<slug>` em e2e; regex de nome de catálogo ancorada | [#73](https://github.com/fsolla/teqo/issues/73) | 3 | `tests/unit/testMunicipalityAllocatorConventions.unit.spec.ts` (3 regras; mutações = assign/touch/create\*/`payload.update` municipality) | PR [#93](https://github.com/fsolla/teqo/pull/93) (Pass 4) |
|
||||
| Sheet compartilhada por lista — chip cells exigem `CampaignListSheetProvider`; invariante dev no overlay | [#52](https://github.com/fsolla/teqo/issues/52) | 3 + 5 | `tests/unit/sharedSheetHostConventions.unit.spec.ts`; `console.error` development-only em `CampaignCellEditOverlay` | PR [#94](https://github.com/fsolla/teqo/pull/94) (Pass 4) |
|
||||
| Navegação e2e assentada — goto→goto sem settle falha o build | [#54](https://github.com/fsolla/teqo/issues/54) | 3 | `tests/unit/e2eNavigationConventions.unit.spec.ts` (settle = await expect/waitForURL/click/reload/login/catch) | PR [#95](https://github.com/fsolla/teqo/pull/95) (Pass 4) |
|
||||
| Readiness de autenticador virtual antes da cerimônia WebAuthn | [#53](https://github.com/fsolla/teqo/issues/53) | 3 | mesma spec (spec com `WebAuthn.addVirtualAuthenticator` deve chamar `expectCampaignBiometricsReady(`) | PR [#95](https://github.com/fsolla/teqo/pull/95) (Pass 4) |
|
||||
| Convenção de navegação e2e (goto = cold load/contrato; in-shell = continuidade) | [#54](https://github.com/fsolla/teqo/issues/54) | 6 (**judgment-only**) | parágrafo em `docs/TESTING.md` | PR [#95](https://github.com/fsolla/teqo/pull/95) (Pass 4) |
|
||||
| Update de globals públicos nunca "qualquer autenticado" | P0 do Pass 4 (sem issue) | 3 | `tests/unit/globalAccessConventions.unit.spec.ts` ban `Boolean(user)` em `src/globals` | PR [#91](https://github.com/fsolla/teqo/pull/91) (Pass 4) |
|
||||
| Loaders de notificação tipados pelo ator (recipientID arbitrário irrepresentável) | P0 adjacente do Pass 4 | 1 | assinatura `(payload, user: CampaignUser)` + `overrideAccess: false` sob `canReadOwnNotifications` | PR [#91](https://github.com/fsolla/teqo/pull/91) (Pass 4) |
|
||||
| Die-helpers da CLI importados onde usados; `DAY_MS` single-source | P1 do Pass 4 (sem issue) | 3 | `tests/unit/scriptCliConventions.unit.spec.ts` | PR [#92](https://github.com/fsolla/teqo/pull/92) (Pass 4) |
|
||||
| `src/lib` não type-importa `@/utilities/**` (direção única também para tipos) | dodge do Pass 3 (suggestionCatalog) | 2 | ESLint `no-restricted-imports` `allowTypeImports: false` na zona `src/lib` | PR [#92](https://github.com/fsolla/teqo/pull/92) (Pass 4) |
|
||||
| Local API com `user` sempre declara `overrideAccess`; import dinâmico de módulos server banido em utilities | guarda ausente (medida baseline 59/0) | 3 | `tests/unit/localApiOverrideAccessConventions.unit.spec.ts` | PR [#92](https://github.com/fsolla/teqo/pull/92) (Pass 4) |
|
||||
|
||||
## Precedentes vivos (anteriores ao primeiro harvest)
|
||||
|
||||
| Guarda | Origem | Classe | Mecanismo |
|
||||
| ------------------------------------------------------------------------ | ----------------- | ------ | ------------------------------------------------------------------------------------------------ |
|
||||
| Vocabulário banido (Praça/Núcleo/Plano de Ação, accent-tolerant) | C13 / W4f / P3-K | 3 | `codebaseConventions.unit.spec.ts` |
|
||||
| `server-only` em utilities acopladas (sweep payload/next/\*) | Pass 2 W2 / P3-K | 3 | `codebaseConventions.unit.spec.ts` |
|
||||
| Escadas de formActions nos wrappers (`runCampaignFormAction`/`Redirect`) | W4d / C8 F4b | 3 | `codebaseConventions.unit.spec.ts` (filename `*FormActions.ts` — dodge `actions/*.ts` ledgerado) |
|
||||
| Ratchet de bypass admin não documentado | P3-E | 3 | `codebaseConventions.unit.spec.ts` (por arquivo, só diminui) |
|
||||
| Mensagens de recusa como constantes + correspondência throw↔safelist | P3-A | 3 | `codebaseConventions.unit.spec.ts:216-263` |
|
||||
| `as never` banido | Pass 1 | 2 | ESLint `no-restricted-syntax` |
|
||||
| `src/lib` não importa `utilities/` (valores) | Pass 2 W2 | 2 | ESLint `no-restricted-imports` |
|
||||
| 0 ciclos de import | Pass 2 | 4 | madge (`pnpm check:cycles`) |
|
||||
| Dead code (arquivos/deps/exports em erro) | Pass 2 W4b | 4 | knip em CI |
|
||||
| `migration-lock` (≤1 PR aberto tocando schema) | agentes paralelos | 4 | job em `ci-pr.yml` |
|
||||
| Seed mínimo obrigatório com migration (contrato manifest) | OPS fluxo PR | 3 + 5 | `tests/unit/seedMinimalManifest.unit.spec.ts` + job CI migrate→seed→int |
|
||||
| Regex de template ancorada em e2e | P3-C | 3 | `codebaseConventions.unit.spec.ts` (ampliada para literais no Pass 4) |
|
||||
| Consent fail-closed por chave estável | D3/Onda 0 | 5 | pins int `submitWhatsapp`/liderança/apoiador |
|
||||
|
||||
## Misses colhidas aguardando guarda (fila do harvest)
|
||||
|
||||
(nenhuma — todas as 5 abertas em 2026-07-31 foram classificadas: 4 com guarda shippada + 1 com guarda já viva. Novas `kind:agent-miss` entram aqui até o próximo audit.)
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
# Improve Code Quality Plan
|
||||
|
||||
Three passes so far: **Pass 1** (2026-07-23/24, phases 0–6 below, all done), **Pass 2** (2026-07-25, see [Pass 2](#pass-2--engineering-consolidation-2026-07-25)) and **Pass 3** (2026-07-28, audit + plan, see [Pass 3](#pass-3--engineering-audit--consolidation-2026-07-28)).
|
||||
Four passes so far: **Pass 1** (2026-07-23/24, phases 0–6 below, all done), **Pass 2** (2026-07-25, see [Pass 2](#pass-2--engineering-consolidation-2026-07-25)), **Pass 3** (2026-07-28, audit + plan, see [Pass 3](#pass-3--engineering-audit--consolidation-2026-07-28)) and **Pass 4** (2026-07-31, audit + P0/P1 remediation + miss guardrails in-session, see [Pass 4](#pass-4--engineering-audit--remediation--guardrails-2026-07-31)).
|
||||
|
||||
## Context
|
||||
|
||||
|
|
@ -162,3 +162,52 @@ Statuses: pending · in-progress · awaiting-evidence · done · deferred: \<rea
|
|||
|
||||
- [ ] P3-A → P3-L as independent deliveries (full gate each; Aikido on edited files)
|
||||
- [ ] Leftovers and new debts → ledger via `capture-review-debts`; oversized items → Issue rastreável via `plan-issue`
|
||||
|
||||
---
|
||||
|
||||
# Pass 4 — Engineering Audit + Remediation + Guardrails (2026-07-31)
|
||||
|
||||
## Context
|
||||
|
||||
- **Started:** 2026-07-31 — first **autonomous (Cursor Cloud)** execution of the `engineering-audit` skill, and the first harvest of `kind:agent-miss` (OPS2 scope: this pass also creates `docs/GUARDRAILS.md`).
|
||||
- **Precheck (fail-closed):** `agent:gh-doctor` OK (GH_TOKEN, Issues read/write); agent pool **desligado** — solitary audit confirmed.
|
||||
- **Delta anchor:** Pass 3 (2026-07-28). 880 files touched in 3 days of agent-paradigm work (homeSearch ×28 files, notifications/push ×7+, wizard ×12, WebAuthn ×8, 5 migrations).
|
||||
- **Method:** canon loaded; hotspot map (delta + churn + static gates); **five parallel sweeps** (lib+scripts, utilities+access, components, app+collections+globals, tests) with measured findings; consolidation hunt under the anti-DRY rule; 5 miss issues harvested and investigated; 16 open ledger rows re-verified; P0/P1 + miss guardrails remediated in-session, each in its own PR with full gate.
|
||||
- **Baseline:** green — `tsc` 0, `lint` 0 warnings, `knip` 0 findings (known `payload.config.ts` loader noise, ledger P3 — verified still true), madge 0 cycles (**774 files**, +124 since Pass 3).
|
||||
|
||||
## Audit headlines (all measured)
|
||||
|
||||
- **1 P0:** the four public globals (`site-settings`, `home`, `metadata`, `privacy-policy`) spelled `update: Boolean(user)` — campaign JWTs authenticate against `/api/*`, so any campaign session (leader included) could PATCH the public site. Collections were locked in Phase 0; globals were missed. **Remediated in-session** ([PR #91](https://github.com/fsolla/teqo/pull/91)).
|
||||
- **1 P1:** `scripts/seed-posts.mjs` called `dieWithLabel` **without importing it** — ReferenceError on the empty-fetch abort path; P3-H half-adoption in the same pass that created the CLI guard. **Remediated in-session** ([PR #92](https://github.com/fsolla/teqo/pull/92)).
|
||||
- **5 misses → 5 guardrails:** #73 deadlock class **reproduced** (7 int violations + 11 hardcoded e2e deep links) → allocator migration + 3-rule guard ([PR #93](https://github.com/fsolla/teqo/pull/93)); #52 residual per-cell Drawers on 3 chip lists → shared host ×3 + dev invariant + guard ([PR #94](https://github.com/fsolla/teqo/pull/94)); #53 one-shot probe race → readiness gate + pin; #54 measured **0 unsettled goto pairs** → calibrated guard + TESTING.md convention ([PR #95](https://github.com/fsolla/teqo/pull/95)); #83's guard (`agent:ship` + hooks, PR #89) verified alive → issue closed, no new guard.
|
||||
- **Flake honesty:** `campaignLeaderships` chip spec fails **deterministically in dev** on clean stage (instrumented: no POST in 60 s; not the hydration class) — prod green 48/48. Registered, **not** retry-fixed.
|
||||
- **Ledger hygiene:** rows 33/34 (safeMessages, petition consentId) verified fixed → closed; row 17 counts → 17 specs/48 cases; rows 35/40/45 updated with verified counts and trigger status (NOT fired); row 19 mitigated (unanchored literals gone + guarded); rows 10/12/14/21/23/28/31 verified still true (23 with updated path). **15 new rows** (P2/P3) with evidence.
|
||||
- **12 guard dodges measured:** 3 hardened in-session (ESLint lib zone type-imports, CLI import-presence, Local API omission + dynamic-import); the rest → workstreams or registered (map in the plan).
|
||||
- **Duplication for the plan:** ~500+ measured lines across P4-A…P4-E + P4-G; 12 defer+trigger entries; 6 look-alikes rejected with reason.
|
||||
|
||||
## Remediation deliveries (done, full gate each)
|
||||
|
||||
| WS | Content | PR | Status |
|
||||
| --------- | --------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------- | --------------------------------------------------- |
|
||||
| P4-P0 | Public globals lockdown ×4 + notification bell loaders actor-typed (no bypass) + int pins + `globalAccessConventions` guard | [#91](https://github.com/fsolla/teqo/pull/91) | open (Ready, base stage) |
|
||||
| P4-P1 | seed-posts import fix + `scriptCliConventions` (die-import, `DAY_MS`) + ESLint lib-zone type-import ban + `localApiOverrideAccessConventions` | [#92](https://github.com/fsolla/teqo/pull/92) | open (Ready, base stage) |
|
||||
| P4-M73 | Miss #73: allocator migration (int class-agnostic rewrite + e2e claim) + `testMunicipalityAllocatorConventions` (3 rules) | [#93](https://github.com/fsolla/teqo/pull/93) | open (Ready, base stage) — `Closes #73` |
|
||||
| P4-M52 | Miss #52: `CampaignListSheetProvider` ×3 lists + dev invariant + `sharedSheetHostConventions` | [#94](https://github.com/fsolla/teqo/pull/94) | open (Ready, base stage) — `Closes #52` |
|
||||
| P4-M53/54 | Misses #53+#54: `expectCampaignBiometricsReady` + unsettled-goto guard + TESTING.md convention | [#95](https://github.com/fsolla/teqo/pull/95) | open (Ready, base stage) — `Closes #53, closes #54` |
|
||||
|
||||
## Pass 4 Decisions
|
||||
|
||||
| ID | Decision | Rationale |
|
||||
| --- | --------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| D1 | Each remediation in its own PR with the recurrence guard in the **same** delivery | Prevention-first (Pass 3 D6); guards that land later never land |
|
||||
| D2 | Editors lose writes to the four config globals (deliberate narrowing) | They are site config, not editorial content; `canManagePublishedContent` untouched; widening later is a named-policy decision |
|
||||
| D3 | #54 guard calibrated to "unsettled goto pairs", not a `goto` ban | Measured: 58 gotos, 0 unsettled; the miss itself warned against a global ban |
|
||||
| D4 | Dev-mode chip flake registered, not retry-fixed | Instrumentation showed no POST ever fires — not a race; retry masks non-race bugs; prod is the gate |
|
||||
| D5 | Class-agnostic int fixtures over class-pinned slugs | The pinned behavior (filter/sort by class) never needed reduto/marginal identity — allocation kills the deadlock class without weakening the pin |
|
||||
| D6 | Aikido scan not run (MCP unavailable in this Cloud environment) | The per-change scan stays mandatory where the server exists; the ledger row (exit-2/SCA-unverified) is unchanged |
|
||||
|
||||
## Next Actions
|
||||
|
||||
- [ ] Human sign-off = merge of the artifacts PR (plan + ledger + GUARDRAILS.md); remediation PRs merge independently
|
||||
- [ ] P4-A → P4-L as independent deliveries ([entrega-engenharia-p4.md](plans/entrega-engenharia-p4.md)); oversized items → Issue rastreável via `plan-issue`
|
||||
- [ ] Leftovers → ledger via `capture-review-debts`
|
||||
|
|
|
|||
|
|
@ -14,13 +14,13 @@ Ledger of known engineering debt. Items large enough to be scheduled work become
|
|||
| Any campaign user can enumerate staff names/roles (`canReadCampaignUsers`) | `src/utilities/access/campaignUsers.ts` | info disclosure | low | S | P2 | open | |
|
||||
| `removeSupporterData` deletes via `overrideAccess: true` while `canDeleteSupporter` is admin-only | `src/app/(campaign)/campanha/actions/supporter.ts` | access asymmetry (intentional LGPD path) | low | S | P3 | closed 2026-07-29 (P3-D): documented in-code — the delete is the staff-driven titular-removal flow, scope already proven by `assertCanManageSupporter` on the exact row; day-to-day deletes stay admin-only | |
|
||||
| Legacy public consent flows hardcode document IDs (`consent: 2`) | `src/app/(frontend)/actions/submitWhatsapp.ts` | correctness | medium | S | P1 | closed 2026-07-25 (Pass 2 D3): resolves `Consent.key = 'whatsapp-inscricao'` fail-closed; migration `20260725_170000` tags the live doc (id 2, key vazio) com a chave | AGENTS Known Gaps #2 |
|
||||
| E2E suite thin (**15 specs / 31 cases** measured 2026-07-28, Pass 3 — the "7 specs" count is stale; most campaign CRUD still int-only, so the substance holds) | `tests/e2e/` | coverage | medium | L | P2 | open | |
|
||||
| E2E suite thin (**17 specs / 48 cases** measured 2026-07-31, Pass 4 — grew from 15/31 at Pass 3; most campaign CRUD still int-only, so the substance holds) | `tests/e2e/` | coverage | medium | L | P2 | open | |
|
||||
| CI runs on GitHub Actions only (no secondary pipeline) | `.github/workflows/ci.yml` | process | low | M | P3 | open | |
|
||||
| E2E campaign journeys flake with 2 workers — **root cause narrowed 2026-07-26 (B13)**, and it is not only latency: (a) `campaignMunicipalities`/`campaign-pwa` click the leader consent checkbox before hydration attaches the handler, and (b) the municipality fixture picks names by regex, so "Conde" also matches "Condeúba". Reproduced on a clean `main` tree, so it is not a B13 regression; both specs pass in isolation | `tests/e2e/campaignMunicipalities.e2e.spec.ts`, `tests/e2e/campaign-pwa.e2e.spec.ts` | test-infra | medium (masks real failures in the delivery gate) | S | P2 | open — wait for an interactive-state signal instead of the element existing, and anchor fixture lookups to exact names/slugs instead of substring regex | |
|
||||
| E2E campaign journeys flake with 2 workers — **root cause narrowed 2026-07-26 (B13)**, and it is not only latency: (a) `campaignMunicipalities`/`campaign-pwa` click the leader consent checkbox before hydration attaches the handler, and (b) the municipality fixture picks names by regex, so "Conde" also matches "Condeúba". Reproduced on a clean `main` tree, so it is not a B13 regression; both specs pass in isolation | `tests/e2e/campaignMunicipalities.e2e.spec.ts`, `tests/e2e/campaign-pwa.e2e.spec.ts` | test-infra | medium (masks real failures in the delivery gate) | S | P2 | open — mitigated 2026-07-31 (Pass 4): hydration helpers adopted (`checkRadixWhenHydrated`); the `/Cairu/i` substring literals are GONE and a catalog-keyed guard bans unanchored municipality-name regexes (miss #73, PR #93); prod full suite 48/48 green three times this session. What remains is the dev-mode `campaignLeaderships` chip failure — different class, own Pass 4 row below | |
|
||||
| Two e2e tests fail **deterministically at one worker on a clean `main` tree** (measured 2026-07-27 by stashing the whole B34 delivery and re-running): `campaignNearestMunicipality` "coordinator in Salvador is sent to the zone list" cannot find the `Salvador — ZE 1` link on `?q=Salvador`, and `campaignMunicipalities` "advisor opens a demand and decides it" cannot find its demand heading. These are **not** the 2-worker flake in the row above — retrying and lowering concurrency do not help — so the gate currently ships with two red specs whose failure nobody has diagnosed. **Widened 2026-07-28 (B17), same stash-and-compare method: it is not two tests but most of `campaignMunicipalities`** — five failed at one worker on the clean tree (`:56`, `:125`, `:292`, `:394`, `:446`) against four with the delivery applied, and the failing set is not stable between runs (`page.waitForURL` timeouts, a `getByLabel('Média')` strict-mode violation matching both the popover trigger and its input, a click whose target the bottom nav intercepts). Practical consequence for the next delivery: comparing the count of red tests against a stashed tree is the only way to read this spec — naming the two tests above is no longer enough. **2026-07-28 (Pass 3): root cause measured** — unanchored municipality-name regex at `campaignMunicipalities.e2e.spec.ts:263` (**23/435 prefix collisions** in the catalog: Conde/Condeúba, Catu/Caturama, Laje ×3, Barra ×4); the `getByLabel('Média')` strict-mode violation and the hydration-race consent checkbox are unchanged; fixture drift measured — the e2e cleanup lacks E14's `engagementLevel`/`levelNote`/catalog-name reset and owns+deletes the leased `apoiador-cadastro` row. Fix scheduled as **P3-C** ([entrega-engenharia-p3.md](plans/entrega-engenharia-p3.md)) | `tests/e2e/campaignNearestMunicipality.e2e.spec.ts:156`, `tests/e2e/campaignMunicipalities.e2e.spec.ts:56,125,292,394,446` | broken test (or real regression) | high — a permanently red gate trains everyone to ignore it, which is how a real failure ships | S | **P1** | closed 2026-07-29 (P3-C): full suite **31/31 green** at 2 workers. Root causes fixed — stale sort-summary copy (`déficit da meta`→`Cobertura`), `exact` trigger name vs appended status suffix (now anchored regex), option click not selecting (role=option + `toPass` hydration-retry loop, same class as `checkRadixWhenHydrated`), stretched-overlay click needing `force`, leader login using the advisor's password (each `createCampaignUser` mints its own), and 30 s test timeout → 60 s under cold-compile load. Guards: unanchored-template-regex ban in `codebaseConventions`, global `expect` 10 s | |
|
||||
| Post listing cache holds all published posts, filters in memory | `src/utilities/posts.ts` | scale (fine at ~tens of posts) | low | M | P3 | open — watch corpus growth | |
|
||||
| Petition `generateStaticParams` uses uncached `getDocuments` without `select`/`depth: 0` | `src/utilities/documents.ts` consumers | over-fetch (build-time) | low | S | P2 | closed 2026-07-23 (Phase 2: `getPetitionIds` with `depth: 0` + empty `select`) | |
|
||||
| Compare mode (`?compare=`) still queries per candidate uncached cross-request | `src/utilities/municipalityMapData.ts` | perf | low | M | P3 | open — React `cache()` dedups in-request; add `unstable_cache` if profiling warrants | |
|
||||
| Compare mode (`?compare=`) still queries per candidate uncached cross-request | `src/utilities/municipalityMapData.ts` | perf | low | M | P3 | open — React `cache()` dedups in-request; add `unstable_cache` if profiling warrants. Verified still true 2026-07-31 (Pass 4): path moved to `src/utilities/municipality/municipalityMapData.ts:177-218` → `loadCandidateVotesByCityZone` (`municipalityElectoralBaseline.ts:242-278`), 3 years in parallel, no `unstable_cache` | |
|
||||
| Map bundle serializes all years × scenarios (~100KB raw), **+63.6 KB after B13** (`competitiveRankByYear` is ~57 KB of it: three years × 417 codes × `{rank, candidates}`, paid on every dashboard load even by the coordinator who never opens the "posição no município" scale) | `municipalityMapContract.ts` consumers | payload size | low — gzip crushes repeating keys to ~10KB on the wire; instant year/scale switching is a core analysis flow worth the bytes | M | P3 | the 2026-07-23 deferral asked for profiling before acting; B13 supplied the number, so the trim is now scheduled as **B13+ F4** (roadmap fill-in) | B13+ F4 |
|
||||
| Props→state `useEffect` mirrors in list controls/comboboxes | `MunicipalityListExpectedVotesControl`, `AsyncSearchCombobox`, `StrictCombobox` | pattern | none — reviewed 2026-07-23: guarded-equality sync is the correct "server echo vs local draft" reconciliation (A10 pattern), not derived state | — | — | closed (accepted as correct) | |
|
||||
| User-visible "Praça" copy survived the M1 rename (map panel title "Mapa das Praças", admin field descriptions, provisional Onda 0 consent texts, `SupportStatusBadge` summary, fallback/error strings, code comments) | `rg -n 'Praça' src` (~96 hits in 53 files, ~55 user-visible — 2026-07-25 audit) | copy drift | low (cosmetic; consent texts are provisional Onda 0 copies) | S | P2 | closed 2026-07-28 (Pass 3 verify): W4f executed 2026-07-25; the remainder is the provisional Onda 0 consent texts (legal batch, D6) | Pass 2 W4f |
|
||||
|
|
@ -30,23 +30,40 @@ Ledger of known engineering debt. Items large enough to be scheduled work become
|
|||
| B34 shipped a **measured** First Load JS regression: `/campanha/liderancas` 246 → **304 kB** and `/campanha/assessores` 253 → **295 kB** (Next's minified figure, two full builds against a stashed clean `main`). Assessores already imported the static Bahia catalog, so its +42 kB is the **cell** (Drawer, links, combobox contract, measuring machinery) and only the remaining ~16 kB on lideranças is the catalog reaching a route that lacked it — the same cost B14 already named. The two fixes **oppose each other**: pulling the catalog out of the browser fattens the payload of every chip toggle, shrinking the payload to `{id,slug}` keeps the bundle | `src/lib/municipalityPortfolio.ts`, `src/utilities/municipalityPortfolioIndex.ts`, `src/components/campaign/shared/MunicipalityPortfolioCell.tsx` | perf (bundle + per-render payload) | medium — two of the most-used staff lists, on phones over 4G | M | — | **closed 2026-07-28 (B34+ F1)** — index `{id,slug}`; `name`/`region` from `municipalityCatalog` (int pins slug+region vs catalog). RSC −21,9 kB raw/rota; First Load JS unchanged vs clean `main` (306/298/271 kB). Drawer `next/dynamic` deferred | B34+ ✓ |
|
||||
| `AdvisorsTable` mixes an edit _mode_ (name/e-mail/phone behind an "Editar nome e contato" toggle) with a column that is always live (municípios). The toggle survives because those are text fields with no chip readout, but the row now speaks two interaction grammars at once | `src/components/campaign/advisor/AdvisorsTable.tsx` | UX inconsistency | low | M | P3 | open 2026-07-27 (B34) — resolve by giving the text fields the same edit-in-place treatment, not by re-hiding the chips | B34 |
|
||||
|
||||
| `safeMessages` entries that can never match their thrown gate messages — exact-string matching in `mapCampaignFormActionError` collapses them to the generic error: "movem demandas", "registram custos", "registram estimativas" (thrown with "e o candidato", safelisted without) and escalated-demand decide (`'…Coordenador Geral ou Candidato.'` vs `'…Coordenador Geral.'`) — the last **user-reachable** (advisor deciding an escalated demanda gets the generic message). +1 dead entry ("podem abrir demandas", thrown nowhere). Same collapse class in copy: `podem gerenciar lideranças.` ×3, `podem gerenciar apoiadores.` ×3, `gerenciam organizações.` ×3, `anexam comprovantes.` ×2, `'Celular brasileiro inválido.'` ×3 | throws: `actions/demand.ts:63,98`, `actions/votePledge.ts:143`, `collections/CampaignDemand.ts:113`; safelists: `demandas/[slug]/formActions.ts:26,27,77`, `pledgeFormActions.ts:63`, `demandas/nova/formActions.ts:34` | correctness (message-only; writes still correctly refused) | medium — real refusal reasons never reach the UI | S | P2 | open — Pass 3 | P3-A |
|
||||
| Petition signature flow trusts a client-posted `consentId` — written into both `signature` and `subscription` with no server-side resolution, while the page renders the text from `petition.form.consent`; a tampered request records consent to a different document than shown (LGPD provenance weakened, no leak). The WhatsApp sibling resolves by stable key, fail-closed (Pass 2 D3) | `src/app/(frontend)/actions/submitPetitionSignature.ts:9,40,49`; supplier `src/components/PetitionForm.tsx:55-63` | LGPD correctness | medium | S | P2 | open — Pass 3 | P3-B |
|
||||
| E2E/int fixture drift: e2e cleanup lacks E14's `engagementLevel`/`levelNote`/catalog-name reset (int got it 2026-07-27); e2e proxy owns+deletes the shared leased-key consent row `apoiador-cadastro` and reimplements the lease machinery; 28 hand-written `campaignUser` blocks (~280 lines) where one builder serves; `campaignListUrl` contract pinned at 3 levels (shared spec + 5 domain re-pins); invite-origin double-pinned across suites (~160 pure lines paying int cost); 43 manual `fixtures.own()` redundant with the undocumented auto-ownership proxy | `tests/e2e/fixtures/campaignE2EFixtures.ts`, `tests/helpers/campaignFixtures.ts`, `tests/e2e/campaignMunicipalities.e2e.spec.ts:12-53` | test-infra drift | medium — flaky/lying fixtures in the delivery gate | M | P2 | partially closed 2026-07-29 (P3-C): e2e cleanup reset aligned (name/engagementLevel/levelNote), leased `apoiador-cadastro` consent via `ensureLeasedConsent`, 28 hand-written `campaignUser` blocks → `fixtures.createCampaignUser` builder. Remaining open: `campaignListUrl` 3-level pinning, invite-origin double-pin, 43 manual `fixtures.own()` | P3-C |
|
||||
| `safeMessages` entries that can never match their thrown gate messages — exact-string matching in `mapCampaignFormActionError` collapses them to the generic error: "movem demandas", "registram custos", "registram estimativas" (thrown with "e o candidato", safelisted without) and escalated-demand decide (`'…Coordenador Geral ou Candidato.'` vs `'…Coordenador Geral.'`) — the last **user-reachable** (advisor deciding an escalated demanda gets the generic message). +1 dead entry ("podem abrir demandas", thrown nowhere). Same collapse class in copy: `podem gerenciar lideranças.` ×3, `podem gerenciar apoiadores.` ×3, `gerenciam organizações.` ×3, `anexam comprovantes.` ×2, `'Celular brasileiro inválido.'` ×3 | throws: `actions/demand.ts:63,98`, `actions/votePledge.ts:143`, `collections/CampaignDemand.ts:113`; safelists: `demandas/[slug]/formActions.ts:26,27,77`, `pledgeFormActions.ts:63`, `demandas/nova/formActions.ts:34` | correctness (message-only; writes still correctly refused) | medium — real refusal reasons never reach the UI | S | P2 | closed 2026-07-31 (Pass 4 verify): P3-A landed 2026-07-29 — shared consts in `lib/schemas/campaignDemand.ts:69-74` / `votePledge.ts:57-58` imported by both throws and safelists (`CAMPAIGN_DEMAND_TRANSITION_SAFE_MESSAGES` inclui escalada, `VOTE_PLEDGE_ESTIMATE_STAFF_MESSAGE`); old mismatched strings absent from `src/`; guard at `codebaseConventions.unit.spec.ts:216-263` bans literals in throws/safeMessages; dead "podem abrir demandas" absent | P3-A |
|
||||
| Petition signature flow trusts a client-posted `consentId` — written into both `signature` and `subscription` with no server-side resolution, while the page renders the text from `petition.form.consent`; a tampered request records consent to a different document than shown (LGPD provenance weakened, no leak). The WhatsApp sibling resolves by stable key, fail-closed (Pass 2 D3) | `src/app/(frontend)/actions/submitPetitionSignature.ts:9,40,49`; supplier `src/components/PetitionForm.tsx:55-63` | LGPD correctness | medium | S | P2 | closed 2026-07-31 (Pass 4 verify): P3-B landed 2026-07-29 — `submitPetitionSignature.ts:19-31` resolves consent from `petition.form.consent`; `petitionFormSchema` has no `consentId` (`lib/schemas/petition-form.ts:5`); `PetitionForm` receives only `consentHTML`; multi-write still transactional (`:33-78`) | P3-B |
|
||||
| E2E/int fixture drift: e2e cleanup lacks E14's `engagementLevel`/`levelNote`/catalog-name reset (int got it 2026-07-27); e2e proxy owns+deletes the shared leased-key consent row `apoiador-cadastro` and reimplements the lease machinery; 28 hand-written `campaignUser` blocks (~280 lines) where one builder serves; `campaignListUrl` contract pinned at 3 levels (shared spec + 5 domain re-pins); invite-origin double-pinned across suites (~160 pure lines paying int cost); 43 manual `fixtures.own()` redundant with the undocumented auto-ownership proxy | `tests/e2e/fixtures/campaignE2EFixtures.ts`, `tests/helpers/campaignFixtures.ts`, `tests/e2e/campaignMunicipalities.e2e.spec.ts:12-53` | test-infra drift | medium — flaky/lying fixtures in the delivery gate | M | P2 | partially closed 2026-07-29 (P3-C): e2e cleanup reset aligned (name/engagementLevel/levelNote), leased `apoiador-cadastro` consent via `ensureLeasedConsent`, 28 hand-written `campaignUser` blocks → `fixtures.createCampaignUser` builder. Remaining open (verified 2026-07-31, Pass 4 — counts drifted): `campaignListUrl` domain re-pins now **6** (shared spec stays the contract owner), invite-origin double-pin still ~**158** pure int lines (unit owns the policy, 92 lines), manual `fixtures.own()` now **49** (was 43) — folded into P4-K | P3-C |
|
||||
| Advisor-scope RBAC fragment spelled 7× across 6 access modules + near-verbatim scoped-read prologue twins + memoized `getAccessible*Ids` machine triplicated (+ `getAccessibleContactIds` missing its siblings' `isCampaignUnrestricted → null` early return — safe today only by call order) + access micro-cluster (byte-identical advisor/leader branches in `canCreateSupporter`; cross-domain alias `allocationDecisions`→`municipalityUpdates`; deprecated `LeadershipConsentDescriptor` wrapper still with 4 call sites) | `src/utilities/access/votePledges.ts:46`, `demands.ts:42`, `municipalityUpdates.ts:40`, `supporters.ts:48`, `activities.ts:43`, `contacts.ts:29,44`, `leaderships.ts:41,73,114`, `municipalities.ts:140-169`, `leaderships.ts:86-129`, `contacts.ts:23-100` | duplication (policy spelled N times) | medium — a rule change must land in 7 spots | M | P2 | closed 2026-07-29 (P3-D): `advisorMunicipalityScopeWhere` + `resolveActorScopedRead` + `resolveAccessibleIds` in `access/shared.ts`; branches collapsed; `LeadershipConsentDescriptor` deleted (full `ConsentDescriptor` now); contacts gained the unrestricted early return; guard spec bans the fragment outside `access/shared.ts` (8-file allowlist); unit pin `accessShared.unit.spec.ts`; 807 unit + 483 int green. The `allocationDecisions`→`municipalityUpdates` alias was verified CORRECT (deliberate same-policy reuse, documented) and stays | P3-D |
|
||||
| "Resolve display names by ids" spelled 7× (one already a type-losing slug-union → `Record<string,unknown>`) with ~8 admin bypasses missing the mandatory justification comment (plus ~10 more across WebAuthn ceremony, import token, phone invariant, consent, invite, map/page data) | `campaignDashboardData.ts:57-74`, `leadershipData.ts:56-72`, `organizationData.ts:64-79`, `campaignDemandData.ts:67-133,250-259`, `stateDeputyData.ts:251-266`, `votePledgeData.ts:130-146`, `activityDetailPageData.ts:25-51`, `municipalityUpdatePageData.ts:86-99` | duplication + convention breach | low-medium | M | P2 | closed 2026-07-29 (P3-E): typed wrappers in `loadNamesByIds.ts` (bypass comment once; the `Record<string,unknown>` union died); ratchet guard in `codebaseConventions` pins per-file undocumented-bypass counts (only shrinks); plan-named sites documented | P3-E |
|
||||
| Pledge aggregate fold spelled twice — list vs dossier surfaces must never disagree (E9 had to add `lastPledgeAt` to both); staff scope-read proliferation ×3 sharing a 5-field base select (E11's own-read documented) | `votePledgeData.ts:53-107` vs `votePledgeViews.ts:80-108`; `campaignMunicipalityScope.ts:39-59`, `municipalityTriggers.ts:202-243`, `visitPlannerData.ts:162-188` | duplication | medium — two surfaces can silently diverge | M | P2 | closed 2026-07-29 (P3-E): `aggregatePledgesWhere` delegates the bucket fold to `aggregateMunicipalityPledgesFromRows` (int pin: dossiê === lista); `loadMunicipalityScope(payload, user, where, { extraSelect })` with extras in the React `cache()` key (unit pin: cache-key separation), adopted by `municipalityTriggers` — its own município read died | P3-E |
|
||||
| `MUNICIPALITY_CATALOG_CACHE_TAG` lives beside a `next/cache` import (no `server-only`) and is imported by a pure module — latent bundle hazard of the B14/B34+ class; `electionCache.ts` (tag alone) is the correct precedent | `src/utilities/municipalityRevalidation.ts:1,11` + importers `revalidateRequest.ts:3`, `municipalityPortfolioIndex.ts:9` | boundary | low | S | P2 | closed 2026-07-29 (P3-E): tag moved to `municipalityCatalogCache.ts` (tag alone, `server-only`, the electionCache precedent); `municipalityRevalidation.ts` marked `server-only` | P3-E |
|
||||
| List-URL machine family spelled per domain: sort-toggle ×4, sort-value codec ×3, multi-toggle+page-reset ×4, canonicalize-round-trip ×6, serialization tail ×4; plus `firstValue` ×3 spellings, `XNotFoundError` ×3, `parseRegionsParam`+`canonicalTerritoryBySearchValue` ×2 (bundle-constrained — can't join `campaignListUrl`) | `municipalityListUrl.ts:426,440,451,508`, `leadershipListUrl.ts:192,203,216`, `stateDeputyListUrl.ts:158,165,178,202`, `territoryListUrl.ts:130,138,150,189`, `detailTabUi.ts:8-10`, `municipalityUpdatePageData.ts:39-42` | duplication | medium | M | P2 | partially closed 2026-07-29 (P3-F): `createSortToggleHref` adopted ×4; `territoryRegionParam.ts` (byte-identical parser died ×2, bundle-constrained as planned); `createEntityNotFoundError` adopted ×3. **Remaining open (defer + trigger: 3rd sort dropdown or a 5th multi-filter):** sort-value codec ×3, multi-toggle+page-reset ×4, canonicalize-round-trip ×6, serialization tail ×4, `firstValue` private spellings ×3 | P3-F |
|
||||
| List-URL machine family spelled per domain: sort-toggle ×4, sort-value codec ×3, multi-toggle+page-reset ×4, canonicalize-round-trip ×6, serialization tail ×4; plus `firstValue` ×3 spellings, `XNotFoundError` ×3, `parseRegionsParam`+`canonicalTerritoryBySearchValue` ×2 (bundle-constrained — can't join `campaignListUrl`) | `municipalityListUrl.ts:426,440,451,508`, `leadershipListUrl.ts:192,203,216`, `stateDeputyListUrl.ts:158,165,178,202`, `territoryListUrl.ts:130,138,150,189`, `detailTabUi.ts:8-10`, `municipalityUpdatePageData.ts:39-42` | duplication | medium | M | P2 | partially closed 2026-07-29 (P3-F): `createSortToggleHref` adopted ×4; `territoryRegionParam.ts` (byte-identical parser died ×2, bundle-constrained as planned); `createEntityNotFoundError` adopted ×3. **Remaining open (defer + trigger: 3rd sort dropdown or a 5th multi-filter):** sort-value codec ×3, multi-toggle+page-reset ×4, canonicalize-round-trip ×6, serialization tail ×4, `firstValue` private spellings ×3. **Verified 2026-07-31 (Pass 4): trigger NOT fired** — still 3 codecs / 4 multi-toggles; leadership has no sort dropdown codec | P3-F |
|
||||
| Legacy filter shells `ActivityFilters`↔`SupporterFilters` duplicate ~50 lines each and bypass `useCampaignListFilterNavigation` (they lose its no-op navigation guard — an RSC round trip fires even when re-selecting the active value); `CampaignSearchForm` hand-serializes `?q=` past the canonical builders (latent: first future filter param silently stripped on search submit); search idiom split (4 lists debounce vs 3 explicit submit, no recorded decision) | `components/campaign/activity/ActivityFilters.tsx:80-146,185-213`, `components/campaign/supporter/SupporterFilters.tsx:109-146,156-227`, `components/campaign/shared/CampaignSearchForm.tsx:30-31,41-43` | duplication + latent bug | medium | M | P2 | closed 2026-07-29 (P3-F): `useCampaignFilterValues` (committed-href no-op guard) + `CampaignCollapsibleFilterPanel` extracted and adopted by both shells; `CampaignSearchForm.filterParams` carries the canonical serialization of active filters (organizações adopted — `?kind=` survives a search submit); search idiom unified on 1 s debounce everywhere (`SEARCH_DEBOUNCE_MS`), explicit submit kept as the immediate path in `CampaignSearchForm` — decision recorded here | P3-F |
|
||||
| Scripts re-spell the CLI skeleton: `die` ×7, `sha256` ×5, download→cache→sha256 ×5 (~20 lines each), `writeJson/writeText` ×5 (the newest script copied the OLD form — live drift), `cacheDir` ×5, dotenv preamble ×6; **two municipality-name resolvers diverged** — the supporter/CSV path lacks the TSE alias table and rejects 4 of 5 curated spellings ("Camacã", "QUINJINGUE", "Santa Terezinha", "Muquém do São Francisco"); `LOCAL_HOSTS` ×2 diverged; one-shot `generate-remodel-municipalities-migration.mjs` left behind holding 2 allowlist entries | `scripts/seed-tse-results.mjs`, `build-bahia-geometries.mjs`, `build-municipality-zone-geometries.mjs`, `build-bahia-demographics.mjs`, `build-bahia-tse-city-codes.mjs`, `seed-posts.mjs`, `db-pull.mjs`; `lib/schemas/supporter.ts:19-28` vs `lib/electionResults.ts:120-184` | duplication + latent import bug | medium | M | P2 | closed 2026-07-29 (P3-H): `scripts/lib/cli.mjs` (`dieWithLabel`/`sha256Hex`/`loadCliEnv`/`ensureCachedDownload`/`writeRepoFile`/`LOCAL_HOSTS`/`isRemoteDbOverrideSet`) adopted by 10 scripts, artifacts proven byte-identical (only `accessed` dates moved, reverted); **latent bug fixed**: single fold `municipalityNameAliases.ts` — supporter import now accepts all 5 curated spellings, pinned for BOTH paths in `municipalityNameAliases.unit.spec.ts`; db-pull gained the `postgres` host (deliberate); one-shot deleted + 2 allowlist entries freed; CLI-skeleton ban guard (die/sha256/dotenv/LOCAL_HOSTS) | P3-H |
|
||||
| **`utilities/` subfoldering trigger (Pass 2 D1) has FIRED:** 122 top-level modules (was 92); new in 30 days (`git --diff-filter=A`): municipality +21, supporter +10, territory +5, leadership +5, webauthn +5, visit +4 — 6 domains past "3rd new module in a month". Pure modules stranded in the layer: `campaignTime.ts` (140 lines, zero imports), `relationship.ts` (33 lines, zero imports) → `lib/` | `src/utilities/` | organization | low (path churn) | M | P2 | closed 2026-07-29 (P3-J): 41 modules moved — `municipality/` (incl. `campaignMunicipalityScope`, `goalCoverage`), `supporter/`, `territory/` (+`loadTerritoryOverview`), `leadership/`, `visit/`, `webauthn/`; direct imports, no barrels; `campaignTime` + `relationship` → `lib/`; top-level of `src/utilities/` pinned in `codebaseConventions` (D1 trigger encoded — new top-level module fails the build unregistered); `codebase-map.mdc` + `ARCHITECTURE.md` updated in the same delivery | P3-J |
|
||||
| `createdBy`/actor-stamp field duplicated verbatim across 5–7 collections + stamping hook ×3 (organic drift already: Supporter/Leadership grew `read:`, CampaignInvite is `required`); a field factory precedent exists (`voteEstimateScenarioFields`); `trimmedText` ×4 byte-identical in collection configs (pure helper outside `lib/`) | `CampaignDemand.ts:382-394`, `Organization.ts:121-133`, `StateDeputy.ts:105-117`, `AllocationDecision.ts:140-151`, `VotePledge.ts:211-222`, `Supporter.ts:214-227`, `Leadership.ts:222-234`; hooks `Organization.ts:54-62`, `StateDeputy.ts:54-62`, `Leadership.ts:63-69` | duplication | low | S | P3 | closed 2026-07-29 (P3-I): `campaignAuditFields.ts` (`systemStampedActorField` with `required`/`indexed`/`readAccess` data + `stampCampaignCreatedBy`) adopted by 9 collections — zero schema delta (`declaredBy` keeps `indexed: false`, no migration); `trimmedText` → `lib/text.ts` ×4; guard bans hand-spelled actor fields | P3-I |
|
||||
| Route layer: W4d filename guard escapes — 7 hand-rolled form-action ladders in `actions/*.ts` invisible to the `*FormActions.ts` guard (3 expressible today with zero delta: profile avatar ×2, leaderSupporter); **convite-login exception stale** (not allowlisted; now expressible via `runCampaignRedirectFormAction`); `password.ts`/`auth.ts` genuinely bespoke but unregistered; staff-page prologue ×22 (~110 lines) with `return null` vs `redirect` divergence ×5; route body schema re-spelled instead of omit/extend (`municipios/advisors/route.ts:16-20`); `requireConsentByKey` called raw where the named wrapper exists (`leaderSupporter.ts:99-104`) | `actions/profile.ts:37-105,107-151`, `actions/leaderSupporter.ts:99-104,151-190`, `campanha/convite/[token]/formActions.ts:68-98`, 22 `(app)` pages | duplication + convention breach | low-medium | M | P3 | partially closed 2026-07-29 (P3-I): `requireCampaignPageActor({gate, redirectTo})` adopted by all 30 `(app)` pages — the `return null` ×5 divergence died (missing session now always redirects to login); guard bans `getCampaignUser()` in route pages. **Remaining open (defer + trigger: the next bespoke ladder or route schema):** 7 hand-rolled `actions/*.ts` ladders (3 expressible: profile avatar ×2, leaderSupporter), stale convite-login exception, password.ts/auth.ts registration in the W4d guard widening, `municipios/advisors/route.ts` omit/extend, `requireConsentByKey` raw call in leaderSupporter | P3-I |
|
||||
| Route layer: W4d filename guard escapes — 7 hand-rolled form-action ladders in `actions/*.ts` invisible to the `*FormActions.ts` guard (3 expressible today with zero delta: profile avatar ×2, leaderSupporter); **convite-login exception stale** (not allowlisted; now expressible via `runCampaignRedirectFormAction`); `password.ts`/`auth.ts` genuinely bespoke but unregistered; staff-page prologue ×22 (~110 lines) with `return null` vs `redirect` divergence ×5; route body schema re-spelled instead of omit/extend (`municipios/advisors/route.ts:16-20`); `requireConsentByKey` called raw where the named wrapper exists (`leaderSupporter.ts:99-104`) | `actions/profile.ts:37-105,107-151`, `actions/leaderSupporter.ts:99-104,151-190`, `campanha/convite/[token]/formActions.ts:68-98`, 22 `(app)` pages | duplication + convention breach | low-medium | M | P3 | partially closed 2026-07-29 (P3-I): `requireCampaignPageActor({gate, redirectTo})` adopted by all 30 `(app)` pages — the `return null` ×5 divergence died (missing session now always redirects to login); guard bans `getCampaignUser()` in route pages. **Remaining open (defer + trigger: the next bespoke ladder or route schema):** 7 hand-rolled `actions/*.ts` ladders (3 expressible: profile avatar ×2, leaderSupporter), stale convite-login exception, password.ts/auth.ts registration in the W4d guard widening, `municipios/advisors/route.ts` omit/extend. **Verified 2026-07-31 (Pass 4):** new actions (notifications/webauthn/suggestion/wizardLeadership) DO use the shared wrappers — count holds at 7; the raw `requireConsentByKey` call is gone (leaderSupporter uses `requireSupporterRegistrationConsent`); 33/34 pages on `requireCampaignPageActor` + 1 redirect (`assessores/novo`, guard covers only `getCampaignUser`) | P3-I |
|
||||
| Convention guards dodgeable: banned-vocabulary regex `/praç|núcleo/i` misses unaccented `praca`/`nucleo` (the sibling "Plano de Ação" row was hardened accent-tolerant); `server-only` guard sweeps only `payload\|@payload-config` value imports — `next/cache`/`next/server` importers (`municipalityRevalidation.ts`, `campaignJsonMutationRoute.ts`) are server-bound and invisible; no mechanism stops a client component value-importing the three client-safe modules that drag the 623 KB TSE artifact (`municipalityTerritorialClass`, `municipalityPotential`, `territoryIntraCaptureBenchmark` — B13 avoided by discipline) | `tests/unit/codebaseConventions.unit.spec.ts:50,71-80` | tooling gap | low | S | P3 | closed 2026-07-29 (P3-K): vocabulary regex now `/pra[çc]\|n[úu]cleo/i` (2 genuine-data hits allowlisted); `server-only` sweep widened to `next/cache`/`next/server`/`next/headers` (`campaignJsonMutationRoute` marked); ESLint `no-restricted-imports` bars value-imports of the 3 artifact carriers + the artifact itself from `src/components/**` (type imports safe — measured: the 2 current consumers are type-only) | P3-K |
|
||||
| Constants/types single-sourcing cluster: territorial-class union declared twice across the lib/utilities boundary (adding a class compiles green and mis-sorts); `DAY_MS` ×3 (one exported from `suggestionCatalog`'s public interface); `1_000_000` vote-count ceiling ×6 across two validation layers; password `8/128` + token `20/256` policies ×2; TSE baseline years mirrored (documented excuse dies with a leaf module); `formatBrazilianPhoneDisplay` near-twin (1 call site); `advisorMunicipalitiesBatchSchema` missing the dedup transform its 3 siblings have; `voteEstimate` formatters re-spell the pt-BR format without the E18 zero-fraction pin; `hasAnyEstimate` weaker twin of `hasAnyVoteEstimate` | `lib/territorialClassSortWeight.ts:10-13`, `lib/suggestionCatalog.ts:15-19,175`, `lib/engagementLevel.ts:97`, `utilities/municipalitySignal.ts:32`, `lib/formData.ts:123-133`, `lib/schemas/votePledge.ts:9,32`, `lib/schemas/invite.ts:11,39`, `lib/voteEstimate.ts:27,102-126`, `lib/schemas/advisor.ts:69-72` | shotgun surgery | low-medium | S each | P3 | closed 2026-07-29 (P3-K): territorial-class union → `lib/territorialClassAnchors.ts` (type error on new class now); `DAY_MS` → `lib/text.ts` (out of `suggestionCatalog`'s public interface); `MAX_VOTE_COUNT` in `lib/schemas/primitives.ts` ×6 folded; `campaignPasswordSchema` exported (invite delegates); `lib/electionYears.ts` leaf (mirror + documented excuse died); `formatBrazilianPhoneDisplay` delegates; batch-schema dedup transform (action's `[...new Set]` died); `formatElectionNumber` in the 3 `voteEstimate` formatters; `hasAnyEstimate` deleted | P3-K |
|
||||
| Form-action feedback JSX at 15+ call sites with `aria-live` on only 5 — **10 error alerts silent to AT**; success→toast effect ×7; borderless inline-input class ×4 verbatim; deprecated `showLabels` prop with zero call sites (knip can't see props) | 15 forms incl. `LeadershipForm.tsx:162`, `DemandForm.tsx:152`, `MunicipalityUpdateForm.tsx:168`, `PledgeEstimateForm.tsx:74`; `VoteEstimateScenarioStrip.tsx:18,27,32` | a11y + duplication | medium (a11y) | S–M | P3 | closed 2026-07-29 (P3-G): `CampaignFormActionMessage` (aria-live on both variants + `formFieldErrors`) adopted by all 15+ sites — every alert now announces; `useCampaignFormSuccessToast` adopted ×7; `campaignInlineInputClassName` ×4; `showLabels` deleted; JSX ban guard + a11y pin (`campaignFormActionMessage.unit.spec.tsx`, unit include extended to `.tsx`) | P3-G |
|
||||
|
||||
| homeSearch text-search pipeline re-spelled per domain (gate staff → normalize → `contains` + `limit: 0` → word-start → relevance → cap), plus **unbounded `limit: 0` reads** ×5 before the in-memory refine | `src/utilities/homeSearch/searchHomeActivities.ts` (88) ↔ `searchHomeDemands.ts` (100), sim 0.72 (~63 shared lines); leaderships 0.53; `limit: 0` at `searchHomeActivities.ts:43`, `Demands:49`, `Leaderships:61`, `Advisors:37`, `StateDeputies:45` | duplication + scale | medium — every keystroke can pull a full collection slice | M | P2 | open — Pass 4 | P4-A |
|
||||
| HomeSearch result groups ×6 near-verbatim (Advisor↔Leadership, Activity↔Demand almost line-for-line; differ only in id/title/href/secondary) | `src/components/campaign/dashboard/HomeSearch*Group.tsx` (40–63 lines each; Municipality is the real variant) | duplication | low | M | P2 | open — Pass 4 | P4-B |
|
||||
| `notificationEvents` re-spells names-by-id ×7 instead of the P3-E wrappers | `src/utilities/notification/notificationEvents.ts` (`:41-57`, `:143-174`, `:212-236` — `findByID` + `select:{name}`/`{name,slug}`) | duplication | low | S | P2 | open — Pass 4 | P4-C |
|
||||
| Notification write path: `markAllCampaignNotificationsRead` updates up to 200 rows with no transaction; `subscribeCampaignPush` validates with `.trim()` only and always creates (re-subscribe → unique violation → generic error); `unsubscribeCampaignPush` deletes with `overrideAccess: true` while `canDeleteOwnPushSubscriptions` already scopes the owner | `src/app/(campaign)/campanha/actions/notifications.ts:69-91,100-143,172-176` | correctness | medium — partial mark-read on mid-batch failure; hostile re-subscribe UX; bypass surface | S | P2 | open — Pass 4 | P4-D |
|
||||
| Wizard info Drawer chrome spelled ×3 (button → bottom sheet with title+description+body); skip trailing spelled ×2 (13 lines each, both `{label, href}`) | `WizardSignalTypeStep.tsx:95-112`, `WizardTrendChoiceStep.tsx:115-132`, `WizardLeadershipStep.tsx:167-179`; `WizardSignalSkipTrailing.tsx` ↔ `WizardTrendSkipTrailing.tsx` | duplication | low | S | P2 | open — Pass 4 | P4-E |
|
||||
| `MunicipalityListMobileCards` (client) value-imports `municipalityColumnLabels` from `municipalityListUrl` — a ~19 KB URL module (pulls `isMunicipalitySlug` → `municipalityCatalog`) for a string map whose type already lives in `municipalityLabels` | `src/components/campaign/municipality/MunicipalityListMobileCards.tsx:26` | boundary (B14 class) | low | S | P2 | open — Pass 4 | P4-F |
|
||||
| `HomeSearchProvider` wraps the whole chrome incl. `actions` + `summarySlot` non-consumers, and the provider value changes on every keystroke | `src/components/campaign/dashboard/CampaignHomeStaffChrome.tsx:41-56`, `useHomeSearchQuery.ts:63-74` | state placement | low — actions/summary re-render per keypress | S | P2 | open — Pass 4 | P4-G |
|
||||
| Unconditional `aria-live="polite"` per chip cell (~50 always-registered regions on lideranças); the overlay family already lazy-mounts its region from first open | `src/components/campaign/shared/RelationChipCell.tsx:821-823` vs `CampaignCellEditOverlay.tsx:114-120` | a11y scale | low | S | P2 | open — Pass 4 | P4-G |
|
||||
| Bypass ratchet dodge: one "bypass" comment in the first 40 lines documents **every** `overrideAccess: true` in the file — `notification/*` has 20, `campaignWebAuthnCeremony` 4, zero per-call justifications | `codebaseConventions.unit.spec.ts:541-623`; `src/utilities/notification/*`; `src/utilities/webauthn/campaignWebAuthnCeremony.ts` | guard dodge | medium — the P3-E "comment justifying" intent is satisfied in letter, not spirit | M | P2 | open — Pass 4 | P4-H |
|
||||
| `campaignLeaderships` chip-cell spec fails **deterministically in dev mode** on clean stage (4/4 runs; stash-and-compare 2026-07-31): instrumented with console/response listeners — **no POST fires in 60 s**; signatures vary per run (option visible + click no-ops; option count 1 → 0 after first fill). Prod build green (48/48 full suite ×3 the same day). NOT the P3-C hydration class (input onChange works, listbox renders); a `toPass` retry was attempted and reverted — retrying cannot fix what is not a race | `tests/e2e/campaignLeaderships.e2e.spec.ts:91-101`; suspect surface `RelationChipCell` suggestion state vs dev-mode RSC streaming of `municipalityIndex` | test-infra / possible dev-mode cell bug | medium — a red local signal teaches people to skip the spec | M | P2 | open — Pass 4 (debug workflow, not retry; `E2E_PROD=1` is the honest local mode meanwhile) | P4-L |
|
||||
| God modules on the delta: `municipalityPageData` 548 lines / `loadMunicipalityListPageBundle` 168; `municipalityListUrl` 500 lines / 23 exports (widest interface in utilities); `RelationChipCell` 826 lines / ~11 state hooks (measurement 80, commit 140, render 190) | `src/utilities/municipality/municipalityPageData.ts:307`, `src/utilities/municipality/municipalityListUrl.ts`, `src/components/campaign/shared/RelationChipCell.tsx` | bloater | low | L | P2 | open — Pass 4 (defer + trigger: next facet/sort; next chip relation or AT-scale complaint) | |
|
||||
| `createSupporter` does not mark `touchedMunicipalities` — a supporter-only fixture path can leave strategy fields dirty on the row | `tests/helpers/campaignFixtures.ts:769-799` | test-infra honesty | low | S | P3 | open — Pass 4 | |
|
||||
| `(app)/assessores/novo/page.tsx` redirects without `requireCampaignPageActor` — the P3-I guard bans `getCampaignUser()` in pages but not a missing actor helper (1 of 34 pages) | `src/app/(campaign)/campanha/(app)/assessores/novo/page.tsx:1-6` | convention gap | low (layout still session-gates) | S | P3 | open — Pass 4 | |
|
||||
| `deriveDecisionAuthor` re-spells the actor stamp (`data.decidedBy = req.user.id`) while `campaignAuditFields.ts` owns the pattern (`stampCampaignCreatedBy` hardcodes `createdBy`) | `src/collections/AllocationDecision.ts:53-57` | duplication | low | S | P3 | open — Pass 4 | |
|
||||
| homeSearch int suites cloned ×7 (656 LOC of same-shaped `it('returns empty…'/'scopes…'/'rejects leaders…')` — `describe.each` over domains would own the shared contract | `tests/int/homeSearch*.int.spec.ts` | duplication | low | M | P3 | open — Pass 4 | |
|
||||
| Flake inventory: fixed timeouts (`CEREMONY_TIMEOUT=60s`, HomeActions `timeout: 15000` ×many, ColumnPicker `REFRESH=20s`), `Date.now()` markers in fixture names, e2e deep links now claimed (Pass 4 PR #93) but shared leased consent rows remain load-bearing | `tests/e2e/*`, `tests/int/municipalityPageData.int.spec.ts` | flake risk | low | M | P3 | open — Pass 4 (prefer event waits; allocate municipalities — both guarded since Pass 4) | |
|
||||
|
||||
## Smell Inventory
|
||||
|
||||
| Smell | Location | Refactoring | Status |
|
||||
|
|
|
|||
|
|
@ -4,9 +4,9 @@
|
|||
|
||||
Three layers, all local-database-only (see `local-database` skill; tests refuse any database not ending in `_test`):
|
||||
|
||||
- **Unit** (`tests/unit/**/*.unit.spec.ts`, 47 specs as of 2026-07-25 (tooling guards)): pure logic; runs with a deliberately invalid `DATABASE_URL` so nothing can touch a database.
|
||||
- **Integration** (`tests/int/**/*.int.spec.ts`, 48 specs): real Payload + Postgres (`teqo_test`); campaign fixtures allocate seeded municipalities via a Postgres sequence so parallel spec files never share one, and purge residue on claim. Shared consent rows with stable keys are leased (`tests/helpers/testDatabaseLease.ts`), never owned.
|
||||
- **E2E** (`tests/e2e`, Playwright, 7 specs): smoke-level; most campaign behavior is pinned at the int layer.
|
||||
- **Unit** (`tests/unit/**/*.unit.spec.ts`, 117 specs as of 2026-07-31, Pass 4 — +6 convention guards land with the Pass 4 remediation PRs): pure logic; runs with a deliberately invalid `DATABASE_URL` so nothing can touch a database.
|
||||
- **Integration** (`tests/int/**/*.int.spec.ts`, 71 specs): real Payload + Postgres (`teqo_test`); campaign fixtures allocate seeded municipalities via a Postgres sequence so parallel spec files never share one, and purge residue on claim. Since the miss #73 guardrail (Pass 4), specs that mutate municipalities must use the allocator (no pinned slugs) — enforced by `testMunicipalityAllocatorConventions`. Shared consent rows with stable keys are leased (`tests/helpers/testDatabaseLease.ts`), never owned.
|
||||
- **E2E** (`tests/e2e`, Playwright, 17 specs / 48 cases): smoke-level; most campaign behavior is pinned at the int layer.
|
||||
|
||||
CI (`.github/workflows/ci-pr.yml`) runs parallel jobs — lint (zero warnings), format, typecheck, knip, cycles, unit/int (**affected by the PR diff**: `vitest --changed` against the PR base via `scripts/test-affected.mjs`, full-suite fallback on high-risk paths), e2e (manifest path→spec via `scripts/e2e-affected.mjs` + `scripts/lib/e2e-affected-manifest.mjs`, blocking; production build + `list`/`github` reporters in CI) and build — against a Postgres 17 service with **`db:seed:minimal`**. Push to `main` (`ci.yml`) always runs the full net including e2e.
|
||||
|
||||
|
|
|
|||
187
docs/plans/entrega-engenharia-p4.md
Normal file
187
docs/plans/entrega-engenharia-p4.md
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
# Pass 4 — Auditoria de engenharia, remediação P0/P1 e guardrails das misses
|
||||
|
||||
## Contexto e manchetes
|
||||
|
||||
- **Executado em 2026-07-31, no modo autônomo (Cursor Cloud)** da skill `engineering-audit`: precheck solitário verde (`agent:gh-doctor` OK; pool **desligado**), varredura completa, remediações P0/P1 e guardrails das misses colhidas na mesma sessão, cada um em PR próprio com gate completo. O sign-off dos artefatos é o merge humano deste PR.
|
||||
- **Âncora de delta:** Pass 3 (2026-07-28). Em 3 dias o paradigma de agentes tocou **880 arquivos** — homeSearch (8 módulos + ~20 componentes), notificações/push (2 collections + 5 módulos), wizard (~12 componentes), WebAuthn (4 rotas + 4 módulos), 5 migrations. A varredura cobriu o repo inteiro com esforço concentrado no delta.
|
||||
- **Baseline verde:** `tsc` 0 erros, `lint` 0 warnings, `knip` 0 findings (ruído conhecido do `payload.config.ts`, ledger P3), madge 0 ciclos (**774 arquivos**, +124 desde o Pass 3).
|
||||
- **Manchetes (todas medidas):**
|
||||
- **1 P0:** os 4 globals públicos (`site-settings`, `home`, `metadata`, `privacy-policy`) com `update: Boolean(user)` — JWT de campanha (liderança inclusa) podia **editar o site público** via `/api/globals/*`. Coleções foram travadas na Fase 0; os globals ficaram. Remediado ([PR #91](https://github.com/fsolla/teqo/pull/91)).
|
||||
- **1 P1:** `seed-posts.mjs` chamava `dieWithLabel` **sem importar** — ReferenceError no caminho de abort de fetch vazio, invisível a lint/tsc/knip. Meia-adoção do P3-H na mesma passada que criou a guarda da CLI. Remediado ([PR #92](https://github.com/fsolla/teqo/pull/92)).
|
||||
- **Harvest de 5 `kind:agent-miss` → 5 guardrails:** #73 (classe **reproduzível** — 7 violações int + 11 deep links e2e; migração para o alocador + guarda de 3 regras, [PR #93](https://github.com/fsolla/teqo/pull/93)); #52 (resíduo de Drawer por célula em 3 listas de chips; provider ×3 + invariante dev + guarda, [PR #94](https://github.com/fsolla/teqo/pull/94)); #53 (probe one-shot correndo com o autenticador CDP; portão de readiness + pin, [PR #95](https://github.com/fsolla/teqo/pull/95)); #54 (0 pares goto→goto não assentados medidos; guarda calibrada + convenção, [PR #95](https://github.com/fsolla/teqo/pull/95)); #83 (guarda `agent:ship` **já viva** via PR #89 — verificada e fechada sem guardrail novo).
|
||||
- **Honestidade de flake:** `campaignLeaderships` (chips) falha **deterministicamente em dev** na árvore limpa (instrumentado: nenhum POST dispara; não é a classe de hidratação P3-C). Prod verde 48/48. **Registrado, não "consertado" com retry** — retry mascara o que não é corrida.
|
||||
- **2 linhas do ledger stale fechadas:** safeMessages (P3-A) e consentId de petição (P3-B), verificadas no código.
|
||||
- **Duplicação medida para o plano:** gêmeos de busca homeSearch (~63 de ~90 linhas compartilhadas ×2), HomeSearch\*Group ×6 (2 pares quase verbatim), Wizard info Drawer ×3, WizardSkipTrailing ×2, names-by-id ×7 em notificationEvents, shell de sucesso vazio ×2.
|
||||
- **12 dodges de guarda registrados** (mapa de guardas abaixo) — 3 endurecidos na sessão, o resto vira workstream P2.
|
||||
|
||||
## Protocolo de delta de comportamento (inalterado do Pass 3)
|
||||
|
||||
Consolidação pode mudar saídas pequenas (copy, ordenação, timing) desde que **cada delta seja listado por item**; pins existentes são a rede de caracterização, atualizados **deliberadamente** na mesma entrega; contratos congelados (URL B18, schema, Consent/LGPD) fora de alcance sem item nomeado.
|
||||
|
||||
## Ondas já executadas na sessão (P0/P1 + misses)
|
||||
|
||||
| PR | Conteúdo | Guarda (classe) |
|
||||
| --------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
|
||||
| [#91](https://github.com/fsolla/teqo/pull/91) | **P0** globals públicos → `payloadAdminOnly` ×4; bell loaders de notificação tipados pelo ator, sem bypass | `globalAccessConventions` (3); assinatura por ator (1); pins int de lockdown ×17 |
|
||||
| [#92](https://github.com/fsolla/teqo/pull/92) | **P1** import do `dieWithLabel`; inversão tipo lib→utilities; `DAY_MS` fold; comentário de bypass stale | `scriptCliConventions` (3); ESLint lib `allowTypeImports:false` (2); `localApiOverrideAccessConventions` (3) |
|
||||
| [#93](https://github.com/fsolla/teqo/pull/93) | **#73** migração int+e2e para o alocador (classe-agnóstico no teste de classe); 11 deep links → `claimMunicipality()` | `testMunicipalityAllocatorConventions`: fixed-slug+mutação, `municipio=<slug>`, regex de nome não ancorada (3) |
|
||||
| [#94](https://github.com/fsolla/teqo/pull/94) | **#52** `CampaignListSheetProvider` em lideranças/dobradinhas/assessores | `sharedSheetHostConventions` (3); invariante dev no overlay (5); fallback lazy mantido |
|
||||
| [#95](https://github.com/fsolla/teqo/pull/95) | **#53** `expectCampaignBiometricsReady` ×2; **#54** guarda de goto não assentado + convenção TESTING.md | `e2eNavigationConventions` (3); convenção doc declarada judgment-only (6) |
|
||||
|
||||
Gate de cada PR: `pnpm push` (espelho serial do ci-pr: lint, format, typecheck, knip, cycles, unit, int, build, e2e afetado ou full conforme blast radius). Evidência: full e2e prod **48/48** em 3 das 5 execuções; int full **71/529+** em todas.
|
||||
|
||||
## Ondas do plano (P2, ordenadas por risco × churn)
|
||||
|
||||
### Onda 1 — Consolidação do delta (homeSearch + notificações)
|
||||
|
||||
#### P4-A Runner único de busca textual do homeSearch
|
||||
|
||||
- **Meta:** `runHomeSearchTextHits({ collection, where, select, mapDoc, tieBreak, cap })` + mappers finos por domínio.
|
||||
- **Evidência:** `searchHomeActivities.ts` (88 linhas) ↔ `searchHomeDemands.ts` (100) com similaridade 0,72 (~63 linhas compartilhadas: gate staff → normalize → `contains` + `limit: 0` + `overrideAccess: false` → word-start → `compareHomeSearchNameRelevance` → cap); leaderships 0,53.
|
||||
- **Conhecimento duplicado (nomeado):** o pipeline staff de hits textuais da busca global.
|
||||
- **Delta de comportamento:** nenhum se caracterizado pelos pins int existentes ×7 suites; caps/rankings inalterados.
|
||||
- **Guarda determinística:** convenção — novo `searchHome*` deve usar o runner (classe 3); pin de acesso por papel por grupo (classe 5, já existe em parte).
|
||||
- **Na mesma entrega:** helper `emptyHomeSearchSuccess(resultKind)` (2 shells idênticos, `searchHomeMunicipalities:23-33` vs `loadHomeSearchSuggestions:77-87`).
|
||||
|
||||
#### P4-B HomeSearch\*Group ×6 → grupo configurável
|
||||
|
||||
- **Meta:** `HomeSearchHitGroup` como dado (`groupId`, `title`, `selectHits`, `rowProps`); Municipality fica como variação estendida (territórios + trailing de votos).
|
||||
- **Evidência:** 6 componentes de 40–63 linhas; Advisor↔Leadership e Activity↔Demand quase linha a linha (diferem id/título/href/secundário).
|
||||
- **Conhecimento:** grupo de hits orçado → seção → `HomeSearchHitRow`.
|
||||
- **Guarda:** snapshot/unit por groupId (aria-label + href) — classe 5.
|
||||
|
||||
#### P4-C notificationEvents → `loadNamesByIds` (P3-E)
|
||||
|
||||
- **Meta:** os 7 `findByID` + `select:{name}`/`{name,slug}` de `notificationEvents.ts` (254 linhas) viram os wrappers tipados do P3-E (estender se slug precisar).
|
||||
- **Conhecimento:** resolver nomes de exibição por ids já autorizados — dono único desde 2026-07-29.
|
||||
- **Guarda:** convenção banindo `findByID`+`select:{name}` ad hoc fora do allowlist (classe 3, mesma família do ratchet P3-E).
|
||||
|
||||
#### P4-D Write-path de notificações/push fail-closed
|
||||
|
||||
- **Meta:** (a) `markAllCampaignNotificationsRead` em `withPayloadTransaction` (até 200 updates — hoje falha parcial visível); (b) `subscribeCampaignPush` com schema zod em `lib/schemas` + upsert por `(user, endpoint)` (re-subscribe hoje estoura unique → erro genérico); (c) `unsubscribeCampaignPush` delete com `overrideAccess: false` (a policy `canDeleteOwnPushSubscriptions` já escopa o dono — bypass desnecessário).
|
||||
- **Evidência:** `actions/notifications.ts:69-91,100-143,172-176`.
|
||||
- **Guarda:** int — falha injetada no N-ésimo update ⇒ 0 ou todos marcados (5); segundo subscribe com o mesmo endpoint ⇒ sucesso (5).
|
||||
|
||||
### Onda 2 — Wizard + camada de apresentação do delta
|
||||
|
||||
#### P4-E Wizard info Drawer ×3 → `WizardInfoDrawer`
|
||||
|
||||
- **Evidência:** chrome idêntico em `WizardSignalTypeStep.tsx:95-112`, `WizardTrendChoiceStep.tsx:115-132`, `WizardLeadershipStep.tsx:167-179` (botão info → bottom sheet com título + descrição + corpo); grids de tiles ficam locais (dado de domínio). Na mesma entrega: `WizardSignalSkipTrailing`/`WizardTrendSkipTrailing` (13 linhas cada, tipos ambos `{label, href}`) → um `CampaignWizardSkipTrailing`.
|
||||
- **Guarda:** pin visual/a11y do botão info → nome do dialog (5).
|
||||
|
||||
#### P4-F `municipalityColumnLabels` fora do `municipalityListUrl` (fronteira B14)
|
||||
|
||||
- **Meta:** mover o mapa de labels para `municipalityLabels` (onde o tipo já vive) ou folha só-de-labels.
|
||||
- **Evidência:** `MunicipalityListMobileCards.tsx:26` (client) value-importa do módulo de URL (~19 KB, que puxa `isMunicipalitySlug` → `municipalityCatalog`) para ler um mapa de strings — a lição dos 21 kB do B14 na versão mobile.
|
||||
- **Guarda:** convenção/ESLint — client components não value-importam de `*ListUrl` fora de href builders allowlistados (classe 3).
|
||||
|
||||
#### P4-G HomeSearchProvider estreito + regiões vivas em escala
|
||||
|
||||
- **Meta:** (a) `HomeSearchProvider` envolve só o slot de busca — hoje envolve `actions`+`summarySlot` não-consumidores e o valor muda a cada tecla (`CampaignHomeStaffChrome.tsx:41-56`); (b) a região `aria-live` incondicional por célula de chips (`RelationChipCell.tsx:821-823` — ~50 regiões polite registradas em lideranças) monta no primeiro anúncio não-vazio, seguindo o overlay.
|
||||
- **Guarda:** pin de árvore React (ações fora do provider) (5); convenção de teto de regiões polite por lista (3).
|
||||
|
||||
### Onda 3 — Endurecimento de guardas (dodges medidos)
|
||||
|
||||
#### P4-H Ratchet de bypass: fim da "cobertura de cabeçalho"
|
||||
|
||||
- **Meta:** comentário com "bypass" nas primeiras 40 linhas deixa de zerar a contagem do arquivo inteiro; vale por-chamada (janela) ou política de módulo nomeada com contagem declarada. `notification/*` (20 bypasses) e `campaignWebAuthnCeremony` (4) recebem justificativas por bloco na mesma entrega.
|
||||
- **Evidência:** `codebaseConventions.unit.spec.ts:541-623`; P3-E pretendia "comentário justificando", a forma atual satisfaz a letra sem o espírito.
|
||||
- **Guarda:** o próprio ratchet endurecido (classe 3).
|
||||
|
||||
#### P4-I `canReadCampaignUsers` sem enumeração para liderança
|
||||
|
||||
- **Meta:** lista de staff restrita a staff/unrestricted; liderança lê a si mesma. Auditar call sites primeiro (homeSearch advisors já tem gate próprio — verificado na varredura).
|
||||
- **Evidência:** `access/campaignUsers.ts:33-34` = admin OU qualquer `isCampaignUser` (inclui `leader`); ledger P2 pré-existente, re-verificado 2026-07-31.
|
||||
- **Guarda:** pins int de acesso — líder nega lista, self permitido (5).
|
||||
|
||||
#### P4-J `authenticateCampaignToken` sem segunda leitura (RS+)
|
||||
|
||||
- **Meta:** `depth: 0` + `select` mínimo; avatar resolvido sem doc inteiro. Pins: `removePrivateAuthFields` + freshness de rebaixamento de papel (pré-requisito do ledger, mantém).
|
||||
- **Evidência:** `campaignAuth.ts:87-109` — `payload.auth` (a JWT strategy já lê o doc) + `findByID(depth: 1)` full em toda página.
|
||||
|
||||
#### P4-K Fixtures int: restos do P3-C
|
||||
|
||||
- **Meta:** (a) 28 blocos `.create({ collection: 'campaignUser' })` em 6 arquivos → `fixtures.createCampaignUser` (salvo mints de sessão legítimos); (b) `fixtures.own()` manual — 49 sites (subiu de 43) — ratchet de contagem; (c) invite-origin: pin unit é o dono (~92 linhas puras), o re-pin int (~158 linhas em `campaignInvite.int.spec.ts:394-552`) vira smoke de integração.
|
||||
- **Guarda:** convenção banindo create crua de campaignUser fora de helpers (3); ratchet `fixtures.own(` ≤ N (3).
|
||||
|
||||
### Onda 4 — Debug com evidência (não é retry, não é palpite)
|
||||
|
||||
#### P4-L `campaignLeaderships` chips em dev — causa raiz
|
||||
|
||||
- **Evidência registrada (2026-07-31):** na árvore limpa de stage, dev mode, 4/4 execuções falham no passo sugestão/commit. Instrumentação com listeners: **nenhum POST em 60s**; assinaturas variando por run — opção visível + clique sem efeito; contagem de options 1 → 0 após o primeiro fill. **Prod verde** (48/48 full). Não é a classe P3-C (onChange do input funciona, listbox renderiza). `toPass` tentado e **revertido** — retry não conserta o que não é corrida.
|
||||
- **Método:** debug subagent com hipótese + instrumentação no código da célula (estado `open`/`suggesting`, streaming RSC dev vs prod do `municipalityIndex`, `hitsKey`), não especulação.
|
||||
- **Guarda:** a pin que nascer do fix; até lá, TESTING.md registra `E2E_PROD=1` como modo honesto para o spec (feito no PR #95).
|
||||
|
||||
## Mapa de guardas (prevenção de recorrência)
|
||||
|
||||
### Guardas NOVAS na sessão (já mergeadas ou em PR próprio)
|
||||
|
||||
| Guarda | Classe | Origem |
|
||||
| ----------------------------------------------------------------------------------------- | ------ | -------------- |
|
||||
| `globalAccessConventions` — ban `Boolean(user)` em update de globals | 3 | P0 encontrado |
|
||||
| Bell loaders tipados pelo ator (recipientID não representável) | 1 | P0 adjacente |
|
||||
| `scriptCliConventions` — die-helper importado + `DAY_MS` single-source | 3 | P1 encontrado |
|
||||
| ESLint `src/lib`: `@/utilities/**` rejeita type-import | 2 | dodge P3-K |
|
||||
| `localApiOverrideAccessConventions` — `user` sem `overrideAccess`; import dinâmico server | 3 | guarda ausente |
|
||||
| `testMunicipalityAllocatorConventions` (3 regras) | 3 | miss #73 |
|
||||
| `sharedSheetHostConventions` + invariante dev no overlay | 3 + 5 | miss #52 |
|
||||
| `e2eNavigationConventions` (goto assentado + readiness WebAuthn) | 3 | misses #54/#53 |
|
||||
| Convenção de navegação e2e em TESTING.md (**judgment-only declarado**) | 6 | miss #54 |
|
||||
|
||||
### Guardas existentes endurecidas na sessão
|
||||
|
||||
- `codebaseConventions` — a família de regex ancorada ganhou a irmã de literais (regra 3 do spec #73, keyed no catálogo).
|
||||
- ESLint `src/lib` — comentário da zona agora codifica a política de módulos de contrato.
|
||||
|
||||
### Resíduo judgment-only (declarado, sem fingir que doc é guarda)
|
||||
|
||||
- Gatilhos de abstração dos itens defer+trigger (tabela abaixo) — convenção em rules/codebase-map.
|
||||
- Migração `AdvisorsTable` → `CampaignTable` e fim do modo "Editar nome e contato" — decisão de produto (edit-in-place), não de guarda.
|
||||
- `relationMembershipDelta` vs `leadershipMunicipalityMembership` (set+floor) — look-alike, não duplicação.
|
||||
|
||||
### Dodges medidos pendentes (viram workstream ou ficam registrados)
|
||||
|
||||
| Guarda | Dodge medido | Destino |
|
||||
| ----------------------------------- | --------------------------------------------------------------------------------------------------- | ------------------ |
|
||||
| Ratchet de bypass (P3-E) | header de 40 linhas cobre 20+ chamadas (`notification/*`, `webauthnCeremony`) | P4-H |
|
||||
| `*FormActions.ts` (W4d) | 7 escadas à mão em `actions/*.ts` (profile ×2, leaderSupporter, password ×4) invisíveis ao filename | defer+trigger |
|
||||
| Mensagens de recusa (P3-A) | const lançada sem presença em safelist de rota (órfã → erro genérico) | defer+trigger |
|
||||
| `getCampaignUser` em páginas (P3-I) | só cobre `page.tsx`; `assessores/novo` redirect sem `requireCampaignPageActor` (1 de 34) | defer+trigger |
|
||||
| Pin de top-level de `utilities/` | é allowlist, não disciplina de domínio | doc (codebase-map) |
|
||||
| `campaignJsonMutationRoute` | `export const POST = handler` indireto | registrado |
|
||||
| Vocabulário banido | tokens quebrados por concatenação; `.md`/`.sql` fora de migrations | registrado |
|
||||
|
||||
## Registrados como defer + gatilho (sem ID novo)
|
||||
|
||||
| Par/candidato | Medida | Gatilho |
|
||||
| ----------------------------------------------------------------------------- | ---------------------------------------------------------------------- | --------------------------------------------------- |
|
||||
| Split de `RelationChipCell` (826 linhas; máquinas overflow/membership/chrome) | 11 states, blocos de 80/140/190 linhas | próxima relação por chips OU queixa de escala de AT |
|
||||
| Codec de sort ×3 + multi-toggle ×4 + round-trip ×6 (P3-F) | verificado 2026-07-31: **gatilho NÃO disparou** | 4º dropdown de sort / 5º multi-filtro |
|
||||
| Escadas `actions/*.ts` (7) + convite-login + schema advisors route | novas actions (notifications/webauthn/suggestion) **usam** os wrappers | próxima escada bespoke → ampliar guarda W4d |
|
||||
| `wizardSignalHref`/`wizardTrendHref` (4–5 params posicionais) | 2 call sites com `undefined` placeholder | próximo href da família → options object |
|
||||
| Split de `suggestionCatalog` (646 linhas, 18 exports) | patternP1 tem 52 linhas | 9º pattern OU 2º consumidor do avaliador |
|
||||
| Extrair `import-projecao.mjs` (1213 linhas) | maior script do repo | 3ª família de planilha |
|
||||
| `municipalityPageData` (548) / `municipalityListUrl` (500, 23 exports) | god modules sob contrato congelado B18 | próxima faceta/sort |
|
||||
| `BahiaMap` (608) + `MunicipalityMapPanel` (734) | clients god, churn alto | próxima feature de mapa que toque ambos |
|
||||
| `ActivityForm` (515) | formulário único | próxima feature de atividade |
|
||||
| `agent-pool.mjs` tick (~118) + `derivePoolClaims` (~70) | já fatorado em `scripts/lib/agent-pool-*` | próxima fase do tick |
|
||||
| `compare ?compare=` sem `unstable_cache` (ledger P3) | 3 anos × scan por candidato | profiling mandar (ladder rung 2) |
|
||||
| Posts `limit: 0` + filtro em memória (ledger P3) | corpus ~39 artigos | crescimento do corpus |
|
||||
|
||||
## Look-alikes rejeitados (não reabrir sem evidência nova)
|
||||
|
||||
- `toggleMunicipalityMulti*` × `toggleLeadershipMulti*` (sim 0,24 textual — mesma família de conhecimento do ×4 deferido, não mergear antes do 5º).
|
||||
- `assertLocalUrl` (stage-snapshot/db-pull, sem override) × `assertLocalDatabase` (com override) — políticas diferentes de propósito.
|
||||
- `parseEnv` (gate-ci/db-doctor, não muta `process.env`) × `loadCliEnv` (muta) — não forçar.
|
||||
- `homeSearch*GroupHasHits` ×5 — `array.length > 0` fino demais para abstrair.
|
||||
- `ui/Sheet` × `ui/Drawer` — consumidor único (Sidebar) não é duplicação.
|
||||
- `createdBy` em `Notification`/`PushSubscription` — notificações são emitidas pelo sistema; push já carrega hash+timestamp de consentimento.
|
||||
|
||||
## Regras de execução (valem para cada workstream)
|
||||
|
||||
- Uma consolidação por entrega; gate completo bare (`tsc`, `lint`, `format:check`, `knip`, `check:cycles`, `test`, `build`) + Aikido quando o MCP estiver disponível (nesta sessão o ambiente Cloud não tinha o servidor — o row do ledger segue).
|
||||
- Commits de estrutura nunca misturam comportamento; teste vermelho no meio do refactor = revert, não debug.
|
||||
- Contratos congelados: URL (B18), schema (migration = entrega própria), Consent/LGPD fail-closed.
|
||||
- Lista rejected-with-reason do Pass 3 continua valendo.
|
||||
- Sobras → ledger via `capture-review-debts`; nada vive só no chat.
|
||||
- Nenhum item fecha sem a classe de prevenção registrada no PR; guarda chega com o fix.
|
||||
Loading…
Reference in a new issue