| 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 |
tests/e2e/campaignNearestMunicipality.e2e.spec.ts:156, tests/e2e/campaignMunicipalities.e2e.spec.ts:394 |
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) — 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 |
|
| 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 |
P1 |
open — measured 2026-07-26, re-measured 2026-07-27 (B34). 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. Cheap adjacent win found in the same pass: getAdvisorMunicipalityIds (2 SQL / 10,7 ms) is a textbook cache() candidate — same request, same args, no dedup — and B34 added a call site in liderancas/page.tsx |
|
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 |
P2 |
open 2026-07-27 (B34) — direction recommended in the fill-in (payload-minimal, since staff edit far more than they navigate), but it trades the DB's region for the snapshot-tested catalog's, so it needs an explicit call. A third, orthogonal lever: load the Drawer behind next/dynamic on first touch — it only opens on pointer-coarse. Sibling, not duplicate: B33+ registers the same catalog in /campanha/municipios's bundle, but by accident (a predicate dragging the module) rather than by need, so the two fixes differ — pay them in one pass if at all, since the measurement bench is shared |
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 |