mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-14 08:53:11 +00:00
81 KiB
81 KiB
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 | recovered 2026-07-27 (B34): aikido_full_scan completed with 0 findings on the changed files; only taint analysis timed out. Exit 2 did return in the same day's follow-up pass, and the useful detail is that it is not a total outage: the wrapper reports the error while Opengrep's own summary says Ran 316 rules on 1 file: 0 findings, so SAST results are present and clean — the exit code tracks "some files were skipped or only partially analyzed", not a failed scan. Read the summary text before treating a red scan as a blocked gate. The SCA half (incl. shpjs) remains unverified |
|
| 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 | |
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 |
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 | open 2026-07-27 (B34 /simplify), widened 2026-07-28 (B17) — start from the saved test-results/**/error-context.md: the Salvador list renders (zones ZE 5/9/10–15 are in the snapshot) but the assertion's exact accessible name is absent, so suspect the E9 deficit default sort plus pagination rather than the card. The strict-mode violation is a real ambiguity in the spec's own locator, fixable on its own |
|
| 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. Re-measured 2026-07-27 (B34) on /campanha/liderancas, where it is an order of magnitude worse: payload.find on leadership at depth: 1 with an actor and overrideAccess: false costs 371 statements / 185 ms, of which 367 are the identical select … from "campaign_user" — the same read once per document and per populated relation. The controls isolate the cause: overrideAccess: true is 4 SQL / 11 ms, and depth: 0 with the actor is 103 SQL / 47 ms, so it is the access layer on the populate path getting a req that does not carry the memo |
src/utilities/access/shared.ts + every overrideAccess: false caller |
perf (round-trip fan-out) | medium — 185 ms of a 229 ms page load, and B34 made that page one the mesa re-renders on every chip toggle (see F7); each statement is ~2 ms local but crosses gru1↔sa-east-1 in production |
M | — | closed 2026-07-28 (B34+) — memoizePerRequest keys on the req object (WeakMap), not req.context (measured: 367 contexts, 1 req; int campaignAccessRequestMemo pins 1 campaign_user read per scoped leadership list at depth 1 and role downgrade on the next op). Role-mutation audit: reloadUnrestrictedActor bypasses the memo path; pins cover cross-actor isolation |
|
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+ |
LeadershipStateDeputyRelationCell (B31) keeps its own Popover, its own ResizeObserver collapse math and a 16 px "Ver mais…", while the sibling column in the same table now uses the shared MunicipalityPortfolioCell (always-mounted on pointer-fine, Drawer on pointer-coarse, 24 px targets, ARIA combobox). Two chip editors, one table, different keyboard and touch contracts |
src/components/campaign/leadership/LeadershipStateDeputyRelationCell.tsx vs src/components/campaign/shared/MunicipalityPortfolioCell.tsx |
duplication + a11y drift | low functionally, medium for consistency — the dobradinha chips are the only ones in the table a thumb cannot edit safely | M | P2 | open 2026-07-27 (B34), trigger fired — the reuse review measured ~105 line-for-line duplicated lines of ResizeObserver collapse math between the two cells (constants, the first-callback guard, the lastVisibleTop + 1 filter, the trailing-width loop, the expandToggle), including one verbatim comment. Scheduled as B34+ F2 (useChipRowOverflow); the duplication predates B34 (it was born between AdvisorMunicipalityCell and B31), which is why it is an extraction and not a regression. B37 still does not by itself justify a generic abstraction — it plugs the shared cell into a third relation. Re-measured post-rebase: ~165 lines, not ~105 — the twin also copies the sameIdSet prop adoption, the showAll/measuring/clamping derivation, the expandToggle, and the optimistic delta with per-delta rollback and toast, so what comes out is useRelationChipCell (measurement plus the optimistic reducer). Which means B37 does not retire this: whoever deletes LeadershipStateDeputyRelationCell does. The drift is now three-way in one row — that cell opens a Popover from a pencil on touch, while the column beside it opens a Drawer, and the Status column's own comment asserts the table is Popover-only |
B34 |
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 |
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 | done 2026-07-25 (Pass 2 W4d): runCampaignFormAction + 13 ações migradas; exceções documentadas (atividades/formActions, apoiadores/[id], convite login). C8 F4b ✓ 2026-07-27: guard ampliado para *FormActions.ts + taskActions.ts; 6 irmãos migrados; taskActions.ts allowlisted — futuros siblings não driftam silenciosamente |
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 lintruns with--max-warnings=0; deliberate placeholders use a_prefix (argsIgnorePattern). as neveris banned (ESLintno-restricted-syntax). Production code uses typed helpers (e.g.hookFilledCreateDatafor hook-completed create data); tests usestub<T>(partial)which keeps property-level checking. A justifiedeslint-disable-next-lineis the only escape hatch.- Dead code is deleted, not commented. knip runs in CI (files/dependencies at error level). After any change:
pnpm exec knipand remove what died. - Collections always declare
accessexplicitly. Payload's default ("any authenticated user") is treated as a bug — seepayloadAdminOnly. - 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
/simplifyregisters surviving debts here or indocs/plans/escala-dry-pos-*.md(existing convention), whichever the item's size warrants.