teqo-backup/docs/TECH-DEBT.md
Francisco Ignácio Fontoura Solla bb5ac362ed Add users.roles so Payload /admin can separate editors from admins.
Editors publish post/tag/media without PII or campaign access; existing users backfill as admin.
2026-07-27 01:54:29 -03:00

32 KiB
Raw Blame History

Tech Debt

Ledger of known engineering debt. Items large enough to be scheduled work become entries in roadmap.md (linked in the Ref column); this file tracks the full inventory so nothing is lost. Sourced initially from the 2026-07-23 engineering-standards audits.

Debt Ledger

Item Location Type Risk Effort Priority Status Ref
164 unused exports + 60 unused types reported by knip (warn-level; 158 at Pass 1) god modules, ui kit families dead code low M P2 closed 2026-07-25 (Pass 2 W4b): 0 findings; exports/types/enumMembers at error in CI; tests entered the knip graph; duplicates off (only hits were the 6 deliberate access aliases canUpdateX = canReadX) Pass 2 W4b
knip loader noise: Error loading src/payload.config.ts (server-only guard) knip run output tooling noise none (exit 0, static graph resolves) S P3 open
Aikido scan unusable: aikido_full_scan answers Opengrep exit code 2 for any input, including a one-line probe file, so the per-change security scan the rules mandate cannot complete. What it reported is an artifact of the failed run, not attributable to the code under review; the Aikido feed lists nothing for this repo. The shpjs devDependency added by B8 F2 is therefore SCA-unverified Aikido MCP / local scanner tooling outage (security gate) medium — the gate is skipped, not passed S P2 open 2026-07-26 (found during B8 F2)
Missing-consent window fixture releases its exclusive lease during the observation window — readers can interleave (rare flake). Full fix: session-level advisory lock spanning delete→op→restore + updated fault-path pins tests/helpers/testDatabaseLease.ts (withMissingInviteConsentFixture) test-infra race low (ms-wide window; shared leases added 2026-07-23 narrow it) M P2 open
users collection has no roles (all admins omnipotent) src/collections/Users.ts RBAC medium (small trusted admin set) M (schema migration) P1 before widening /admin closed 2026-07-27 (admin|editor; migration 20260727_032523_add_users_roles; roles-admin-payload.md) AGENTS Known Gaps #1
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 (likely intentional LGPD path) low S P3 open — document or align
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 (7 specs; most campaign CRUD int-only) tests/e2e/ coverage medium L P2 open
CI runs on GitHub mirror only (no Codeberg/Woodpecker 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
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
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 open — scheduled Pass 2 W4f (consent texts stay for the legal batch) Pass 2 W4f
Access checks re-read the actor per Local API call: getFreshCampaignUser caches in req.context, but a payload.find({ user, overrideAccess: false }) that passes no req gets a fresh context, so one /campanha/municipios render issues ~5 identical campaignUser reads on top of the layout's own pair src/utilities/access/shared.ts + every overrideAccess: false caller perf (round-trip fan-out) low since the gru1 move (~2 ms each; it was ~120 ms each while functions ran in iad1) M P2 open — measured 2026-07-26. A React cache() layer would dedupe across requests, but it changes actor freshness inside server actions that mutate roles (reloadUnrestrictedActor), so it needs its own delivery with pins
authenticateCampaignToken reads the actor twice: Payload's JWT strategy already fetches the user document, then findByID(depth: 1) re-reads it — the second read's only unique contribution is populating avatar for the sidebar src/utilities/campaignAuth.ts perf (a round trip in the layout, so on every page) low S P2 open — dropping it needs the avatar resolved another way, plus pins on removePrivateAuthFields and on role-downgrade freshness RS+

Smell Inventory

Smell Location Refactoring Status
God module: RBAC for every collection in one file (964 lines, ~80 exports) src/utilities/campaignAccess.ts split per domain under src/utilities/access/ done 2026-07-23 (14 domain modules; campaignAccess.ts kept as re-export surface)
God module: ~71 exports of electoral math src/lib/electionInsights.ts split by concern when next touched done 2026-07-25 (Pass 2 W4a): module DELETED; survivors in electionFormat.ts + voteTrend.ts
Multi-feature action bag: CRUD + CSV import pipeline + LGPD in one file (722 lines) src/app/(campaign)/campanha/actions/supporter.ts split CRUD vs import done 2026-07-23 (supporterImport.ts)
Clone twins: create/update/staff-assert/unique-conflict skeleton actions/organization.ts ↔ actions/stateDeputy.ts extract shared factory done 2026-07-23 (runStaffEntityMutation in campaignEntityActions.ts)
14 near-identical formActions.ts try/catch→mapError→redirect/return ladders src/app/(campaign)/campanha/**/formActions.ts shared form-action wrapper partially done — C8 F4 (2026-07-21/23) unified the municipality staff wrappers + 4 */nova ladders on runCampaignRedirectFormAction; 10 ladders written since remain hand-rolled (844 lines: assessores, planos, apoiadores ×2, demandas/[slug], liderancas/[id], organizacoes/[slug], dobradinhas/[slug], municipios editar, convite) — done 2026-07-25 (Pass 2 W4d): runCampaignFormAction + 13 ações migradas; exceções documentadas em comentário (planos, apoiadores/[id], convite login)
Flat 80+-file src/utilities/ junk drawer; lib/ vs utilities/ boundary undocumented src/utilities/, src/lib/ document convention + move misfiled modules done 2026-07-23 (convention in .cursor/rules/engineering-standards.mdc; RefreshRouteOnSave moved to components/)
Praça→Município rename leftovers in identifiers (PLAZA_MAP_YEARS, ACCESSIBLE_PLAZA_IDS_CONTEXT_KEY, …) map/list utilities finish rename done 2026-07-23
Props→state useEffect mirrors MunicipalityListExpectedVotesControl, comboboxes key-remount or fully controlled closed 2026-07-23 (accepted as correct — see Debt Ledger row)
Detail-page RSCs mixing loading + tab UI + forms (300+ lines) municipios/[slug]/page.tsx, planos/[slug]/page.tsx extract tab loaders/sections done 2026-07-25 (Pass 2 W4c): tabs em MunicipalityDetailTabs.tsx / ActionPlanOverviewTab.tsx; gêmeos de tab dedupados em detailTabUi.ts
God module: list URL + filters + sort + labels + freshness in one file (765 lines, 59 exports) src/utilities/municipalityUi.ts split along url/filters/sort/labels/freshness seams done 2026-07-25 (Pass 2 W1-D1): municipalityListUrl / municipalityListFilters / municipalityLabels / municipalitySignal
components/campaign/ flat with 106 files; utilities/ flat with 92 src/components/campaign/, src/utilities/ within-layer domain subfolders components done 2026-07-25 (Pass 2 W2, 15 domínios); utilities/ deferred with trigger: a domain gaining its 3rd new module in a month gets its subfolder

Sprout / Wrap Register

Host Sprouted/Wrapped Notes
(none yet)

Adopted Conventions

  • Zero lint warnings. pnpm lint runs with --max-warnings=0; deliberate placeholders use a _ prefix (argsIgnorePattern).
  • as never is banned (ESLint no-restricted-syntax). Production code uses typed helpers (e.g. hookFilledCreateData for hook-completed create data); tests use stub<T>(partial) which keeps property-level checking. A justified eslint-disable-next-line is the only escape hatch.
  • Dead code is deleted, not commented. knip runs in CI (files/dependencies at error level). After any change: pnpm exec knip and remove what died.
  • Collections always declare access explicitly. Payload's default ("any authenticated user") is treated as a bug — see payloadAdminOnly.
  • Generated migration signatures are exempt from unused-args linting (src/migrations/** override); never hand-edit migration content for lint cosmetics.
  • Shared consent fixture rows are leased, never owned in integration tests (tests/helpers/testDatabaseLease.ts); deleting them steals from parallel spec files.

Debt Budget & Broken-Windows Policy

  • New warnings/as never/unused exports fail or get ledgered the same day — no untracked // TODOs.
  • Each delivery that runs /simplify registers surviving debts here or in docs/plans/escala-dry-pos-*.md (existing convention), whichever the item's size warrants.