mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-12 22:13:05 +00:00
feat(potencial): artefato TSE 2026 por seção + ingestão nacional validada
O bulk oficial do TSE (votacao_secao_2026_BR + detalhe_votacao_secao_2026) é ingerido uma vez em 27 shards por UF (497.897 seções, 13,6 MB) sob public/dados/potencial-secao-2026/, com manifest de hashes. A ingestão valida totais nacionais e do recorte BA pinados (aborta em divergência), conta os votáveis Lula/Flávio vistos e faz merge do recibo em runs --only. Cache gitignored em data/tse-2026/.
This commit is contained in:
parent
3e6afeb58f
commit
6aad284e11
31 changed files with 812 additions and 0 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -172,6 +172,7 @@ next-env.d.ts
|
||||||
# Changelog aggregate is a regenerable artifact (OPS85) — entries live in docs/changelog/
|
# Changelog aggregate is a regenerable artifact (OPS85) — entries live in docs/changelog/
|
||||||
/docs/CHANGELOG-AGENTS.md
|
/docs/CHANGELOG-AGENTS.md
|
||||||
/data/tse-2022/
|
/data/tse-2022/
|
||||||
|
/data/tse-2026/
|
||||||
|
|
||||||
# Briefing Geral de Militância (interno; repo público).
|
# Briefing Geral de Militância (interno; repo público).
|
||||||
/data/briefing-geral-militancia/
|
/data/briefing-geral-militancia/
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,7 @@
|
||||||
"build:demographics": "cross-env NODE_OPTIONS=\"--no-deprecation --import=tsx/esm\" node scripts/build-bahia-demographics.mjs",
|
"build:demographics": "cross-env NODE_OPTIONS=\"--no-deprecation --import=tsx/esm\" node scripts/build-bahia-demographics.mjs",
|
||||||
"build:election-aggregates": "cross-env NODE_OPTIONS=\"--no-deprecation --import=tsx/esm --import=./scripts/seed-loader.mjs\" node scripts/build-election-aggregates.mjs",
|
"build:election-aggregates": "cross-env NODE_OPTIONS=\"--no-deprecation --import=tsx/esm --import=./scripts/seed-loader.mjs\" node scripts/build-election-aggregates.mjs",
|
||||||
"build:geometries": "cross-env NODE_OPTIONS=\"--no-deprecation --import=tsx/esm\" node scripts/build-bahia-geometries.mjs",
|
"build:geometries": "cross-env NODE_OPTIONS=\"--no-deprecation --import=tsx/esm\" node scripts/build-bahia-geometries.mjs",
|
||||||
|
"build:potencial-secoes": "cross-env NODE_OPTIONS=\"--no-deprecation --import=tsx/esm\" node scripts/build-potencial-secoes-2026.mjs",
|
||||||
"build:state-deputy-card-assets": "cross-env NODE_OPTIONS=\"--no-deprecation --import=tsx/esm\" node scripts/build-state-deputy-card-assets.mjs",
|
"build:state-deputy-card-assets": "cross-env NODE_OPTIONS=\"--no-deprecation --import=tsx/esm\" node scripts/build-state-deputy-card-assets.mjs",
|
||||||
"build:municipality-zone-geometries": "cross-env NODE_OPTIONS=\"--no-deprecation --import=tsx/esm\" node scripts/build-municipality-zone-geometries.mjs",
|
"build:municipality-zone-geometries": "cross-env NODE_OPTIONS=\"--no-deprecation --import=tsx/esm\" node scripts/build-municipality-zone-geometries.mjs",
|
||||||
"build:tse-city-codes": "cross-env NODE_OPTIONS=\"--no-deprecation --import=tsx/esm\" node scripts/build-bahia-tse-city-codes.mjs",
|
"build:tse-city-codes": "cross-env NODE_OPTIONS=\"--no-deprecation --import=tsx/esm\" node scripts/build-bahia-tse-city-codes.mjs",
|
||||||
|
|
|
||||||
1
public/dados/potencial-secao-2026/AC.json
Normal file
1
public/dados/potencial-secao-2026/AC.json
Normal file
File diff suppressed because one or more lines are too long
1
public/dados/potencial-secao-2026/AL.json
Normal file
1
public/dados/potencial-secao-2026/AL.json
Normal file
File diff suppressed because one or more lines are too long
1
public/dados/potencial-secao-2026/AM.json
Normal file
1
public/dados/potencial-secao-2026/AM.json
Normal file
File diff suppressed because one or more lines are too long
1
public/dados/potencial-secao-2026/AP.json
Normal file
1
public/dados/potencial-secao-2026/AP.json
Normal file
File diff suppressed because one or more lines are too long
1
public/dados/potencial-secao-2026/BA.json
Normal file
1
public/dados/potencial-secao-2026/BA.json
Normal file
File diff suppressed because one or more lines are too long
1
public/dados/potencial-secao-2026/CE.json
Normal file
1
public/dados/potencial-secao-2026/CE.json
Normal file
File diff suppressed because one or more lines are too long
1
public/dados/potencial-secao-2026/DF.json
Normal file
1
public/dados/potencial-secao-2026/DF.json
Normal file
File diff suppressed because one or more lines are too long
1
public/dados/potencial-secao-2026/ES.json
Normal file
1
public/dados/potencial-secao-2026/ES.json
Normal file
File diff suppressed because one or more lines are too long
1
public/dados/potencial-secao-2026/GO.json
Normal file
1
public/dados/potencial-secao-2026/GO.json
Normal file
File diff suppressed because one or more lines are too long
1
public/dados/potencial-secao-2026/MA.json
Normal file
1
public/dados/potencial-secao-2026/MA.json
Normal file
File diff suppressed because one or more lines are too long
1
public/dados/potencial-secao-2026/MG.json
Normal file
1
public/dados/potencial-secao-2026/MG.json
Normal file
File diff suppressed because one or more lines are too long
1
public/dados/potencial-secao-2026/MS.json
Normal file
1
public/dados/potencial-secao-2026/MS.json
Normal file
File diff suppressed because one or more lines are too long
1
public/dados/potencial-secao-2026/MT.json
Normal file
1
public/dados/potencial-secao-2026/MT.json
Normal file
File diff suppressed because one or more lines are too long
1
public/dados/potencial-secao-2026/PA.json
Normal file
1
public/dados/potencial-secao-2026/PA.json
Normal file
File diff suppressed because one or more lines are too long
1
public/dados/potencial-secao-2026/PB.json
Normal file
1
public/dados/potencial-secao-2026/PB.json
Normal file
File diff suppressed because one or more lines are too long
1
public/dados/potencial-secao-2026/PE.json
Normal file
1
public/dados/potencial-secao-2026/PE.json
Normal file
File diff suppressed because one or more lines are too long
1
public/dados/potencial-secao-2026/PI.json
Normal file
1
public/dados/potencial-secao-2026/PI.json
Normal file
File diff suppressed because one or more lines are too long
1
public/dados/potencial-secao-2026/PR.json
Normal file
1
public/dados/potencial-secao-2026/PR.json
Normal file
File diff suppressed because one or more lines are too long
1
public/dados/potencial-secao-2026/RJ.json
Normal file
1
public/dados/potencial-secao-2026/RJ.json
Normal file
File diff suppressed because one or more lines are too long
1
public/dados/potencial-secao-2026/RN.json
Normal file
1
public/dados/potencial-secao-2026/RN.json
Normal file
File diff suppressed because one or more lines are too long
1
public/dados/potencial-secao-2026/RO.json
Normal file
1
public/dados/potencial-secao-2026/RO.json
Normal file
File diff suppressed because one or more lines are too long
1
public/dados/potencial-secao-2026/RR.json
Normal file
1
public/dados/potencial-secao-2026/RR.json
Normal file
File diff suppressed because one or more lines are too long
1
public/dados/potencial-secao-2026/RS.json
Normal file
1
public/dados/potencial-secao-2026/RS.json
Normal file
File diff suppressed because one or more lines are too long
1
public/dados/potencial-secao-2026/SC.json
Normal file
1
public/dados/potencial-secao-2026/SC.json
Normal file
File diff suppressed because one or more lines are too long
1
public/dados/potencial-secao-2026/SE.json
Normal file
1
public/dados/potencial-secao-2026/SE.json
Normal file
File diff suppressed because one or more lines are too long
1
public/dados/potencial-secao-2026/SP.json
Normal file
1
public/dados/potencial-secao-2026/SP.json
Normal file
File diff suppressed because one or more lines are too long
1
public/dados/potencial-secao-2026/TO.json
Normal file
1
public/dados/potencial-secao-2026/TO.json
Normal file
File diff suppressed because one or more lines are too long
246
public/dados/potencial-secao-2026/manifest.json
Normal file
246
public/dados/potencial-secao-2026/manifest.json
Normal file
|
|
@ -0,0 +1,246 @@
|
||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"generatedAt": "2026-10-07T21:38:06.060Z",
|
||||||
|
"scope": "BR",
|
||||||
|
"sources": [
|
||||||
|
{
|
||||||
|
"key": "votacao_secao_2026_BR",
|
||||||
|
"url": "https://cdn.tse.jus.br/estatistica/sead/odsele/votacao_secao/votacao_secao_2026_BR.zip",
|
||||||
|
"sha256": "2d42006d0f6c6e00bad83f5c23cd3d78cd3e3182dbc1ad6718222b6b132c23e1",
|
||||||
|
"bytes": 161616519
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "detalhe_votacao_secao_2026",
|
||||||
|
"url": "https://cdn.tse.jus.br/estatistica/sead/odsele/detalhe_votacao_secao/detalhe_votacao_secao_2026.zip",
|
||||||
|
"sha256": "0b1297dc7ae4a46b6ee0428d0f60b9ab9da6b53419ef7580d9ca20662a192cb1",
|
||||||
|
"bytes": 176923590
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"candidates": {
|
||||||
|
"lula": 13,
|
||||||
|
"flavio": 22,
|
||||||
|
"count": 13,
|
||||||
|
"votesByNumber": {
|
||||||
|
"13": 53722151,
|
||||||
|
"14": 2667307,
|
||||||
|
"16": 42730,
|
||||||
|
"21": 22272,
|
||||||
|
"22": 55960603,
|
||||||
|
"27": 39218,
|
||||||
|
"28": 5123,
|
||||||
|
"29": 14825,
|
||||||
|
"30": 323159,
|
||||||
|
"35": 16508,
|
||||||
|
"55": 2600440,
|
||||||
|
"70": 3438835,
|
||||||
|
"80": 121858
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"totals": {
|
||||||
|
"sections": 497897,
|
||||||
|
"aptos": 157828968,
|
||||||
|
"comparecimento": 124934069,
|
||||||
|
"nominais": 118975029,
|
||||||
|
"brancos": 2294982,
|
||||||
|
"nulos": 3664058,
|
||||||
|
"lula": 53722151,
|
||||||
|
"flavio": 55960603,
|
||||||
|
"candidateVotes": 118975029,
|
||||||
|
"candidates": 13
|
||||||
|
},
|
||||||
|
"ufs": {
|
||||||
|
"AC": {
|
||||||
|
"file": "AC.json",
|
||||||
|
"sha256": "960365d6a2be892f2bbe4fd0c15e0608da6de52401abf5d7f2f09a87e63935ff",
|
||||||
|
"bytes": 61295,
|
||||||
|
"municipalities": 22,
|
||||||
|
"sections": 2270
|
||||||
|
},
|
||||||
|
"AL": {
|
||||||
|
"file": "AL.json",
|
||||||
|
"sha256": "14a549e2dd5771101ca0e17fbffff22fd9102486691bbffd56fed1edda44b07a",
|
||||||
|
"bytes": 201778,
|
||||||
|
"municipalities": 102,
|
||||||
|
"sections": 7101
|
||||||
|
},
|
||||||
|
"AP": {
|
||||||
|
"file": "AP.json",
|
||||||
|
"sha256": "d94c4021e15b80e7a13c2969bdfcfca6cb1a3a3455198061884abebc42f7465b",
|
||||||
|
"bytes": 52935,
|
||||||
|
"municipalities": 16,
|
||||||
|
"sections": 1914
|
||||||
|
},
|
||||||
|
"AM": {
|
||||||
|
"file": "AM.json",
|
||||||
|
"sha256": "3737ff633e331c24aaca95bc67ea3db1eaedb861e10d8fcb0898c74a3e0f5159",
|
||||||
|
"bytes": 229562,
|
||||||
|
"municipalities": 62,
|
||||||
|
"sections": 8157
|
||||||
|
},
|
||||||
|
"BA": {
|
||||||
|
"file": "BA.json",
|
||||||
|
"sha256": "cdd5e49e1eeede84c11c3585d15457b24639aecf7d654c4d5d60d748532310c1",
|
||||||
|
"bytes": 1015815,
|
||||||
|
"municipalities": 417,
|
||||||
|
"sections": 35476
|
||||||
|
},
|
||||||
|
"CE": {
|
||||||
|
"file": "CE.json",
|
||||||
|
"sha256": "d14b88aa6fe167fee17d00a1db999460df3773a71dceac68cd65e980bed8df74",
|
||||||
|
"bytes": 671614,
|
||||||
|
"municipalities": 184,
|
||||||
|
"sections": 23765
|
||||||
|
},
|
||||||
|
"DF": {
|
||||||
|
"file": "DF.json",
|
||||||
|
"sha256": "9a7a8c47b510be58549ae4b04d72692060e113de17fd423c77775aed8ef8545e",
|
||||||
|
"bytes": 192975,
|
||||||
|
"municipalities": 1,
|
||||||
|
"sections": 6969
|
||||||
|
},
|
||||||
|
"ES": {
|
||||||
|
"file": "ES.json",
|
||||||
|
"sha256": "3d19acea027d3b721016f36d912340b9850d41cc843b579a426716a14789eea8",
|
||||||
|
"bytes": 275027,
|
||||||
|
"municipalities": 78,
|
||||||
|
"sections": 9844
|
||||||
|
},
|
||||||
|
"GO": {
|
||||||
|
"file": "GO.json",
|
||||||
|
"sha256": "cf6996f0e31b99051e2f9a2e737a52ead53503b868019fe13118cae023fad31a",
|
||||||
|
"bytes": 446875,
|
||||||
|
"municipalities": 246,
|
||||||
|
"sections": 15686
|
||||||
|
},
|
||||||
|
"MA": {
|
||||||
|
"file": "MA.json",
|
||||||
|
"sha256": "7904d70d6c7d1526920ae20a359f0b8568e459c4e8fb24a65df5877b4bb8dcaa",
|
||||||
|
"bytes": 509087,
|
||||||
|
"municipalities": 217,
|
||||||
|
"sections": 18093
|
||||||
|
},
|
||||||
|
"MT": {
|
||||||
|
"file": "MT.json",
|
||||||
|
"sha256": "8db7fcd3905bfcdfa023e1ef752d242f80fe2f888fd2118d239df11196070678",
|
||||||
|
"bytes": 235688,
|
||||||
|
"municipalities": 142,
|
||||||
|
"sections": 8287
|
||||||
|
},
|
||||||
|
"MS": {
|
||||||
|
"file": "MS.json",
|
||||||
|
"sha256": "3ab6f988fdbd46771e945223f08ea953cf97f7b9a37b0eb1e49c690776c7f67a",
|
||||||
|
"bytes": 197048,
|
||||||
|
"municipalities": 79,
|
||||||
|
"sections": 7106
|
||||||
|
},
|
||||||
|
"MG": {
|
||||||
|
"file": "MG.json",
|
||||||
|
"sha256": "0edfd5996809d7fb42999105639e62a6e361da8b5acdceac0bb6d4f89547a2de",
|
||||||
|
"bytes": 1515937,
|
||||||
|
"municipalities": 853,
|
||||||
|
"sections": 52062
|
||||||
|
},
|
||||||
|
"PA": {
|
||||||
|
"file": "PA.json",
|
||||||
|
"sha256": "84b2acabf2041803cdb7da626f9e70eb495a5ca508d995d5edbd79fb4939ad37",
|
||||||
|
"bytes": 585090,
|
||||||
|
"municipalities": 144,
|
||||||
|
"sections": 20827
|
||||||
|
},
|
||||||
|
"PB": {
|
||||||
|
"file": "PB.json",
|
||||||
|
"sha256": "b8b73cdf1655e12cb27ab9ac14b0d7c57688c253703bb126fe7bd261fec55d36",
|
||||||
|
"bytes": 306198,
|
||||||
|
"municipalities": 223,
|
||||||
|
"sections": 10712
|
||||||
|
},
|
||||||
|
"PR": {
|
||||||
|
"file": "PR.json",
|
||||||
|
"sha256": "a5dc5796142b82293fd59af129c6cabac6829569f13f61fbeb6e53152ba32eac",
|
||||||
|
"bytes": 778534,
|
||||||
|
"municipalities": 399,
|
||||||
|
"sections": 27142
|
||||||
|
},
|
||||||
|
"PE": {
|
||||||
|
"file": "PE.json",
|
||||||
|
"sha256": "b884c02765060e098c87e395e76b6ef65709ff14c3c466020633a792eb6e8d1e",
|
||||||
|
"bytes": 608913,
|
||||||
|
"municipalities": 185,
|
||||||
|
"sections": 21418
|
||||||
|
},
|
||||||
|
"PI": {
|
||||||
|
"file": "PI.json",
|
||||||
|
"sha256": "05acc7852de832db1737bfa20dd0f301488dcd710020b6c15d538c6c54f6165f",
|
||||||
|
"bytes": 289635,
|
||||||
|
"municipalities": 224,
|
||||||
|
"sections": 10225
|
||||||
|
},
|
||||||
|
"RJ": {
|
||||||
|
"file": "RJ.json",
|
||||||
|
"sha256": "895b7c9df23101736538dcbb35a291c69dc67c04b2cb76e3e019abb4ab879ccf",
|
||||||
|
"bytes": 1076255,
|
||||||
|
"municipalities": 92,
|
||||||
|
"sections": 37675
|
||||||
|
},
|
||||||
|
"RN": {
|
||||||
|
"file": "RN.json",
|
||||||
|
"sha256": "3417841406d73af230bd941cc299375db34982792b565d0b8ea8cdc741c1d3e4",
|
||||||
|
"bytes": 231337,
|
||||||
|
"municipalities": 167,
|
||||||
|
"sections": 8115
|
||||||
|
},
|
||||||
|
"RS": {
|
||||||
|
"file": "RS.json",
|
||||||
|
"sha256": "323c83a03191ca438357d45f1e776119a1ba9fa49750a8baed0f3c602e8a39e9",
|
||||||
|
"bytes": 792050,
|
||||||
|
"municipalities": 497,
|
||||||
|
"sections": 27547
|
||||||
|
},
|
||||||
|
"RO": {
|
||||||
|
"file": "RO.json",
|
||||||
|
"sha256": "e6ab63b463e161b4a843678226580767a4c0853da2aabaa05e29bff1e27a45f1",
|
||||||
|
"bytes": 129953,
|
||||||
|
"municipalities": 52,
|
||||||
|
"sections": 4698
|
||||||
|
},
|
||||||
|
"RR": {
|
||||||
|
"file": "RR.json",
|
||||||
|
"sha256": "4fc426fb2afa7b036145f9613f18539825e3bf3d31360aa334edbfc907a1d637",
|
||||||
|
"bytes": 40985,
|
||||||
|
"municipalities": 15,
|
||||||
|
"sections": 1519
|
||||||
|
},
|
||||||
|
"SC": {
|
||||||
|
"file": "SC.json",
|
||||||
|
"sha256": "599a4de71abb85aa9669f4107114080d3cd12a32726a7283ffa1b62d3c00e9bd",
|
||||||
|
"bytes": 492499,
|
||||||
|
"municipalities": 295,
|
||||||
|
"sections": 17326
|
||||||
|
},
|
||||||
|
"SP": {
|
||||||
|
"file": "SP.json",
|
||||||
|
"sha256": "16a706be4f791425afd73736b00b197ee422d95a16460aa3e77fa90d649cf9a3",
|
||||||
|
"bytes": 2991786,
|
||||||
|
"municipalities": 645,
|
||||||
|
"sections": 103656
|
||||||
|
},
|
||||||
|
"SE": {
|
||||||
|
"file": "SE.json",
|
||||||
|
"sha256": "5df9b985e4a2a9c6171e0843d73504ab954eecc4a2bcbed27c1272487dc32a60",
|
||||||
|
"bytes": 165772,
|
||||||
|
"municipalities": 75,
|
||||||
|
"sections": 5923
|
||||||
|
},
|
||||||
|
"TO": {
|
||||||
|
"file": "TO.json",
|
||||||
|
"sha256": "715fc87b236caf73ab8e25bb2db4ce9b0268b6360769d9e25bce43078a636400",
|
||||||
|
"bytes": 125305,
|
||||||
|
"municipalities": 139,
|
||||||
|
"sections": 4384
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"validation": [
|
||||||
|
"estruturais: comparecimento = nominais + brancos + nulos; soma das 13 candidaturas = nominais; nenhuma seção sem aferição; Lula+Flávio ≤ comparecimento.",
|
||||||
|
"auditoria nacional pinada confere (seções, aptos, comparecimento, nominais, brancos, nulos, Lula, Flávio, candidaturas)."
|
||||||
|
]
|
||||||
|
}
|
||||||
537
scripts/build-potencial-secoes-2026.mjs
Normal file
537
scripts/build-potencial-secoes-2026.mjs
Normal file
|
|
@ -0,0 +1,537 @@
|
||||||
|
/**
|
||||||
|
* S46 — "story da sua seção": plataforma por seção eleitoral, presidente T1 2026.
|
||||||
|
*
|
||||||
|
* Constrói o artefato commitado que a página pública `/potencial` lê: para cada
|
||||||
|
* seção eleitoral do Brasil, os números do cenário aprovado — aptos,
|
||||||
|
* comparecimento, votos de Lula (13), votos de Flávio (22) e válidos
|
||||||
|
* (nominais) — de onde saem X₁ = comparecimento − Lula − Flávio (nulos +
|
||||||
|
* brancos + TODOS os terceiros) e X₂ = aptos − Lula − Flávio.
|
||||||
|
*
|
||||||
|
* As constantes de auditoria nacional/BA estão pinadas: um bulk re-publicado
|
||||||
|
* pelo TSE falha fechado até um PR deliberado atualizar os pins.
|
||||||
|
*
|
||||||
|
* Fontes (Portal de Dados Abertos do TSE, 1º turno de 04/10/2026):
|
||||||
|
* votacao_secao_2026_BR.zip votos por candidato por seção (presidente)
|
||||||
|
* detalhe_votacao_secao_2026.zip aferição por seção × cargo (CSV BRASIL)
|
||||||
|
* Portal: https://dadosabertos.tse.jus.br/dataset/resultados-2026
|
||||||
|
* Licença: Creative Commons Atribuição (dados abertos TSE).
|
||||||
|
*
|
||||||
|
* Saída: public/dados/potencial-secao-2026/<UF>.json + manifest.json.
|
||||||
|
* Nunca roda em build/deploy — o artefato é commitado.
|
||||||
|
*
|
||||||
|
* Uso:
|
||||||
|
* pnpm build:potencial-secoes
|
||||||
|
* pnpm build:potencial-secoes -- --only=BA
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { parse as parseCsv } from 'csv-parse'
|
||||||
|
import { readFile } from 'node:fs/promises'
|
||||||
|
import { dirname, join } from 'node:path'
|
||||||
|
import { fileURLToPath } from 'node:url'
|
||||||
|
import { open as openZip } from 'yauzl'
|
||||||
|
|
||||||
|
import {
|
||||||
|
dieWithLabel,
|
||||||
|
ensureCachedDownload,
|
||||||
|
parseEqualsFlags,
|
||||||
|
sha256Hex,
|
||||||
|
writeRepoFile,
|
||||||
|
} from './lib/cli.mjs'
|
||||||
|
|
||||||
|
// Donos únicos (tsx resolve os paths do tsconfig): a lista de UFs e a versão do
|
||||||
|
// shard vivem no lib que o runtime lê; as divergências de nome da BA e o
|
||||||
|
// normalizador de busca vêm dos módulos compartilhados do repo.
|
||||||
|
const { POTENTIAL_SHARD_VERSION, POTENTIAL_UFS } = await import('../src/lib/sectionPotential.ts')
|
||||||
|
const { MUNICIPALITY_NAME_ALIASES } = await import('../src/lib/municipalityNameAliases.ts')
|
||||||
|
const { normalizeSearchPhrase } = await import('../src/lib/wordStartFilter.ts')
|
||||||
|
|
||||||
|
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..')
|
||||||
|
const LABEL = 'build-potencial-secoes-2026'
|
||||||
|
const die = dieWithLabel(LABEL)
|
||||||
|
|
||||||
|
const CACHE_DIR = join(ROOT, 'data/tse-2026')
|
||||||
|
const OUTPUT_DIR = 'public/dados/potencial-secao-2026'
|
||||||
|
|
||||||
|
/** O CDN do TSE recusa user-agent headless (C161); o download usa UA de browser. */
|
||||||
|
const BROWSER_UA =
|
||||||
|
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36'
|
||||||
|
|
||||||
|
const PRESIDENT_OFFICE = '1'
|
||||||
|
const FIRST_TURN = '1'
|
||||||
|
const LULA_VOTAVEL = 13
|
||||||
|
const FLAVIO_VOTAVEL = 22
|
||||||
|
const BRANCO_VOTAVEL = 95
|
||||||
|
const NULO_VOTAVEL = 96
|
||||||
|
|
||||||
|
const SOURCES = [
|
||||||
|
{
|
||||||
|
key: 'votacao_secao_2026_BR',
|
||||||
|
url: 'https://cdn.tse.jus.br/estatistica/sead/odsele/votacao_secao/votacao_secao_2026_BR.zip',
|
||||||
|
// sha256 do zip baixado em 2026-10-07 (ver manifest.json).
|
||||||
|
sha256: '2d42006d0f6c6e00bad83f5c23cd3d78cd3e3182dbc1ad6718222b6b132c23e1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'detalhe_votacao_secao_2026',
|
||||||
|
url: 'https://cdn.tse.jus.br/estatistica/sead/odsele/detalhe_votacao_secao/detalhe_votacao_secao_2026.zip',
|
||||||
|
sha256: '0b1297dc7ae4a46b6ee0428d0f60b9ab9da6b53419ef7580d9ca20662a192cb1',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const VOTACAO_ENTRY = 'votacao_secao_2026_BR.csv'
|
||||||
|
const DETALHE_ENTRY = 'detalhe_votacao_secao_2026_BRASIL.csv'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auditoria nacional do presidente T1 2026 (medida em 2026-10-07 dos CSVs,
|
||||||
|
* escopo Brasil — o exterior `ZZ` fica fora do produto, que consulta a UF do
|
||||||
|
* título): qualquer divergência aborta a ingestão. Atualizar exige PR.
|
||||||
|
*/
|
||||||
|
const NATIONAL_AUDIT = {
|
||||||
|
sections: 497_897,
|
||||||
|
aptos: 157_828_968,
|
||||||
|
comparecimento: 124_934_069,
|
||||||
|
nominais: 118_975_029,
|
||||||
|
brancos: 2_294_982,
|
||||||
|
nulos: 3_664_058,
|
||||||
|
lula: 53_722_151,
|
||||||
|
flavio: 55_960_603,
|
||||||
|
candidateVotes: 118_975_029,
|
||||||
|
candidates: 13,
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Auditoria do recorte BA (mesma data) — usada no `--only=BA`. */
|
||||||
|
const UF_AUDIT = {
|
||||||
|
BA: {
|
||||||
|
sections: 35_476,
|
||||||
|
aptos: 11_312_752,
|
||||||
|
comparecimento: 9_053_601,
|
||||||
|
nominais: 8_560_542,
|
||||||
|
brancos: 154_936,
|
||||||
|
nulos: 338_123,
|
||||||
|
lula: 5_664_771,
|
||||||
|
flavio: 2_442_585,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const int = new Intl.NumberFormat('pt-BR')
|
||||||
|
|
||||||
|
const downloadZip = async (url) => {
|
||||||
|
const response = await fetch(url, { headers: { 'user-agent': BROWSER_UA } })
|
||||||
|
if (!response.ok) throw new Error(`download falhou (${response.status}): ${url}`)
|
||||||
|
return Buffer.from(await response.arrayBuffer())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Zip streaming (yauzl + csv-parse; nada é descompactado em disco)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const openZipEntryStream = (zipPath, matchEntry) =>
|
||||||
|
new Promise((resolve, reject) => {
|
||||||
|
openZip(zipPath, { lazyEntries: true }, (error, zipfile) => {
|
||||||
|
if (error || !zipfile) {
|
||||||
|
reject(error ?? new Error(`não abriu ${zipPath}`))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
zipfile.readEntry()
|
||||||
|
zipfile.on('entry', (entry) => {
|
||||||
|
if (/\/$/.test(entry.fileName) || !matchEntry(entry.fileName)) {
|
||||||
|
zipfile.readEntry()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
zipfile.openReadStream(entry, (streamError, stream) => {
|
||||||
|
if (streamError || !stream) {
|
||||||
|
reject(streamError ?? new Error(`não leu ${entry.fileName}`))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
stream.on('end', () => {
|
||||||
|
try {
|
||||||
|
zipfile.close()
|
||||||
|
} catch {
|
||||||
|
// o fd já pode ter sido fechado pelo autoClose do yauzl
|
||||||
|
}
|
||||||
|
})
|
||||||
|
resolve(stream)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
zipfile.on('end', () => reject(new Error(`entrada não encontrada em ${zipPath}`)))
|
||||||
|
zipfile.on('error', reject)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Linhas de um CSV dentro do zip (TSE: ISO-8859-1, `;`, campos citados).
|
||||||
|
* `latin1` é aplicado no stream antes do parser para não decodificar UTF-8.
|
||||||
|
*/
|
||||||
|
async function* zipEntryRows(zipPath, matchEntry) {
|
||||||
|
const stream = await openZipEntryStream(zipPath, matchEntry)
|
||||||
|
stream.setEncoding('latin1')
|
||||||
|
const parser = parseCsv({
|
||||||
|
columns: true,
|
||||||
|
delimiter: ';',
|
||||||
|
relax_quotes: true,
|
||||||
|
relax_column_count: true,
|
||||||
|
skip_empty_lines: true,
|
||||||
|
trim: true,
|
||||||
|
bom: true,
|
||||||
|
})
|
||||||
|
stream.pipe(parser)
|
||||||
|
for await (const row of parser) yield row
|
||||||
|
}
|
||||||
|
|
||||||
|
const num = (value) => {
|
||||||
|
const parsed = Number(value)
|
||||||
|
return Number.isFinite(parsed) ? parsed : 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Nomes de exibição (TSE em caixa alta → nome canônico do cadastro do repo)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Divergências de grafia TSE × cadastro do repo. As da Bahia vêm do dono
|
||||||
|
* (`MUNICIPALITY_NAME_ALIASES`, o mesmo fold dos imports de apoiador); as
|
||||||
|
* demais UFs não têm cadastro curado — o fallback é o próprio nome do TSE em
|
||||||
|
* title case, nunca um erro.
|
||||||
|
*/
|
||||||
|
const EXTRA_NAME_ALIASES = {
|
||||||
|
// Chave já no formato de `normalizeSearchPhrase` (minúsculas, pontuação → espaço).
|
||||||
|
'RS|sant ana do livramento': "Sant'Ana do Livramento",
|
||||||
|
}
|
||||||
|
|
||||||
|
const titleCaseName = (value) => {
|
||||||
|
const small = new Set(['de', 'da', 'do', 'das', 'dos', 'e'])
|
||||||
|
return value
|
||||||
|
.toLocaleLowerCase('pt-BR')
|
||||||
|
.split(' ')
|
||||||
|
.map((word, index) =>
|
||||||
|
index > 0 && small.has(word)
|
||||||
|
? word
|
||||||
|
: word.replace(/^([a-zà-öø-ÿ])/, (letter) => letter.toLocaleUpperCase('pt-BR')),
|
||||||
|
)
|
||||||
|
.join(' ')
|
||||||
|
}
|
||||||
|
|
||||||
|
const buildNameResolver = (citiesByState, baAliases) => {
|
||||||
|
const canonical = new Map()
|
||||||
|
for (const [uf, cities] of Object.entries(citiesByState)) {
|
||||||
|
for (const name of cities) canonical.set(`${uf}|${normalizeSearchPhrase(name)}`, name)
|
||||||
|
}
|
||||||
|
for (const [variant, name] of baAliases) {
|
||||||
|
canonical.set(`BA|${normalizeSearchPhrase(variant)}`, name)
|
||||||
|
}
|
||||||
|
for (const [key, name] of Object.entries(EXTRA_NAME_ALIASES)) canonical.set(key, name)
|
||||||
|
const stats = { matched: 0, fallback: 0, examples: [] }
|
||||||
|
return {
|
||||||
|
resolve: (uf, tseName) => {
|
||||||
|
const hit = canonical.get(`${uf}|${normalizeSearchPhrase(tseName)}`)
|
||||||
|
if (hit) {
|
||||||
|
stats.matched += 1
|
||||||
|
return hit
|
||||||
|
}
|
||||||
|
stats.fallback += 1
|
||||||
|
if (stats.examples.length < 200) stats.examples.push(`${uf}: ${tseName}`)
|
||||||
|
return titleCaseName(tseName)
|
||||||
|
},
|
||||||
|
stats,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Pipeline
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const auditPatch = (totals, audit) => {
|
||||||
|
const diffs = []
|
||||||
|
for (const [field, expected] of Object.entries(audit)) {
|
||||||
|
const found = totals[field]
|
||||||
|
if (found !== expected)
|
||||||
|
diffs.push(`${field}: esperado ${int.format(expected)}, encontrado ${int.format(found)}`)
|
||||||
|
}
|
||||||
|
return diffs
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const { flags } = parseEqualsFlags(process.argv.slice(2))
|
||||||
|
const selected = new Set(
|
||||||
|
flags.only
|
||||||
|
? String(flags.only)
|
||||||
|
.split(',')
|
||||||
|
.map((uf) => uf.trim().toUpperCase())
|
||||||
|
.filter(Boolean)
|
||||||
|
: POTENTIAL_UFS,
|
||||||
|
)
|
||||||
|
if (selected.size === 0) die('--only está vazio — informe ao menos uma UF (ex.: --only=BA).')
|
||||||
|
for (const uf of selected) if (!POTENTIAL_UFS.includes(uf)) die(`UF fora do Brasil: ${uf}`)
|
||||||
|
|
||||||
|
const runAll = selected.size === POTENTIAL_UFS.length
|
||||||
|
console.log(`[${LABEL}] UFs: ${[...selected].join(', ')}${runAll ? ' (nacional)' : ''}`)
|
||||||
|
|
||||||
|
const { CitiesByState } = await import('../src/lib/cities.ts')
|
||||||
|
const names = buildNameResolver(CitiesByState, MUNICIPALITY_NAME_ALIASES)
|
||||||
|
|
||||||
|
// 1. Downloads (cache em data/tse-2026/, gitignored) + hashes pinados.
|
||||||
|
const downloaded = {}
|
||||||
|
const sourceReceipt = []
|
||||||
|
for (const source of SOURCES) {
|
||||||
|
console.log(`[${LABEL}] garantindo ${source.key}.zip`)
|
||||||
|
const cached = await ensureCachedDownload({
|
||||||
|
label: LABEL,
|
||||||
|
key: source.key,
|
||||||
|
url: source.url,
|
||||||
|
ext: 'zip',
|
||||||
|
cacheDir: CACHE_DIR,
|
||||||
|
expectedSha256: source.sha256,
|
||||||
|
download: downloadZip,
|
||||||
|
})
|
||||||
|
downloaded[source.key] = join(CACHE_DIR, `${source.key}.zip`)
|
||||||
|
sourceReceipt.push({
|
||||||
|
key: source.key,
|
||||||
|
url: cached.url,
|
||||||
|
sha256: cached.hash,
|
||||||
|
bytes: cached.buffer.length,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Aferição por seção (CSV BRASIL): universo de seções + aptos/comp/válidos.
|
||||||
|
const sections = new Map()
|
||||||
|
const tseNames = new Map()
|
||||||
|
const totals = {
|
||||||
|
sections: 0,
|
||||||
|
aptos: 0,
|
||||||
|
comparecimento: 0,
|
||||||
|
nominais: 0,
|
||||||
|
brancos: 0,
|
||||||
|
nulos: 0,
|
||||||
|
lula: 0,
|
||||||
|
flavio: 0,
|
||||||
|
}
|
||||||
|
console.log(`[${LABEL}] lendo aferição por seção (presidente, T1)...`)
|
||||||
|
for await (const row of zipEntryRows(downloaded.detalhe_votacao_secao_2026, (name) =>
|
||||||
|
name.endsWith(DETALHE_ENTRY),
|
||||||
|
)) {
|
||||||
|
if (row.NR_TURNO !== FIRST_TURN || row.CD_CARGO !== PRESIDENT_OFFICE) continue
|
||||||
|
const uf = row.SG_UF
|
||||||
|
if (!selected.has(uf)) continue
|
||||||
|
const key = `${uf}|${row.CD_MUNICIPIO}|${Number(row.NR_ZONA)}|${Number(row.NR_SECAO)}`
|
||||||
|
sections.set(key, [
|
||||||
|
num(row.QT_APTOS),
|
||||||
|
num(row.QT_COMPARECIMENTO),
|
||||||
|
num(row.QT_VOTOS_NOMINAIS),
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
])
|
||||||
|
tseNames.set(`${uf}|${row.CD_MUNICIPIO}`, row.NM_MUNICIPIO)
|
||||||
|
totals.sections += 1
|
||||||
|
totals.aptos += num(row.QT_APTOS)
|
||||||
|
totals.comparecimento += num(row.QT_COMPARECIMENTO)
|
||||||
|
totals.nominais += num(row.QT_VOTOS_NOMINAIS)
|
||||||
|
totals.brancos += num(row.QT_VOTOS_BRANCOS)
|
||||||
|
totals.nulos += num(row.QT_VOTOS_NULOS)
|
||||||
|
}
|
||||||
|
console.log(`[${LABEL}] seções: ${int.format(sections.size)}`)
|
||||||
|
if (sections.size === 0) die('nenhuma seção carregada — confira a UF do --only.')
|
||||||
|
|
||||||
|
// 3. Votos por candidato (arquivo BR inteiro, um passe): Lula/Flávio por seção
|
||||||
|
// + total nacional de todas as candidaturas (auditoria).
|
||||||
|
const candidateVotes = new Map()
|
||||||
|
let missingSections = 0
|
||||||
|
let seenLula = 0
|
||||||
|
let seenFlavio = 0
|
||||||
|
console.log(`[${LABEL}] lendo votos por seção (presidente, T1)...`)
|
||||||
|
for await (const row of zipEntryRows(downloaded.votacao_secao_2026_BR, (name) =>
|
||||||
|
name.endsWith(VOTACAO_ENTRY),
|
||||||
|
)) {
|
||||||
|
if (row.NR_TURNO !== FIRST_TURN || row.CD_CARGO !== PRESIDENT_OFFICE) continue
|
||||||
|
const uf = row.SG_UF
|
||||||
|
if (!selected.has(uf)) continue
|
||||||
|
const number = num(row.NR_VOTAVEL)
|
||||||
|
const votes = num(row.QT_VOTOS)
|
||||||
|
if (number !== BRANCO_VOTAVEL && number !== NULO_VOTAVEL) {
|
||||||
|
candidateVotes.set(number, (candidateVotes.get(number) ?? 0) + votes)
|
||||||
|
}
|
||||||
|
if (number !== LULA_VOTAVEL && number !== FLAVIO_VOTAVEL) continue
|
||||||
|
const key = `${uf}|${row.CD_MUNICIPIO}|${Number(row.NR_ZONA)}|${Number(row.NR_SECAO)}`
|
||||||
|
const section = sections.get(key)
|
||||||
|
if (!section) {
|
||||||
|
missingSections += 1
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (number === LULA_VOTAVEL) {
|
||||||
|
seenLula += 1
|
||||||
|
section[3] += votes
|
||||||
|
totals.lula += votes
|
||||||
|
} else {
|
||||||
|
seenFlavio += 1
|
||||||
|
section[4] += votes
|
||||||
|
totals.flavio += votes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const candidateTotal = [...candidateVotes.values()].reduce((sum, votes) => sum + votes, 0)
|
||||||
|
totals.candidateVotes = candidateTotal
|
||||||
|
totals.candidates = candidateVotes.size
|
||||||
|
|
||||||
|
// 4. Validações estruturais e pinadas (fail-closed).
|
||||||
|
const validation = []
|
||||||
|
const structural = []
|
||||||
|
if (totals.comparecimento !== totals.nominais + totals.brancos + totals.nulos) {
|
||||||
|
structural.push(
|
||||||
|
`comparecimento (${int.format(totals.comparecimento)}) ≠ nominais+brancos+nulos ` +
|
||||||
|
`(${int.format(totals.nominais + totals.brancos + totals.nulos)})`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (totals.candidateVotes !== totals.nominais) {
|
||||||
|
structural.push(
|
||||||
|
`soma das candidaturas (${int.format(totals.candidateVotes)}) ≠ nominais ` +
|
||||||
|
`(${int.format(totals.nominais)})`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (missingSections > 0) {
|
||||||
|
structural.push(`${int.format(missingSections)} votos de seção fora da aferição`)
|
||||||
|
}
|
||||||
|
// Sem os dois votáveis o shard sairia com colunas zeradas em silêncio (as
|
||||||
|
// auditorias pinadas só cobrem BR e BA).
|
||||||
|
if (seenLula === 0 || seenFlavio === 0) {
|
||||||
|
structural.push(
|
||||||
|
`votáveis ausentes no recorte: Lula=${int.format(seenLula)} linhas, Flávio=${int.format(seenFlavio)} linhas`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
let negative = 0
|
||||||
|
for (const section of sections.values()) {
|
||||||
|
if (section[1] < section[3] + section[4]) negative += 1
|
||||||
|
}
|
||||||
|
if (negative > 0)
|
||||||
|
structural.push(`${int.format(negative)} seções com Lula+Flávio > comparecimento`)
|
||||||
|
if (structural.length > 0) die(`invariantes violadas:\n - ${structural.join('\n - ')}`)
|
||||||
|
validation.push(
|
||||||
|
`estruturais: comparecimento = nominais + brancos + nulos; soma das ${int.format(candidateVotes.size)} candidaturas = nominais; nenhuma seção sem aferição; Lula+Flávio ≤ comparecimento.`,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (runAll) {
|
||||||
|
const diffs = auditPatch(totals, NATIONAL_AUDIT)
|
||||||
|
if (diffs.length > 0) die(`auditoria nacional divergiu:\n - ${diffs.join('\n - ')}`)
|
||||||
|
validation.push(
|
||||||
|
'auditoria nacional pinada confere (seções, aptos, comparecimento, nominais, brancos, nulos, Lula, Flávio, candidaturas).',
|
||||||
|
)
|
||||||
|
} else if (selected.size === 1) {
|
||||||
|
const uf = [...selected][0]
|
||||||
|
const audit = UF_AUDIT[uf]
|
||||||
|
if (audit) {
|
||||||
|
const diffs = auditPatch(totals, audit)
|
||||||
|
if (diffs.length > 0) die(`auditoria de ${uf} divergiu:\n - ${diffs.join('\n - ')}`)
|
||||||
|
validation.push(`auditoria pinada de ${uf} confere.`)
|
||||||
|
} else {
|
||||||
|
validation.push(`sem auditoria pinada para ${uf} — receita gerada sem conferência.`)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
validation.push(
|
||||||
|
'recorte com mais de uma UF e sem auditoria nacional — receita gerada sem conferência.',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Shards por UF.
|
||||||
|
const byUf = new Map()
|
||||||
|
for (const [key, value] of sections) {
|
||||||
|
const [uf, code, zone, section] = key.split('|')
|
||||||
|
let municipalities = byUf.get(uf)
|
||||||
|
if (!municipalities) {
|
||||||
|
municipalities = new Map()
|
||||||
|
byUf.set(uf, municipalities)
|
||||||
|
}
|
||||||
|
let municipality = municipalities.get(code)
|
||||||
|
if (!municipality) {
|
||||||
|
municipality = {
|
||||||
|
code: Number(code),
|
||||||
|
name: names.resolve(uf, tseNames.get(`${uf}|${code}`) ?? String(code)),
|
||||||
|
sections: [],
|
||||||
|
}
|
||||||
|
municipalities.set(code, municipality)
|
||||||
|
}
|
||||||
|
municipality.sections.push([
|
||||||
|
Number(zone),
|
||||||
|
Number(section),
|
||||||
|
value[0],
|
||||||
|
value[1],
|
||||||
|
value[3],
|
||||||
|
value[4],
|
||||||
|
value[2],
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
const ufReceipt = {}
|
||||||
|
let totalBytes = 0
|
||||||
|
for (const uf of selected) {
|
||||||
|
const municipalities = [...(byUf.get(uf)?.values() ?? [])]
|
||||||
|
for (const municipality of municipalities) {
|
||||||
|
municipality.sections.sort((left, right) => left[0] - right[0] || left[1] - right[1])
|
||||||
|
}
|
||||||
|
municipalities.sort((left, right) => left.code - right.code)
|
||||||
|
const body = JSON.stringify({ version: POTENTIAL_SHARD_VERSION, uf, municipalities })
|
||||||
|
await writeRepoFile({
|
||||||
|
label: LABEL,
|
||||||
|
root: ROOT,
|
||||||
|
relativePath: `${OUTPUT_DIR}/${uf}.json`,
|
||||||
|
body,
|
||||||
|
})
|
||||||
|
const bytes = Buffer.byteLength(body)
|
||||||
|
totalBytes += bytes
|
||||||
|
ufReceipt[uf] = {
|
||||||
|
file: `${uf}.json`,
|
||||||
|
sha256: sha256Hex(Buffer.from(body)),
|
||||||
|
bytes,
|
||||||
|
municipalities: municipalities.length,
|
||||||
|
sections: municipalities.reduce((sum, item) => sum + item.sections.length, 0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. Manifest: o run nacional grava o recibo completo; um `--only` só
|
||||||
|
// atualiza as UFs regravadas dentro do recibo existente (totais e auditoria
|
||||||
|
// nacionais continuam os do run completo).
|
||||||
|
const manifestPath = `${OUTPUT_DIR}/manifest.json`
|
||||||
|
let manifest
|
||||||
|
if (runAll) {
|
||||||
|
manifest = {
|
||||||
|
version: POTENTIAL_SHARD_VERSION,
|
||||||
|
generatedAt: new Date().toISOString(),
|
||||||
|
scope: 'BR',
|
||||||
|
sources: sourceReceipt,
|
||||||
|
candidates: {
|
||||||
|
lula: LULA_VOTAVEL,
|
||||||
|
flavio: FLAVIO_VOTAVEL,
|
||||||
|
count: candidateVotes.size,
|
||||||
|
votesByNumber: Object.fromEntries(
|
||||||
|
[...candidateVotes].sort((left, right) => left[0] - right[0]),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
totals,
|
||||||
|
ufs: ufReceipt,
|
||||||
|
validation,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const existing = await readFile(join(ROOT, manifestPath), 'utf8').catch(() => null)
|
||||||
|
if (!existing) die(`manifest nacional ausente — rode o run completo antes de um --only`)
|
||||||
|
manifest = JSON.parse(existing)
|
||||||
|
manifest.generatedAt = new Date().toISOString()
|
||||||
|
manifest.ufs = { ...manifest.ufs, ...ufReceipt }
|
||||||
|
manifest.validation = [
|
||||||
|
...manifest.validation,
|
||||||
|
`recorte ${[...selected].join(', ')} regravado em ${new Date().toISOString()} — ${validation.join(' ')}`,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
await writeRepoFile({
|
||||||
|
label: LABEL,
|
||||||
|
root: ROOT,
|
||||||
|
relativePath: manifestPath,
|
||||||
|
body: `${JSON.stringify(manifest, null, 2)}\n`,
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`[${LABEL}] pronto: ${Object.keys(ufReceipt).length} UFs, ${int.format(sections.size)} seções, ` +
|
||||||
|
`${(totalBytes / 1024 / 1024).toFixed(1)} MB de dados; nomes canônicos=${int.format(names.stats.matched)} ` +
|
||||||
|
`fallback=${int.format(names.stats.fallback)}${names.stats.examples.length ? ` (ex.: ${names.stats.examples.join('; ')})` : ''}`,
|
||||||
|
)
|
||||||
|
console.log(
|
||||||
|
`[${LABEL}] totais: Lula=${int.format(totals.lula)} Flávio=${int.format(totals.flavio)} nominais=${int.format(totals.nominais)}`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
await main()
|
||||||
Loading…
Reference in a new issue