teqo-backup/src/lib/campaignIntelligenceConcepts.ts
Francisco Ignácio Fontoura Solla fb4ba831c8 B22: explicacao por coluna no header das listas — description em CampaignTableHead/MunicipalitySortableHead, campaignConceptOneLiner como fonte unica E18, municipalityColumnDescriptions cobrindo as 11 colunas de /campanha/municipios
/simplify: extrai campaignHoverExplanationClassName/campaignHoverTooltipAlign para CampaignHoverTooltip.tsx (elimina duplicacao entre CampaignTableHead, MunicipalitySortableHead e MunicipalityGoalAccountCard), troca non-null assertion de campaignConceptOneLiner por lookup em Record, corrige copy da coluna Assessores (coordenador, nao candidato) e restaura comentario de invariante do sort de goalCoverage
2026-07-26 02:58:54 -03:00

204 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* E18 — curated documentation of the campaign-intelligence numbers the product
* derives, rendered by `/campanha/conceitos` and linked from the tooltips that
* expose each number.
*
* Content lives here, in code, next to the formulas it describes (E8:
* `municipalityPotential.ts` / `goalCoverage.ts`) so a change to a formula and
* the change to its explanation land in the same review — a Payload collection
* would let the copy drift silently. Every entry must describe what the code
* actually computes; when a formula changes, this text changes with it.
*
* v1 covers the E8 "conta da cadeira" numbers; E10 added dominância relativa
* and a classe territorial. Each later slice of the intelligence program (B13
* escala do mapa, E11 sugestões, E12 TI, E13 giros, E14 níveis) appends its
* own entries as part of its own delivery.
*/
export type CampaignConceptId =
| 'votos-validos-projetados'
| 'teto-do-campo'
| 'captura'
| 'share-intracampo'
| 'roll-off'
| 'dominancia-relativa'
| 'classe-territorial'
| 'meta'
| 'cobertura-da-meta'
export type CampaignConceptCategoryId = 'base' | 'diagnostico' | 'meta'
export type CampaignIntelligenceConcept = {
id: CampaignConceptId
categoryID: CampaignConceptCategoryId
title: string
/** One sentence: what the number measures. */
oneLiner: string
/** How it is calculated, in plain text (never rendered as math notation). */
formula: string
/** Hand-written illustration; never computed from live data. */
example?: string
/** What decision the number supports — including when it supports none. */
whyItMatters: string
/** Where in `/campanha` the number is shown. */
whereItAppears: string
}
export const CAMPAIGN_CONCEPT_CATEGORIES: ReadonlyArray<{
id: CampaignConceptCategoryId
label: string
}> = [
{ id: 'base', label: 'Base do cálculo' },
{ id: 'diagnostico', label: 'Diagnóstico' },
{ id: 'meta', label: 'Meta e cobertura' },
]
export const campaignIntelligenceConcepts: ReadonlyArray<CampaignIntelligenceConcept> = [
{
id: 'votos-validos-projetados',
categoryID: 'base',
title: 'Votos válidos projetados',
oneLiner: 'Quantos votos válidos a disputa de deputado federal deve ter no município em 2026.',
formula:
'válidos projetados = (válidos 2014 + válidos 2018 + 2 × válidos 2022) ÷ 4, sobre o 1º turno de deputado federal.',
example:
'Um município com 20.000 válidos em 2014, 22.000 em 2018 e 24.000 em 2022 projeta (20.000 + 22.000 + 2 × 24.000) ÷ 4 = 22.500.',
whyItMatters:
'É a régua de tamanho do município: entra no teto do campo projetado e, por consequência, na meta sugerida. 2022 pesa o dobro por ser a eleição mais próxima. É uma fórmula fixa e auditável, não uma previsão estatística — o produto não faz modelo de votos.',
whereItAppears:
'Não aparece como número na tela; sustenta o teto do campo projetado e a meta sugerida.',
},
{
id: 'teto-do-campo',
categoryID: 'base',
title: 'Teto do campo (projetado)',
oneLiner:
'Quantos votos o campo (PT e aliados de esquerda) pode alcançar no município em 2026.',
formula:
'teto 2022 = votos do presidencial do campo no município (1º turno de 2022). Teto projetado = teto 2022 × (válidos projetados ÷ válidos de 2022).',
example:
'Teto de 12.000 votos em 2022 num município que projeta crescer 5% de válidos vira um teto projetado de 12.600.',
whyItMatters:
'A disputa majoritária mostra o tamanho real do campo no município — muito maior do que a eleição fragmentada de deputado federal consegue revelar. É o denominador da captura e o peso usado para decompor a meta estadual. Usa só 2022: a majoritária de 2014 e 2018 já foi importada, mas ainda não entra nestas contas.',
whereItAppears: 'Card "Conta da cadeira", no detalhe do município ("Teto do campo (proj.)").',
},
{
id: 'captura',
categoryID: 'diagnostico',
title: 'Captura (2022)',
oneLiner: 'Quanto do teto do campo o Jorge Solla efetivamente conquistou no município em 2022.',
formula: 'captura = votos de Solla para deputado federal em 2022 ÷ teto do campo de 2022.',
example: '1.200 votos de Solla sobre um teto de 12.000 = 10% de captura.',
whyItMatters:
'Separa "município pequeno" de "município onde o campo é grande e a campanha não chegou". Captura baixa com teto alto é espaço a ocupar; captura alta é base a defender. É só diagnóstico: não entra no cálculo da meta, para não premiar o que já está consolidado nem punir onde ainda não se trabalhou.',
whereItAppears: 'Card "Conta da cadeira", no detalhe do município ("Captura (2022)").',
},
{
id: 'share-intracampo',
categoryID: 'diagnostico',
title: 'Share intracampo',
oneLiner:
'Dos votos que o campo recebeu para deputado federal naquele ano, a fração que foi para Jorge Solla.',
formula:
'share intracampo = votos de Solla no ano ÷ votos de todos os partidos do campo para deputado federal no mesmo ano.',
whyItMatters:
'É a leitura do fogo amigo: o denominador é o próprio campo na mesma disputa, então o número mostra a divisão interna do voto de esquerda no município, ano a ano (2014, 2018, 2022). Diferente da captura, cujo denominador é o teto presidencial de 2022. O campo é uma lista curada de siglas por ano (2022 e 2026: PT, PC do B, PV), não a coligação registrada no TSE — coligações proporcionais juntam aliados sem afinidade só para somar quociente.',
whereItAppears: 'Card "Conta da cadeira", no detalhe do município ("Share intracampo").',
},
{
id: 'roll-off',
categoryID: 'diagnostico',
title: 'Roll-off (2022)',
oneLiner:
'Quanto voto em branco e nulo a disputa de deputado federal atraiu a mais que a presidencial, no mesmo comparecimento.',
formula:
'roll-off = (brancos + nulos de deputado federal em 2022) − (brancos + nulos do presidencial em 2022). O percentual mostra esse saldo sobre o comparecimento de deputado federal.',
whyItMatters:
'Mede quanto eleitor comparece, vota para presidente e desiste de escolher deputado. Roll-off alto indica eleitorado disponível para quem chegar com nome e razão para votar — mas é diagnóstico de contexto, não um reservatório de votos garantidos. Também usa só 2022.',
whereItAppears: 'Card "Conta da cadeira", no detalhe do município ("Roll-off (2022)").',
},
{
id: 'dominancia-relativa',
categoryID: 'diagnostico',
title: 'Dominância relativa',
oneLiner:
'Quanto o desempenho do Jorge Solla no município se afasta do padrão dele no estado inteiro.',
formula:
'dominância = (votos de Solla ÷ votos válidos do município, em 2022) ÷ (votos de Solla ÷ votos válidos da Bahia, em 2022). Acima de 1 é acima do padrão dele; abaixo de 1 é abaixo.',
example:
'Um município onde ele fez 4% dos válidos, quando o padrão estadual dele é 2%, tem dominância 2 — "aqui rendemos o dobro do nosso normal". Onde ele fez 1,2%, a dominância é 0,6: "aqui rendemos 40% abaixo do nosso padrão".',
whyItMatters:
'Em deputado federal, o percentual absoluto engana: 3% dos válidos parece pouco e pode ser o melhor município da campanha. A comparação útil é com o próprio padrão, não com 50% + 1 — que é conta de eleição majoritária. É a leitura que separa "pequeno" de "fraco".',
whereItAppears:
'Como o "por quê" da classe, no card "Conta da cadeira", na capa do dossiê e na coluna "Classe" da lista de municípios.',
},
{
id: 'classe-territorial',
categoryID: 'diagnostico',
title: 'Classe do município',
oneLiner: 'Uma palavra para o que o município pede: Reduto, Expansão, Manutenção ou Marginal.',
formula:
'Reduto = dominância 2 ou mais. Marginal = dominância abaixo de 0,5 com pouco voto do campo sem captura (abaixo da mediana dos 435 municípios). Expansão = dominância abaixo de 0,5 mas com campo grande sem captura, ou município do bloco que concentra metade da votação dele. Manutenção = o meio, entre 0,5 e 2. Sem base = município sem série do TSE.',
example:
'Dominância 0,3 com 9.000 votos do campo sem captura é Expansão: o campo está lá e a campanha não chegou. A mesma dominância com 300 votos do campo disponíveis é Marginal — não compensa a perna.',
whyItMatters:
'Responde onde a perna vai na semana: defender reduto dormente ou abrir rede em expansão. A classe é sugestão, não sentença — ela nunca aparece sozinha, sempre com os dois fatores que a produziram, e a mesa pode decidir contra ela. Os cortes exatos (2 e 0,5) são ilustrativos: valem até o backtest contra 2014–2022 calibrá-los.',
whereItAppears:
'Card "Conta da cadeira" e capa do dossiê, no detalhe do município, e a coluna "Classe" da lista de municípios (com filtro e ordenação).',
},
{
id: 'meta',
categoryID: 'meta',
title: 'Meta e meta sugerida',
oneLiner: 'Quantos votos a campanha quer no município no cenário escolhido.',
formula:
'meta = estimativa da mesa para o cenário (pessimista, média ou otimista), quando existe; sem estimativa, usa a meta sugerida. Meta sugerida = meta estadual × (teto do campo projetado do município ÷ soma dos tetos projetados dos 435 municípios).',
example:
'Com meta estadual de 150.000 votos, um município que responde por 2% da soma dos tetos projetados recebe uma meta sugerida de 3.000.',
whyItMatters:
'A estimativa da mesa sempre vence a sugestão: quem conhece o município decide. A meta sugerida existe para que nenhum dos 435 municípios fique sem régua enquanto a mesa não preencher. A decomposição é proporcional só ao teto projetado — captura e share ficam de fora de propósito, como diagnóstico. A meta estadual (e a margem de segurança) é editada em Metas da campanha.',
whereItAppears:
'Card "Conta da cadeira" ("Meta usada na conta") e o campo de votos estimados no detalhe e na lista de municípios.',
},
{
id: 'cobertura-da-meta',
categoryID: 'meta',
title: 'Cobertura da meta',
oneLiner:
'Quanto da meta já está coberto por compromissos de lideranças — não por expectativa da mesa.',
formula:
'cobertura = comprometido ÷ meta. Comprometido = soma dos votos das lideranças do município (estimativa do cenário quando existe, senão o número declarado pela liderança). Déficit = meta − comprometido.',
example:
'Meta de 3.000 com 1.200 votos comprometidos em lideranças = 40% de cobertura, déficit de 1.800.',
whyItMatters:
'É a diferença entre querer e ter: a expectativa da mesa define a meta, mas nunca conta como compromisso — se contasse, todo município com estimativa preenchida apareceria coberto sem uma única liderança por trás. O déficit descoberto é o que ordena onde a campanha precisa de mais rede.',
whereItAppears:
'Início (staff), visão geral e coluna "Cobertura da meta" na lista de municípios, e o card "Conta da cadeira" no detalhe.',
},
]
export const campaignConceptsByCategory = (
categoryID: CampaignConceptCategoryId,
): CampaignIntelligenceConcept[] =>
campaignIntelligenceConcepts.filter((concept) => concept.categoryID === categoryID)
export const CAMPAIGN_CONCEPTS_PATH = '/campanha/conceitos'
/** Anchor href for a documented concept — typed so a renamed id can't leave a dead link behind. */
export const campaignConceptHref = (id: CampaignConceptId): string =>
`${CAMPAIGN_CONCEPTS_PATH}#${id}`
/**
* `oneLiner` indexed by id, built once at module init. Callers (e.g. the B22
* column-header tooltips) quote the same sentence the glossary page shows
* instead of writing a second, driftable one. The cast is the single place
* that assumes every `CampaignConceptId` has a matching concept — the
* "unique ids" test above pins that assumption for the whole union, so
* `campaignConceptOneLiner` itself stays a plain, assertion-free lookup.
*/
const campaignConceptOneLinerByID = Object.fromEntries(
campaignIntelligenceConcepts.map((concept) => [concept.id, concept.oneLiner]),
) as Record<CampaignConceptId, string>
export const campaignConceptOneLiner = (id: CampaignConceptId): string =>
campaignConceptOneLinerByID[id]