mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-14 08:53:11 +00:00
Document B8 F2 and apply the simplify pass findings.
Records the Salvador zone delivery where the next agent will look for it — roadmap, plan as-built, notebook and the geometry/map sections of AGENTS.md — including the two measurements that are easy to re-derive wrongly (the bay water in Salvador's municipal polygon, and ZE 16 wrapping ZE 14, which is why neither an area ratio nor a centroid check is a valid overlap test). Simplify fixes: the build script re-indexes simplified geometries by a unique id instead of neighbourhood name, so a repeated name in a future mesh cannot silently collapse two polygons inside a committed artifact; a stale comment claiming zone polygons do not exist; and two int assertions that the Fase 0 catalog hydration turned tautological. Ledgers the Aikido outage: the scanner returns Opengrep exit 2 for any input, so the mandatory per-change scan was skipped, not passed.
This commit is contained in:
parent
84e796ab49
commit
7b70322118
8 changed files with 84 additions and 39 deletions
File diff suppressed because one or more lines are too long
13
AGENTS.md
13
AGENTS.md
|
|
@ -70,12 +70,14 @@ Backlog consolidado (bloqueadores, site, campanha, white-label): [`docs/roadmap.
|
|||
|
||||
**Recently resolved (2026-07-25): Engineering Consolidation Pass 2** (tracker: [`docs/IMPROVE-CODE-QUALITY-PLAN.md`](docs/IMPROVE-CODE-QUALITY-PLAN.md) § Pass 2; architecture: [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md); agent map: `.cursor/rules/codebase-map.mdc`). Highlights: **W1 sistema de listas** — colunas como dado em `CampaignTable` (seams B17/B18), URL state compartilhado (`campaignListUrl` + módulos por domínio; o god module `municipalityUi.ts` virou `municipalityListUrl`/`municipalityListFilters`/`municipalityLabels`/`municipalitySignal`), shells (`CampaignListFooter`, `CampaignListEmptyState`), 8 superfícies migradas (municípios, lideranças, organizações, dobradinhas, demandas, apoiadores, assessores, atividades-shells), B16+ absorvido (`useOptimistic`, hrefs por serializador canônico, facet por slugs, facets no mesmo `Promise.all`); **W2** — fronteiras: `slug`/`phone`/`wordStartFilter`/`voteEstimate` movidos para `lib/`, 21 loaders `server-only`, `campaignRoles.ts` client-safe (sidebar sem o barrel de access), `votePledgeViews` (contract) vs `votePledgeData` (loaders), ciclo supporter quebrado, `components/campaign` em 15 subpastas de domínio; decisão D1: sem `src/domains/`, sem ports-and-adapters; **W4a** — `electionInsights.ts` (941 linhas, 2 exports vivos) deletado; sobreviventes em `electionFormat.ts`/`voteTrend.ts`; E10 nasce limpo; **W4b** — knip `exports`/`types`/`enumMembers` em **ERROR** no CI (0 findings; kit ui sem peças mortas); **W4c** — tabs das páginas de detalhe extraídas (`MunicipalityDetailTabs`, `ActivityOverviewTab`) + factory `detailTabUi`; **W4d** — `runCampaignFormAction` fecha o C8 F4 (13 ações em 7 arquivos; exceções documentadas); **W4e** — tipos de fonte única; **W4f** — copy "Praça"→"Município" varrida (apoiadores desambiguado: cidade do contato = "Cidade"); **D3** — consent do WhatsApp por chave estável (`whatsapp-inscricao`, fail-closed). Suites: 46 unit / 48 int / 7 e2e.
|
||||
|
||||
**Recently resolved (2026-07-26):** **B8 F2** Polígonos dos Municípios-zona de Salvador (ZE 1–19) — Salvador used to be **one** polygon carrying the whole city's votes, so the choropleth could not answer "which piece of Salvador is weak, or mine"; the only per-zone view was the textual `zoneBreakdown`. Two halves shipped. **(1) The mesh:** a new committed artifact `src/lib/geometries/bahia-municipality-zones.topo.json` (72,310 bytes, ~20 KB gzip, 19 features keyed by `municipalitySlug`) built by `pnpm build:municipality-zone-geometries` — see "Campaign map geometries" for the source, the parser and the three curated reconciliation tables. The interesting part is not the dissolve, it is that **the curation lives in the script, declared line by line with its evidence, and the build fails closed**: an orphan polygon or a TRE neighbourhood with no polygon aborts it, so "170/170 → 19/19" is a tested invariant. Periperi, which the resolution splits mid-street between two ZE, goes **whole** to ZE 17 — the neighbourhood is the smallest geometry available and cutting a polygon from a prose description of a street was the rabbit hole the plan already refused. The 5 polygons the resolution never names are assigned by **longest shared boundary**, and that rule was _validated_ against the 4 it names under another label before being trusted. **(2) The key:** the choropleth is no longer keyed by `codarea` but by a **map key** (`mapKeyForMunicipality`: slug for `kind === 'zona'`, `ibgeCode` otherwise) — see the "Map" bullet in the Município model for the contract, the non-interactive Salvador base and the per-city rank limitation. Because each map key now maps to exactly one catalog entry, the rollups stopped accumulating with `+=` and `computeAggregateTerritorialClass` gave way to `computeMunicipalityTerritorialClass`; `resolveMunicipalityMapNavigation` became 1:1, and with it the `kind: 'zones'` branch, its "toque de novo" readout copy and the panel's scroll-anchor all died. `buildMunicipalitiesByIbgeCode` survives untouched: B14's list href is per city on purpose. **Two measurements, so nobody re-derives them:** Salvador's municipal polygon is **~690 km²** against **~324 km²** for the 19 zones summed — the difference is the bay water the municipal limit encloses and a neighbourhood mesh never draws, which is why the int test asserts a **land**-area range instead of a ratio against the município (the first version demanded ≥95% and was comparing land to land+water); and centroid-inside-another-zone is **not** a valid overlap check here, because ZE 16 is concave and wraps ZE 14, so 16's centroid legitimately falls in 14 — non-overlap is guaranteed structurally at build time plus the area sum. New e2e `campaignZoneMap.e2e.spec.ts` pins 436 paths / 435 interactive (the one non-interactive path is Salvador's base) and that a zone opens `/campanha/municipios/salvador-ze-N`. No migration, no collection, no server action, no `Consent`. **Open debts with triggers (in the plan, not silent):** the B14 "Onde estou" card still routes Salvador to the filtered list even though resolving the exact ZE is now possible, and per-ZE competitive rank needs a new dimension in the TSE artifact. Plano: [`docs/plans/poligonos-pracas-zona.md`](docs/plans/poligonos-pracas-zona.md).
|
||||
|
||||
**Recently resolved (2026-07-26):** **E12** Camada TI — extends the E17/B21 rollup (`territoryOverview.ts` + `loadTerritoryOverview.ts`) with E8 goal coverage (`central`), MAUP-safe regional capture (aggregate ratio Σ own votes ÷ Σ field ceiling — never the mean of municipal ratios; median, min–max, named critical municipality and beacon), and aggregated territorial class via `computeAggregateTerritorialClass` (same rule as B13 for TI groups). Staff surface: three new columns on `/campanha/territorios` (Cobertura, Captura, Classe) with B22 header descriptions and sorts `cobertura`/`captura`/`classe`; municipality detail gets intra-TI benchmark T4 on `MunicipalityGoalAccountCard` (`territoryIntraCaptureBenchmark.ts` — Salvador zone slugs compare among the 19 zones, other Metropolitano peers among RMS municipalities). Two E18 concepts (`captura-regional`, `benchmark-intra-ti`). v1 cuts kept: no `?group=territorio` on the municipality list, no TI map mode. No migration. Plano: [`docs/plans/camada-territorios-identidade.md`](docs/plans/camada-territorios-identidade.md).
|
||||
|
||||
**Recently resolved (2026-07-26):** **B24** Auto-save + justificativa no popover de Tendência — `MunicipalityListTrendControl` grava sozinho via `POST /campanha/municipios/political-trend` (status ~150 ms, nota 600 ms, flush blur/fechamento), `Textarea` de justificativa no Popover, sem `revalidatePath` da lista; `/editar` mantém submit explícito. Plano: [`docs/plans/autosave-tendencia-lista-municipios.md`](docs/plans/autosave-tendencia-lista-municipios.md).
|
||||
|
||||
**Recently resolved (2026-07-26):** **B14** Município mais próximo (atalho geo no Início staff) — a new client island `NearestMunicipalityCard` ("Onde estou") answers "open the município I am standing in", and the answer is **point-in-polygon**, not a nearest-centroid guess. The source plan had rejected polygons as a bundle rabbit hole; the premise was false by the time it shipped, because `MunicipalityMapPanel` always mounts in `mode="municipality"` and `BahiaMap` calls `loadMunicipalityGeometryModule()` on mount, so the 417 polygons (132 KB raw, **41 KB gzip**, own memoized chunk) are already in the browser of anyone on `/campanha` — the card reuses that promise and adds no geometry. So **no centroid artifact, no build script, no migration, no collection, no server action and no `Consent` key**: the position is read by `navigator.geolocation` and matched in the browser, and never leaves the device. Pure math in `src/lib/municipalityProximity.ts` (haversine, ray casting with holes, area-weighted centroid, discriminated union `inScope | zoneCity | outOfScope | outsideBahia`; **measured, so nobody re-derives it:** the mesh decodes to 417 plain `Polygon`s with zero holes and zero MultiPolygons, a full containment scan costs **0.106 ms** and the whole fallback **0.32 ms** — 81% of it centroids — so there is no hot path here and a centroid artifact would still not pay for itself; building a fresh `Map` per call measured _slower_, and the module's existing `getMunicipalityFeature` index is passed in only because the map already built it); browser plumbing in `src/utilities/campaignGeolocation.ts` (sibling of `recentVisits.ts`: session flag `teqo:campaign:geo-prompted-session` so the automatic prompt fires at most once per tab, guarded permission read where Safari's missing descriptor legitimately answers `unknown`, typed failures). Centroid distance survives only to rank the fallback: an advisor standing outside his portfolio is told where he is **and** offered the closest município he can open, up to `NEAREST_IN_SCOPE_MAX_KM` = 150 km. Salvador still gets no guessed ZE (trigger stays **B8 F2**) — it links to the filtered list, with the zone count limited to what the actor can read. `StaffDashboardView` gained `accessibleMunicipalities` (~6.6 KB gzip of RSC payload for all 435), and the zone-list href reaches the island as a `zoneCityHrefs` prop serialized by `CampaignDashboard` (the RSC that already builds list hrefs) rather than by the Payload loader: the first version imported `buildMunicipalityListHref` into the island and cost **21 kB** of First Load JS for a single link, because the canonical serializer drags `bahiaTerritories` + `municipalityCatalog` in — the "client components must not import server data modules for values" rule applies to **URL serializers** too, so the href is serialized on the server. `/campanha` First Load JS: 265 → **268 kB** (measured against a stashed baseline, not against the number in a previous note). A coarse fix (accuracy > 10 km, typical of a desk) renders the result **with** an explicit "localização aproximada" caveat rather than silently naming the wrong município. The shortcuts row hides itself in **CSS** (`hidden … has-[>*]:grid`, written inline in `CampaignDashboard`): having the children report visibility upward cost two state slots, a callback prop, an effect and a layout shift on every load, because the server rendered the row hidden and the effect only unhid it after first paint — and with that state gone, the `DashboardShortcuts` wrapper was a named `div`, so it is gone too. `loadMunicipalityGeometryModule` memoizes the dynamic import's **rejection**, so the card catches it and falls into an actionable failure state — otherwise a ChunkLoadError left a spinner with a retry button that could never recover. Plano: [`docs/plans/municipio-mais-proximo.md`](docs/plans/municipio-mais-proximo.md).
|
||||
**Recently resolved (2026-07-26):** **B13** Escala relativa no mapa dos Municípios — the dashboard choropleth was nearly monochrome because 0–100% of valid votes compresses a race whose statewide ceiling is 4.71%. It now offers three scales **relative to the candidate himself**, all as **discrete classes**: **quantis** (the new default), **padrão próprio (LQ)** and **posição no município**, next to the existing "% dos válidos" (B11) and "Total (votos)". Pure math in `src/lib/mapScaleClasses.ts` (new file — `bahiaMapStyle.ts` already owned path styling); `resolvePathStyle` gained a `fillByKey` path that **replaces** the continuous ramp when present, where a missing key is "no data", which is not a value of zero. **LQ is 5 named classes cut on the E10 anchors, not diverging** — the source plan asked for both and contradicted itself; red×blue keeps meaning "Solla × adversário" and stays exclusive to the compare mode. `TERRITORIAL_CLASS_ANCHORS` / `AT_STANDARD_LQ` moved to `src/lib/territorialClassAnchors.ts` because the map panel is a client component and importing the classifier there would drag the ~600 KB TSE artifact into the browser bundle; numbers only, so recalibration (**E15**) stays a one-line diff in one file. Quantiles are computed over the **user's access scope** (435 for coordinator/candidate, the portfolio for an advisor) and **degrade to 3 classes below 10 municípios**, with the legend stating the effective count. "Posição no município" existed nowhere, so it went into the **committed artifact (v3)** rather than a request-time query — see "Election baseline data" above. New in E10: `computeAggregateTerritorialClass(slugs)` classes a GROUP as one territory by summing inputs and running the **same** `classifyMunicipalityTerritory`, never averaging per-slug classes (LQ is a ratio, and the mean of ratios is not the ratio of the sums) — **E12's TI rollup must inherit this helper instead of growing a second one**. The map loader fills `statewideShareByYear`, `territorialClassByCode`, `competitiveRankByYear` and `projectedValidVotesByCode`, all pure over the artifact: **no new query per request**. UI: discrete `MapScaleLegend` with real ranges tied to the selector via `aria-describedby`, a readout that always spells out the reason ("4ª de 5 faixas", "1,8× o padrão estadual do candidato", "12º entre 663 candidatos votados aqui"), and an opt-in proportional-symbol layer (area ∝ projected valid votes, E10 class colour, `interactive: false` so the bubble does not stack a second hover target on its own município) — 417 markers were **measured** before optimizing and did not drop the frame, so the top-K fallback was not built. Two new concepts in `/campanha/conceitos`. No migration, no collection, no server action; `/campanha`'s First Load JS is byte-identical (9.46 kB / 265 kB) because the map panel already ships in its own `ssr: false` chunk — that chunk itself grew, and the RSC payload grew ~64 KB uncompressed (~10 KB gzipped), dominated by `competitiveRankByYear`. **Known flake (pre-existing, reproduced on a clean tree):** `campaignMunicipalities.e2e.spec.ts` fails intermittently with 2 workers — the `LeaderContactsPanel` consent checkbox clicked before hydration, and a `getByRole` regex that matches both "Conde" and "Condeúba" when the fixture picks the former. Plano: [`docs/plans/escala-relativa-mapa.md`](docs/plans/escala-relativa-mapa.md).
|
||||
**Recently resolved (2026-07-26):** **B14** Município mais próximo (atalho geo no Início staff) — a new client island `NearestMunicipalityCard` ("Onde estou") answers "open the município I am standing in", and the answer is **point-in-polygon**, not a nearest-centroid guess. The source plan had rejected polygons as a bundle rabbit hole; the premise was false by the time it shipped, because `MunicipalityMapPanel` always mounts in `mode="municipality"` and `BahiaMap` calls `loadMunicipalityGeometryModule()` on mount, so the 417 polygons (132 KB raw, **41 KB gzip**, own memoized chunk) are already in the browser of anyone on `/campanha` — the card reuses that promise and adds no geometry. So **no centroid artifact, no build script, no migration, no collection, no server action and no `Consent` key**: the position is read by `navigator.geolocation` and matched in the browser, and never leaves the device. Pure math in `src/lib/municipalityProximity.ts` (haversine, ray casting with holes, area-weighted centroid, discriminated union `inScope | zoneCity | outOfScope | outsideBahia`; **measured, so nobody re-derives it:** the mesh decodes to 417 plain `Polygon`s with zero holes and zero MultiPolygons, a full containment scan costs **0.106 ms** and the whole fallback **0.32 ms** — 81% of it centroids — so there is no hot path here and a centroid artifact would still not pay for itself; building a fresh `Map` per call measured _slower_, and the module's existing `getMunicipalityFeature` index is passed in only because the map already built it); browser plumbing in `src/utilities/campaignGeolocation.ts` (sibling of `recentVisits.ts`: session flag `teqo:campaign:geo-prompted-session` so the automatic prompt fires at most once per tab, guarded permission read where Safari's missing descriptor legitimately answers `unknown`, typed failures). Centroid distance survives only to rank the fallback: an advisor standing outside his portfolio is told where he is **and** offered the closest município he can open, up to `NEAREST_IN_SCOPE_MAX_KM` = 150 km. Salvador still gets no guessed ZE — it links to the filtered list, with the zone count limited to what the actor can read. `StaffDashboardView` gained `accessibleMunicipalities` (~6.6 KB gzip of RSC payload for all 435), and the zone-list href reaches the island as a `zoneCityHrefs` prop serialized by `CampaignDashboard` (the RSC that already builds list hrefs) rather than by the Payload loader: the first version imported `buildMunicipalityListHref` into the island and cost **21 kB** of First Load JS for a single link, because the canonical serializer drags `bahiaTerritories` + `municipalityCatalog` in — the "client components must not import server data modules for values" rule applies to **URL serializers** too, so the href is serialized on the server. `/campanha` First Load JS: 265 → **268 kB** (measured against a stashed baseline, not against the number in a previous note). A coarse fix (accuracy > 10 km, typical of a desk) renders the result **with** an explicit "localização aproximada" caveat rather than silently naming the wrong município. The shortcuts row hides itself in **CSS** (`hidden … has-[>*]:grid`, written inline in `CampaignDashboard`): having the children report visibility upward cost two state slots, a callback prop, an effect and a layout shift on every load, because the server rendered the row hidden and the effect only unhid it after first paint — and with that state gone, the `DashboardShortcuts` wrapper was a named `div`, so it is gone too. `loadMunicipalityGeometryModule` memoizes the dynamic import's **rejection**, so the card catches it and falls into an actionable failure state — otherwise a ChunkLoadError left a spinner with a retry button that could never recover. Plano: [`docs/plans/municipio-mais-proximo.md`](docs/plans/municipio-mais-proximo.md).
|
||||
**Recently resolved (2026-07-26):** **B13** Escala relativa no mapa dos Municípios — the dashboard choropleth was nearly monochrome because 0–100% of valid votes compresses a race whose statewide ceiling is 4.71%. It now offers three scales **relative to the candidate himself**, all as **discrete classes**: **quantis** (the new default), **padrão próprio (LQ)** and **posição no município**, next to the existing "% dos válidos" (B11) and "Total (votos)". Pure math in `src/lib/mapScaleClasses.ts` (new file — `bahiaMapStyle.ts` already owned path styling); `resolvePathStyle` gained a `fillByKey` path that **replaces** the continuous ramp when present, where a missing key is "no data", which is not a value of zero. **LQ is 5 named classes cut on the E10 anchors, not diverging** — the source plan asked for both and contradicted itself; red×blue keeps meaning "Solla × adversário" and stays exclusive to the compare mode. `TERRITORIAL_CLASS_ANCHORS` / `AT_STANDARD_LQ` moved to `src/lib/territorialClassAnchors.ts` because the map panel is a client component and importing the classifier there would drag the ~600 KB TSE artifact into the browser bundle; numbers only, so recalibration (**E15**) stays a one-line diff in one file. Quantiles are computed over the **user's access scope** (435 for coordinator/candidate, the portfolio for an advisor) and **degrade to 3 classes below 10 municípios**, with the legend stating the effective count. "Posição no município" existed nowhere, so it went into the **committed artifact (v3)** rather than a request-time query — see "Election baseline data" above. New in E10: `computeAggregateTerritorialClass(slugs)` classes a GROUP as one territory by summing inputs and running the **same** `classifyMunicipalityTerritory`, never averaging per-slug classes (LQ is a ratio, and the mean of ratios is not the ratio of the sums) — **E12's TI rollup must inherit this helper instead of growing a second one**. The map loader fills `statewideShareByYear`, `territorialClassByCode`, `competitiveRankByYear` and `projectedValidVotesByCode`, all pure over the artifact: **no new query per request**. _(B8 F2 later re-keyed the last two to `territorialClassByMapKey` / `projectedValidVotesByMapKey`.)_ UI: discrete `MapScaleLegend` with real ranges tied to the selector via `aria-describedby`, a readout that always spells out the reason ("4ª de 5 faixas", "1,8× o padrão estadual do candidato", "12º entre 663 candidatos votados aqui"), and an opt-in proportional-symbol layer (area ∝ projected valid votes, E10 class colour, `interactive: false` so the bubble does not stack a second hover target on its own município) — 417 markers were **measured** before optimizing and did not drop the frame, so the top-K fallback was not built. Two new concepts in `/campanha/conceitos`. No migration, no collection, no server action; `/campanha`'s First Load JS is byte-identical (9.46 kB / 265 kB) because the map panel already ships in its own `ssr: false` chunk — that chunk itself grew, and the RSC payload grew ~64 KB uncompressed (~10 KB gzipped), dominated by `competitiveRankByYear`. **Known flake (pre-existing, reproduced on a clean tree):** `campaignMunicipalities.e2e.spec.ts` fails intermittently with 2 workers — the `LeaderContactsPanel` consent checkbox clicked before hydration, and a `getByRole` regex that matches both "Conde" and "Condeúba" when the fixture picks the former. Plano: [`docs/plans/escala-relativa-mapa.md`](docs/plans/escala-relativa-mapa.md).
|
||||
|
||||
**Recently resolved (2026-07-25):** **C13** "Plano de Ação" → "Atividade" — the vertical speaks one word now, the one the mesa already uses. Collection `actionPlan` → `activity` (`src/collections/Activity.ts`, labels "Atividade"/"Atividades"), `campaignDemand.actionPlan` → `activity`, route `/campanha/planos` → `/campanha/atividades` (create at `/campanha/atividades/nova`), sidebar entry "Atividades", and field labels realigned to the entity ("Tipo de atividade", "Origem da atividade", "Resultado da atividade") — enum **values** (`kind`/`status`/`origin`) are unchanged. Schema moved by a **hand-written data-preserving migration** (`20260725_213000_rename_action_plan_to_activity`): 4 tables, 3 enums, 2 columns, 39 indexes (4 pkeys included — renaming a pkey index renames its constraint), 14 FKs and 2 sequences, every statement guarded on the source object existing so environments converge either way, with a symmetric `down()`. Payload's generator would have emitted DROP+CREATE (data loss) and cannot see the hand-written partial index `action_plan_upcoming_start_at_idx`, so the object list came from `pg_indexes`/`pg_constraint`/`pg_type`/`pg_sequences` and the `.json` snapshot was written by transforming its predecessor — validated by `pnpm migrate:create __probe` reporting no schema changes. The same migration aligned the **8 fossil `%plaza%` index names** the Município remodel left behind (it renamed the `plaza_id` columns but not their indexes, so the database and the snapshot had been disagreeing since 2026-07-23). Guard: the `codebaseConventions.unit.spec.ts` "banned campaign terminology" describe is now table-driven and sweeps `src` + `tests` + `scripts` (migrations stay out — frozen history), banning the `actionPlan` compounds, the pt-BR copy and the old route, but **not** bare `plan`/`plano` (that would collide with the white-label subscription plans of Fase 2); allowlist covers the spec itself, `scripts/generate-remodel-municipalities-migration.mjs` and the historical comment in `campaignMigrationReconciliation.int.spec.ts`. Plano: [`docs/plans/renomear-plano-acao-para-atividade.md`](docs/plans/renomear-plano-acao-para-atividade.md).
|
||||
|
||||
|
|
@ -155,7 +157,7 @@ The internal `/campanha` area is gated by its own authentication, deliberately k
|
|||
- **Vote pledge asymmetry:** staff declare `declaredVotes` and record `estimatedVotes` (pessimistic/central/optimistic); leaders never see estimates. Aggregates use `estimated[S] ?? declared`, default `central` (`src/utilities/votePledgeData.ts`).
|
||||
- **Demandas:** staff-only create/read (`canCreateCampaignDemand`); optional `leadership` provenance selected by staff. Escalated decisions: coordinator **or** candidate.
|
||||
- **Dobradinhas:** `stateDeputy` entity + `municipality.stateDeputies` / `leadership.stateDeputies` hasMany; UI at `/campanha/dobradinhas`.
|
||||
- **Map:** staff dashboard (`/campanha`) loads `MunicipalityMapPanel`; `/campanha/municipios` is list/overview only. Scales (B13): **quantis** (default), **padrão próprio (LQ)**, **posição no município**, **% dos válidos** (B11) and **Total (votos)**. The three relative ones are **discrete classes, never a continuous ramp** — the pure math lives in `src/lib/mapScaleClasses.ts` (quantiles degrade to 3 classes below 10 municípios and the legend says so; LQ is cut on the E10 anchors in `src/lib/territorialClassAnchors.ts`). Diverging red×blue stays exclusive to the compare mode, where it means "Solla × adversário" — do not reuse it for LQ. Quantiles are computed over the **user's access scope**, so an advisor sees his portfolio classed against itself. Rank is unavailable in 2026 (TSE data) and LQ in 2026 reuses the 2022 statewide standard, the same precedent `percentValid` set. An optional proportional-symbol layer sizes bubbles by projected valid votes (area ∝ value) and colours them by the E10 class. A class label never renders alone: the readout always spells out the reason.
|
||||
- **Map:** staff dashboard (`/campanha`) loads `MunicipalityMapPanel`; `/campanha/municipios` is list/overview only. **Keyed by "map key", not `ibgeCode`** (B8 F2, 2026-07-26): `mapKeyForMunicipality` returns the `municipalitySlug` for `kind === 'zona'` and the `ibgeCode` otherwise, so each of Salvador's 19 ZE is painted, hovered and opened on its own from `bahia-municipality-zones.topo.json`. Every bundle field follows that key (`municipalitiesByMapKey`, `territorialClassByMapKey`, `projectedValidVotesByMapKey`, `valuesByYear`, …) and each key maps to exactly one catalog entry, so rollups assign instead of accumulating. Salvador's **municipal** polygon is still drawn, underneath, as a non-interactive base with no data (`baseKeys` in `BahiaMap`) — two independently simplified meshes do not close on the same coastline, and deleting it opened a gap in the state outline; being out of the key index, it cannot double-count or steal a hover. Feature→key resolution is `featureMapKey` (`municipalitySlug ?? codarea ?? code`). **`competitiveRank` is still per city:** the TSE artifact only carries `federalRankByIbgeCode`, so all 19 zones share the city's placement — which is what the legend already claims, and an int test pins it. Scales (B13): **quantis** (default), **padrão próprio (LQ)**, **posição no município**, **% dos válidos** (B11) and **Total (votos)**. The three relative ones are **discrete classes, never a continuous ramp** — the pure math lives in `src/lib/mapScaleClasses.ts` (quantiles degrade to 3 classes below 10 municípios and the legend says so; LQ is cut on the E10 anchors in `src/lib/territorialClassAnchors.ts`). Diverging red×blue stays exclusive to the compare mode, where it means "Solla × adversário" — do not reuse it for LQ. Quantiles are computed over the **user's access scope**, so an advisor sees his portfolio classed against itself. Rank is unavailable in 2026 (TSE data) and LQ in 2026 reuses the 2022 statewide standard, the same precedent `percentValid` set. An optional proportional-symbol layer sizes bubbles by projected valid votes (area ∝ value) and colours them by the E10 class. A class label never renders alone: the readout always spells out the reason.
|
||||
- **URLs:** `/campanha/municipios[/slug]`, `/campanha/dobradinhas`, `/campanha/liderancas[/id]`, `/campanha/organizacoes[/slug]`, `/campanha/demandas[/slug]`, `/campanha/atividades[/slug]`, `/campanha/apoiadores`, `/campanha/assessores` (coordinator/candidate), `/campanha/conceitos` (staff glossary), `/campanha/perfil`.
|
||||
|
||||
## Campaign supporters (C2)
|
||||
|
|
@ -182,10 +184,11 @@ Mobilization events / agenda backing the campaign's field operations (roadmap it
|
|||
|
||||
Static Bahia map foundation for future Leaflet surfaces (roadmap B2; UI is B3). No migration, collection, Consent, or server action — same static-data pattern as `bahiaTerritories` / `bahiaTseZones`.
|
||||
|
||||
- **Artifacts (committed):** `src/lib/geometries/bahia-municipalities.topo.json` (~132 KB; Topology object `municipalities`, 417 features, `properties: { codarea, name }`) and `src/lib/geometries/bahia-identity-territories.topo.json` (~15 KB; object `territories`, 27 features, `properties: { code, name }`). Soft size budget in tests: ≤ ~600 KB each.
|
||||
- **Artifacts (committed):** `src/lib/geometries/bahia-municipalities.topo.json` (~132 KB; Topology object `municipalities`, 417 features, `properties: { codarea, name }`), `src/lib/geometries/bahia-identity-territories.topo.json` (~15 KB; object `territories`, 27 features, `properties: { code, name }`) and — since B8 F2 (2026-07-26) — `src/lib/geometries/bahia-municipality-zones.topo.json` (~72 KB; object `municipalityZones`, **19** features, `properties: { municipalitySlug, name, zoneNumber, ibgeCode }`), the Salvador ZE mesh. Soft size budget in tests: ≤ ~600 KB each.
|
||||
- **Code table:** `src/lib/bahiaMunicipalityCodes.ts` — canonical municipality name → IBGE 7-digit `codarea`, plus `codeForMunicipality` / `municipalityForCode`. Fixture `tests/fixtures/bahia-municipality-codes.official.json` + int tests for bijection/coverage.
|
||||
- **Runtime helpers:** `src/lib/bahiaGeometries.ts` — `getMunicipalityFeature` / `getTerritoryFeature`, typed topologies, feature arrays. TopoJSON→GeoJSON decode is LAZY (dynamic import via `loadMunicipalityGeometryModule`; B5 F1 done) — the default `/campanha` server graph never pays it.
|
||||
- **Runtime helpers:** `src/lib/bahiaGeometries.ts` — `getMunicipalityFeature` / `getTerritoryFeature` / `getMunicipalityZoneFeature`, typed topologies, feature arrays. TopoJSON→GeoJSON decode is LAZY (dynamic import via `loadMunicipalityGeometryModule` / `loadMunicipalityZoneGeometryModule`; B5 F1 done) — the default `/campanha` server graph never pays it. Shared feature types live in `bahiaGeometriesTypes.ts`; geometry math that must accept any of the three meshes (point-in-polygon, centroid) takes `BahiaGeometryFeature`.
|
||||
- **Territory polygons:** dissolved from IBGE municipality meshes via `bahiaIdentityTerritoryRecords` (`topojson` `merge`), not the IDE Bahia shapefile (IDE remains provenance/validation reference only).
|
||||
- **Rebuild (Salvador zones):** `pnpm build:municipality-zone-geometries` (`scripts/build-municipality-zone-geometries.mjs`; `--report` reconciles without writing) downloads IBGE `BA_bairros_CD2022.zip` from geoftp, parses the shapefile with **`shpjs`** (devDependency — that cut is SHP-only, SIRGAS 2000), keeps `CD_MUN === '2927408'` (170 polygons) and dissolves neighbourhood→zone against **TRE-BA RA 02/2017** (`municipalityZoneNeighborhoods.ts`). Reconciliation is **curated data in the script header**, not cleverer matching: an alias table, a table for TRE neighbourhoods with no polygon of their own (the build refuses a merge that would cross zones), and a table for the one neighbourhood the resolution splits mid-street (it goes whole to one ZE — the neighbourhood is the atom). The 5 polygons the resolution never names are assigned by **longest shared boundary**, a rule validated against the 4 it names under another label. The build fails on an orphan polygon or a TRE neighbourhood with no polygon, so 170/170 → 19/19 is an invariant, not a hope. Same cache dir and the same "**does not touch the database**" guarantee as the sibling below.
|
||||
- **Rebuild:** `pnpm build:geometries` (`scripts/build-bahia-geometries.mjs`) downloads IBGE Malhas (qualidade intermediaria) + Localidades, reconciles names with `canonicalizeMunicipalityName`, simplifies/quantizes, emits the artifacts above. Cache under `data/geometries/` (gitignored; override with `GEOMETRIES_CACHE_DIR`). **Does not touch the database** — no `assertLocalDatabase` / no Neon risk. Not part of `pnpm build` or `pnpm dev`. Details: `docs/plans/mapa-bahia-geometrias.md`. Scale/DRY follow-ups from `/simplify`: `docs/plans/escala-dry-pos-b2.md` (B5).
|
||||
|
||||
## Election baseline data (TSE 2022)
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ Ledger of known engineering debt. Items large enough to be scheduled work become
|
|||
| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ | --------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | -------------------- | --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------- |
|
||||
| 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` | 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 | |
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
# B8 — Polígonos dos Municípios-zona de Salvador (ZE 1–19)
|
||||
|
||||
Status: F1 entregue em código (2026-07-21; em produção desde 2026-07-23); F2 pendente
|
||||
Atualizado em: 2026-07-24
|
||||
Status: F1 entregue em código (2026-07-21; em produção desde 2026-07-23); **F2 entregue em código (2026-07-26)** — deploy pendente
|
||||
Atualizado em: 2026-07-26
|
||||
Revisão 2026-07-26 (F2 entregue): as duas questões em aberto foram fechadas na implementação (malha **IBGE 2022**, sem pivô para GeoSalvador; chave = **híbrido A+B** — `municipalitySlug` nas zonas, `ibgeCode` no resto, com o polígono municipal de Salvador **mantido como base sem interação** em vez de removido). As-built completo na seção "F2 as-built" abaixo; débitos novos em "Adiado com gatilho".
|
||||
Revisão 2026-07-24 (remodelagem Municípios M1): **Camaçari saiu do escopo** — virou município inteiro (ZE 170/171 agregadas), e as entradas de Camaçari foram removidas do catálogo (`MunicipalityZoneNeighborhoodCity = 'Salvador'`; fonte única `tre-ra-02-2017`). B8 agora cobre **só as 19 zonas de Salvador**. Identificadores renomeados `plaza*` → `municipality*` na M1. As menções a Camaçari abaixo foram mantidas apenas como histórico de pesquisa.
|
||||
Revisão 2026-07-21: F1 implementada — `municipalityZoneNeighborhoods.ts`, fixture+int, `MunicipalityZoneNeighborhoodsCard` no overview do Município-zona; mapa inalterado (F2).
|
||||
Revisão 2026-07-21 (pós-`/simplify` + capture-review-debts): débitos S1–S3 absorvidos como **F2 prep** (~½ dia, dentro do appetite F2); S4–S10 em Explicitamente fora / Adiado; JR1–JR7 em Já resolvido (não reabrir no simplify).
|
||||
|
|
@ -58,10 +59,10 @@ _Histórico Camaçari (fora de escopo desde M1):_ não havia lista oficial zona
|
|||
- **Polígonos = dissolve de bairros (IBGE e/ou malha municipal), não geocodificar seções.** Mesmo padrão de dissolução já usado em TIs (B2). Gaps: polígono manual leve ou omitir pedaço com nota. **Rejeitado:** geocodificar milhares de seções → casco; pedir shapefile ao TRE neste ciclo; PostGIS.
|
||||
- **i18n e naming** (AGENTS.md): identificadores `municipalityZoneNeighborhoods` (existente), `build-municipality-zone-geometries.mjs`, `bahia-municipality-zones.topo.json`, `getMunicipalityZoneFeature(slug)`; strings UI em pt-BR.
|
||||
|
||||
## Questões em aberto
|
||||
## Questões em aberto — fechadas na F2 (2026-07-26)
|
||||
|
||||
- **Malha de bairros: IBGE 2022 vs GeoSalvador?** **Opções:** A) só IBGE BA | B) GeoSalvador. **Recomendação:** A na F2 com reconciliação de nomes TRE↔`NM_BAIRRO`; se cobertura de Salvador for insuficiente no spike de ½ dia, pivotar B. Validar no spike antes de lock de artefato.
|
||||
- **Chave do coroplético no mapa: `municipalitySlug` ou continuar `ibgeCode` + overlay?** **Opções:** A) mode/`values` por `municipalitySlug` para Municípios-zona + município para o resto | B) split Salvador em features de zona substituindo o polígono municipal. **Recomendação:** B no footprint de Salvador (um polígono por Município-zona; o polígono municipal some lá) e A/`ibgeCode` no restante da Bahia — evita dupla contagem visual. _(assumido — validar no craft)_
|
||||
- **Malha de bairros: IBGE 2022 vs GeoSalvador?** → **A (IBGE `BA_bairros_CD2022`)**, sem pivô. O spike deu **170/170** polígonos de Salvador atribuídos e **19/19** zonas cobertas; os unmatched críticos que disparariam GeoSalvador (>10% dos bairros TRE sem polígono) não apareceram. Custo: `shpjs` como devDependency, porque o IBGE só publica SHP (SIRGAS 2000) nesse recorte.
|
||||
- **Chave do coroplético: `municipalitySlug` ou `ibgeCode` + overlay?** → **híbrido A+B**, com um ajuste sobre a recomendação: o polígono municipal de Salvador **não sumiu**, ficou desenhado por baixo como **base sem dado e sem pointer**. Duas malhas simplificadas de forma independente não fecham na mesma linha de costa, e apagar o polígono municipal abria sliver de fundo no contorno do estado. A base não entra no índice de chaves, então não há dupla contagem visual nem alvo duplo de hover — o risco que a recomendação original queria evitar.
|
||||
|
||||
## Abordagem proposta
|
||||
|
||||
|
|
@ -88,11 +89,11 @@ Componentes:
|
|||
|
||||
### Fases
|
||||
|
||||
| Fase | Entrega | Appetite |
|
||||
| ------- | ------------------------------------------------------------------------------ | ------------- |
|
||||
| F1 ✓ | Catálogo + testes + bloco no município (entregue; Salvador-only desde M1) | ~1 dia |
|
||||
| F2 prep | Hidratação via `municipalityCatalog` + canônico JSON→TS + teste ordem de slugs | ~½ dia |
|
||||
| F2 | Spike cobertura malha → script → TopoJSON → mapa | ~1,5–2,5 dias |
|
||||
| Fase | Entrega | Appetite |
|
||||
| --------- | ------------------------------------------------------------------------------------------- | ------------- |
|
||||
| F1 ✓ | Catálogo + testes + bloco no município (entregue; Salvador-only desde M1) | ~1 dia |
|
||||
| F2 prep ✓ | Hidratação via `municipalityCatalog` + teste ordem de slugs (S1+S3; **S2 cortado**, abaixo) | ~½ dia |
|
||||
| F2 ✓ | Spike cobertura malha → script → TopoJSON → mapa (entregue 2026-07-26) | ~1,5–2,5 dias |
|
||||
|
||||
**F2 prep** (débitos pós-`/simplify`, capture-review-debts 2026-07-21 — não item paralelo no roadmap):
|
||||
|
||||
|
|
@ -122,6 +123,30 @@ Componentes:
|
|||
- **Desenhar 19 polígonos à mão no QGIS sem catálogo.** Irrepetível e sem teste. **Mitigação:** F1 obrigatória (feita); manual só para unmatched.
|
||||
- **Tratar polígono derivado como limite oficial TSE.** Risco jurídico/comunicação. **Mitigação:** copy “aproximação a partir de bairros; não é limite oficial”.
|
||||
|
||||
## F2 as-built (2026-07-26)
|
||||
|
||||
**Artefato.** `src/lib/geometries/bahia-municipality-zones.topo.json` — **72.310 bytes** (~20 KB gzip; budget de 600 KB do padrão B2 sobrou inteiro), objeto `municipalityZones`, 19 features com `properties: { municipalitySlug, name, zoneNumber, ibgeCode }`. Gerado por `pnpm build:municipality-zone-geometries` (`scripts/build-municipality-zone-geometries.mjs`, flag `--report` para só reconciliar sem escrever), que baixa/cacheia `BA_bairros_CD2022.zip` em `data/geometries/` (gitignored), parseia com `shpjs`, filtra `CD_MUN === '2927408'`, simplifica no quantil 0.35, dissolve por zona com `topojson.merge` e quantiza em 1e4 — mesma ordem do irmão `build-bahia-geometries.mjs`, e como ele **não toca o banco**. Consumo lazy por `loadMunicipalityZoneGeometryModule()` (`bahiaMunicipalityZoneGeometries.ts`), no padrão memoizado de B5 F1: o grafo default do `/campanha` não paga o decode.
|
||||
|
||||
**Reconciliação (a parte que não é código).** Três tabelas curadas no cabeçalho do script, cada linha com a evidência:
|
||||
|
||||
- `IBGE_TO_TRE_NEIGHBORHOOD` — 7 aliases de grafia/composição (`Beiru/Tancredo Neves`, CAB, ilhas da baía).
|
||||
- `TRE_NEIGHBORHOODS_MERGED_INTO` — bairro da resolução sem polígono próprio no IBGE (Jardim Cruzeiro dentro de Vila Ruy Barbosa). O build **recusa** um merge cujos dois lados caiam em zonas diferentes.
|
||||
- `SPLIT_NEIGHBORHOOD_ZONES` — Periperi, que a RA 02/2017 corta na Rua das Pedrinhas entre duas zonas, vai **inteiro** para a ZE 17: o bairro é o átomo geométrico disponível, e partir polígono por descrição de rua era o rabbit hole que este plano já recusava.
|
||||
|
||||
Atribuição em dois passes: (1) nome citado pela resolução; (2) **maior fronteira compartilhada** para os 5 bairros que a RA nunca nomeia (Ilha Amarela, Dois de Julho, Chame-Chame, Horto Florestal, Mirantes de Periperi). O passe 2 foi **validado**, não assumido: rodado sobre os 4 bairros que a resolução nomeia sob outro rótulo, ele reproduz a zona oficial nos 4. O build falha se sobrar polígono órfão ou bairro TRE sem polígono — hoje 170/170 → 19/19.
|
||||
|
||||
**Re-keying (o que mudou fora do mapa).** Map key = `kind === 'zona' ? municipalitySlug : ibgeCode`, via `mapKeyForMunicipality` + `buildMunicipalitiesByMapKey` (`municipalityMapNavigation.ts`). O bundle inteiro passou a ser keyed por ela (`valuesByYear`, `values2026ByScenario`, `validVotesByYear`, `diffByYear`, `territorialClassByMapKey`, `projectedValidVotesByMapKey`, `municipalitiesByMapKey`); os rollups pararam de acumular com `+=` porque cada chave é agora 1:1, e por isso `computeAggregateTerritorialClass` deu lugar a `computeMunicipalityTerritorialClass`. `resolveMunicipalityMapNavigation` virou 1:1 e o ramo `kind: 'zones'` morreu junto com a copy "toque de novo para ver as zonas" e o scroll-anchor do painel. `buildMunicipalitiesByIbgeCode` sobrevive intacto: o href de lista do **B14 ✓** é por cidade, de propósito.
|
||||
|
||||
**Rank competitivo continua por cidade.** O artefato TSE (v3) só tem `federalRankByIbgeCode`, então as 19 zonas herdam a posição da cidade inteira — exatamente o que a legenda da escala "posição no município" já afirma. Int novo pina que as 19 compartilham **uma** posição, para que a limitação seja um invariante testado e não uma surpresa. Rank por ZE exigiria uma dimensão nova no artefato (gatilho abaixo).
|
||||
|
||||
**Mapa.** `BahiaMap` monta a camada de município a partir de duas malhas (`loadLayerFeatures`), com o polígono municipal de Salvador desenhado por baixo como base sem dado e sem pointer (`baseKeys`). `keyPropertyForMode`/`featureKeyFromProperties` deram lugar a `featureMapKey` (`municipalitySlug ?? codarea ?? code` — as três nunca coexistem, então o argumento de modo era supérfluo); `fitMapToHighlights` passou a medir os paths desenhados (`pathByKeyRef`) em vez do módulo de geometria, que já não sabe sozinho de qual malha veio a chave, e `geometryModuleRef` foi apagado.
|
||||
|
||||
**Dois fatos medidos (não re-derivar).** (1) O polígono municipal de Salvador tem **~690 km²** e a soma das 19 zonas **~324 km²**: a diferença é a água da baía que o limite municipal encerra e a malha de bairros não desenha. Por isso o int de área compara contra a área de **terra** (280–360 km²) e não contra o município — a primeira versão do teste, que exigia ≥95% de razão, estava medindo terra contra terra+água. (2) Centróide-dentro-de-outra-zona **não** é teste de sobreposição válido aqui: a ZE 16 é concava e envolve a ZE 14, então o centróide da 16 cai legitimamente na 14. A não-sobreposição é garantida estruturalmente (cada bairro entra em exatamente uma zona, verificado no build) mais a soma de áreas.
|
||||
|
||||
**Testes.** Int de contrato do artefato (19 features, slugs == catálogo `kind === 'zona'`, budget, centróide de cada zona dentro do polígono municipal de Salvador, área somada); int de rank compartilhado; unit de `featureMapKey` e de `mapKeyForMunicipality`/`buildMunicipalitiesByMapKey`; e2e `campaignZoneMap.e2e.spec.ts` — 436 paths / 435 interativos (o único não-interativo é a base de Salvador), readout "Salvador — ZE N" e clique abrindo `/campanha/municipios/salvador-ze-N`.
|
||||
|
||||
**Gate.** tsc, lint, format, knip (P3 pré-existente em `payload.config.ts`), `check:cycles`, unit+int, e2e e build rodados. **Aikido não fechou:** o scanner responde Opengrep exit code 2 em qualquer entrada, inclusive num arquivo-probe trivial de uma linha, então os achados que ele reportou são artefato da execução quebrada e não são atribuíveis; o feed do Aikido não lista nada deste repo. SCA do `shpjs` (devDependency nova) também pendente — ambos registrados como débito abaixo.
|
||||
|
||||
## Já resolvido no simplify/critique (não reabrir)
|
||||
|
||||
- **JR1** — `MunicipalityZoneNeighborhoodsCard`: um lookup `municipalityZoneNeighborhoodEntryForSlug` (sem duplo hit no `entryBySlug`).
|
||||
|
|
@ -132,6 +157,9 @@ Componentes:
|
|||
- **JR6** — cobertura usa `zoneMunicipalities.length` em vez de constante fixa.
|
||||
- **JR7** — _(histórico)_ exclusividade de Camaçari sem teste — obsoleto: Camaçari saiu do catálogo na M1.
|
||||
- Impeccable compacto na F1: sem snapshot `.impeccable/critique/` nem P0–P3 abertos nesta superfície; polish visual amplo → **R6**.
|
||||
- **JR8** _(F2)_ — comentário de `NearbyMunicipalityResolution.zoneCity` que afirmava não existirem polígonos de zona: corrigido para dizer que existem e que o card **deliberadamente** não os usa.
|
||||
- **JR9** _(F2)_ — duas asserções do int de bairros que a hidratação da Fase 0 tornou tautológicas (comparavam `city`/`zoneNumber` com o catálogo de onde acabaram de ser copiados) removidas; a verificação real segue no `deepEqual` contra o fixture transcrito à mão.
|
||||
- **JR10** _(F2)_ — o script re-indexava geometrias simplificadas por **nome** de bairro; passou a usar `id` próprio (o irmão usa `codarea`, único por construção). Hoje são 170 nomes distintos, mas um nome repetido em malha futura colapsaria dois polígonos em silêncio dentro de um artefato commitado. Rebuild saiu byte-idêntico.
|
||||
|
||||
## Explicitamente fora (skips `/simplify` + descartes do triage)
|
||||
|
||||
|
|
@ -144,7 +172,10 @@ Componentes:
|
|||
|
||||
## Adiado com gatilho
|
||||
|
||||
- **S6 — CSS compartilhado lista zona** (`MunicipalityMapPanel` zone breakdown ↔ `MunicipalityZoneNeighborhoodsCard`). **Gatilho:** F2 alterar `MunicipalityMapPanel`/`BahiaMap` ou R6 citar duplicação visual em ≥2 superfícies de zona.
|
||||
- **S6 — CSS compartilhado lista zona** (`MunicipalityMapPanel` zone breakdown ↔ `MunicipalityZoneNeighborhoodsCard`). **Gatilho disparou** na F2 (o painel foi alterado) e foi reavaliado: as duas listas continuam pequenas e com estrutura diferente, então extrair peça compartilhada agora seria abstração por coincidência visual. Segue adiado, agora com gatilho único: **R6** citar a duplicação em ≥2 superfícies de zona.
|
||||
- **F2-D1 — card "Onde estou" (B14 ✓) resolver a ZE exata de Salvador.** Os polígonos passaram a existir nesta entrega, e `featureContainsPoint` já é genérico o suficiente para recebê-los; o card continua levando à lista filtrada por decisão. **Gatilho:** pedido de campo ("estou em Salvador e quero abrir minha zona"). Custo real é uma segunda malha no chunk do card, não matemática nova. Aresta já registrada no grafo do roadmap.
|
||||
- **F2-D2 — rank competitivo por ZE.** Exige dimensão nova no artefato TSE (hoje `federalRankByIbgeCode`), o que mexe no budget de 700 KB. **Gatilho:** E11 pedir o eixo de competição dentro de Salvador, ou o dossiê de um Município-zona exibir posição própria. Hoje as 19 zonas herdam a posição da cidade, com int pinando o fato.
|
||||
- **S2 — fonte única TS ↔ fixture do catálogo de bairros** (gerar `municipalityZoneNeighborhoods.ts` a partir do JSON). **Cortado na F2 prep:** a curadoria que a F2 exigia foi para o **script** de geometrias, não para o catálogo, então o gerador não teria a quem servir; o `deepEqual` fixture↔código já pega drift. **Gatilho:** um terceiro consumidor precisar do catálogo em runtime não-TS.
|
||||
- **Malha GeoSalvador em vez de IBGE.** Revisitar quando: spike F2 mostrar unmatched críticos em Salvador (>10% bairros TRE sem polígono).
|
||||
- **Camada ZE estadual (B4 antigo).** Revisitar quando: produto pedir mapa de ZE fora de Salvador (improvável neste ciclo).
|
||||
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -258,9 +258,12 @@ const main = async () => {
|
|||
const neighborhoodTopology = topology({
|
||||
neighborhoods: {
|
||||
type: 'FeatureCollection',
|
||||
features: features.map((feature) => ({
|
||||
// `id` is what the simplified topology is re-indexed by further down.
|
||||
// Neighborhood names are unique in today's mesh but nothing guarantees it,
|
||||
// and keying by name would silently collapse two polygons into one.
|
||||
features: features.map((feature, index) => ({
|
||||
type: 'Feature',
|
||||
properties: { name: feature.properties?.NM_BAIRRO ?? '' },
|
||||
properties: { id: index, name: feature.properties?.NM_BAIRRO ?? '' },
|
||||
geometry: feature.geometry,
|
||||
})),
|
||||
},
|
||||
|
|
@ -351,16 +354,16 @@ const main = async () => {
|
|||
// Simplification keeps geometry order and properties, so the zone buckets
|
||||
// above still address the right shapes.
|
||||
const simplified = simplifyTopology(neighborhoodTopology)
|
||||
const simplifiedByName = new Map(
|
||||
const simplifiedById = new Map(
|
||||
simplified.objects.neighborhoods.geometries.map((geometry) => [
|
||||
geometry.properties.name,
|
||||
geometry.properties.id,
|
||||
geometry,
|
||||
]),
|
||||
)
|
||||
|
||||
const zoneFeatures = zoneEntries.map((entry) => {
|
||||
const members = (geometriesByZone.get(entry.slug) ?? []).map((geometry) =>
|
||||
simplifiedByName.get(geometry.properties.name),
|
||||
simplifiedById.get(geometry.properties.id),
|
||||
)
|
||||
if (members.length === 0 || members.some((member) => !member)) {
|
||||
die(`Missing neighborhood geometry for ${entry.slug}.`)
|
||||
|
|
|
|||
|
|
@ -52,10 +52,13 @@ export type NearbyMunicipalityResolution =
|
|||
| { kind: 'inScope'; municipality: AccessibleMunicipality }
|
||||
/**
|
||||
* The point falls inside a city modeled as several zone municipalities
|
||||
* (Salvador). Without zone polygons (B8 F2) the honest answer is the filtered
|
||||
* list, never a guessed ZE. `zoneCount` is what the actor can open, so an
|
||||
* advisor with three zones is not told there are nineteen. `ibgeCode` is how
|
||||
* the caller finds the filtered-list href the server serialized for it.
|
||||
* (Salvador). The answer is the filtered list, never a guessed ZE. B8 F2 has
|
||||
* since drawn the zone polygons, so resolving the exact ZE became possible —
|
||||
* this resolver deliberately still does not, because that is a separate
|
||||
* decision (an open debt) rather than a side effect of the map delivery.
|
||||
* `zoneCount` is what the actor can open, so an advisor with three zones is
|
||||
* not told there are nineteen. `ibgeCode` is how the caller finds the
|
||||
* filtered-list href the server serialized for it.
|
||||
*/
|
||||
| { kind: 'zoneCity'; city: string; ibgeCode: string; zoneCount: number }
|
||||
/** Inside Bahia, but in a município outside the actor's portfolio. */
|
||||
|
|
|
|||
|
|
@ -55,8 +55,9 @@ describe('Municipality zone neighborhood catalog (Salvador only)', () => {
|
|||
for (const municipality of zoneMunicipalities) {
|
||||
const record = municipalityZoneNeighborhoodEntryForSlug(municipality.slug)
|
||||
expect(record, municipality.slug).toBeDefined()
|
||||
expect(record?.city).toBe(municipality.city)
|
||||
expect(record?.zoneNumber).toBe(municipality.zoneNumber)
|
||||
// `city`/`zoneNumber` are copied from this same catalog, so comparing them
|
||||
// here could not fail. The fixture test below is what actually checks them,
|
||||
// against an independent transcription of the resolution.
|
||||
expect(record?.neighborhoods.length, municipality.slug).toBeGreaterThan(0)
|
||||
}
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in a new issue