"`) acima de um `Command` (`shouldFilter={false}` + `matchesAtWordStart`, acento-insensível), com gravação **por delta** (um toggle por vez, não replace do array). Fase 1 — `nextAdvisorIdsAfterMembership` subiu de `actions/advisor.ts` para `src/lib/municipalityAdvisorMembership.ts` (compartilhado com a carteira do B19); `setMunicipalityAdvisorMembershipRecord`/`setMunicipalityAdvisorMembership` em `actions/municipality.ts` (mesma transação + lock `municipality-advisors:{id}` + `reloadUnrestrictedActor`, mas sem `assertTargetAdvisor` — o alvo pode ser coordenador/candidato, legal nesta tela). Fase 2 — endpoint `POST /campanha/municipios/advisors` espelhando `expected-votes/route.ts` (`isSameOriginRequest` local, zod, `mapCampaignFormActionError`). Fase 3 — `MunicipalityListAdvisorsControl.tsx` reescrito: estado otimista + contador de pendências (reconcilia só quando toda entrega em voo assentou, nunca aborta um delta anterior), `Alert` de erro, `aria-live` `sr-only`; polish do `/impeccable` alinhou o gatilho (`aria-expanded`/`aria-haspopup`/realce `bg-muted/60` ao abrir) ao padrão do `MunicipalityListExpectedVotesControl` irmão (B24). `assignMunicipalityAdvisorsRecord` (replace do array) sobrevive só para `/editar`. Achado de infraestrutura no caminho: o Playwright `webServer` (`pnpm dev`) recompila uma rota POST-only no primeiro hit e o Fast Refresh resultante aborta o fetch em voo do teste (`ERR_ABORTED`) — `tests/e2e/setup.e2e.spec.ts` ganhou um prewarm dummy de `/campanha/municipios/advisors` (e retroativamente de `expected-votes/`, mesmo padrão) antes dos specs reais. Gate verde: tsc/lint/format/`check:cycles` limpos, 393 unit+int (6 novos), e2e do fluxo (isolado e em suíte) verde — falhas observadas em rodadas full-suite eram flakiness de ambiente sob carga sustentada (testes não relacionados falhando de forma rotativa, sempre verdes isolados), não regressão do B27; build produção ok; Aikido 0 achados nos 9 arquivos novos/editados. Plano: [`combobox-assessores-lista-municipios.md`](../../../docs/plans/combobox-assessores-lista-municipios.md).
diff --git a/docs/plans/escala-dry-pos-b13.md b/docs/plans/escala-dry-pos-b13.md
index 46f6ba8b..bdb13db4 100644
--- a/docs/plans/escala-dry-pos-b13.md
+++ b/docs/plans/escala-dry-pos-b13.md
@@ -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`
diff --git a/docs/roadmap.md b/docs/roadmap.md
index a5f29045..09292d28 100644
--- a/docs/roadmap.md
+++ b/docs/roadmap.md
@@ -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 `` 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` (435 strings fora do payload RSC do Início), `polygonRingsOf` exportado e os helpers de geometria retipados em `PolygonalFeature`/`BahiaFeature`/`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)
diff --git a/scripts/build-election-aggregates.mjs b/scripts/build-election-aggregates.mjs
index bb1b0288..846c490a 100644
--- a/scripts/build-election-aggregates.mjs
+++ b/scripts/build-election-aggregates.mjs
@@ -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.',
diff --git a/src/app/(campaign)/campanha/(app)/quadro/page.tsx b/src/app/(campaign)/campanha/(app)/quadro/page.tsx
index 8015e4f7..108562b7 100644
--- a/src/app/(campaign)/campanha/(app)/quadro/page.tsx
+++ b/src/app/(campaign)/campanha/(app)/quadro/page.tsx
@@ -110,7 +110,11 @@ const DashboardMapSection = async ({
return (
-
+
)
}
diff --git a/src/components/campaign/map/MunicipalityMapPanel.tsx b/src/components/campaign/map/MunicipalityMapPanel.tsx
index 779f3416..418909a2 100644
--- a/src/components/campaign/map/MunicipalityMapPanel.tsx
+++ b/src/components/campaign/map/MunicipalityMapPanel.tsx
@@ -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
diff --git a/src/components/campaign/map/MunicipalityMapPanelDynamic.tsx b/src/components/campaign/map/MunicipalityMapPanelDynamic.tsx
index 96820656..572f1412 100644
--- a/src/components/campaign/map/MunicipalityMapPanelDynamic.tsx
+++ b/src/components/campaign/map/MunicipalityMapPanelDynamic.tsx
@@ -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[]
-}) =>
+ actorRole: CampaignRole
+}) => (
+
+)
diff --git a/src/utilities/municipality/municipalityElectoralBaseline.ts b/src/utilities/municipality/municipalityElectoralBaseline.ts
index e55b4cae..dc019b51 100644
--- a/src/utilities/municipality/municipalityElectoralBaseline.ts
+++ b/src/utilities/municipality/municipalityElectoralBaseline.ts
@@ -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