From 650080a8b72a77973fb42cef9eda35db17e3fa40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francisco=20Ign=C3=A1cio=20Fontoura=20Solla?= Date: Tue, 28 Jul 2026 19:50:21 -0300 Subject: [PATCH] Format B34+ docs after Prettier check on push. --- docs/TECH-DEBT.md | 4 ++-- docs/plans/escala-dry-pos-b34.md | 22 +++++++++++----------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/docs/TECH-DEBT.md b/docs/TECH-DEBT.md index 81fb8d0c..fe1abc16 100644 --- a/docs/TECH-DEBT.md +++ b/docs/TECH-DEBT.md @@ -24,10 +24,10 @@ Ledger of known engineering debt. Items large enough to be scheduled work become | 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 | | +| 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+ ✓ | +| 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 diff --git a/docs/plans/escala-dry-pos-b34.md b/docs/plans/escala-dry-pos-b34.md index d3c44fcd..72bf1de4 100644 --- a/docs/plans/escala-dry-pos-b34.md +++ b/docs/plans/escala-dry-pos-b34.md @@ -6,17 +6,17 @@ Fill-in de engenharia com os débitos que sobreviveram ao `/simplify` do **B34 **Appetite real ~3d** (sete fases abertas + Fase 0 do N+1; roadmap atualizado). Três rotas com célula de município: `/campanha/liderancas`, `/campanha/assessores`, `/campanha/dobradinhas`. -| Fase | Veredito | -| --- | --- | -| **0 — N+1 de access** | `memoizePerRequest` em `WeakMap` (não `req.context` — medido: 367 contextos, 1 `req` em `/campanha/liderancas`). `getFreshCampaignUser` + escopos de município/liderança/contato/telefone. Int `campaignAccessRequestMemo.int.spec.ts`. | -| **F7 — revalidate da lista** | `revalidatePath` da própria lista removido nos toggles de chip (liderança, dobradinha); assessor só revalida detalhe + municípios quando `slugs.length > 0`. | -| **F8 — cache do índice** | `loadMunicipalityPortfolioIndex` em `unstable_cache` + tag `municipality-catalog` no allowlist de `revalidateRequest.ts`. | -| **F1 — payload mínimo** | Índice `{ id, slug }`; `name`/`region` do `municipalityCatalog`. RSC do índice **−21.900 B raw / −3.773 B gzip** por rota (medido). First Load JS das 3 rotas **byte-idêntico** ao `main` limpo (306/298/271 kB) — o catálogo já estava no bundle. Int: slug+`region` vs catálogo. | -| **F4 — assessor lista** | `AdvisorRowViewModel.municipalityIDs`; detalhe mantém `{id,name,slug}`. | -| **F3 — delta liderança** | `withLeadershipRelationDelta` **não** — extraído `openLeadershipForRelationDelta` (lock + leitura escopada). | -| **F5 — combobox kit** | **Não** — `ui/combobox.tsx` é valor único; mantido combobox ARIA 1.2 à mão com `aria-controls` só quando expandido. | -| **F6 — baixos** | `expectPostResponse` + `createStaffLeadership` em `campaignE2EFixtures.ts`; `relationId`/`relationIds` exportados; cleanup de fixture restaura `name` do catálogo. `municipalityIdsForEntries` **não** (só caller `municipalityIdsForTseZone`). | -| **F10** | Adiado (gatilho inalterado). | +| Fase | Veredito | +| ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **0 — N+1 de access** | `memoizePerRequest` em `WeakMap` (não `req.context` — medido: 367 contextos, 1 `req` em `/campanha/liderancas`). `getFreshCampaignUser` + escopos de município/liderança/contato/telefone. Int `campaignAccessRequestMemo.int.spec.ts`. | +| **F7 — revalidate da lista** | `revalidatePath` da própria lista removido nos toggles de chip (liderança, dobradinha); assessor só revalida detalhe + municípios quando `slugs.length > 0`. | +| **F8 — cache do índice** | `loadMunicipalityPortfolioIndex` em `unstable_cache` + tag `municipality-catalog` no allowlist de `revalidateRequest.ts`. | +| **F1 — payload mínimo** | Índice `{ id, slug }`; `name`/`region` do `municipalityCatalog`. RSC do índice **−21.900 B raw / −3.773 B gzip** por rota (medido). First Load JS das 3 rotas **byte-idêntico** ao `main` limpo (306/298/271 kB) — o catálogo já estava no bundle. Int: slug+`region` vs catálogo. | +| **F4 — assessor lista** | `AdvisorRowViewModel.municipalityIDs`; detalhe mantém `{id,name,slug}`. | +| **F3 — delta liderança** | `withLeadershipRelationDelta` **não** — extraído `openLeadershipForRelationDelta` (lock + leitura escopada). | +| **F5 — combobox kit** | **Não** — `ui/combobox.tsx` é valor único; mantido combobox ARIA 1.2 à mão com `aria-controls` só quando expandido. | +| **F6 — baixos** | `expectPostResponse` + `createStaffLeadership` em `campaignE2EFixtures.ts`; `relationId`/`relationIds` exportados; cleanup de fixture restaura `name` do catálogo. `municipalityIdsForEntries` **não** (só caller `municipalityIdsForTseZone`). | +| **F10** | Adiado (gatilho inalterado). | **e2e:** suíte cheia **4 falhas** nesta árvore (picker de colunas + 3 em `campaignMunicipalities`); baseline em stash não completou por timeout de ambiente — chip specs (`campaignLeaderships`, `campaignZoneMap`) verdes a 1 worker. **Gate:** tsc/lint/format/check:cycles/build; unit 770; int 482; knip P3 pré-existente; Aikido 0 findings nos arquivos escaneados (Opengrep exit 2 do runner).