B13+: concordância LQ, readout de escopo no mapa e DRY do build de agregados.

Rede de segurança para E15, copy honesta no Quadro para assessores, uma varredura federal por ano no artefato e tupla de rank no bundle RSC.
This commit is contained in:
Francisco Ignácio Fontoura Solla 2026-07-29 16:26:37 -03:00
parent 7dcfd2989f
commit e462d39fda
13 changed files with 308 additions and 135 deletions

File diff suppressed because one or more lines are too long

View file

@ -1,7 +1,7 @@
# Escala e DRY pós-B13 (escala relativa no mapa dos Municípios)
Status: rascunho
Atualizado em: 2026-07-26
Status: entregue (2026-07-29)
Atualizado em: 2026-07-29
Item do roadmap: [docs/roadmap.md](../roadmap.md) (Fill-ins abertos, **B13+**, fill-in de engenharia)
Impeccable: A — N/A (nenhuma superfície nova; as quatro fases preservam o comportamento visível do B13, exceto uma linha de copy em F2)
Appetite: ~0,5–0,75 dia eng; quatro fases independentes, nenhuma com migration, collection ou `Consent`

View file

@ -168,7 +168,7 @@ O discovery literatura→persona→entrevista ([relatório aprovado](research/re
- ~~**Cenário junto aos filtros**~~ — **✓ (entregue 2026-07-29)** `VoteEstimateScenarioField` na barra slim de `MunicipalityFilters` (desktop/mobile); overview só consome contexto; provider envolve filtros + resultados · ~0,25–0,5d · Impeccable B · [plano](plans/cenario-junto-filtros-municipios.md)
- ~~**B16+** escala/DRY pós-B16~~ — **absorvido pelo Pass 2 W1-D1 (2026-07-25)**: `useOptimistic`, facet por slugs, hrefs via serializador canônico, facets no mesmo `Promise.all` · [plano](plans/escala-dry-pos-b16.md)
- **E10+** escala/DRY pós-E10 — quatro fases do `/simplify` da classificação territorial: **F1** a lista lê `municipality` 2× por request (`listQuery` + `loadMunicipalityScope`, mesmo `where`, select superset) — **fecha o "Adiado com gatilho" do A11**, cujo gatilho ("3º path derivado no loader") disparou com `classe` sendo o 4º (`votos`, `deficit`, `frescor`); **F2** `computeAllMunicipalityPotentials()` recomputa 435 linhas puras do artefato por request (memo de processo, como `bahiaElectionAggregates`/`municipalityVoteRank`); **F3** um `TooltipProvider` por tooltip (~30 na lista) anula o agrupamento de delay do Radix — hoist para o layout de `(app)`; **F4** os 4 multi-filtros do mobile saem da mesma `municipalityFilterDefinitions` que os single-select vizinhos já usam · ~0,75–1d eng · Impeccable A · sem migration · cortável · [plano](plans/escala-dry-pos-e10.md)
- **B13+** escala/DRY pós-B13 — quatro fases do `/simplify` da escala relativa no mapa, as duas primeiras de **coerência** e não de custo: **F1** a razão do LQ é escrita duas vezes (painel cliente `:210` × classificador `server-only` `:108`, separação **deliberada** — o classificador arrasta o artefato de 600 KB) sem nenhum teste travando a concordância, que é a rede de segurança do **E15**; **F2** no readout do mapa a posição vale para a cidade inteira enquanto a classe é calculada sobre o escopo do usuário, então dois assessores veem classes diferentes para o mesmo polígono de Salvador — declarar a geografia, sem mudar o cálculo; **F3** `pnpm build:election-aggregates` varre a mesma fatia estadual do TSE 3× por ano (~225k docs extras) porque `loadFederalVotesByCityZoneAndCandidate` só precisa de `party` no `select` para ser superconjunto estrito dos outros dois; **F4** `competitiveRankByYear` soma ~57 dos ~63,6 KB que o B13 pôs no payload RSC, pagos mesmo por quem nunca abre a escala — tupla `[rank, candidates]` corta ~metade e responde ao "profiling antes de agir" que o ledger pediu em 2026-07-23 · ~0,5–0,75d eng · Impeccable A · sem migration · cortável · [plano](plans/escala-dry-pos-b13.md)
- ~~**B13+** escala/DRY pós-B13~~ — **✓ (entregue 2026-07-29)** quatro fases do `/simplify` pós-B13: **F1** `tests/unit/mapLqConcordance.unit.spec.ts` pina o LQ do painel contra `computeMunicipalityTerritorialClass` (rede de segurança do E15, sem extrair helper); **F2** `actorRole` no mapa do **Quadro** (`quadro/page.tsx` → `MunicipalityMapPanelDynamic`) e copy de escopo no readout (quantis/LQ "no seu escopo" para assessor; posição "na cidade inteira"; bolha E10 "(lista)"); **F3** uma varredura federal por ano no `build:election-aggregates` via `candidateVotesByCityZoneFromFederalSlice` / `campoFederalVotesByCityZoneFromFederalSlice` (`party` no select do slice); **F4** `competitiveRankByYear` no bundle RSC como tupla `[rank, candidates]` + `readFederalCompetitivePlacement` (artefato JSON inalterado) · sem migration · [plano](plans/escala-dry-pos-b13.md)
- ~~**B8+** escala/DRY pós-B8 F2~~ — **✓ (entregue 2026-07-27)** as quatro fases do `/simplify` dos polígonos das ZE. **F1:** a política de escala de chão nasceu em `scripts/lib/topology.mjs` (primeiro módulo de `scripts/lib/` — **B5 F2** pousa nele) declarando tolerância de Visvalingam em **m² de chão** e derivando os dígitos de quantização do bbox de cada topologia; `SIMPLIFY_QUANTILE`/`QUANTIZE_DIGITS` morreram nos dois scripts. A auditoria corrigiu a alavanca do plano: quantização sozinha quase não corta (mesmo na grade estadual as zonas ficariam em 203 pontos/feature), o peso absoluto é que manda — as zonas caíram de **9.120 → 574 pontos de arco (480 → 30,2/feature) e 72.310 → 7.687 bytes**, `bahia-municipalities` saiu **byte-idêntico** e territórios mudou 1 byte (grade 102,730 → 102,822 m). Tolerância própria por malha, com o número medido no comentário: a tolerância municipal chegaria a 17,9/feature nas zonas mas comeria 5% da área de Salvador (306 → 291 km²), que é justamente a grandeza que prova que nenhum bairro sumiu. `MAX_TOPO_BYTES` ganhou ao lado `MAX_POINTS_PER_FEATURE = 60`. **F2:** `featureMapKey` passou a ser exercido sobre features **reais** das três malhas, cada uma no laço que já a varre inteira (417 `codarea` + 27 `code` + 19 `slug`) — o `/simplify` corrigiu a primeira escrita, que asseria a cidade dentro do laço das zonas e portanto cobria **uma** feature municipal 19 vezes; o teste de precedência sintético caiu e o comentário passou a declarar o invariante real (as três propriedades de chave são mutuamente exclusivas; `ibgeCode` na zona deliberadamente não é chave). **F3:** `.catch()` na metade das zonas — o mapa segue sem elas (com o motivo logado), com Salvador como polígono sem dado nem interação — e a ressalva de aproximação saiu do gate `!comparisonActive` para um `<p>` condicionado a `bundle.hasZoneMunicipalities`, campo novo publicado pelo loader a partir de `kind === 'zona'`: o `/simplify` **rejeitou** o predicado `isZoneMapKey` que lia isso da forma da chave (7 dígitos ⇒ município), porque era uma segunda codificação da regra de `mapKeyForMunicipality`, e gatear em `zoneBreakdown` recriaria o acoplamento que o fill-in da lista (✓ 2026-07-28) deletou. **F4:** dois acessores órfãos deletados, a união de navegação de 3 estados colapsada em `string | undefined`, `MunicipalitiesByMapKey` virou `Record<string, string>` (435 strings fora do payload RSC do Início), `polygonRingsOf` exportado e os helpers de geometria retipados em `PolygonalFeature`/`BahiaFeature<P>`/`BahiaMeshFeature`. **Um item do plano foi rejeitado por medição:** `city`/`zoneNumber` de `MunicipalityZoneNeighborhoodEntry` **ficam** — entram nas linhas hasheadas por `canonicalEvidenceRows` e são colunas do fixture de proveniência da RA 02/2017, então não são código morto (só o `zoneNumber` do **artefato** TopoJSON saiu) · sem migration · [plano](plans/escala-dry-pos-b8f2.md)
- **E12+** escala/DRY pós-E12 — **✓ (entregue 2026-07-29)** F1: `src/lib/metropolitanoTerritoryPeers.ts` (peers/sub-linhas Metropolitano unificados para rollup, classe agregada e benchmark T4); F2 adiada — `benchmarkBySlug` já cobre · ~0,25d · Impeccable A · [plano](plans/escala-dry-pos-e12.md)
- **O0+** escala/DRY pós-Onda 0 · [plano](plans/escala-dry-pos-onda0.md)

View file

@ -48,7 +48,8 @@ const { municipalityCatalog } = await import('../src/lib/municipalityCatalog.ts'
const { municipalityElectionGeography } =
await import('../src/utilities/municipality/municipalityElectionGeography.ts')
const {
loadCampoFederalVotesByCityZone,
campoFederalVotesByCityZoneFromFederalSlice,
candidateVotesByCityZoneFromFederalSlice,
loadCandidateVotesByCityZone,
loadFederalVotesByCityZoneAndCandidate,
loadOfficeTallyByCityZone,
@ -83,17 +84,30 @@ for (const entry of municipalityCatalog) {
}
}
const geographiesByIbgeCode = new Map()
for (const entry of municipalityCatalog) {
const geographies = geographiesByIbgeCode.get(entry.ibgeCode) ?? []
geographies.push(municipalityElectionGeography(entry))
geographiesByIbgeCode.set(entry.ibgeCode, geographies)
}
const federalRankByIbgeCode = {}
for (const year of HISTORICAL_SERIES_YEARS) {
const [candidateVotes, validVotes, campoVotes, federalTally] = await Promise.all([
loadCandidateVotesByCityZone(payload, cliReader, { year, candidateNumber }),
const [federalNominalSlice, validVotes, federalTally] = await Promise.all([
loadFederalVotesByCityZoneAndCandidate(payload, cliReader, { year }),
loadValidVotesByCityZone(payload, cliReader, { year }),
loadCampoFederalVotesByCityZone(payload, cliReader, { year }),
loadOfficeTallyByCityZone(payload, cliReader, {
year,
office: FEDERAL_DEPUTY_OFFICE,
turn: '1',
}),
])
const candidateVotes = candidateVotesByCityZoneFromFederalSlice(
federalNominalSlice,
candidateNumber,
)
const campoVotes = campoFederalVotesByCityZoneFromFederalSlice(federalNominalSlice, year)
if (candidateVotes.size === 0) {
console.error(
@ -122,6 +136,36 @@ for (const year of HISTORICAL_SERIES_YEARS) {
`[build:election-aggregates] ${year}: ${yearTotal} votes for #${candidateNumber} ` +
`(campo total: ${campoTotal}).`,
)
for (const [ibgeCode, geographies] of geographiesByIbgeCode) {
const byCandidate = new Map()
for (const geography of geographies) {
for (const [number, votes] of sumCandidateVotesForGeography(federalNominalSlice, geography)) {
byCandidate.set(number, (byCandidate.get(number) ?? 0) + votes)
}
}
const ownVotes = byCandidate.get(candidateNumber) ?? 0
if (ownVotes <= 0) continue
let ahead = 0
let candidates = 0
for (const votes of byCandidate.values()) {
if (votes <= 0) continue
candidates += 1
if (votes > ownVotes) ahead += 1
}
federalRankByIbgeCode[ibgeCode] ??= {}
federalRankByIbgeCode[ibgeCode][String(year)] = { rank: ahead + 1, candidates }
}
const ranked = Object.values(federalRankByIbgeCode).filter(
(byYear) => byYear[String(year)] !== undefined,
).length
console.log(
`[build:election-aggregates] ${year}: placement computed for ${ranked}/${geographiesByIbgeCode.size} IBGE municipalities.`,
)
}
// 2022-only: majoritarian (presidente/governador #13) votes + tally, used for
@ -171,63 +215,6 @@ console.log(
`[build:election-aggregates] 2022 majoritarian: ${presidentTotal} votes for president #${BASELINE_TICKET_2022.president.candidateNumber}.`,
)
// B13 "posição no município": where the candidate placed among every federal
// deputy voted inside each municipality. Keyed by IBGE code rather than by
// catalog slug because the competitive question only has an answer for a whole
// city — Salvador's 19 zone municipalities are one placement, and the map
// paints one polygon for them anyway.
const geographiesByIbgeCode = new Map()
for (const entry of municipalityCatalog) {
const geographies = geographiesByIbgeCode.get(entry.ibgeCode) ?? []
geographies.push(municipalityElectionGeography(entry))
geographiesByIbgeCode.set(entry.ibgeCode, geographies)
}
const federalRankByIbgeCode = {}
for (const year of HISTORICAL_SERIES_YEARS) {
const votesByCityZoneAndCandidate = await loadFederalVotesByCityZoneAndCandidate(
payload,
cliReader,
{ year },
)
for (const [ibgeCode, geographies] of geographiesByIbgeCode) {
const byCandidate = new Map()
for (const geography of geographies) {
for (const [number, votes] of sumCandidateVotesForGeography(
votesByCityZoneAndCandidate,
geography,
)) {
byCandidate.set(number, (byCandidate.get(number) ?? 0) + votes)
}
}
const ownVotes = byCandidate.get(candidateNumber) ?? 0
// No votes here means no placement — a last place would read as a fact
// when it is really absence of data.
if (ownVotes <= 0) continue
let ahead = 0
let candidates = 0
for (const votes of byCandidate.values()) {
if (votes <= 0) continue
candidates += 1
if (votes > ownVotes) ahead += 1
}
federalRankByIbgeCode[ibgeCode] ??= {}
// Competition rank: tied candidates share a placement.
federalRankByIbgeCode[ibgeCode][String(year)] = { rank: ahead + 1, candidates }
}
const ranked = Object.values(federalRankByIbgeCode).filter(
(byYear) => byYear[String(year)] !== undefined,
).length
console.log(
`[build:election-aggregates] ${year}: placement computed for ${ranked}/${geographiesByIbgeCode.size} IBGE municipalities.`,
)
}
const artifact = {
provenance:
'Derived from TSE open data seeded by pnpm db:seed:tse; regenerate with pnpm build:election-aggregates.',

View file

@ -110,7 +110,11 @@ const DashboardMapSection = async ({
return (
<MunicipalityEstimateScenarioProvider>
<MunicipalityMapPanelDynamic bundle={mapBundle} candidateOptions={candidateOptions} />
<MunicipalityMapPanelDynamic
bundle={mapBundle}
candidateOptions={candidateOptions}
actorRole={user.role}
/>
</MunicipalityEstimateScenarioProvider>
)
}

View file

@ -22,6 +22,7 @@ import {
type ChoroplethValues,
} from '@/lib/bahiaMapStyle'
import type { CampaignConceptId } from '@/lib/campaignIntelligenceConcepts'
import { isUnrestrictedCampaignRole, type CampaignRole } from '@/lib/campaignRoles'
import {
choroplethMaxValue,
computeValidVoteShares,
@ -55,6 +56,7 @@ import {
municipalityMapScaleModeHints,
municipalityMapScaleModeLabels,
municipalityMapYearLabels,
readFederalCompetitivePlacement,
type MunicipalityMapBundle,
type MunicipalityMapScaleMode,
type MunicipalityMapYear,
@ -132,12 +134,14 @@ const MapBubbleKey = ({ largestValue }: { largestValue: number }) => (
type MunicipalityMapPanelProps = {
bundle: MunicipalityMapBundle
candidateOptions: FederalCandidateOption[]
actorRole: CampaignRole
defaultYear?: MunicipalityMapYear
}
export const MunicipalityMapPanel = ({
bundle,
candidateOptions,
actorRole,
defaultYear = 2022,
}: MunicipalityMapPanelProps) => {
const router = useRouter()
@ -157,6 +161,7 @@ export const MunicipalityMapPanel = ({
const comparison = bundle.comparison
const comparisonActive = comparison !== null && year !== 2026
const portfolioScopedActor = !isUnrestrictedCampaignRole(actorRole)
// Comparing forces the diverging absolute scale, and the competitive
// placement has no 2026 reading (it is a TSE result). Rather than let the
@ -180,7 +185,14 @@ export const MunicipalityMapPanel = ({
return bundle.values2026ByScenario[estimateScenario] ?? {}
}
return bundle.valuesByYear[String(year)] ?? {}
}, [bundle, comparison, comparisonActive, estimateScenario, year])
}, [
bundle.valuesByYear,
bundle.values2026ByScenario,
comparison,
comparisonActive,
estimateScenario,
year,
])
const validVotesForYear = useMemo(
() => bundle.validVotesByYear[String(year)] ?? {},
@ -206,6 +218,8 @@ export const MunicipalityMapPanel = ({
/** LQ per município against his statewide standard — the same ratio E10 classes on. */
const lqByMapKey = useMemo(() => {
if (effectiveScaleMode !== 'lq') return {}
const statewideShare = bundle.statewideShareByYear[String(year)] ?? 0
if (statewideShare <= 0) return {}
@ -216,7 +230,7 @@ export const MunicipalityMapPanel = ({
lq[mapKey] = votes / validVotes / statewideShare
}
return lq
}, [bundle.statewideShareByYear, rawValues, validVotesForYear, year])
}, [bundle.statewideShareByYear, effectiveScaleMode, rawValues, validVotesForYear, year])
const competitiveRankForYear = useMemo(
() => bundle.competitiveRankByYear[String(year)] ?? {},
@ -234,7 +248,7 @@ export const MunicipalityMapPanel = ({
Object.fromEntries(
Object.entries(competitiveRankForYear).map(([mapKey, placement]) => [
mapKey,
placement.rank,
placement[0],
]),
),
)
@ -248,7 +262,7 @@ export const MunicipalityMapPanel = ({
// classed and grey out every polygon under a red gradient legend.
const classed = classing !== null && classing.classes.length > 0
const fillByKey = useMemo(
() => (classed && classing ? fillsForClassing(classing) : undefined),
() => (classed && classing !== null ? fillsForClassing(classing) : undefined),
[classed, classing],
)
@ -342,9 +356,19 @@ export const MunicipalityMapPanel = ({
const territorialClass = bundle.territorialClassByMapKey[key]
const classLabel = territorialClass ? territorialClassLabels[territorialClass] : null
return `${formatElectionNumber(projected)} válidos projetados em jogo${classLabel ? ` · ${classLabel}` : ''}`
const classSuffix = classLabel
? portfolioScopedActor
? ` · ${classLabel} (lista)`
: ` · ${classLabel}`
: ''
return `${formatElectionNumber(projected)} válidos projetados em jogo${classSuffix}`
},
[bundle.projectedValidVotesByMapKey, bundle.territorialClassByMapKey, showBubbles],
[
bundle.projectedValidVotesByMapKey,
bundle.territorialClassByMapKey,
portfolioScopedActor,
showBubbles,
],
)
/**
@ -361,22 +385,26 @@ export const MunicipalityMapPanel = ({
case 'quantile': {
if (classIndex === undefined) return null
const range = classing.classes[classIndex]?.label
return `${classIndex + 1}ª de ${classing.classes.length} faixas${range ? ` (${range} votos)` : ''}`
const scopeSuffix = portfolioScopedActor ? ' no seu escopo' : ''
return `${classIndex + 1}ª de ${classing.classes.length} faixas${range ? ` (${range} votos)` : ''}${scopeSuffix}`
}
case 'lq': {
const lq = lqByMapKey[key]
return lq === undefined ? null : formatDominanceAgainstOwnStandard(lq)
if (lq === undefined) return null
const reading = formatDominanceAgainstOwnStandard(lq)
return portfolioScopedActor ? `${reading} no seu escopo` : reading
}
case 'competitiveRank': {
const placement = competitiveRankForYear[key]
if (!placement) return null
return `${formatPlacementOrdinal(placement.rank)} entre ${formatElectionNumber(placement.candidates)} candidatos votados aqui`
const tuple = competitiveRankForYear[key]
if (!tuple) return null
const placement = readFederalCompetitivePlacement(tuple)
return `${formatPlacementOrdinal(placement.rank)} entre ${formatElectionNumber(placement.candidates)} candidatos votados aqui, na cidade inteira`
}
default:
return null
}
},
[classing, competitiveRankForYear, effectiveScaleMode, lqByMapKey],
[classing, competitiveRankForYear, effectiveScaleMode, lqByMapKey, portfolioScopedActor],
)
// Optimistic on the control, honest pending on the map: the comparison data

View file

@ -2,6 +2,7 @@
import dynamic from 'next/dynamic'
import type { CampaignRole } from '@/lib/campaignRoles'
import type { FederalCandidateOption } from '@/utilities/electionCandidateOptions'
import type { MunicipalityMapBundle } from '@/utilities/municipality/municipalityMapContract'
@ -24,7 +25,15 @@ const MunicipalityMapPanelLazy = dynamic(
export const MunicipalityMapPanelDynamic = ({
bundle,
candidateOptions,
actorRole,
}: {
bundle: MunicipalityMapBundle
candidateOptions: FederalCandidateOption[]
}) => <MunicipalityMapPanelLazy bundle={bundle} candidateOptions={candidateOptions} />
actorRole: CampaignRole
}) => (
<MunicipalityMapPanelLazy
bundle={bundle}
candidateOptions={candidateOptions}
actorRole={actorRole}
/>
)

View file

@ -277,45 +277,6 @@ export const loadCandidateVotesByCityZone = async (
return votes
}
/**
* cityCode:zone → sum of nominal deputado-federal T1 votes for every
* candidate whose party is in the curated `campoParties.ts` field for that
* year (E8 "share intracampo" denominator — CLI/build-time use only, this
* scans the whole statewide slice for the office).
*/
export const loadCampoFederalVotesByCityZone = async (
payload: Payload,
user: ElectionReader,
{ year }: { year: number },
): Promise<Map<string, number>> => {
assertCanReadElectionData(user)
const result = await payload.find({
collection: 'electionCandidateVote',
where: {
and: [
{ year: { equals: year } },
{ office: { equals: FEDERAL_DEPUTY_OFFICE } },
{ turn: { equals: '1' } },
{ voteType: { equals: 'nominal' } },
],
},
depth: 0,
limit: 0,
pagination: false,
select: { cityCode: true, zoneNumber: true, party: true, votes: true },
overrideAccess: true,
})
const votes = new Map<string, number>()
for (const row of result.docs) {
if (!isCampoParty(row.party, year)) continue
const key = `${row.cityCode}:${row.zoneNumber}`
votes.set(key, (votes.get(key) ?? 0) + (row.votes ?? 0))
}
return votes
}
/**
* cityCode:zone → (candidateNumber → nominal deputado-federal T1 votes) for
* one year — the whole statewide slice, ungrouped by candidate, so a caller
@ -326,11 +287,16 @@ export const loadCampoFederalVotesByCityZone = async (
* scans ~100k rows and must never run on a request path — the ranking it
* feeds is precomputed into the committed artifact.
*/
export type FederalNominalVoteCell = {
votes: number
party: string | null
}
export const loadFederalVotesByCityZoneAndCandidate = async (
payload: Payload,
user: ElectionReader,
{ year }: { year: number },
): Promise<Map<string, Map<number, number>>> => {
): Promise<Map<string, Map<number, FederalNominalVoteCell>>> => {
assertCanReadElectionData(user)
const result = await payload.find({
@ -346,37 +312,87 @@ export const loadFederalVotesByCityZoneAndCandidate = async (
depth: 0,
limit: 0,
pagination: false,
select: { cityCode: true, zoneNumber: true, candidateNumber: true, votes: true },
select: {
cityCode: true,
zoneNumber: true,
candidateNumber: true,
votes: true,
party: true,
},
overrideAccess: true,
})
const byCityZone = new Map<string, Map<number, number>>()
const byCityZone = new Map<string, Map<number, FederalNominalVoteCell>>()
for (const row of result.docs) {
const key = `${row.cityCode}:${row.zoneNumber}`
let byCandidate = byCityZone.get(key)
if (!byCandidate) {
byCandidate = new Map<number, number>()
byCandidate = new Map<number, FederalNominalVoteCell>()
byCityZone.set(key, byCandidate)
}
byCandidate.set(
row.candidateNumber,
(byCandidate.get(row.candidateNumber) ?? 0) + (row.votes ?? 0),
)
const previous = byCandidate.get(row.candidateNumber)
byCandidate.set(row.candidateNumber, {
votes: (previous?.votes ?? 0) + (row.votes ?? 0),
party: row.party ?? previous?.party ?? null,
})
}
return byCityZone
}
/** Derives the per-candidate map used by `loadCandidateVotesByCityZone` from one federal slice. */
export const candidateVotesByCityZoneFromFederalSlice = (
slice: Map<string, Map<number, FederalNominalVoteCell>>,
candidateNumber: number,
): Map<string, number> => {
const votes = new Map<string, number>()
for (const [key, byCandidate] of slice) {
const cell = byCandidate.get(candidateNumber)
if (cell && cell.votes > 0) votes.set(key, cell.votes)
}
return votes
}
/** Derives the campo-parties map used by `loadCampoFederalVotesByCityZone` from one federal slice. */
export const campoFederalVotesByCityZoneFromFederalSlice = (
slice: Map<string, Map<number, FederalNominalVoteCell>>,
year: number,
): Map<string, number> => {
const votes = new Map<string, number>()
for (const [key, byCandidate] of slice) {
let total = 0
for (const cell of byCandidate.values()) {
if (cell.party && isCampoParty(cell.party, year)) total += cell.votes
}
if (total > 0) votes.set(key, total)
}
return votes
}
/**
* cityCode:zone → sum of nominal deputado-federal T1 votes for campo parties
* (E8 "share intracampo"). One DB scan via the federal nominal slice.
*/
export const loadCampoFederalVotesByCityZone = async (
payload: Payload,
user: ElectionReader,
{ year }: { year: number },
): Promise<Map<string, number>> => {
assertCanReadElectionData(user)
const slice = await loadFederalVotesByCityZoneAndCandidate(payload, user, { year })
return campoFederalVotesByCityZoneFromFederalSlice(slice, year)
}
/** Folds `loadFederalVotesByCityZoneAndCandidate` over one municipality's cityCode×zones. */
export const sumCandidateVotesForGeography = (
votesByCityZoneAndCandidate: Map<string, Map<number, number>>,
votesByCityZoneAndCandidate: Map<string, Map<number, FederalNominalVoteCell>>,
geography: MunicipalityElectionGeography,
): Map<number, number> => {
const total = new Map<number, number>()
for (const zone of geography.zones) {
const byCandidate = votesByCityZoneAndCandidate.get(`${geography.cityCode}:${zone}`)
if (!byCandidate) continue
for (const [candidateNumber, votes] of byCandidate) {
total.set(candidateNumber, (total.get(candidateNumber) ?? 0) + votes)
for (const [candidateNumber, cell] of byCandidate) {
total.set(candidateNumber, (total.get(candidateNumber) ?? 0) + cell.votes)
}
}
return total

View file

@ -1,4 +1,3 @@
import type { FederalCompetitiveRank } from '@/lib/bahiaElectionAggregates'
import { HISTORICAL_SERIES_YEARS } from '@/lib/electionResults'
import type { VoteEstimateScenario } from '@/lib/voteEstimate'
import type { MunicipalitiesByMapKey } from '@/utilities/municipality/municipalityMapNavigation'
@ -86,6 +85,21 @@ export type MunicipalityMapComparison = {
diffByYear: Record<string, Record<string, number>>
}
/** B13+ F4 — compact wire format for competitive placement in the RSC bundle. */
export type FederalCompetitiveRankTuple = readonly [rank: number, candidates: number]
const FEDERAL_COMPETITIVE_RANK_INDEX = {
rank: 0,
candidates: 1,
} as const
export const readFederalCompetitivePlacement = (
tuple: FederalCompetitiveRankTuple,
): { rank: number; candidates: number } => ({
rank: tuple[FEDERAL_COMPETITIVE_RANK_INDEX.rank],
candidates: tuple[FEDERAL_COMPETITIVE_RANK_INDEX.candidates],
})
/**
* Every record below is keyed by MAP KEY, not by IBGE codarea: since B8 F2 the
* map paints one polygon per catalog unit, so Salvador's 19 zones are addressed
@ -113,7 +127,7 @@ export type MunicipalityMapBundle = {
* TSE artifact ranks by codarea only, so Salvador's zones all carry the city's
* position (the legend says so).
*/
competitiveRankByYear: Record<string, Record<string, FederalCompetitiveRank>>
competitiveRankByYear: Record<string, Record<string, FederalCompetitiveRankTuple>>
/** map key → projected 2026 valid votes (E8) — the size of the proportional symbol. */
projectedValidVotesByMapKey: Record<string, number>
/** map key → the municipality it opens on click. */

View file

@ -6,7 +6,6 @@ import {
getFederalCompetitiveRank,
getMunicipalityFederalBaseline,
getStatewideFederalTotals,
type FederalCompetitiveRank,
} from '@/lib/bahiaElectionAggregates'
import { BASELINE_TICKET_2022, HISTORICAL_SERIES_YEARS } from '@/lib/electionResults'
import { getMunicipalityCatalogEntry } from '@/lib/municipalityCatalog'
@ -33,6 +32,7 @@ import {
type MunicipalityListState,
} from '@/utilities/municipality/municipalityListUrl'
import type {
FederalCompetitiveRankTuple,
MunicipalityMapBundle,
MunicipalityMapComparison,
} from '@/utilities/municipality/municipalityMapContract'
@ -105,7 +105,7 @@ const buildMunicipalityMapBundleFromMunicipalities = async (
const valuesByYear: Record<string, Record<string, number>> = {}
const validVotesByYear: Record<string, Record<string, number>> = {}
const statewideShareByYear: Record<string, number> = {}
const competitiveRankByYear: Record<string, Record<string, FederalCompetitiveRank>> = {}
const competitiveRankByYear: Record<string, Record<string, FederalCompetitiveRankTuple>> = {}
// One polygon per catalog unit since B8 F2, so this index — the same one the
// map navigates by — is also the key of every rollup below.
const municipalitiesByMapKey = buildMunicipalitiesByMapKey(municipalities)
@ -134,10 +134,10 @@ const buildMunicipalityMapBundleFromMunicipalities = async (
// Rank exists per city only, so every zone of Salvador carries the city's
// position — stated in the legend rather than faked per zone.
const ranks: Record<string, FederalCompetitiveRank> = {}
const ranks: Record<string, FederalCompetitiveRankTuple> = {}
for (const municipality of municipalities) {
const rank = getFederalCompetitiveRank(municipality.ibgeCode, year)
if (rank) ranks[mapKeyForMunicipality(municipality)] = rank
if (rank) ranks[mapKeyForMunicipality(municipality)] = [rank.rank, rank.candidates]
}
competitiveRankByYear[String(year)] = ranks
}

View file

@ -6,6 +6,7 @@ import { beforeAll, describe, expect, it } from 'vitest'
import { municipalityCatalog } from '@/lib/municipalityCatalog'
import config from '@/payload.config'
import { readFederalCompetitivePlacement } from '@/utilities/municipality/municipalityMapContract'
import { loadMunicipalityMapBundle } from '@/utilities/municipality/municipalityMapData'
import { installCampaignFixtures } from '../helpers/campaignFixtures'
@ -49,7 +50,15 @@ describe('loadMunicipalityMapBundle — list URL filters', () => {
// The zones share the city's TSE rank — the artifact ranks by codarea only.
const ranks = bundle!.competitiveRankByYear['2022'] ?? {}
const positions = new Set(mapKeys.map((key) => ranks[key]?.rank))
const sampleKey = mapKeys[0]
const sampleTuple = ranks[sampleKey]
expect(sampleTuple).toEqual([expect.any(Number), expect.any(Number)])
const positions = new Set(
mapKeys.map((key) => {
const tuple = ranks[key]
return tuple ? readFederalCompetitivePlacement(tuple).rank : undefined
}),
)
expect(positions.size).toBe(1)
})

View file

@ -0,0 +1,29 @@
import { describe, expect, it } from 'vitest'
import { BASELINE_TICKET_2022, ELECTION_YEAR_2022 } from '@/lib/electionResults'
import {
campoFederalVotesByCityZoneFromFederalSlice,
candidateVotesByCityZoneFromFederalSlice,
type FederalNominalVoteCell,
} from '@/utilities/municipality/municipalityElectoralBaseline'
describe('federal nominal slice folds (B13+ F3)', () => {
it('derives candidate votes and campo totals from one in-memory slice', () => {
const candidateNumber = BASELINE_TICKET_2022.candidate.candidateNumber
const slice = new Map<string, Map<number, FederalNominalVoteCell>>([
[
'2927408:1',
new Map([
[candidateNumber, { votes: 40, party: 'PT' }],
[9999, { votes: 10, party: 'PL' }],
]),
],
])
const byCandidate = candidateVotesByCityZoneFromFederalSlice(slice, candidateNumber)
expect(byCandidate.get('2927408:1')).toBe(40)
const campo = campoFederalVotesByCityZoneFromFederalSlice(slice, ELECTION_YEAR_2022)
expect(campo.get('2927408:1')).toBe(40)
})
})

View file

@ -0,0 +1,75 @@
import { describe, expect, it } from 'vitest'
import {
getMunicipalityFederalBaseline,
getStatewideFederalTotals,
} from '@/lib/bahiaElectionAggregates'
import { ELECTION_YEAR_2022 } from '@/lib/electionResults'
import { municipalityCatalog } from '@/lib/municipalityCatalog'
import { computeMunicipalityTerritorialClass } from '@/utilities/municipality/municipalityTerritorialClass'
/**
* B13+ F1 — the map panel computes LQ client-side; E10 classifies server-side
* over the same artifact. E15 will touch the anchors — this pin is the safety
* net that both paths still agree on the ratio.
*/
const mapLqForSlug = (slug: string, year: number): number | null => {
const baseline = getMunicipalityFederalBaseline(slug)
const votes = baseline.votesByYear[String(year)] ?? 0
const validVotes = baseline.validVotesByYear[String(year)] ?? 0
const statewide = getStatewideFederalTotals(year)
const statewideShare = statewide.validVotes > 0 ? statewide.ownVotes / statewide.validVotes : 0
if (votes <= 0 || validVotes <= 0 || statewideShare <= 0) return null
return votes / validVotes / statewideShare
}
const firstSlugWithFederalVotes2022 = (): string => {
const slug = municipalityCatalog.find((entry) => {
const baseline = getMunicipalityFederalBaseline(entry.slug)
return (baseline.votesByYear[String(ELECTION_YEAR_2022)] ?? 0) > 0
})?.slug
if (!slug) {
throw new Error('expected at least one municipality with 2022 federal votes in the catalog')
}
return slug
}
describe('map LQ concordance (B13+ F1)', () => {
it('matches computeMunicipalityTerritorialClass.lq for a municipality with 2022 votes', () => {
const slug = firstSlugWithFederalVotes2022()
const mapLq = mapLqForSlug(slug, ELECTION_YEAR_2022)
const classifierLq = computeMunicipalityTerritorialClass(slug).lq
expect(mapLq).not.toBeNull()
expect(classifierLq).not.toBeNull()
if (mapLq === null || classifierLq === null) return
expect(mapLq).toBeCloseTo(classifierLq, 10)
})
it('uses the same statewide standard as the classifier inputs', () => {
const slug = firstSlugWithFederalVotes2022()
const totals = getStatewideFederalTotals(ELECTION_YEAR_2022)
const statewideShare = totals.validVotes > 0 ? totals.ownVotes / totals.validVotes : 0
const baseline = getMunicipalityFederalBaseline(slug)
const ownVotes = baseline.votesByYear[String(ELECTION_YEAR_2022)] ?? 0
const validVotes = baseline.validVotesByYear[String(ELECTION_YEAR_2022)] ?? 0
expect(ownVotes).toBeGreaterThan(0)
expect(validVotes).toBeGreaterThan(0)
expect(statewideShare).toBeGreaterThan(0)
const expectedLq = ownVotes / validVotes / statewideShare
const classifierLq = computeMunicipalityTerritorialClass(slug).lq
expect(classifierLq).not.toBeNull()
if (classifierLq === null) return
expect(classifierLq).toBeCloseTo(expectedLq, 10)
})
})