diff --git a/docs/TECH-DEBT.md b/docs/TECH-DEBT.md index 72ab821f..d519a509 100644 --- a/docs/TECH-DEBT.md +++ b/docs/TECH-DEBT.md @@ -73,28 +73,28 @@ Ledger of known engineering debt. Items large enough to be scheduled work become | 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 | +| 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 — the three AdvisorRelationCell files) | 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/` — the 13 Files.tsx filter shells (~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 the two local HIT_CAP constants = 25) | `src/utilities/homeSearch/` (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 and seedOf | `src/utilities/` — the 13 Omnibox adapters (~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 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 the knip entry list covering every top-level script: `github-pr.mjs`, `measure-e2e-family.mjs`, `regenerate-municipality-catalog-snapshot.mjs` (0 referencers) | `src/lib/projectionSheetParse.ts`, `src/lib/searchOnlyListOmnibox.ts`, `scripts/` top-level | 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/` top-level | guard dodge + duplication (P3-H class) | low | S | P2 | open — Pass 6 (P6-H: extend guard to ban local die() and 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 the 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/` — the 3 submit actions | 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 selected 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 + the 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/` specs | 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 3 names (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, profile, password | 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 | +| 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/` (incl. `CampaignAIChat.tsx:339`) | a11y (B32+ class) | low | S | P2 | open — Pass 6 (P6-V: sr-only polite region announcing the answer) | 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 modules — 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