From 3b0fad85f9f6fa4a6cf6866bc8fe1545808210fb Mon Sep 17 00:00:00 2001 From: Francisco Solla Date: Tue, 25 Aug 2026 10:10:13 -0300 Subject: [PATCH] =?UTF-8?q?docs(audit-pass6):=20artefatos=20=E2=80=94=20se?= =?UTF-8?q?=C3=A7=C3=A3o=20Pass=206,=20ledger=20truth-up,=20plano=20de=20e?= =?UTF-8?q?ntrega?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/IMPROVE-CODE-QUALITY-PLAN.md | 47 ++++++++++++++ docs/TECH-DEBT.md | 68 +++++++++++++------- docs/changelog/2026-08-25-audit-pass6.md | 3 + docs/plans/entrega-engenharia-p6.md | 80 ++++++++++++++++++++++++ 4 files changed, 176 insertions(+), 22 deletions(-) create mode 100644 docs/changelog/2026-08-25-audit-pass6.md create mode 100644 docs/plans/entrega-engenharia-p6.md diff --git a/docs/IMPROVE-CODE-QUALITY-PLAN.md b/docs/IMPROVE-CODE-QUALITY-PLAN.md index 41bc1d07..aab11b40 100644 --- a/docs/IMPROVE-CODE-QUALITY-PLAN.md +++ b/docs/IMPROVE-CODE-QUALITY-PLAN.md @@ -254,3 +254,50 @@ Statuses: pending · in-progress · awaiting-evidence · done · deferred: \ Boolean(user)`; Local API shorthand `user,`; scripts local `function die(`; allocator find-by-slug+mutate / encoded `municipio`; e2e settle tokens in comments | `tests/unit/*Conventions.unit.spec.ts`; `scripts/check-plans-only-pr-closes.mjs:12-15` | guard dodge | medium on access guards | M | P2 | open — Pass 5 | P5-G | -| `/campanha/municipio/[slug]/v2` parallel detail shell (B147) alongside `/municipios/[slug]` | `src/app/(campaign)/campanha/(app)/municipio/[slug]/v2/` | intentional parallel — cutover debt | low until cutover; then divergent change | M | P3 | open — Pass 5 (defer + trigger: v2 replaces v1) | B147 | +| Wizard/action href knowledge dual-switched (`wizardActionChain` chain + previous) and wide URL surface (`campaignActionRoutes` 252 LOC / 31 exports) | `src/lib/wizardActionChain.ts`; `src/lib/campaignActionRoutes.ts` | duplication + wide interface | medium — every new wizard step touches both switches | M | P2 | **closed 2026-08-25 (Pass 6 verify):** superseded — wizard domain deleted (OPS95); `wizardActionChain` has 0 importers | P5-A | +| Staff-detail wizard action id list re-spelled ×3 (activity / leadership / demand adapters) | `src/lib/activityQuickActions.ts:18-24`; `campaignQuickActionLeadership.ts:19-25`; `campaignQuickActionDemands.ts:14-20` | duplication | low | S | P2 | open — Pass 6 (verified: `activityQuickActions.ts`, `campaignQuickActionLeadership.ts`, `campaignQuickActionDemands.ts` still re-spell the id list) | P5-B | +| `/campanha/acoes/[slug]` page is a 296-line auth+load+branch dispatcher | `src/app/(campaign)/campanha/(app)/acoes/[slug]/page.tsx` | bloater / divergent change | medium — every quick-action lands here | M | P2 | open — Pass 6 (re-measured: **208 lines**, was 296 — shrunk since Pass 5) | P5-C | +| Detail pages double-run actor+load in `generateMetadata` and page body (org / dobradinha / demanda) | `organizacoes/[slug]/page.tsx`, `dobradinhas/[slug]/page.tsx`, `demandas/[slug]/page.tsx` | duplication | low — extra DB round-trips per detail view | S | P2 | open — Pass 6 (WORSE on the flagship route: `municipio/[slug]/v2/page.tsx:34-48` runs `requireCampaignPageActor` + all 4 V2 loaders in `generateMetadata` and repeats them in the body, zero `cache()`) | P5-D | +| Client auth forms value-import pure helpers from `utilities/campaignFormFields` | `LoginForm.tsx`, `ForgotPasswordForm.tsx` ← `src/utilities/campaignFormFields.ts` | boundary | low | S | P2 | open — Pass 6 (verified: 4 client components import `@/utilities/campaignFormFields` — `CampaignPasswordFields.tsx:5`, `CampaignProfileSettings.tsx:19`, `ContactCreateRow.tsx:15`, `ContactFormFields.tsx:10`) | P5-E | +| Guard dodges measured: `globalAccessConventions` only `=> Boolean(user)`; Local API shorthand `user,`; scripts local `function die(`; allocator find-by-slug+mutate / encoded `municipio`; e2e settle tokens in comments | `tests/unit/*Conventions.unit.spec.ts`; `scripts/check-plans-only-pr-closes.mjs:12-15` | guard dodge | medium on access guards | M | P2 | open — Pass 6 (new dodge measured: `perfil/page.tsx:25` calls `getCampaignUserWithAvatar()` — P3-I regex matches only `getCampaignUser\(` — **fixed by P6-2**; `globalAccessConventions` and the others verified still standing) | P5-G | +| `/campanha/municipio/[slug]/v2` parallel detail shell (B147) alongside `/municipios/[slug]` | `src/app/(campaign)/campanha/(app)/municipio/[slug]/v2/` | intentional parallel — cutover debt | low until cutover; then divergent change | M | P3 | open — Pass 6 (cutover tracked as issue #335) | B147 | +| Saved-filters machinery re-spelled ×6 despite the committed "must not be written twice" comment (`useMunicipalitySavedFilters.ts:26`): hooks 43/48 LOC, controls 144/160 LOC, navs 154/166 LOC | `shared/usePeopleSavedFilters.ts` ↔ `useMunicipalitySavedFilters.ts`, `SavePeopleFilterControl` ↔ `SaveMunicipalityFilterControl`, `PeopleNavSavedFilters` ↔ `MunicipalityNavSavedFilters` | duplication (B18 machine, store layer already shared) | medium — every saved-filter fix lands twice; the people twin cloned the spec | M | P2 | open — Pass 6 (P6-A: one hook/control/nav trio over the shared store; guard 3 bans new twins) | P6-A | +| Advisor-relation cell twins ×3 (89/96/99 LOC) — `PeopleAssessoradoCell` / `LeadershipAdvisorRelationCell` / `StateDeputyAdvisorRelationCell` differ only in FormData field, drawer title, trigger label | `components/campaign/{people,leadership,stateDeputy}/*AdvisorRelationCell.tsx` | duplication (RelationChipCell engine already owns the machine; DIRECTION_COPY-as-data precedent) | medium — a11y/copy drift across 3 surfaces (B37 class regrown) | M | P2 | open — Pass 6 (P6-B: one cell, owner-kind record as data; guard 3 bans new advisor-cell files not delegating) | P6-B | +| Filter-bar shells ×13 re-spell the ~60-LOC list-filter machine (hook + query state + chips/seeds/suggestions + omnibox wiring); 9/13 near-clean, `StateDeputyFilters` 96L ≈ `AdvisorFilters` 103L | `components/campaign/{municipality,leadership,activity,supporter,demand,people,contacts,...}/*Filters.tsx` (~1.700 LOC total) | duplication + divergent change | high — every filter UX change lands 13× | L | P2 | open — Pass 6 (P6-C: shared shell with slots/typed adapter record; omnibox adapters already export the identical 6-fn surface) | P6-C | +| `searchHome*` skeletons ×6 share the ~55-LOC machine (staff-gate → contains+`limit: 0` → word-start → relevance → cap); cap under two names (`HOME_SEARCH_RESULT_HIT_CAP` vs local `_ACTIVITY/_DEMAND_HIT_CAP` = 25) | `src/utilities/homeSearch/searchHome*.ts` (847 LOC / 10 files) | duplication | medium | M | P2 | open — Pass 6 (P6-D: typed core + named wrappers, runStaffEntityMutation pattern) | P6-D | +| `chipLabel` re-spelled in 12/13 omnibox adapters; `municipalityOmnibox.ts:401-445` re-implements `filterOmniboxSuggestionSeeds` incl. local `SUGGESTION_CAP_PER_GROUP`/`seedOf` | `src/utilities/*Omnibox.ts` (~3.211 LOC family) | duplication | low | S-M | P2 | open — Pass 6 (P6-E: single `chipLabel` export + delete the holdout re-spell; guard 3) | P6-E | +| DAY_MS re-spelled as bare `86_400_000` ×11 in tests + 1 in lib (`campaignTime.ts:134` local `msPerDay`) — the P3-K consolidation claim ("×3") never caught literals | `calendarFeed.int.spec.ts:41,532`, `homeSearchActivities.int.spec.ts:24`, `prioritiesTool.unit.spec.ts:13,263`, `suggestionCatalog.unit.spec.ts:365,404`, … | shotgun surgery (P3-K dodge) | low | S | P2 | open — Pass 6 (P6-F: import `DAY_MS` from `@/lib/text`; guard 3 flags the literal with allowlist) | P6-F | +| Dead modules survive knip because test imports count as usage: `projectionSheetParse.ts` (173 LOC, 7 exports, 0 production importers) and `searchOnlyListOmnibox.ts` (98 LOC, 7 exports); dead scripts survive via `entry: ["scripts/*.mjs"]`: `github-pr.mjs`, `measure-e2e-family.mjs`, `regenerate-municipality-catalog-snapshot.mjs` (0 referencers) | `src/lib/projectionSheetParse.ts`, `src/lib/searchOnlyListOmnibox.ts`, `scripts/*.mjs` | dead code + tooling dodge (knip entry list) | low | S | P2 | open — Pass 6 (P6-G: delete the 5; knip config so spec-only modules surface — class 4) | P6-G | +| CLI skeleton guard dodged twice: 4 local `function die()` (`check-plans-only-pr-closes.mjs:12`, `check-changelog-append-only.mjs:29`, `build-changelog.mjs:35`, `check-docs-conflict-markers.mjs:19`) + ~20 raw `process.exit(1)` — `scriptCliConventions` catches neither | `scripts/*.mjs` | guard dodge + duplication (P3-H class) | low | S | P2 | open — Pass 6 (P6-H: extend guard to `function die(`/`process.exit(`; migrate 4 files to `dieWithLabel`) | P6-H | +| `taskActions.ts:20-27` hand-rolls try/catch and passes raw `error.message` to the UI (no safelist); `resultFormActions.ts:17` / `updateFormActions.ts:16-20` safelist bare literals duplicating `ACTIVITY_RESULT_STAFF_MESSAGE`/`ACTIVITY_UPDATE_*` constants — the B32+ class, both directions | `atividades/[slug]/taskActions.ts`, `resultFormActions.ts`, `updateFormActions.ts` | correctness (message-only) + B32+ contract | medium — raw Payload/DB errors can reach the toast; literal drift silently collapses refusals | S | P2 | open — Pass 6 (P6-I: wrap in `runCampaignFormAction` + import constants; guard 2/3 sweeps `error.message` passthrough and literal safelists) | P6-I | +| Public form actions create a new `contact` row per submission with no phone/email dedup (`submitWhatsapp.ts:30`, `submitPetitionSignature.ts:40`, `submitCampaignNewsletter.ts:26`) — campaign actions dedup via `findOrCreateContactByPhone`; repeat submissions multiply PII rows and inflate the public signature counter | `src/app/(frontend)/actions/submit*.ts` | LGPD hygiene + counter integrity | medium | M | P2 | open — Pass 6 (P6-J: dedupe before create; guard 5 int pin — same input twice → 1 contact) | P6-J | +| `findPublishedPosts` has no `select` — every listing/article page ships full published docs incl. richText bodies; article pages read 3 full-doc sets per render | `src/utilities/posts.ts:102-114`; `[type]/[category]/[slug]/page.tsx:35,72,142` | over-fetch (engineering-standards: view models with selected fields) | medium — per-render DB payload + cache size | M | P2 | open — Pass 6 (P6-K: listing projection + related-posts from the same select'd query; guard 3) | P6-K | +| `server-only` sweep is blind to `next/navigation` value imports (`campaignFormActionError.ts:1` — isomorphic today, invisible either way) and to Payload-as-param modules (`personDelete.ts:9`) | `tests/unit/codebaseConventions.unit.spec.ts` server-only sweep; `src/utilities/campaignFormActionError.ts:1` | guard dodge (P3-K class) | low — a future server-bound `next/navigation` module passes green | S | P2 | open — Pass 6 (P6-M: widen regex to `next/navigation` + allowlist current clean importers) | P6-M | +| D1 trigger has no teeth: 12 top-level `activity*` modules + `googleCalendar*` trio + 3 top-level omniboxes coexist because the pin is a manual allowlist; `person/` (1 file) vs `people/` (9 files) split-brain | `src/utilities/` top-level | organization + guard dodge (P3-J D1) | low (path churn) | M | P2 | open — Pass 6 (P6-N/P6-O: prefix-count spec — N top-level files sharing a domain prefix fail; fold `personContactWriteScope.ts` into `people/`) | P6-N | +| `as unknown as` double-cast ×21 spec files — the unguarded twin of the `as never` ban (electionToolsLockdown 5×, instagramSync 7×, calendarFeed 5×, …); `stub` (148 uses) exists | `tests/unit/*.unit.spec.ts` | type honesty + guard gap (Pass 1 class) | low | M | P2 | open — Pass 6 (P6-P: swap to `stub`; ESLint ban in tests — class 2) | P6-P | +| Actor-field convention guard regex covers only `createdBy|decidedBy|declaredBy` — a hand-spelled actor under any other name passes; `CampaignDemand` is whole-file allowlisted | `tests/unit/codebaseConventions.unit.spec.ts:571` | guard dodge (P3-I class) | low — new collection can twin the factory silently | S | P2 | open — Pass 6 (P6-R: invert — flag ANY `relationTo: 'campaignUser'` actor field not built by the factory) | P6-R | +| `overrideAccess: true` without per-site comment at ~30 of 66 sites across 15 action files (all spot-checked scope-guarded — review-quality debt, not a hole) | `actions/{advisor,leadership,municipality,notifications,person,...}.ts` | convention erosion | medium — reviewers can't distinguish guarded from unguarded | M | P2 | **counted by P6-1** (ratchet pins at current counts; only per-site comments lower them) | P6-1 | +| `fixtures.own()` manual calls grew 49 → **75**; fixture hub god modules (`campaignFixtures.ts` 1.168 LOC, `testDatabaseLease.ts` 799) absorb every suite's builders | `tests/helpers/*`, `tests/int/*` | test-infra drift + bloater | low | M | P3 | open — Pass 6 | P6-Q | +| Gender labels re-spelled ×4 with a deliberate display-policy fork ('Não binário' vs 'Outro') — merge rejected (look-alike), drift risk noted | `contacts/contactListFilters.ts:33`, `contactListUrl.ts`, `contactOmnibox.ts`, `leadership/leadershipUi.ts:3` | shotgun surgery (rejected-merge) | low | S | P3 | open — Pass 6 (defer + trigger: 3rd distinct policy fork re-opens) | P6-S | +| `getSignatureCount` runs an uncached `payload.count` per render | `src/app/(frontend)/actions/getSignatureCount.ts:9` | caching ladder step 1 | low | S | P3 | open — Pass 6 | P6-T | +| AI shell has zero live regions across 8 files — assistant turns append with no announcement (B32+ contract not inherited by new domains) | `components/campaign/shell/ai/*.tsx` (incl. `CampaignAIChat.tsx:339`) | a11y (B32+ class) | low | S | P2 | open — Pass 6 (P6-V: sr-only polite region announcing "Resposta pronta") | P6-V | +| `peopleData.ts` (627 LOC/19 exports) mixes view models + merge + scope + facets + loader; `personDelete.ts` (521 LOC) mixes enumeration + guard + 28-bypass cascade | `src/utilities/people/*` | bloater | low | M | P2 | open — Pass 6 (P6-U: split facets/sort from loader; enumeration read-only split) | P6-U | +| `campaignNavigationUrls.ts` (636 LOC, 3 exports) imports only `@/lib/*` — pure client-safe code stranded in the server layer | `src/utilities/ai/campaignNavigationUrls.ts` | boundary (misfiled pure module) | low | M | P2 | open — Pass 6 (move to `lib/`) | P6-V2 | +| Miss #909 residual: the disarm sequence was fixed in the skill (judgment) — deterministic guard shipped as `scripts/testing-audit-disarm.mjs` (create→disarm→verify-null, atomic), wired into the skill's PR step | `scripts/testing-audit-disarm.mjs` (new), `.agents/skills/testing-audit/SKILL.md` | guardrail (class 3) | — | S | P1 | **closed 2026-08-25 (Pass 6, P6-M909)** — Closes #909 | P6-M909 | +| Miss #895: intention plans registering a data decision must record the literal values (tables/IDs), not narrative — no deterministic detector possible for missing prose data | `.agents/skills/plan-issue/SKILL.md` | guardrail (class 6, judgment-only declared) | — | S | P2 | **closed 2026-08-25 (Pass 6, P6-M895)** — convention registered; declared judgment-only | P6-M895 | ## Smell Inventory diff --git a/docs/changelog/2026-08-25-audit-pass6.md b/docs/changelog/2026-08-25-audit-pass6.md new file mode 100644 index 00000000..4c2c9778 --- /dev/null +++ b/docs/changelog/2026-08-25-audit-pass6.md @@ -0,0 +1,3 @@ +# 2026-08-25 — audit pass 6 + +Engineering audit Pass 6 (skill v2: autônomo, sub-agentes, branch única `audit/pass-6`, PR único sem auto-merge — OPS98). Delta 2026-08-03→25: 681 commits non-merge, 2.041 arquivos tocados; novos domínios people/ai/contacts/calendar (2 collections novas), wizard deletado (OPS95). Baseline verde (tsc/lint/knip/ciclos; 1.526 arquivos TS). 0 P0 (access/transações/consent verificados nos 6 sweeps). 2 P1 (ratchet de bypass dodged por comentário de módulo — `personDelete.ts` 28 bypasses; P3-I dodged por `getCampaignUserWithAvatar` no `perfil`) remediados com guarda na mesma entrega. 2 misses colhidas: #909 → `scripts/testing-audit-disarm.mjs` (desarme atômico create→disarm→verify-null); #895 → convenção no plan-issue (dados literais em planos de intenção, classe 6 declarada). 5 linhas de ledger fechadas com evidência, 19 novas. Artefatos: seção Pass 6 em `docs/IMPROVE-CODE-QUALITY-PLAN.md`, ledger atualizado, plano `docs/plans/entrega-engenharia-p6.md`. diff --git a/docs/plans/entrega-engenharia-p6.md b/docs/plans/entrega-engenharia-p6.md new file mode 100644 index 00000000..504b3be4 --- /dev/null +++ b/docs/plans/entrega-engenharia-p6.md @@ -0,0 +1,80 @@ +# Entrega de engenharia — Pass 6 (2026-08-25) + +Retrato consolidado do audit (Pass 6) e o plano de execução dos workstreams remanescentes. As melhorias elegíveis (P0/P1 × S/M) foram implementadas **na mesma branch** (`audit/pass-6`, PR único sem auto-merge — OPS98); todo o resto é planejado aqui por severidade, com a guarda determinística classificada na escada 1–6 (1 tipo · 2 ESLint · 3 spec de convenção · 4 análise estática CI · 5 pin comportamental · 6 doc/judgment-only declarado). + +## Executado no Pass (commitados na branch) + +| WS | Entrega | Guarda (classe) | +| --- | --- | --- | +| P6-1 | Ratchet de bypass endurecido: comentário de módulo não zera mais o arquivo (conta como indocumentado); arquivos novos entram pinados na contagem atual; só comentário per-site abaixa | 3 — `codebaseConventions` (P3-E endurecida) | +| P6-2 | `perfil` usa `requireCampaignPageActor`; regex do P3-I ampliada para `getCampaignUser[A-Za-z0-9]*\(` | 3 — `codebaseConventions` (P3-I endurecida) | +| P6-M909 | Guardrail da miss #909: `scripts/testing-audit-disarm.mjs` (cria PR → desarma → verifica `autoMergeRequest: null`, atômico) + fiação na skill /testing-audit | 3 — script no passo de criação do PR (a ordem deixa de depender do agente) | +| P6-M895 | Guardrail da miss #895: convenção no skill `plan-issue` — plano de intenção que registra decisão de DADOS registra os valores literais (tabela/IDs), não só narrativa | 6 — declarada judgment-only (prosa não tem detector determinístico) | + +## Workstreams planejados (fora do teto do Pass) + +Ordenados por severidade; dentro dela, por blast radius crescente. Teto do Pass (P0/P1 S/M) já consumido — estes entram como entregas independentes via `plan-issue`/agent-paradigm. + +### P2 + +| WS | Conteúdo | Guarda (classe) | Evidência | +| --- | --- | --- | --- | +| P6-A | Saved-filters ×6 → 1 trio (hook/control/nav) sobre o store compartilhado; copy como data | 3 — spec bane novo twin de saved-filter fora do dono | `usePeopleSavedFilters` 43L ↔ `useMunicipalitySavedFilters` 48L; comentário "must not be written twice" violado em `:26` | +| P6-B | Advisor-relation cells ×3 → 1 célula com record de owner-kind (precedente `DIRECTION_COPY`) | 3 — spec: arquivo `*AdvisorRelationCell`/`*AssessoradoCell` novo precisa delegar ao dono | 89/96/99 LOC, só diferem FormData field/título/label | +| P6-C | Filter-bar shells ×13 → shell com slots + adapter record (adapters já exportam a superfície de 6 fns idêntica) | 6 — judgment-only (abstração grande); gatilho: próximo shell novo | ~1.700 LOC, `StateDeputyFilters` 96L ≈ `AdvisorFilters` 103L | +| P6-D | `searchHome*` ×6 → core tipado + wrappers nomeados (padrão `runStaffEntityMutation`); cap single-source | 3 — spec: cap com nome único; skeleton ban | 847 LOC; cap ×2 nomes (25) | +| P6-E | `chipLabel` ×12 → 1 export; deletar re-spell de `filterOmniboxSuggestionSeeds` em `municipalityOmnibox.ts:401-445` | 3 — spec: `const chipLabel` permitido 1× | 12/13 arquivos | +| P6-F | Bare `86_400_000` ×12 → `DAY_MS` | 3 — `scriptCliConventions`/spec flag do literal com allowlist | 11 tests + `campaignTime.ts:134` | +| P6-G | Deletar módulos mortos (`projectionSheetParse` 173L, `searchOnlyListOmnibox` 98L) + scripts mortos (`github-pr.mjs`, `measure-e2e-family.mjs`, `regenerate-municipality-catalog-snapshot.mjs`) | 4 — knip config: spec não conta como usage (entry de testes excluído) | 0 importadores de produção | +| P6-H | Guard de CLI estendido: ban `function die(` + `process.exit(`; migrar 4 scripts para `dieWithLabel` | 3 — `scriptCliConventions` endurecida | 4 `function die()` + ~20 `process.exit(1)` | +| P6-I | `taskActions.ts` no wrapper `runCampaignFormAction`; safelists importam as constantes (`ACTIVITY_RESULT_STAFF_MESSAGE` etc.) | 2/3 — spec varre `error.message` em `(campaign)` + literal em safelist | `taskActions.ts:20-27`; `resultFormActions.ts:17` | +| P6-J | Dedup de contact nos 3 forms públicos (`findOrCreateContactByPhone` pattern) | 5 — pin int: mesma submissão 2× → 1 contact | `submitWhatsapp.ts:30`, `submitPetitionSignature.ts:40`, `submitCampaignNewsletter.ts:26` | +| P6-K | `findPublishedPosts` com `select` (projeção de listagem); related-posts derivam da mesma query | 3 — spec: `find` de post sem `select` falha | `posts.ts:102-114` sem `select` nenhum | +| P6-M | Sweep `server-only` ampliado para `next/navigation` (+ allowlist dos limpos) | 3 — `codebaseConventions` | `campaignFormActionError.ts:1` | +| P6-N | D1 trigger com dentes: spec conta N módulos top-level com prefixo de domínio; 12 `activity*` + trio `googleCalendar*` + 3 omniboxes top-level → subpastas | 3 — spec de contagem de prefixo | `src/utilities/` top-level | +| P6-O | Fold `personContactWriteScope.ts` → `people/` | 3 — spec de split-brain por prefixo | `person/` com 1 arquivo | +| P6-P | `as unknown as` → `stub` + ban ESLint em tests | 2 — ESLint `no-restricted-syntax` em tests | 21 arquivos de spec | +| P6-R | Guard de actor-field invertido: qualquer `relationTo: 'campaignUser'` não vindo da factory falha (não só os 3 nomes) | 2/3 — spec | `codebaseConventions.unit.spec.ts:571` | +| P6-U | `peopleData`/`personDelete` split (facets/sort vs loader; enumeração read-only vs mutação) | 6 — judgment | 627/521 LOC | +| P6-V | AI shell: região polite anunciando resposta; usar `CampaignFormActionMessage` nos contatos | 6 — judgment (a11y) | 0 aria-live em 8 arquivos `shell/ai/` | +| P6-V2 | `campaignNavigationUrls.ts` → `lib/` (puro, client-safe) | 3 — spec de pureza top-level | 636 LOC, imports só `@/lib/*` | + +### P3 + +| WS | Conteúdo | Guarda | Evidência | +| --- | --- | --- | --- | +| P6-Q | `fixtures.own()` 75 → auto-ownership; split do hub de fixtures | 3 | `campaignFixtures.ts` 1.168L | +| P6-S | Gender labels: manter fork deliberado (rejeitado como look-alike), registrar gatilho | 6 | 'Não binário' vs 'Outro' | +| P6-T | `getSignatureCount` com `cache()` | 2 | `getSignatureCount.ts:9` | +| P6-X | Migração de schema (ex.: users-roles) e URLs públicas: **fora de escopo** por construção | — | ledger | + +## Defer + trigger registrados (sem ID próprio) + +| Candidato | Motivo do defer | Gatilho de reabertura | +| --- | --- | --- | +| homeSearch int triplet → `describe.each` | 2 specs gêmeos de fixture-heavy int; restruturar carrega cada assertion verbatim | 4º irmão do triplet | +| `agent-pool.mjs` + `lib/agent-pool-*` (516L) | pool dormente (OPS65); `agent:pool` ainda tem entrada em package.json | limpeza pós-pool | +| Parsers quick-action V1/V2 | B147 paralelo deliberado (ledger, issue #335) | cutover do v2 | +| `municipio/[slug]/v2` prologue `cache()` | mesma classe P5-D — a rota v2 é a pior instância; corrigir junto do P5-D | — | +| `measure-e2e-family.mjs` | ferramenta OPS87 com docs próprias | deletar junto das docs do workflow | + +## Rejeitados como look-alike (não re-propor) + +- `loadNamesByIds` vs `resolveAssessorados`/`peopleNameSubline` em `peopleData.ts` — algoritmos distintos (entity-union; display policy), não name-loaders. +- Cap+message template ×4 — valores de política diferem (10 vs 435) e copy é por domínio; `nextIdsAfterMembership` já recebe o cap como dado. +- `hasHits` ×5 — aliases nomeados de 1 linha são declarações deliberadas de interface (canon: `canUpdateX = canReadX`). +- `homeSearchShare.ts` pass-through — sem conhecimento duplicado; só re-export. + +## Guard map do Pass + +| Classe de achado | Guarda | Mecanismo | Status | +| --- | --- | --- | --- | +| Bypass admin indocumentado | P3-E ratchet | `codebaseConventions` por-arquivo, só diminui | **endurecida P6-1** (comentário de módulo não zera; novos arquivos pinados) | +| Ator de página hand-rolled | P3-I | regex em page.tsx de `(app)` | **endurecida P6-2** (variantes `getCampaignUser*`) | +| Automerge armado em PR de audit | OPS98 veto + #909 | safety-net pula `audit/*`; script de desarme atômico | **nova P6-M909** | +| Plano de intenção sem dados literais | #895 convenção | skill `plan-issue` | **nova P6-M895** (classe 6 declarada) | +| `as never` twin (`as unknown as`) | P6-P | ESLint em tests | planejada | +| Literal `86_400_000` | P6-F | spec com allowlist | planejada | +| `function die(`/`process.exit(` em scripts | P6-H | `scriptCliConventions` estendida | planejada | +| Módulo morto sustentado por spec | P6-G | knip config | planejada | +| Actor-field fora da factory | P6-R | spec invertida (nome-independente) | planejada |