teqo-backup/docs/TECH-DEBT.md
Francisco Ignácio Fontoura Solla 66feecd66b simplify(B13): corrige classe agregada, bolhas em pane proprio e registra debitos
O /simplify da escala relativa do mapa achou um bug semantico real em
computeAggregateTerritorialClass — somava o headroom dos slugs e comparava
contra a mediana de UMA unidade, classificando um grupo grande como expansao
quando ele e marginal. A mediana agora escala por slugs.length, com teste.

As bolhas sairam do centro do bounding box para o centroide real e ganharam
pane proprio (z-index 450), porque o bringToFront do hover as escondia atras
do poligono. Legenda e pintura passaram a compartilhar o mesmo predicado,
a copy dos quantis parou de prometer faixas de igual largura, e o subtitulo
do painel parou de dizer "em 2026" com 2022 selecionado (defeito do B11).

Fonte unica onde havia duas: NO_DATA_FILL, o raio das bolhas e
formatPlacementOrdinal (ex-formatMunicipalityVoteRank, agora client-safe em
electionFormat.ts).

Os quatro debitos maiores que o cleanup viraram o fill-in B13+
(docs/plans/escala-dry-pos-b13.md); a flakiness dos e2e, reproduzida em main
limpo, foi para o ledger com a causa afinada.
2026-07-26 05:12:37 -03:00

60 lines
28 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Tech Debt
Ledger of known engineering debt. Items large enough to be scheduled work become entries in [roadmap.md](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 | |
| 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` | open | 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 |
## 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 `// TODO`s.
- 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.