An open-source platform for visualizing and analyzing how Bulgaria is governed — starting with every parliamentary election since 2005 (down to the ~13,000 polling sections), and extending to the parliament and its MPs, their business interests and declared assets, the state budget, public procurement, EU funds, local councils and taxes, polling accuracy, campaign financing, and the everyday cost of living. Live at electionsbg.com.
The app started as election results visualization and has grown to cover the broader political picture. The current feature surface:
- Elections — results and turnout from settlement up to national level for every parliamentary election since 2005, plus drill-down to the ~13,000 polling sections and side-by-side comparison across cycles.
- Local elections (общински избори) — a parallel dashboard tree for the regular local cycles (mi2023, mi2019, minr2015, mipvr2011):
/local/:cycle(national mayors-control choropleth + council vote share + top regions),/local/:cycle/region/:oblast(per-oblast municipalities map + mayors-won/council-seats rankings + município directory),/local/:cycle/:obshtinaCode(the per-município dashboard — a section map (one dot per polling station, coloured by the leading council party) beside compact mayor-candidate and council-party tiles, a council-composition hemicycle, kmetstvo + район mayors, top councillors by preference, and — for every regular cycle 2011-2023 — a top-sections leaderboard plus a searchable per-polling-station council table with turnout), with the full mayor R1/R2 ranking and the full council party-by-party breakdown (expandable elected-councillor lists) on dedicated/local/:cycle/:obshtinaCode/mayorand/local/:cycle/:obshtinaCode/councilpages, and/local/:cycle/settlement/:ekatte(the village-mayor / кметство race for that settlement plus the parent município context). The settlement and район dashboards also carry a cross-cycle trend tile — how the place voted for the council (party share, line chart) and the mayoral race (winner per cycle) across the regular cycles, aggregated from its own polling stations (--local-place-trends→data/local_place_trends/); for a Sofia район this is the район's own council vote, which the city-wide council bundle can't show. The per-município page derives mayor + council mandates from CIK's HTML result pages; the section-level votes and turnout come from CIK's CSV bundle (votes.txt/protocols.txt, downloaded and cp866-extracted by the--local-csvstep), which also completes the council vote share where the HTML was partial — 2015's summary is mandates-only, and 2011's HTML table omits also-ran parties. The same section shards feed an estimated council vote-flow (Sankey) on the country + region dashboards — a Goodman ecological-regression + RAS transition matrix between consecutive cycles, reusing the parliamentary vote-flow engine and served fromdata/transitions_local/; it is produced by the flag-gated--local-flowsstep (a manual derived recompute run after a new cycle is ingested, mirroring the parliamentary--flows). Time-anchored to the selected parliamentary election (shows the local government in effect as of that date), with the extraordinary elections between cycles — partial mayors, new mayors, and full council re-elections, going back to 2012 (~380 events) — surfaced chronologically via/local/chmiand contextually on each affected município (each mayor by-election carrying its exact official turnout and its own per-section mayor map, parsed from ЦИК's «Числови данни» protocol HTML rather than the votes-only results page), plus the/sverkaofficials-vs-CIK reconciliation. Parliamentary geography pages carry a summary tile that links through to the matching local dashboard. The cycle/region/município pages are prerendered (BG + EN<meta>+ JSON-LD + OG cards) and listed in the sitemap; settlement and the per-município mayor/council sub-pages are SPA-only and canonicalised to the município. - Anomaly reports — concentration, turnout outliers, top gainers/losers, invalid ballots, recount-flagged sections, voting-machine flash-memory corrections (SUEMG), risk neighborhoods.
- Election integrity & risk analysis (
/risk-analysis) — a composite Election Risk Index per cycle; a per-section 0–100 risk-screening score from seven independent signals (recount delta, flash-memory mismatch, invalid ballots, day-of additions, vote concentration, peer-section outlier, and cross-election swing vs. the same section last cycle); per-party Benford 2BL tests; and a geographic risk-cluster map that groups physically adjacent same-party elevated-risk sections — the spatial fingerprint of a controlled / corporate vote. - Parties — per-party regional/municipal/settlement performance, candidate preferences, vote-flow timelines, head-to-head comparisons, AI-generated campaign retrospects.
- Candidates — profile pages with regional results, preference votes, donations, declared assets, and business connections.
- Parliament — current and past MPs with bios, photos, declared assets, vehicles, and a graph of their business connections (companies they own or manage, plus shared officers and addresses). Each MP profile opens with a four-metric scorecard (party loyalty, attendance, declared net worth, contracts to connected firms) carrying a parliament-wide rank or median for context.
- Public officials — non-MP property and interest declarations for the executive branch (cabinet members, deputy ministers, state and executive agency heads, regional governors). Sourced from the same Court of Audit register that covers MPs (
register.cacbg.bg). Sortable ranking at/officials/assetswith filter chips per role, and a per-official profile page with assets summary, year-over-year delta, and the full declarations timeline. The municipal tier from the same register — mayors, deputy-mayors, municipal-council chairs, ~4,800 municipal councillors and chief architects (~6,400 declarations) — is also ingested into its own scope underdata/officials/municipal/, and surfaced as a Local government section on every/settlement/{обshtina}page (mayor card, role-count strip, full roster with collapsible expansion). The data also stages the cross-MP connections graph. Magistrates are out of this register's scope — they file with the Инспекторат към ВСС rather than the Сметна палата, and their filings are indexed separately indata/judiciary/declarations.json(the index of what was filed and when, not the contents). - Unified person profile (
/person/{slug}) — a single identity page that resolves every people dataset — MPs, CIK candidates (all 10 elections with data since 2014), local mayors & councillors (every mi/chmi cycle), ЕРИК donors, executive & municipal officials, magistrates (ИВСС), Commerce-Registry company officers/owners and ЮЛНЦ (NGO) board members — to ONE stableperson_idin Postgres, so a politician, the companies they own or manage, the public money those companies won (Σ post-annex contract value), their civic board seats (thengofacet, distinct from business interests), their свързани лица (public co-officers of a shared company or NGO board, association-noise-guarded), any official OFAC/EU sanctions (cited, e.g. Delyan Peevski → US Global Magnitsky), and any ДС/COMDOS affiliation (Комисия по досиетата findings of established affiliation to State Security, cited to their решение № + date, e.g. Ahmed Dogan → агент „Сергей", реш. № 14/2007) all appear on one page. Built by a defamation-conscious deterministic resolver (scripts/person/resolve_persons.ts, plandocs/plans/person-identity-v1.md): a zero-false-public-merge invariant, a Bulgarian 2-vs-3-part name matcher with a patronymic-conflict veto, an aggressive-merge review queue for ambiguous same-name people, and TR bridging only via a shared declared company or a globally-unique name. The same layer backs thepersonProfileandpersonConnectionsAI chat tools. Kept fresh by theupdate-personsskill (a pure re-derivation) whenever an upstream people source changes; the sanctions register (data/person/sanctions.json) is hand-curated from the OFAC SDN list, and the ДС/COMDOS register (data/person/ds.json) is hand-curated from the Комисия по досиетата решения — both attach a finding only via a stable id (and, for ДС, an exact birth-date match) so they never implicate a same-named person. - Unified person profile (
/person/{slug}) — a single identity page that resolves every people dataset — MPs, CIK candidates (all 10 elections with data since 2014), local mayors & councillors (every mi/chmi cycle), ЕРИК donors, executive & municipal officials, magistrates (ИВСС), Commerce-Registry company officers/owners and ЮЛНЦ (NGO) board members — to ONE stableperson_idin Postgres, so a politician, the companies they own or manage, the public money those companies won (Σ post-annex contract value), their civic board seats (thengofacet, distinct from business interests), their свързани лица (public co-officers of a shared company or NGO board, association-noise-guarded), any official OFAC/EU sanctions (cited, e.g. Delyan Peevski → US Global Magnitsky), and any seats on the independent / regulatory bodies (theregulator"кой решава" facet — Конституционен съд, Сметна палата, КФН, БНБ, СЕМ, КЗК, Омбудсман) all appear on one page. Built by a defamation-conscious deterministic resolver (scripts/person/resolve_persons.ts, plandocs/plans/person-identity-v1.md): a zero-false-public-merge invariant, a Bulgarian 2-vs-3-part name matcher with a patronymic-conflict veto, an aggressive-merge review queue for ambiguous same-name people, and TR bridging only via a shared declared company or a globally-unique name. The same layer backs thepersonProfileandpersonConnectionsAI chat tools. Kept fresh by theupdate-personsskill (a pure re-derivation) whenever an upstream people source changes; the sanctions register (data/person/sanctions.json) is hand-curated from the OFAC SDN list and attaches a designation only via a stable id so it never implicates a same-named person. - Governments — coalition compositions and ministerial line-ups by parliamentary term.
- Polls — pre-election polls scraped from Wikipedia, accuracy metrics per agency, and editorial narratives.
- Demographics — Census 2021 overlays (age, education, ethnicity) at country/region/municipality level; annual sub-national indicators per municipality (registered unemployment, state-matura DZI scores, NSI population change, plus natural population change and net migration from NSI vital statistics) with year-over-year deltas and choropleths; an oblast layer (Eurostat GDP/population/migration, recorded-theft rate and active-enterprise density, plus long-term-unemployment share from Агенция по заетостта and three НСИ open-data series — cumulative FDI per capita, museum visits and hospital beds); quarterly ГРАО settlement-level registered population (permanent + current address) surfaced on settlement pages; and a municipality-grain vote ↔ demographics correlation — a cross-tab scatter and per-party "demographic fingerprint" computed across all 265 municipalities.
- Campaign financing — donors, income, expenses, donor leaderboards, parsed from the Court of Audit's Smetna Palata register. Plus a year-by-year annual-report filing-status catalogue (
/financing/annual-reports): which parties filed their statutory annual financial report on time, late, with deficiencies, or not at all — 2011 onward, scraped from the Court of Audit's gfopp register. - Public procurement — fortnightly OCDS contract bundles from the АОП (Агенция за обществени поръчки) feed on data.egov.bg, aggregated per contractor / awarding body / month, with a cross-reference layer that surfaces awards going to companies owned or managed by sitting MPs and non-MP officials (cabinet, agency heads, governors, mayors, deputy-mayors, councillors). Each contract row carries an explainable Corruption Risk Index (0–100 = the share of the applicable red-flag checks that fired, in the Fazekas/Government-Transparency-Institute tradition) computed by a shared pure scorer (
src/data/procurement/computeProcurementRisk.ts): contractor on the АОП "Стопански субекти с нарушения" debarred register, contractor MP-tied or official-tied (mayor / councillor / minister / governor / agency head, viaderived/pep_connected.json— a declared stake the Commerce Registry confirms, or a registry role on a folded name the registry says belongs to exactly one human; a shared name is refused rather than graded), awarder concentration ≥30% of its lifetime spending, post-award amendment, single bidder (read fromrelease.bids.statistics[]— the field the OCDS export actually populates, with the corpus-wide back-fill run viaprocurement:ingest --renormalize— and gated against the per-CPV-division single-bid baseline inderived/cpv_competition.jsonso structurally single-bid markets don't false-positive), non-open procedure, and short tender window (<14 days). Sortable column + per-signal chip tooltips + a "N of M checks failed" meter on the contract page, which also offers the raw record as a JSON download (mirrors SIGMA's per-contract export). Money-flow is explorable several ways: the national money-flow Sankey at/procurement/flows(awarder → company → connected MP or official — the/procurementlanding embeds a trimmedderived/flow.jsonpreview that links out to the fullderived/flow_full.jsongraph here), a per-entity buyer→supplier flow + portfolio treemap on every/company/:eikand/awarder/:eik(each carrying an institution-type badge, an average-contract figure and a share-of-total column, mirroring SIGMA's entity views), a company-name search on the/procurementdashboard that jumps from any of the ~26k contractors (by name, Cyrillic or Latin-transliterated, lazy-loaded on first focus fromderived/contractors_search.jsonso it never burdens election-first visitors) straight to its/company/:eikpage, a public money scanner (/procurement/people) that searches the political class — sitting/former MPs and non-MP officials (cabinet, agency heads, governors, mayors, deputy-mayors, councillors) — and surfaces the procurement reachable through their connected companies (MP rows drill into/candidate/mp-:id/procurement, official rows into a procurement section on/officials/:slug, backed byderived/pep-by-slug/), a red-flag feed (/procurement/flags) of single-supplier concentration + active debarments, and three side-by-side per-oblast choropleths (total / per-capita / average contract value, shown together rather than behind a metric toggle — click any oblast to filter the table below) on/procurement/by-settlement. The flags page + scanner read slim pre-selected derived indices (derived/risk_feed.json,derived/person_procurement_index.json— one row per person taggedkind: mp|official) rather than the full corpus. Readers can follow any buyer, supplier, politician, settlement or individual contract from a star on its row or page (on-site, localStorage-backed, no account);/procurement/watchlistis then a live monitoring dashboard — each followed entity renders a card with its total awarded, contract count, latest contract and top counterparty, and the page surfaces new activity since you last looked (computed client-side by diffing each entity's live rollup against a per-item snapshot) with an unread badge on the section nav. The badge + overview digest read a cached count so they cost no network on other pages; only the watchlist page itself fetches the followed entities' rollups. Local procurement also surfaces on each My-Area / Governance place dashboard viaMyAreaProcurementTile, and connected officials on each/company/:eik. Procurement by settlement (added 2026) groups each local-tier awarder's contracts by buyer HQ — schools, hospitals, municipalities, universities, regional government offices, forestry districts and local utilities pin to their EKATTE; central ministries, state agencies and nationally-operating state companies aggregate into a separate "national procurement" rollup so their Sofia HQ doesn't pollute the city's spending bubble. The buyer-HQ → EKATTE resolver is postal-primary (data/ekatte_index.jsoncarries 5,267 settlements with postal codes including Sofia EKATTE 68134) and 99.9%-accurate on the 2026 OCDS sample; the tier classifier uses name heuristics plus a curated EIK override table (scripts/procurement/awarder_tier.ts) for the long tail (АЕЦ Козлодуй, БДЖ, НЕК etc. ride national). Surfaced as/procurement/by-settlement(a paginated, sortable, CSV-exportable national table — with an average-contract column and a single-buyer concentration flag for settlements whose entire total rides on one buyer — plus the national-procurement card),/procurement/settlement/{ekatte}(per-EKATTE buyer breakdown + biggest contracts), and aSettlementProcurementTileon each existing settlement detail page. Sector & contract browsing (added 2026): each contract is enriched with its CPV sector, procedure type, realised bid count and EU-funding flag via a content-join against the ЦАИС ЕОП flat feed (scripts/procurement/eop_field_map.ts— 2020–2026 CPV coverage ~98%, map-safe: the join only adds per-contract fields, never the awarder geo that drives the maps), which powers a "Какво купува / В кои сектори печели" sector + procedure + EU-share breakdown tile on every/company/:eikand/awarder/:eik, and a faceted/procurement/contractsbrowser (year-sharded slim index; filter by sector / procedure / value / EU funding; an inline per-row Corruption Risk Index with a "flagged only" filter and one-click sort-by-risk; a summary strip of count / total € / %EU / %flagged; a parliament-vs-all-years scope toggle matching the rest of the module, with cross-year text search in "all years"; full CPV codes; sortable + CSV + URL-encoded). Every row deep-links to its contract page —/procurement/contract/:keynow resolves for every contract via a prefix-sharded by-id detail store (contracts/by-id/shard/<3-hex>.json, ~70 rows/shard), not just the top-N + MP-tied single-file set — and carries a follow star; the contract page adds the procedure rationale, tender window and EU-programme rows. Tender-stage procedures (/procurement/tenders,/tenders/:unp) carry a lifecycle stepper, per-lot detail, forecast-vs-signed and a 0–100 transparency (field-completeness) score; each buyer/company page shows an A–F procurement risk grade (Hlídač-státu-style: single-bid, direct-award, supplier-concentration and political-connection shares), and/procurementranks the riskiest buyers. КЗК appeals (Комисия за защита на конкуренцията,reg.cpc.bg) are joined to the procedure by exact УНП: every complaint (~7,800 across 2020–2026) carries its status and — where the decision resolves 1:1 — its merits outcome (upheld / rejected), surfaced as "under appeal" / "suspended" chips + an appeals tile on/tenders/:unpand a "Recent appeals" feed on/procurement. The КЗК ingest (scripts/procurement/kzk_appeals.ts) is a manual headed-Playwright crawl (the register is geo-gated) — an appeal is a review of the procedure, not proof of wrongdoing. - EU funds — the ИСУН 2020 public registers (
2020.eufunds.bg). The beneficiary register lists ~53,000 organisations that have signed an EU-funds contract under the 2014-2020, 2021-2027 and Recovery-Plan programmes, with funds contracted and funds actually paid, aggregated by organisation type and by public-/private-law form (most of the money flows through state bodies, not private firms). Ingested intodata/funds/, with an MP cross-reference layer (derived/mp_connected.json) that surfaces EU-funds beneficiaries owned or managed by sitting MPs — the same EIK-keyed join the procurement feature uses. The companion project register carries the same disbursements at contract grain (~80,700 rows, €43.5 B total / €16.8 B paid) with a per-contract implementation location (Местонахождение), resolved againstdata/settlements.json+data/municipalities.jsoninto EKATTE / муни / NUTS-region / national buckets so EU-funds spending can be attributed to specific settlements and municipalities. Ingested intodata/funds/projects/(per-EKATTE, per-муни, per-EIK, per-programme shards; ~99% of contracts resolve to a single settlement or муни). Surfaced on/fundsas a Projects section below the beneficiary rollup: a per-муни choropleth map (toggleable between Total € / Per-capita / Disbursement %), a status-mix tile (Completed / In-progress / Signed / Terminated with disbursement rates per bucket), a top-10 programmes leaderboard with red/amber/green absorption badges, and a location-kind histogram. The per-муни contract rollup also backs the EU-funds tile on the My-Area (governance) dashboard and the AIplaceEuProjectstool — totals, disbursement rate, top contracts (contracts spanning several municipalities are split evenly, so a place's money maps once), and a per-capita € rank within the oblast, computed against Census 2021 population. - Farm subsidies (земеделски субсидии) — every CAP payment made by ДФ „Земеделие" (the accredited paying agency), ~€11 bn across 2015–2025 to ~16.7 k legal-entity recipients plus hundreds of thousands of individuals. Two sources feed one Postgres-only pack (no static JSON — the ingest writes the
agri_subsidiesper-payment table +agri_payloadsprecomputed jsonb directly): the data.egov.bg org-56 open-data CSVs for FY2015–2023 (EIK-keyed for legal entities — the join that links every recipient to its/company/:eik, procurement and EU-funds record) and, for the current rolling years egov hasn't published yet (FY2024/2025), the ДФЗ СЕУ interactive register (seu.dfz.bg) via a one-download-per-year headless-Playwright CSV export (npm run agri:seu); the СЕУ years carry no EIK column, so the ingest recovers it by exact name-match against the egov entities (recurring recipients relink; genuinely new entrants stay name-only, like individuals). Amounts are BGN in the source, converted to EUR at ingest. Surfaced on/subsidies(dashboard: headline total, a concentration tier bar — top-10/100/1000 marginal shares — showing that ~1 000 firms take just over half the legal-entity money, a by-scheme breakdown with full-name tooltips, an oblast choropleth with hover detail, an all-years payment trend, and a scope-aware top-recipients table),/subsidies/browse(a server-sideDbDataTableover ~2.5 M payment rows), and/farm/:eik(a per-recipient page — yearly trajectory, scheme mix, scoped payment browse — linking across to/company/:eikwhere the same entity's subsidies sit beside its procurement + EU-funds record, the cross-program money map). Every recipient's/company/:eikalso carries a "Земеделски субсидии" tile; the paying agency itself (EIK 121100421) is excluded from the recipient analytics (its own rows are техническа помощ / публично складиране, not farm money received) and instead links out to the pack. The whole surface shares the procurement?pscopetime scope (ns→ the latest financial year for this annual data,all, ory:<year>), the year picker restricted to the CAP financial years actually present. Ingest:npm run agri:seu && npm run agri:ingest; watcherdfz_subsidies+ theupdate-agriskill keep it fresh. - Education (Образование / МОН) — a per-school view of the state-matura (ДЗИ) results МОН publishes on data.egov.bg, deliberately refusing a single "grade".
/educationis the explorer (national trend, a school-finder map, a socioeconomic-context scatter, best/worst and an oblast breakdown);/school/:idis the report card, which shows the matura level (national percentile, БЕЛ + maths) and trend separately, plus two honest context signals — "постижение спрямо средата" (the score set against the community's socioeconomic context: a per-município z-scored index of tertiary-education share, low-education share and unemployment from Census 2021, with an OLS expectation line and a над/близо/под-очакваното verdict) and true 7→12 value-added (the ДЗИ cohort's own 7th-grade НВО intake as prior attainment — ДЗИ year Y paired with НВО year Y−5 — again as an OLS residual). Every figure carries its cohort size, with small-N suppression below 10 graduates applied on every surface (the map dot greys, the percentile and averages are withheld). The МОН sector pack on/awarder/000695114adds textbook-market concentration — the CPV-22112 procurement slice (~€51M) with an HHI gauge (DOJ bands), CR-4/top-2 ratios and per-publisher-group shares (Просвета's 3 EIKs, Klett's Анубис+Булвест rolled up), framed with the чл.79 single-bid-by-law caveat so direct award isn't misread as a red flag. Schools are resolved to their ЕИК from the procurement awarder corpus, so a matched school's/company/:eik(or/awarder/:eik) surfaces a "това е училище" tile linking to its report card. Served from Postgres (migration 055):schools/school_scores/school_contexttables plus a precomputeddirectorypayload (the SES + value-added regressions computed once in the loader, ~150 KB vs the 1.25 MB raw index) that/education+/school/:idread via/api/db/education-payload; the small My-Area schools tile + the textbook-market tile stay GCS-served. Pipeline:fetch_nvo.ts→build_index.ts→gen_school_context.ts→gen_textbook_market.ts→db:load:schools:pg, kept fresh by theindicators_mon_dzi+indicators_mon_nvowatchers and theupdate-schoolsskill. AschoolMaturaAI chat tool answers per-school matura questions from the same payload. - Water (Води / ВиК) — a consolidated view of the water sector's public procurement at
/water. Български ВиК холдинг (EIK 206086428) is the principal of ~26 regional ВиК operators, and the parent procures almost nothing, so the water sector pack (on/waterand the/awarder/206086428awarder page) rolls up the whole group's contracts — per operator (each linking to its own awarder page) and by operating function (network construction, water supply, sewerage & treatment, pipes/pumps, electricity), classified from CPV divisions (src/lib/vikAttributes.ts), scope-aware via the shared?pscope. The operator EIK universe is a hand-curated crosswalk resolved from the procurement corpus (src/lib/vikReferenceData.ts; holding membership is best-effort, pending reconciliation withvikholding.bg). A sector browse pack (?sector=wateron/procurement/contractsand/procurement/tenders) restricts the shared browser to the sector's EIK-set and mounts the group summary above the table — the same seam generalises to roads/НОИ/НЗОК/agri/judiciary (SECTOR_BROWSE_PACKSinsectorPacks.tsx, keyed onawarder_eik/buyer_eikIN-filters). A riverbed-cleaning dataset (data/water/flood_maintenance.json, generated byscripts/water/write_flood_maintenance.tsfrom the corpus — ~€322M across ~686 contracts and ~184 awarders, split between municipalities, regional governors and „Напоителни системи") surfaces the flood-maintenance spend behind the deadly Царево 2023 / Свети Влас 2024 floods (spend only — the flood-risk map РЗПРН and КЕВР loss/tariff indicators are later phases). AriverbedCleaningAI chat tool answers from the same artifact, which regenerates with the procurement corpus (no separate watcher). - State administration (Държавна администрация) — the state administration as an institution at
/sector/administration, a bespoke institution-first dashboard. Size & workforce from the annual Доклад за състоянието на администрацията (data/budget/personnel.json): щатна численост (145,623), 581 structures, vacancies, per-ministry cost-per-FTE, and the administration-vs-population divergence (admin +10% while population −10% over the decade). Cost from COFOG GF01 (general public services, BG 2.9% of GDP vs the EU average — 25th of 26). Service quality parsed from the Доклад's административно обслужване section (data/administration/service_quality.json) — signals volume (4,341→6,190) + satisfaction-measurement compliance (таен клиент / one-stop-shop are prose-only and deliberately not extracted). Digital: e-government use BG 26.6% vs the EU 57.6% (near the bottom of the Union) from Eurostatisoc_ciegi_ac(data/administration/egov.json), plus a citizen digital-skills companion stat (Eurostatisoc_sk_dskl_i21,data/administration/digital_skills.json) — at least basic digital skills BG 38% vs the EU 60%, 26th of 27 — the demand e-government assumes. Digital skills is a human-capital measure, so the full band lives on/indicators/society(the five DigComp competence areas, the 2021/2023/2025 composition, and — dead last in the EU — young people 16-24 at 53% vs the EU 75%, rendered on a reusable EU choroplethEuChoroplethMap, geometrydata/maps/europe/countries.json, that any Eurostat vs-EU indicator can reuse once it ingests the full 27-member cross-section);/sector/administrationkeeps only the companion stat, linking there. The administrative-services register — ~2,668 services from the Административен регистър (ИИСДА), scraped via the register's xajax endpoint and served from Postgres (migration 068admin_services+ a server-sideDbDataTableat/sector/administration/services, searchable by name, faceted by provider tier;services_overview.jsonholds the tile aggregate). The e-government procurement money — the МЕУ + ИА ИЕУ + ДАЕУ group (EIKs180680495/180742160/177098809) folded server-side byawarder_group_model. The page fetches an ~8 KB precomputeddata/administration/context.json(baked from personnel + macro + cofog) instead of the ~324 KB of shared files those numbers would otherwise cost. Kept fresh by theiisda_services+eurostat_egov+eurostat_digital_skills+iisda_dokladwatchers and theupdate-administrationskill;administrationOverviewanddigitalSkillsAI chat tools answer from the same artifacts. - Security / Interior (Сигурност / МВР) — a consolidated view of the Ministry of Interior's public procurement at
/sector/security(sector idsecurity). МВР is a ~75-EIK security-cluster group (~€1.9bn contracted: ГД Гранична полиция, ГД Национална полиция, 28 ОДМВР, ГДПБЗН + РДПБЗН fire directorates, the Медицински институт health confound, ДУССД logistics, ДКИС), rolled up server-side byawarder_group_model. The МВР pack (MvrPack, on/sector/security+ the/awarder/000695235page) leads with the iceberg — МВР spends ~€2.1bn/yr but ~90% is salaries and part of its security buys are classified (ЗОП Част четвърта / чл. 149 / чл. 13), so visible open procurement (~6% of one year's budget) is the tip — then what МВР buys by function (IT/surveillance €407M, vehicles, fuel at 58% single-bid), the biggest contracts (award-level, served by theawarder-group-top-contractsPG route — ID documents €129M, license plates €44.7M · 1-bid, border-surveillance €35.8M · EU), contractor HHI, a per-unit single-bid competition heatmap, a per-oblast €/capita choropleth (OblastChoropleth), and the security-exemption transparency gap. Universe-segmentable (SECURITY_UNIVERSES) so the Медицински институт doesn't read as "МВР buys medicines"; scope-aware via?pscope. A?sector=securitybrowse pack restricts/procurement/contracts|tendersto the group. Buyer EIK universe hand-curated insrc/lib/securityReferenceData.ts(an EIK allowlist, never a name/1290*-prefix sweep — that would misattribute €255M of Ministry-of-Justice prisons/court-guard spend). Outcomes (data/security/road_safety.json, Eurostatsdg_11_40, watchereurostat_road_safety): a road-death trend (708 peak 2015 → 478 in 2024, −32%) paired with МВР vehicle procurement, plus a per-oblast spend-vs-theft scatter (fromdata/regional.json) — honestly framed as context, not causation. AsecurityRoadSafetyAI chat tool answers from the same artifact. - Transport (Транспорт) — the state transport group's money at
/sector/transport(sector idtransport). An 11-EIK group (~€5.9bn contracted: МТС, НКЖИ rail infrastructure, БДЖ passenger + freight, Port Infrastructure, and the maritime/aviation/rail/road-safety regulators — rolled up byawarder_group_model; АПИ road-building is a separateroadssector, cross-linked, not folded). The transport pack (TransportPack) leads with an infrastructure map (the physical assets named in contract titles — funded rail sections drawn as lines between the two towns they name, ports/stations/junctions as typed points — served bytransport_project_map; ~73 sections + ~72 sites), then spend by mode (rail 58%), the EU-peer transport %GDP (BG 3.4%, #6 in the EU, COFOG GF04.5), the rail subsidy per passenger (€5.55/pax in 2025, from the budget law + Eurostat), EU-funds absorption (ИСУН — €4.4bn contracted, 30% paid), what it buys by function, contractor HHI, a single-bid competition heatmap, a road-safety-vs-2030-target tile, and a minimal roads cross-link. Universe-segmentable (rail/maritime/aviation/road); scope-aware via?pscope. A?sector=transportbrowse pack restricts/procurement/contracts|tendersto the group. EIK universe hand-curated insrc/lib/transportReferenceData.ts. AI toolstransportSpending,transportEuFunds,railSubsidy. - Environment (Околна среда / МОСВ) — the last untouched top-level COFOG function (GF05) at
/sector/environment(sector idenvironment), and the one sector where the app measures the outcome — the air. A 27-EIK group (~€227M contracted: the Ministry of Environment and Water, ИАОС the air-monitoring agency — itself nearly the size of the whole ministry, the ПУДООС environment fund, the Rila/Pirin/Central-Balkan national-park directorates, НИМХ, the 4 river-basin directorates and the 16 РИОСВ inspectorates — rolled up byawarder_group_model; forestry/МЗХ and ВиК are separate sectors, cross-linked). The environment pack (EnvironmentPack) leads with an air-station map (one marker per municipality with an ИАОС station, coloured by latest ФПЧ10 vs the 50 µg/m³ EU limit — rendered client-side offdata/air/index.json+ municipality centroids, no server route), then the signature money-vs-outcome tile (air-monitoring procurement + the ModAIRn air grant next to the measured PM10/PM2.5 snapshot — context, not causation, since the air data is a snapshot not a trend), the GF05 EU-peer strip (BG 0.6% of GDP, #17/26, below the EU average), the МОСВ budget bridge, EU-funds absorption by OP code (ОП „Околна среда" 2014-20 ~95% vs Програма „Околна среда" 2021-27 ~18%, fromdata/funds/derived/absorption.json), the recycling-rate-vs-EU-target tile (Eurostatcei_wm011— BG peaked ~35% in 2020 then fell to 16.7% in 2023, 38pp below the 55% 2025 Waste Framework Directive target), what МОСВ buys by function (CPV-classified, coverage disclosed), contractor HHI, and a single-bid competition heatmap. Universe-segmentable (ministry/agency/fund/parks/basin/riosv/meteo); scope-aware via?pscope. A?sector=environmentbrowse pack restricts/procurement/contracts|tendersto the group. EIK universe hand-curated insrc/lib/environmentReferenceData.ts(an EIK allowlist, never a name sweep — that would false-positive on the Шипка park-museum and МЗХ forestry). - Social assistance (Социално подпомагане / МТСП + АСП) — the single biggest slice of the state at
/sector/social(sector idsocial): social protection is ~€15bn = 37% of all government spending (COFOG GF10), the largest and least-visible function. Unlike the procurement-led sector packs this one is inverted — the 6-EIK group (МТСП, АСП, Агенцията по заетостта, ГИТ, АХУ, АКСУ) procures only ~€285M cumulative, ~1% of the МТСП budget — so theSocialPackleads with disbursement + outcomes, not contracts. It opens with the iceberg hero (the sharedPassThroughHero): the €15bn function split into the МТСП/АСП disbursement (~€1.5bn) vs pensions + other (НОИ, cross-linked to the separate/pensionsview), with the group's procurement as a <0.5% sliver. Then the budget by benefit type (МТСП planned expenditure 2018→2025 from the budget law — the disability program's ×7 climb, €145M→€1.05bn, after the 2019 Закон за хората с увреждания and its личната помощ mechanism), the benefits АСП pays households (national/annual from the АСП годишен отчет — детски надбавки, помощи за хора с увреждания, целева помощ за отопление ~357k households/€110M, ГМД — each with recipients × amount and average per recipient; no per-oblast breakdown is published anywhere, so the tiles are national,data/social/benefits.json), and the outcome the money buys: a before/after-transfers poverty dumbbell (Eurostatilc_li10/ilc_li02— Bulgaria's transfers cut poverty by only ~27% vs the EU's ~33%,data/social/poverty_impact.json) beside a value-for-money scatter (social spend %GDP × poverty-reduction effect) and the GF10 EU-peer strip (BG 14.4% of GDP vs the EU 19.6%). Below sit the procurement tiles — what the group buys by function (CPV-classified), contractor HHI, a single-bid competition heatmap — universe-segmentable and scope-aware via?pscope. A?sector=socialbrowse pack restricts/procurement/contracts|tendersto the group; the АСП awarder (EIK121015056, which the corpus mislabels "РДСП Видин") is name-pinned to its canonical name. This also fixes a redundancy — thesocialslot used to duplicate the НОИ pension view, which now reclaims its ownNoiPack. EIK universe hand-curated insrc/lib/socialReferenceData.ts. Kept fresh by theasp_benefitswatcher (a new annual report) + theeurostatSILC releases and theupdate-socialskill (a curated +pdftotext-verified benefits series); AI toolssocialSpending,socialBenefits,socialPovertyImpact. - Regional development (Регионално развитие / МРРБ) — the pass-through ministry at
/sector/regional(sector idregional): МРРБ directs ~€1.06bn/yr but procures only ~€100M through its own tenders — the rest leaves as capital transfers to municipalities and EU-cohesion co-financing, so theRegionalPackis inverted like the social pack. A 31-EIK group (the ministry, the cadastre agency АГКК, the building-control directorate ДНСК and the 28 областни администрации — the per-oblast backbone — rolled up byawarder_group_model; АПИ roads (~€6.3bn, ~63× the group) and ВиК water are separate sectors, cross-linked, never folded). It opens with the pass-through hero (the €1.06bn budget vs the thin ~€100M procured slice,data-og="regional-hero"), then the flagship cohesion absorption burn-down — ОП „Региони в растеж" 2014-20 (~96%, closed) vs Програма „Развитие на регионите" 2021-27 (~20%), against the 31 Dec 2029 eligibility end (Art. 63(2) of Reg. (EU) 2021/1060) plus the annual n+3 tranche rule (Art. 105, commitments 2021-2026 — money is forfeited every year, not once at the end; the 2027 tranche settles at closure) (served from Postgresfund_payloads(kind=absorption) viauseFundsAbsorption, by OP code — never the staticdata/funds/derived/absorption.json, whichbucket:syncno longer maintains) — the ИСУН-per-oblast choropleth (all-funds, per-capita/total toggle,OblastChoropleth, served from Postgresfund_payloads(kind=muni-map) viauseFundsMuniMap, folded to the 28 oblasts; each contract is pinned to its declared place of implementation and nationally-scoped ones are held out of the map entirely), the convergence scatter ("do the poorest oblasts get more €/capita?" — GDP/capita fromdata/regional.jsonvs absorbed €/capita, median quadrants, Sofia dropped — the tile joining money to the outcome thatregionalprofiles.bgnever links), the GF06 EU-peer strip (BG 1.0% of GDP, #5/26, above the EU average — the capital-transfer signature), the budget bridge, what МРРБ buys by function (CPV-classified), contractor HHI, a single-bid competition heatmap and the roads/water cross-link. Universe-segmentable (ministry/cadastre/control/governors); scope-aware via?pscope. A?sector=regionalbrowse pack restricts/procurement/contracts|tendersto the group. EIK universe hand-curated insrc/lib/regionalReferenceData.ts(an EIK allowlist, never a name sweep — that would false-positive on the РЗИ/РДПБЗН/РДГ/РИОСВ „регионална дирекция" of other ministries and the АСП РДСП). Renders off the existing corpus + already-ingested cohesion/budget/COFOG/NUTS3 assets — no new ingest (spine watched byupdate-funds/update-budget/update-macro/update-regional); AI toolsmrrbSpending,cohesionAbsorption,regionalInvestment. - State budget — consolidated execution time series from the data.egov.bg КФП feed, per-ministry appropriations parsed from the State Budget Law (Държавен вестник HTML), and per-ministry "Отчет за изпълнението на програмния бюджет" reports reconciled at admin + program grain (law → amended → executed). Cross-linked to procurement so each spending unit's awards show on its ministry page. The revenue side is itemised by drilling each KFP wedge into its sub-flows: excise + import VAT + customs duties from Агенция "Митници" "Митническа хроника" annual reports (excise by product group — fuels split into diesel/petrol/LPG/natural-gas/kerosene + tobacco + alcohol; customs duties by top-5 country of origin), domestic VAT by КИД-2008 economic sector from the НАП annual report, and personal income tax by income type (employment / freelance / final tax & dividends) plus by employment sector. The two big revenue collectors each also get their own entity dashboard — the revenue-first mirror of the spender packs: a НАП pack on
/awarder/131063188(tax revenue by type — ДДС / ДДФЛ / ЗКПО / акцизи / мита — from the КФП snapshots, the 2024 VAT-by-sector drill, and the EU VAT/PIT tax gap, where Bulgaria collects VAT better than the EU average) and a Митници pack on/awarder/000627597(акцизи / ДДС при внос / мита / глоби composition by year 2022–2025, the 2025 excise product split, and duty by country of origin), which also carries the licensed excise-warehouse register (лицензирани складодържатели и данъчни складове, from the Агенция „Митници" BACIS feed) — the operators ranked by their public-procurement footprint (each linking to its/company/:eik), a per-category split, and a bonded-warehouse count map (one marker per city, badge = how many active данъчни складове sit there, coloured by that count, geolocated at ingest from each warehouse's own BACIS address → settlement centroid with anawarder_seatsEKATTE fallback, ~98% placed), with the full register at/customs/warehouses. The register writesdata/customs/excise_register.json(operators, GCS-served) +data/customs/excise_warehouses.json(the map, Postgres-served viaexcise_warehouses_map, migration 072, over/api/db/excise-warehouses), kept fresh by thecustoms_excise_registerwatcher and theexciseRegisterAI chat tool. A Personnel layer overlays the Sankey: per-programme headcount ("Численост на щатния персонал") + Персонал spend extracted from the same per-ministry execution reports (PDF, XLSX-in-ZIP, and DOCX/DOCX-in-ZIP formats handled), joined with national-level aggregates from the annual "Доклад за състоянието на администрацията" (iisda.government.bg, 2017–2025) — total positions, vacancy rate, headcount by administration type. Surfaced as a tile on/budget, a click-to-expand drill-down on the Персонал node in the budget flow Sankey, and a per-programme breakdown on each ministry's detail page with derived average annual cost per FTE. Below the national tier, the same/budgettree carries the sub-national capital & transfer layers: per-municipality state-transfer envelopes from Article 53 of the State Budget Law (data/budget/municipal_transfers/), the Приложение III investment program of per-project capital allocations (data/budget/investment_program/), the МРРБ ИПОП municipal-project execution feed (data/budget/ipop/), per-município annual capital programmes for 26 oblast-centre municipalities (data/budget/capital_programs/— Sofia, Plovdiv, Burgas, Stara Zagora, Ruse, Varna, Pleven and 19 more; the scan-only sources go through a Gemini Vision OCR pre-step), and касово изпълнение по ЕБК cash-execution for the municipalities that publish a MINFIN B3 report (data/budget/municipal_execution/). Social-security fund execution (ДОО / УчПФ / ГВРС) is parsed from the НОИ monthly B1 files intodata/budget/noi/. Pensions get their own top-level/pensionsview built from the НОИ pension statistical yearbook (the clean ZIP-of-XLSX edition, not the PDF): who pays for pensions (contributions vs the ~47% state-budget transfer, from the B1Трансфериline), the pension size distribution (the mountain against the statutory minimum — ~40% of pensioners at or below it — and the wall at the таван), average pension and cash-vs-bank payment by oblast (a choropleth nobody else publishes), the national wage/insurable-income/pension series, a reform sandbox (a CRFB-style "close the deficit" simulator reusing the tax-simulator scorers), an OECD-"Pensions at a Glance"-style replacement-rate signature computed from the КСО formula on synthetic biographies, and the interactive 2070 projection (НОИ actuarial vs EC Ageing Report). The funded private pillars 2 & 3 (КФН УПФ/ППФ/ДПФ per-fund net assets + insured,data/budget/kfn/funds.json) sit alongside as the "missing half". The НЗОК (health-fund) pack on/awarder/121858220sits the fund's ~€79M of public procurement inside the ~€5.5bn it administers, from the nhif.bg source files underdata/budget/nzok/: the budget-law breakdown ("Къде отиват парите на НЗОК",budget.json) with a Bulgaria-vs-EU public-health-spend benchmark (COFOG GF07) and a plan-vs-actual execution-pace curve built from the monthly B1 cash-execution history (execution.json+execution_history.json, every B1 since 2022); per-hospital БМП payments (hospital_payments.json, ~381 facilities, PG-served from the multi-periodnzok_hospital_paymentstable via migrations 045+047) with a momentum tile (year-over-year fastest-rising / falling movers) and a per-oblast per-capita choropleth; drug reimbursement by INN/ATC (drug_reimbursement.json, oncology-dominated) with full-year-vs-full-year YoY fastest-rising / falling / newly-reimbursed molecules; and a searchable compare-two-hospitals side-by-side. Matched hospitals (via a verified Рег.№→EIK crosswalk) cross-link to their/company/:eikpage, which shows НЗОК reimbursement-in against procurement-out plus a transparent spend-growth percentile ("faster than N % of hospitals", the published-formula alternative to black-box anomaly scores). The pack extends into deeper layers, each PG-served and self-hiding until its migration reaches the DB: the payments are split into three streams (БМП / drugs-in-hospital / medical devices, migration 050); per-hospital drug UNIT prices (Справка 5, Наредба 10/2009,drug_unit_prices.json, migration 052) compare what each hospital pays for the same pack of the same medicine at pack identity (Национален №), never at INN, with a volume floor and the explicit caveat that dispersion is not wrongdoing — each molecule and pack drills into its own page,/molecule/:inn(which hospitals paid above the peer median for that molecule's packs) and/molecule/:inn/pack/…(one pack's month-by-month median/p25–p75 price trend, the persistent-dispersion evidence a single year cannot give), reachable from those tiles and from a per-hospital "drugs above median" strip on the facility's own/company/:eikpage; the ЕЕОФ hospital financials (МЗ, Наредба № 5/2019, 26 quarters since 2019,hospital_financials.json, migration 051) add revenue, total and overdue liabilities, cost per patient, bed occupancy and length of stay; and the clinical-pathway activity corpus (activities.json+ the compactactivities_overview.json, migration 053) carries cases + insured-persons (ЗОЛ) per КП/АПр/КПр per hospital — the case-mix denominator the spending reports lack — surfaced as national procedure volumes plus a pathway-internal cases-per-bed outlier (a facility compared only to same-type peers on the same procedure; a signpost, not a verdict). On top of these corpora sit four views adopted from the international transparency canon (OpenPrescribing, NHSU Ukraine, France Assurance Maladie, CMS Care Compare): a drug-savings leaderboard (migration 054/060 — the national "€X of avoidable overpay if every hospital paid the peer-median unit price for the same pack" figure, ranked per hospital); a per-hospital report card on/company/:eikbadging each financial ratio measure над / около / под the national median (the p40–p60 band gives the "same as national" middle state) alongside an OpenPrescribing-style decile fan that threads the hospital through the whole peer distribution over 26 quarters (migration 056), with a reporting-coverage strip so a missing quarter reads as a reporting gap rather than a real drop (migration 058); and a pathway tree (migration 059's serving fn over the activity corpus — pick a клинична пътека and see which hospitals bill it, ranked by cases). Once the НРД pathway tariffs are ingested (migration 059, the opt-in--pathway-tariffsstep; the price factor the activity corpus lacks), the volume tree gains a spend reading (cases × list price) and each hospital a case-mix expected-vs-actual ratio (Σ tariff × cases vs the actual БМП paid — the STAR-PU / MSPB signal, "paid X× what its case-mix predicts at list price"). No per-patient or per-unit figure is ever ranked without a case-mix denominator, and private hospitals are included throughout. All of it flows outside public procurement (чл. 45 ЗЗО). When a fiscal year opens with no adopted budget — as FY2026 did, after the draft was withdrawn following the December-2025 protests and Bulgaria ran the year on an interim "Закон за събирането на приходи и извършването на разходи" (удължителен закон) — the budget-journey index records that bridging law (and its amendments) in place of the missing State Budget Law. On top of the revenue layer sits a tax-policy simulator (/budget/simulator) with two scoring modes — dynamic (the default: per-lever behavioral base responses, a reduced-form GDP feedback from IMF Bulgaria multipliers, and a seeded Monte-Carlo 90% band on the headline; enginesrc/lib/bgBehavioral.ts) and static (?mode=static, the official-costings convention) — with second-order effects netted consistently: the mechanical labour-tax feedback (the budget recovers ~30.6% of any indexed pay as PIT+SSC —labourTaxFeedbackOnCost, now shared by administration cuts, public-wage indexation and the teachers' peg) plus two banded behavioral cross-effects (a maternity-cut return-to-work PIT+SSC recapture, and dividend↔salary relabeling) — plus a goal scoreboard (Maastricht −3% / debt ≤ 40% by 2030 / defense-3% missions with a today→scenario→target gauge), a winners-and-losers strip by wage decile, whole-country quick-select pills (COUNTRY_PROFILESinsrc/lib/euPolicyPresets.ts— load Estonia/Poland/Hungary/Germany/France/Sweden/Ireland/Greece's full tax model in one click: VAT, the PIT schedule, corporate, defense, pensions and excise at once), a 1200×630 share-card PNG export, and a public tally — visitors can submit their scenario to a Firestore-backed "what the public chose" card (explicit button, rate-limited, no PII; see the Cloud Functions note under Architecture). The static core scores ДДС (standard rate + six re-ratable consumption categories), the flat ДДФЛ, корпоративен данък, данък върху дивидентите, the МОД insurable-income cap, the excise duties as per-product absolute rates (diesel & petrol €/1000 L, cigarettes €/1000, spirits €/hl, plus a new wine excise €/hl — each anchored to the Митническа хроника product split inpolicy_baseline.jsonand carrying an EU-country "like in…" quick-select popover with sourced rates fromsrc/lib/euPolicyPresets.ts), and the gambling ЗХ variable fee on gross gaming revenue (GGR). The spending side adds the road charges (е-винетки and тол priced on their separate АПИ revenue slices), an SOE-subsidy cut (БДЖ/НКЖИ/Пощи), and three spending-expansion levers — social benefits, interest on debt and general subsidies — each a ± % move on its consolidated base (interest/subsidies re-sourced from the КФП execution, social derived as COFOG GF10 minus the pension mass), with a one-click "Бюджет 2026: консолидация" preset and a "Накъде отиват парите" COFOG functional-breakdown panel answering where the spending goes. Each scenario is shown as the Δ in consolidated budget revenue per year (МОД with an explicit Pareto-tail uncertainty band) side by side with the Δ on one worked payslip per month, with shareable query-string scenarios. The VAT side runs on Eurostat household consumption by COICOP purpose (nama_10_co3_p3, scaled bynama_10_gdpP31_S14 totals) mapped to statutory VAT regimes and calibrated against actual КФП ДДС revenue (factor ~1.17, stable 2021–2025); the МОД side recovers the above-cap wage mass from the gap between the uncapped НАП PIT base and the capped insurable base (Eurostatgov_10a_taxagD613CE), backtested against МФ's own scoring of the 2025 cap raise. Progressive scenarios (необлагаем минимум, a second rate above a threshold) and МОД moves in both directions run over a fitted earnings distribution — split log-normal body from the SES decile ratios (earn_ses_hourly), level from НОИ's average insurable income, Pareto tail from the same identity — emitted as ~120 quantile bands and validated by reproducing the НАП employment-PIT line at the flat 10% (κ = 1.00), with the fitted tail backtesting the legislated 2025 cap raise at €113M vs МФ's €128M. The fitted body and tail are additionally cross-checked against the published НАП taxable-base distribution by income group (tax year 2023 — 3.11M ДДФЛ filers / €3.07B / €30.7B base, a MoF parliamentary answer hand-keyed inscripts/budget/nap_income_tiers.tsas a manual one-off backfill since minfin.bg is WAF-blocked): the body validates where the employee and all-filer populations coincide, and the employee tail α≈2.27 is confirmed to sit above the all-filer НАП α≈1.67 (the top groups blend in dividend/business income, so the all-filer tail is fatter). Emitted intopolicy_baseline.jsonasincomeTiers, gated byscripts/budget/__smoke_income_tiers.ts. The remaining ask — a machine-readable, annual, finer-grained version — is the ЗДОИ request explained in the methodology article at/articles/2026-06-12-tax-policy-simulator. The same engine powers the AI assistant'ssimulateTaxChangetool, which answers a natural-language what-if ("какво става ако съкратим майчинството") by mirroring every simulator lever — every revenue, spending and debate instrument (incl. road charges, the SOE-subsidy cut and the social/interest/subsidies spending levers below), leading with the dynamic estimate; the chat↔engine equality and the whole calculation layer are locked bynpm run budget:test(golden engine math + per-lever AI parity + the baseline smokes). - Macro & governance indicators —
/indicatorsis a KPI dashboard front door (12 headline tiles — GDP, HICP, unemployment, sentiment, debt, balance, fiscal reserve, EU funds, WGI corruption, trust, youth unemployment, poverty — each with a YoY arrow tinted by semantic direction, an EU27 rank badge where available, and a 10-year sparkline shaded with cabinet-colour bands) above a CabinetScoreRow that summarises each government's tenure in six averaged metrics (GDP avg, inflation avg, unemployment avg, debt change, balance avg, net EU funds). Detail lives on five domain sub-pages:/indicators/economy(GDP + HICP + unemployment + a labour-market section — the unemployment rate at monthly cadence (une_rt_m) as the main line with the quarterly series as reference dots and youth unemployment alongside, the 20-64 employment and activity rates (lfsi_emp_q), and a labour-market-slack callout (lfsi_sla_a, the broad "true unemployment" measure) — + inflation breakdown by ECOICOP group + consumer sentiment + ESI + activity indices),/indicators/fiscal(debt/balance/current account both as % of GDP and in nominal EUR — debt stock, quarterly issuance Δ, the fiscal reserve with its statutory floor as a reference line, government revenue/expenditure, FDI, plus the sortable sovereign debt emissions table covering international Eurobonds since 2002 and domestic ДЦК auctioned by the BNB since 2019, and the EU funds vs contribution chart),/indicators/budgets(Бюджети по кабинети — the public debate over which PM/finance-minister duo ran the budget best: per-year budget balance on two bases — accrual ESA (the authoritative annual figure from the Eurostat EDP notificationgov_10dd_edpt1, never reconstructed by summing the seasonally-adjusted quarterly series) and the cash КФП headline from the MoF annual report — with the EU −3% line, year-end overdue obligations (просрочени задължения) and the fiscal reserve, grouped into cabinet eras with a hero chart whose cabinet + finance-minister tenure tracks mark who defended and who revised each year's budget law, plus an "Издръжка по ведомства" heatmap — each first-level spending unit's operating cost (издръжка = current spending minus personnel, subsidies, interest and household transfers — the residual Asen Vasilev charts in "Бюджет 2026: Перо по перо") across the last nine budgets, 2018→2026, reconstructed from the State Budget Laws intodata/budget/izdrazhka_by_institution.json, shaded red→green by year-over-year change with each column crediting the finance minister who authored and revised that budget),/indicators/governance(Transparency CPI + Worldwide Governance Indicators + Eurobarometer trust in parliament/government/EU), and/indicators/society(youth unemployment + house-price YoY + Gini + at-risk-of-poverty). A standalone peer explorer at/indicators/comparelays out all eight peer-eligible indicators side-by-side against BG, EU27 and four CEE peers (Romania, Greece, Hungary, Croatia) with a country chip multi-select that persists in?peers=. A page-wide "Compare with EU peers" toggle on each sub-page (URL-shareable via?compare=1) layers the same peers onto every applicable chart, with a per-section snapshot table colouring values green/red vs. the EU27 average and showing Bulgaria's rank within the 27 member states. - Consumption (Потребление) — a fourth top-level place view (alongside Governance / Parliamentary / Local), framing everyday cost of living at country → Sofia City → region → município → settlement. It surfaces the КЗП "Колко струва" retail-price basket index since the euro (national + per-oblast series, cheapest chains and cheapest/fastest-rising places, a per-settlement product breakdown, and a municipality price choropleth), the monitoring basket set against official Eurostat HICP consumer-price inflation (food / overall / energy / core), and a regional affordability index — basket cost relative to Eurostat GDP-per-capita per oblast (a regional-income proxy, explicitly not net wage). It also ships a product catalogue — ~118k canonical products derived from the daily feed's names (there is no EAN) at
/consumption/productswith per-product/product/:slugpages (cross-chain price ladder + since-euro history chart), and a "did the euro raise prices?" four-bucket verdict. The/consumptionlanding is a navigation-first hub (a product search over the ~118k catalogue + a tile grid) fronting: a personal basket (/consumption/basket, localStorage — live total + change since the euro), a retail-chain leaderboard (/consumption/chains) whose rows bridge to the company's money-flows profile via its EIK (/consumption/chain/:eik→/company/:eik— public procurement, EU funds, ownership; e.g. Метро's basket + its €4.6M in state contracts), category pages (/consumption/category/:cat— per-food-group trend + products), an EU price comparison (/consumption/eu, "Цените спрямо ЕС") from official Eurostat price level indices (prc_ppp_ind_1, EU=100) showing the whole household basket vs the EU average — a headline overall price level paired with the income-adjusted real per-capita consumption volume, per-division bars (food expandable to its detail), and a since-2010 convergence trend, and a set of energy cost-of-living pages setting BG prices against the EU average and the RO/GR/HU/HR neighbour peers: fuel (/consumption/fuel, "Горива") — petrol 95 & diesel from the EC Weekly Oil Bulletin (BG runs ~20-24% below the EU), electricity (/consumption/electricity, "Ток") and natural gas (/consumption/gas, "Природен газ") — household prices from Eurostat (nrg_pc_204/nrg_pc_202, both ~half the EU average). The three price trends share one chart component (PriceTrendChart+ the flag-legend peer lines). The КЗП feed is served from Postgres (migration 048, no static JSON); the affordability + inflation tiles reusemacro.json, the EU comparison apricePliblock inmacro_peers.json, and the fuel tile a small committedfuel.json. Routes:/consumption(hub) +/consumption/{overview,products,basket,chains,chain/:eik,categories,category/:cat,eu,fuel,electricity,gas,region/:oblast}and/consumption/:id; the country node is prerendered (BG + EN) and in the sitemap, while the region/place tiers are SPA-only and<link rel="canonical">to their Governance twins (which carry the same price tile) to avoid thin duplicates. AI chat tools answer cost-of-living questions in the assistant —basketAffordability,basketVsInflation,euFoodPriceLevels(BG food vs the EU, Eurostat PLI), plus the energy-price triofuelPrices,electricityPricesandgasPrices(BG vs the EU + neighbours). (Per-oblast wages, sub-national NSI house prices, and КЕВР water tariffs were evaluated as data sources but rejected — the per-oblast values live only behind NSI's Cloudflare-walled Infostat / chart-locked PDFs and КЕВР's multi-system PDFs; seedocs/consumption_data_sources.md.) - Vote flows — transition matrices estimating where each party's votes moved between consecutive elections, for both parliamentary cycles and the local-council ballot (the latter generated by the separate
--local-flowsstep and served fromdata/transitions_local/). A third "pre-vote" flow (--prevote-flows→data/transitions_prevote/) estimates where the most recent parliamentary vote before each local cycle landed in that cycle's council ballot — the flow shown by default on the local dashboards. - Articles — long-form editorials and methodology notes (plain markdown, BG + EN).
A simulator at /simulator lets you redistribute votes and see the resulting seat allocation under Bulgaria's electoral formula.
- React 19 + TypeScript (strict), Vite 6 with SWC
- React Router v7 with every screen lazy-loaded
- TanStack React Query v5 for all data fetching (
staleTime: Infinity, no refetch on focus) - Tailwind CSS + CSS Modules; Radix UI primitives with shadcn-style wrappers in
src/components/ui/ - Recharts for charts, D3 for Sankey/vote-flow diagrams, Leaflet for maps (CSS dynamically loaded so it stays off the landing critical path)
- TanStack Table for the data grids
- react-markdown for the long-form articles (plain
.mdwith YAML frontmatter, not MDX) - i18next with English and Bulgarian, preference stored in
localStorage - Self-hosted Inter + Fraunces under
/public/fonts/(refreshed vianode scripts/fonts/fetch-fonts.mjs) - Playwright for E2E, SEO, performance, and responsive smoke tests
- Firebase Hosting for the SPA shell (rewrites in
firebase.json) - Google Cloud Storage (
gs://data-electionsbg-com) for the data layer — fetched at runtime via thedataUrl()helper so data updates don't require a Firebase deploy - GitHub Actions for the daily upstream watcher + ingest jobs (see
.github/workflows/)
src/
routes.tsx All route definitions
data/ React Query hooks per domain (regions, municipalities,
settlements, sections, parties, candidates, parliament,
polls, governments, census, articles, voteFlows, ...)
data/dataUrl.ts Resolves data paths to local (dev) or GCS bucket (prod)
data/ElectionContext Selected election date — every data hook reads from here
screens/ Page-level components matching the route structure
screens/components/ Reusable cross-screen components
components/ui/ Low-level UI primitives
components/article/ Shared ArticleLayout + ArticleProse for long-form pages
ux/ Data tables, tooltips, touch handling, media queries
locales/ i18n strings (also public/locales/ at runtime)
scripts/ Offline data pipeline + watcher + bucket helpers
(see "Data pipeline" below)
public/ App-bundle assets that ship through Firebase Hosting:
favicons, fonts, OG cards, sitemaps, llms.txt,
robots.txt, articles markdown + images
data/ Election + parliament + polls + census JSON consumed
by the SPA at runtime. Served from GCS bucket in
production; Vite middleware mounts it at root in dev
state/ Watcher fingerprints (`state/watch/`) and per-skill
ingest markers (`state/ingest/`) — committed so
the orchestrator survives multi-day gaps
raw_data/ CIK CSV/ZIP exports and other inputs to the pipeline
data/_cache/ Cached upstream artifacts that the pipeline re-fetches
on demand (PDFs, XLSX). Gitignored except for
user-facing READMEs that explain manual workflows
(e.g. `minfin_fr_xlsx/README.md` for dropping
fiscal-reserve XLSX files past the Wayback cutoff)
docs/plans/ PRDs for in-flight and planned work
@/* is a tsconfig path alias for src/*.
The generated election data under data/2*/, data/sections/, data/settlements/, data/municipalities/, and data/regions/ is not committed to git — it is reproduced from raw_data/ by the pipeline. After cloning:
npm install
npm run prod # regenerate data/ from raw_data/ (a few minutes)
npm run dev # start the Vite dev serverThe Vite dev server includes a serveDataDir plugin (see vite.config.ts) that mounts /data/ at the root of the dev server, so fetch("/2026_04_19/national_summary.json") resolves to data/2026_04_19/national_summary.json locally without needing the bucket.
In production the same fetch resolves to https://storage.googleapis.com/data-electionsbg-com/2026_04_19/national_summary.json because VITE_DATA_BASE_URL is set in .env.production. The dataUrl() helper in src/data/ is the single seam — every data fetch goes through it.
npm run build # tsc -b && vite build, then OG images, prerender, llms.txt, image opt
npm run lint # ESLint
npm run format # ESLint --fix
npm run preview # serve the production build locally
npm test # Playwright (also: test:ui, test:seo, test:perf, test:desktop, test:mobile)
npm run sitemap # regenerate sitemap_*.xml (also auto-runs in postbuild)
npm run llms # rebuild llms.txt and llms-full.txt
npm run census # rebuild Census 2021 JSON from raw_data/census_2021/
npm run polls # scrape + analyze polls + regenerate analysis narratives
npm run watch # Tier-1 watcher: diff fingerprints across upstream sources
# (also writes data-reports/<date>.md for the orchestrator)
npm run rollcall:scrape # ingest new parliament.bg roll-call vote sessions
npm run derived:rebuild # recompute MP loyalty / attendance / similarity / party cohesion
npm run bucket:sync # incremental rsync of data/ to GCS bucket
npm run bucket:sync:dry # same, but -n (preview only)
npm run bucket:gz # gzip-upload hot large JSON (Content-Encoding: gzip)
npm run bucket:gz:dry # preview which files + the gzip savings
npm run bucket:sync:all # bucket:sync then bucket:gz (recommended deploy)
# Helpers invoked by Claude Code skills (no top-level wrapper):
# npx tsx scripts/financing/scrape_index.ts # Сметна палата annual-reports year index
# npx tsx scripts/financing/scrape_reports.ts # gfopp per-party annual-report filing status
# npx tsx scripts/parliament/scrape_mps.ts --all # parliament.bg MP roster
# npx tsx scripts/macro/fetch_eurostat.ts # Eurostat + WGI + curated tables
# npx tsx scripts/macro/fetch_cofog.ts # Eurostat COFOG (gov_10a_exp) general-gov spend by function → data/cofog.json
# npx tsx scripts/macro/fetch_eu_peers.ts # Eurostat peer comparison (BG + EU27 + RO + GR + HU + HR) → data/macro_peers.json
# npm run data:tourism # Eurostat BG tourism nights — seasonality (tour_occ_nim) + source markets (tour_occ_ninraw) → data/tourism/visitors.json
# # gov_10a_main (% GDP triple) + quarterly per-indicator peer series for /indicators "Compare with EU" overlay
# npx tsx scripts/macro/fetch_bnb_auctions.ts # BNB domestic ДЦК auctions → debt-emissions-domestic.json
# npx tsx scripts/macro/fetch_bnb_fdi.ts # БНБ monthly FDI flows (balance of payments, BPM6) → data/macro_fdi.json
# npx tsx scripts/macro/fetch_fiscal_reserve.ts # minfin.bg fiscal-reserve mreport/BULETIN/FRA XLSX → data/_cache/fiscal-reserve.json
# npx tsx scripts/macro/fetch_fiscal_reserve_history.ts # MANUAL: minfin year-end FR PDFs (pre-2015, Cloudflare-walled) dropped in data/_cache/minfin_fr_history/ → merge into macro.json
# npx tsx scripts/macro/fetch_arrears.ts # MANUAL: minfin "Просрочени задължения" Q4 XLS/PDF (statistics/10, Cloudflare-walled) dropped in data/_cache/minfin_arrears/ → macro.json series.arrears
# npx tsx scripts/macro/fetch_cash_balance.ts # MANUAL: minfin КФП annual workbook (statistics/13, Cloudflare-walled) dropped in data/_cache/minfin_kfp/ → macro.json series.cashBalance (cash budget balance)
# npx tsx scripts/regional/fetch_eurostat.ts # Eurostat NUTS 3 (per oblast) indicators incl. recorded-theft rate + derived active-enterprise density
# npx tsx scripts/regional/fetch_az_oblast.ts # АЗ oblast long-term-unemployment → merge into regional.json
# npx tsx scripts/regional/fetch_nsi.ts # NSI JSON-stat open-data (FDI per capita, museum visits, hospital beds) → merge into regional.json
# npx tsx scripts/indicators/fetch.ts # AZ unemployment + МОН DZI + NSI vital stats per municipality
# npx tsx scripts/landuse/fetch.ts # NSI Баланс на територията annex (per-oblast land-use composition from АГКК cadastral map)
# npx tsx scripts/grao/fetch.ts # ГРАО settlement registered population (permanent + current)
# npx tsx scripts/stamp-ingest.ts <skill> # mark a skill ingest as successful
npm run procurement:ingest # АОП fortnight OCDS bundles → data/procurement/
npm run procurement:ingest -- --renormalize # re-apply the parser to cached bundles (bid-count back-fill) + rebuild
npm run procurement:ingest-legacy # АОП annual CSVs (pre-2026) → data/procurement/
npm run budget:ingest # data.egov.bg КФП feed + State Budget Law (DV HTML)
# + per-ministry execution reports → data/budget/
# (also emits Article 53 municipal transfers)
npm run budget:revenue-breakdown # Митница + НАП revenue split → data/budget/revenue_breakdown/
npm run budget:policy-baseline # Eurostat COICOP consumption + tax aggregates → consumption.json
# + derived/policy_baseline.json (the /budget/simulator base)
npm run budget:test # regression suite for every simulator calculation
# (10 files: golden engine math, AI↔engine parity, smokes)
# side-ingests (own scripts): investment_program, ipop,
# capital_programs/<muni>, municipal_execution, noi
npm run funds:ingest # ИСУН EU-funds beneficiary register (2020.eufunds.bg) → data/funds/
npm run funds:ingest-projects # ИСУН EU-funds project register (per-contract, with location) → data/funds/projects/
npm run deploy # Firebase deploy (production)
npm run deploy:fast # Firebase deploy without re-running the data pipeline (SKIP_PREDEPLOY=1)
npm run staging # Firebase deploy (staging)
npm run staging:fast # same with SKIP_PREDEPLOY=1
npm run stats # bundle size visualizer- Raw inputs live in
raw_data/— CIK CSV/ZIP exports per election, NSI Census XLSX, scraped Wikipedia/Smetna-Palata/parliament.bg/Court-of-Audit data. - The pipeline in
scripts/transforms those into static JSON underdata/YYYY_MM_DD/(per election) and a handful of cross-cutting directories (data/parliament/,data/polls/,data/census/,data/governments.json, etc.). - The SPA fetches those JSON files via the
dataUrl()helper, which prefixes the bucket origin in production. There is no backend server, no database, and no runtime API.
The tax-policy simulator (/budget/simulator) mixes pipeline data with sourced constants in code. Every external anchor is covered by a daily-watcher source; pipeline-fed ones re-ingest via skills, code constants are flagged for a manual edit (.claude/skills/process-watch-report/SKILL.md → "Simulator anchors: manual edits"). Research notes and source URLs: docs/budget_simulator_grounding.md.
| What | Lives in | Upstream | Watcher source |
|---|---|---|---|
| Revenue/expenditure baseline (VAT model, earnings fit, МОД identity, excise anchors, …) | data/budget/derived/policy_baseline.json |
КФП, НАП annual, Митническа хроника (excise split), Eurostat COICOP/SES, НОИ STATB | egov_budget_execution, nap_annual, customs_revenue, eurostat_policy, policy_baseline_local |
| EU comparators — per-lever "Като в… (ЕС)" picks and whole-country quick-select profiles (VAT/PIT/CIT per country) | src/lib/euPolicyPresets.ts (EU_LEVER_PRESETS, COUNTRY_PROFILES) |
PwC Worldwide Tax Summaries quick charts | eu_tax_rates (manual edit; auto-checked) |
| EU comparators — defence % of GDP (per-lever + country profiles) | src/lib/euPolicyPresets.ts |
NATO defence-expenditure compendium (annual PDF) | nato_defence (manual edit; auto-checked) |
| EU comparators — excise per product (diesel/petrol/cigarettes/spirits/wine; per-lever + country profiles) | src/lib/euPolicyPresets.ts |
Tax Foundation energy/cigarette tables + EC DG TAXUD alcohol tables | none — curated constants, manual ≈yearly refresh |
| 5-year projection baseline (EC balance path, growth/HICP/unemployment) | src/lib/bgFiscalProjection.ts |
EC economic forecast — Bulgaria country page | ec_forecast_bg (manual edit) |
| 2025 ESA anchors (deficit/debt/GDP) and interest-rate block | src/lib/bgFiscalProjection.ts |
НСИ EDP notification (Apr/Oct), debt strategy, БНБ auctions | nsi_edp (manual edit); bnb_auctions for yields |
| Dynamic-mode behavioral elasticities (ETI, CIT/dividend semi-elasticities, VAT compliance response, fiscal multipliers, Monte-Carlo bands) | src/lib/bgBehavioral.ts |
Academic literature, EC VAT gap report, IMF (WP/13/49, WEO vintages) | ec_vat_gap, imf_weo_bg (manual edits) |
| Benchmark costings + the dividend-lever behavioral calibration (≤ €50M) | src/lib/bgBehavioral.ts, bench i18n strings |
Фискален съвет publications | fiscal_council_bg (manual edit) |
| June-2026 debate levers (maternity Y2 spend, MP pay mass, party subsidy) | src/lib/bgTaxPolicy.ts |
НОИ ДОО execution, NSI public-sector wage, НС decisions | annual manual review alongside nssi_b1 / budget-law flips |
Invariants for the projection engine are locked in scripts/budget/__smoke_fiscal_projection.ts, and for the behavioral layer (zero-draw identity, the Фискален-съвет dividend calibration, Tier-2 multiplier magnitudes, Monte-Carlo determinism) in scripts/budget/__smoke_behavioral.ts — run both after touching any anchor.
The eu_tax_rates / ec_forecast_bg / nato_defence anchors also have an automated drift check — npx tsx scripts/budget/check_policy_anchors.ts --source all --stamp re-probes the upstreams via the watcher sources' own fingerprints, compares against the engine constants (EU_LEVER_PRESETS, EC_FORECAST_EDITION, NATO_COMPENDIUM_EDITION) and stamps the ingest markers itself when everything matches; only genuine drift needs a human edit.
The site is split across two origins to decouple data updates from app deploys:
- Firebase Hosting (
electionsbg.com) serves the SPA shell: prerendered HTML, JS bundle, fonts, OG cards, sitemaps, articles markdown, favicons. Anything in/public/ships here. SPA rewrites and per-route prerendering live infirebase.json. - GCS bucket (
gs://data-electionsbg-com) serves the data layer: per-election JSON, parliament/, polls/, census/, declarations/. Anything in/data/syncs here vianpm run bucket:sync.
A scraper writing fresh polls or roll-call data only needs npm run bucket:sync — no Firebase deploy. App code changes still need a deploy because the prerendered HTML (~445k files) ships through Firebase. Both bucket and Firebase are gzipped; the bucket has CORS open to all SPA origins.
Two small Cloud Functions (one codebase, functions/index.js) are the only server-side pieces:
llm(projectelectionsbg-ai) — the OpenRouter proxy for the AI chat atai.electionsbg.com/api/llm(keeps the API key server-side). Deploy:firebase deploy --only functions:llm -P ai.scenarios(projectelections-bg) — the budget simulator's public tally ("what the public chose"):POST /api/scenarios/submitvalidates a scenario query string against the simulator's URL contract and increments Firestore aggregates (per-IP daily rate limit, salted-hash IPs only, no PII);GET /api/scenarios/statsreturns the cached aggregates. Reached same-origin via the/api/scenarioshosting rewrite; the AI chat origin is CORS-allowlisted. Firestore (firestore.rules) denies all client access — only the function's Admin SDK touches it. Deploy:firebase deploy --only functions:scenarios -P default(+--only firestore -P defaultfor rules,--only hosting:main -P defaultfor the rewrite).
Files and directories the SPA fetches at runtime — all under /data/ locally and gs://data-electionsbg-com/ in production:
| Path | Contents |
|---|---|
YYYY_MM_DD/ |
Per-election results, reports, party assessments, candidate data, financing |
<cycle>/ (local: *_mi, *_chmi*) |
Local-elections (общински избори) data tree, one folder per cycle (2023_10_29_mi, 2019_10_27_mi, …, plus *_chmi* partials). index.json — national rollup (council R1 vote share by canonical party + mayors-won + município catalogue). index_trends.json — lightweight sidecar (just councilVoteShare + mayorsByCanonical) so the country-dashboard cross-cycle trends tile fans out ~50 KB per cycle instead of ~100 KB. municipalities/<obshtinaCode>.json — per-município bundle (mayor R1/R2 + elected, council parties with elected-councillor lists + mandates, kmetstvo mayors, район mayors; the synthetic SOF city bundle fans out to 24 S2*** район shards, additionally surfaced as a per-район choropleth on the Sofia page by merging data/maps/regions/S23.json+S24.json+S25.json). region/<oblast>.json — per-oblast rollup (mayors-won + council seats by party + município directory carrying per-município electedMayor AND topCouncil; the region dashboard's single fetch and the source of the council-support choropleth, keyed by the parliamentary region code incl. the split PDV/PDV-00, Sofia city normalised to SOF). regions_summary.json — lightweight per-oblast control rows driving both the national mayoral and council choropleths (each row carries topMayor + topCouncil) + top-regions table. national_leaders.json — precomputed country-dashboard leaderboards (top mayors by % vote, closest races, split-control list, independent mayors) so the SPA renders them from one fetch instead of fanning out all ~265 município bundles. officials_diff.json (+ officials_diff/<obshtinaCode>.json) — CIK-winners-vs-current-officials reconciliation (the /sverka screen). _unmatched_coalitions.json — operator-review queue. sections/<obshtinaCode>.json — light per-município section index: every polling station with turnout + the top-5 council parties + GPS/building-address, driving the per-município section map (one dot per station coloured by leading council party), the top-sections leaderboard, and the searchable table. sections/<obshtinaCode>/<sectionCode>.json — per-station full council breakdown, fetched by the section detail page (so it loads ~2 KB instead of the whole shard — Sofia's was ~2 MB). GPS + address are stamped onto the index by the --local-coords step, joining to the parliamentary section archive on a settlement-name-gated key (~98% coverage; section codes and numbers diverge between local and parliamentary cycles, so the settlement name is the safe anchor). problem_sections.json — curated Roma-neighborhood "problem sections" (council ballot), grouped by neighborhood with pre-aggregated per-party council totals (votes summed across the neighborhood's flagged sections — the tile needs no per-station rows, so the file stays ~20 KB and the SPA fetches it once, cached), driving the município dashboard's Risk votes / Problem votes by party tile; produced by --local-problem-sections (reuses the parliamentary watchlist, matched against local sections by EKATTE + address). The region/* + regions_summary.json + national_leaders.json + index_trends.json artifacts are all produced by scripts/parsers_local/build_region_json.ts, folded into the parse so they regenerate on every local ingest / canonical re-resolve. Cross-cycle partials aggregate into the top-level data/local_chmi_history.json (national /local/chmi feed) and are also sharded to data/chmi_history/<obshtinaCode>.json (per-município page + settlement dashboard fetch their own ≤ 1 KB file instead of the 61 KB global). Surfaced as the local dashboards: /local/:cycle (country — both maps + leaderboards + cross-cycle trends + extraordinary feed), /local/:cycle/region/:oblast (region — both maps + split-control list + município directory with mayor and council columns), /local/:cycle/:obshtinaCode (município — mayor R1/R2, council with top councillors by preference, mayor-vs-council alignment), /local/:cycle/SOF (Sofia — both maps colored across 24 районы), and /local/:cycle/settlement/:ekatte (settlement — the village-mayor / кметство race), all anchored in time to the selected parliamentary election via src/data/local/localAsOf.ts. The settlement dashboard name-matches the parent município's kmetstva[] (the CIK HTML leaves their ekatte empty) and links from each município page's kmetstvo rows via data/local_mayors/kmetstvo_to_ekatte.json — a kметство-name → EKATTE lookup built by scripts/parsers_local/backfill_kmetstvo_ekatte.ts (refreshed automatically by the update-local-elections skill). |
parliament/ |
MP index, profiles, declarations, business-connections graph, companies index, cached MP photos (.webp), roll-call sessions + derived metrics. Per-MP roster shards under by-id/<id>.json (one index.json entry each, ~0.4 KB; written by scripts/parliament/lib/writeMpById.ts) let a candidate page resolve a single MP by id (src/data/parliament/useMpEntry.tsx) without the ~950 KB index.json roster — the procurement→MP deep-link for a former / off-ballot MP loads this instead, falling back to the full roster only if the shard 404s. Per-session JSONs under votes/sessions/<date>.json carry itemSlugs + itemTopics alongside itemTitles (drive canonical /votes/:date/:slug URLs + the topic taxonomy). Derived bundle under votes/derived/: loyalty.json, attendance.json (per-MP present-vs-absent counts), similarity.json (with topK + bottomK), cohesion.json, embedding.json, party_correlation.json, topic_index.json (cross-session vote-title index for header search + landing-page contested-feed), dissents.json (per-MP recent defections, NS-keyed), party_pair_breaks.json (top contested items per unordered party pair). Plus per-MP shards under votes/derived/per-mp/<ns>/<mpId>.json — bundle {loyalty, attendance, dissents, similarity} for one candidate page so it doesn't pay the chamber-wide aggregate cost (~3 MB gz → ~2 KB gz) |
officials/ |
Non-MP property/interest declarations from the same Court-of-Audit register: per-slug declarations under declarations/<slug>.json, plus index.json (role + institution) and assets-rankings.json (net worth + YoY delta, with byCategory slices for cabinet / agency heads / regional governors). The municipal tier (mayors, councillors, etc.) lands in its own municipal/ subdir — per-slug declarations + a roster index.json, no rankings — plus per-obshtina roster shards under municipal/by_obshtina/<obshtina>.json (~288 files, p50 7 KB / 1.5 KB gz, max 36 KB / 5.5 KB gz). Each shard pre-sorts entries in roster-display order so the municipality-page tiles render one fetch per page; the 2.2 MB global index.json is reserved for cross-cutting (search) use. Aliases at scripts/officials/_aliases.json map registry-name oddities to obshtina codes; the synthetic SFO_CITY code carries the Sofia city-wide tier and is staged for a future Sofia-wide tile |
polls/ |
Polls, agencies, accuracy metrics, narrative analyses |
census/ |
Per-region and per-municipality Census 2021 slices |
regions/ municipalities/ settlements/ sections/ |
Geography-keyed per-location detail (gitignored — regenerated by npm run prod) |
transitions/ |
Vote-flow transition matrices between consecutive elections |
transitions_local/ |
Local-council vote-flow transition matrices between consecutive local cycles (<fromCycle>_<toCycle>/{national,<oblast>,persistence}.json + index.json); produced by --local-flows, consumed by the local country + region vote-flow Sankey |
transitions_prevote/ |
"Pre-vote" flow matrices — most recent parliamentary election before each local cycle → that cycle's council ballot (<parlDate>_<toCycle>/{national,<oblast>,persistence}.json + index.json); produced by --prevote-flows, consumed by the local country + region pre-vote Sankey (the LocalVoteFlowTile auto-picks the parliamentary "from") |
local_place_trends/ (gitignored; bucket-shipped) |
Per-place cross-cycle trend shards — {s/<ekatte>, r/<rayonId>, p/<S2xxx>}.json, each {cyclesAsc, trend:{council[], mayor[], rayonMayor?}} (council party share + mayoral winner per cycle, raw bucket ids resolved client-side). Produced by --local-place-trends, consumed by the settlement + район dashboards' trend tiles (one ~3 KB shard per page). Regenerable, so gitignored like the procurement / funds per-entity shards |
maps/ |
Per-region/municipality GeoJSON slices |
canonical_parties.json |
Master party register (name variants, history, colors) |
governments.json |
Government coalitions and ministers by parliamentary term |
parliament_groups.json |
Parliamentary group (faction) memberships |
macro.json |
Macroeconomic + governance indicators for the cabinet timeline (Eurostat GDP/HICP/unemployment (quarterly SA) + a labour-market set — unemploymentMonthly (une_rt_m, full monthly SA series), employmentRate + activityRate (lfsi_emp_q, 20-64) and labourSlack (lfsi_sla_a, annual) behind the /indicators/economy labour panel, fiscal triple as % of GDP + nominal EUR, the authoritative annual ESA deficit ratio esaBalanceAnnual (EDP notification gov_10dd_edpt1 — drives the per-cabinet Салдо (ЕСС) scorecard), FDI inward, government revenue/expenditure, criminal-justice indicators (Eurostat crim_off_cat intentional-homicide rate per 100K + crim_pris_age prisoners per 100K — drives the "Safety and criminal justice" subsection on /indicators/society), World Bank WGI, Transparency International CPI, Eurobarometer trust, EU funds, plus the fiscal-reserve end-of-quarter stock series derived from minfin.bg monthly bulletins) |
cofog.json |
General-government expenditure by COFOG-99 function from Eurostat gov_10a_exp (S13, annual, MIO_NAC converted at the 1.95583 BGN/EUR parity). Top-level functions GF01..GF10 + TOTAL, plus a per-function 27-member EU peer band (BG rank + EU27 average). Also carries peerSeries — per-peer (BG + EU27 + RO + GR + HU + HR) % of GDP per function at the latest year both BG and ≥20 peers report — for the side-by-side stacked-bar tile on /indicators/compare. Drives the functional-classification tile, the "what did your taxes buy" calculator, and the per-function peer chips on /budget |
macro_peers.json |
EU peer comparison for the /budget peer tile, the /indicators "Compare with EU peers" overlay, and the dedicated /indicators/compare dashboard. Six-country roster: BG, EU27, RO + GR (geographic neighbors), HU + HR (CEE peers — HR replaced PL, GR joined as a southern neighbor). Four sections in the same file: (a) legacy — Eurostat gov_10a_main annual % of GDP series for revenue / expenditure / balance, plus a 27-member EU rank distribution per naItem; (b) per-indicator quarterly — peer series for inflation (HICP), real GDP growth, unemployment, the 20-64 employment + activity rates (lfsi_emp_q), government debt %GDP, budget balance %GDP, current account %GDP, house-price index YoY, and youth unemployment, plus a 27-member latest-quarter EU rank snapshot for each direction-unambiguous indicator (powers the snapshot-table "rank N/27" pills + the /indicators peer-overlay); (c) per-indicator annual — SILC Gini (ilc_di12), S80/S20 quintile share ratio (ilc_di11), AROPE (ilc_peps01n), life expectancy at birth (demo_mlexpec), intentional-homicide rate per 100K (crim_off_cat, ICCS0101), prisoners per 100K (crim_pris_age), and labour-market slack (lfsi_sla_a, 20-64), each with the same per-peer + 27-member rank shape — for the inequality panel + spend-vs-outcome scatters on /indicators/compare. The two crim_* series use a computeEu27FromMembers flag because Eurostat doesn't publish the EU27_2020 aggregate for those datasets — the fetcher reconstructs it as an unweighted mean across the 27 members (≥20-reporter threshold per year, mirroring the WGI pattern); (d) WGI — World Bank Worldwide Governance Indicators source 3, all six dimensions (VA, PV, GE, RQ, RL, CC, with both Estimate and 0-100 Score variants), per peer + a client-side-computed EU27 unweighted mean (WB does not publish a regional WGI aggregate) — powers the radar tile on /indicators/compare; (e) pricePli — Eurostat price level indices (prc_ppp_ind_1, EU27=100) for the whole COICOP consumption basket (overall + 12 divisions + food detail), the income-adjusted real per-capita consumption volumes (VI_PPS_EU27_2020_HAB, EU27=100), and the overall-price-level convergence trend since 2010, written by scripts/macro/fetch_food_pli.ts — powers the Bulgaria-vs-EU "Цените спрямо ЕС" page on /consumption/eu |
fuel.json |
Consumer fuel prices (Euro-super 95 & automotive diesel, EUR/L, VAT-inclusive, weekly since 2013) — Bulgaria vs the EU average and the RO/GR/HU/HR neighbour peers, from the EC Weekly Oil Bulletin (scripts/consumption/fetch_fuel.ts). Per-geo petrol/diesel maps per week. Served like macro.json; drives the Горива / Fuel page on /consumption/fuel. Small committed JSON. Watcher ec_oil_bulletin; the ingest self-reports its /data/updates row. |
debt-emissions.json + debt-emissions-domestic.json |
Sovereign debt emissions list. International Eurobonds since 2002 are hand-curated (debt-emissions.json); domestic ДЦК auctions 2019+ are scraped from BNB Fiscal Agent pages (debt-emissions-domestic.json). Merged client-side on /indicators into one sortable table |
regional.json |
Per-oblast indicators — Eurostat NUTS 3 (GDP per capita, population, net migration, recorded-theft rate, derived active-enterprise density) plus long-term-unemployment share from Агенция по заетостта and four НСИ JSON-stat open-data series (cumulative FDI per capita, museum visits per 1000, hospital beds per 1000, crude death rate per 1000 — the last a descriptive demographic outcome, age-dominated, NOT a spend-adjustable measure) — drives the oblast drilldown tile and the /demographics regional choropleth |
indicators.json |
Per-municipality annual indicators (registered unemployment from Агенция по заетостта, DZI matura scores from МОН via data.egov.bg, natural population change + net migration from НСИ vital statistics) — drives the municipality drilldown tile and the muni-granularity /demographics choropleth, with Sofia city aggregate fallback for the 24 districts. Per-municipality slices under indicators/<code>.json for the tile |
landuse/index.json |
Per-oblast land-use composition from NSI's annual "Баланс на територията" press-release annex — itself computed off АГКК's digital cadastral map. 28 oblasts × 8 categories (urbanized / transport / agricultural / forest / water / protected / disturbed / unclassified), both km² and % of total area, plus population density (total / urbanized / total-excl-water). National row alongside. This is the closest open-data substitute for cadastre composition — АГКК does not publish bulk parcel polygons, and KAIS is per-parcel + CAPTCHA only. Município-level figures NSI mentions in its methodology are not published. Drives the "Имотен фонд / Property stock" tile on the My-Area dashboard |
schools/index.json |
Per-school ДЗИ (matura) average scores from МОН via data.egov.bg (latest year 2025; НВО 7th-grade not yet included), keyed schoolsByObshtina across 242 общини with a subjects dictionary. Built by scripts/schools/build_index.ts off the same МОН CSVs update-indicators downloads, re-run via the indicators_mon_dzi watcher. Drives the school-quality rows on the My-Area / Governance dashboard quality strip and the funds + procurement settlement tiles (useSchools). (data/services/index.json is the sibling "public services per município" file — scaffolding only, ingest still pending.) |
air/index.json |
ИАОС atmospheric-monitoring stations (PM10 + PM2.5) via data.egov.bg — 37 measuring stations + background stations with the latest quarterly readings and snapshotAsOf. Drives the air-quality tile on the My-Area / Governance dashboards (update-air-quality skill). |
local_taxes/index.json + local_taxes/<obshtina>.json |
Per-município local-tax rates: five ИПИ indicators across all 265 общини (property tax on legal entities, property-transfer tax, vehicle tax 74-110 kW, retail + taxi patent) plus per-naredba blocks (residential ТБО, property tax for individuals, tourist + dog tax) for the oblast capitals. index.json carries the indicator dictionary + national averages + rank totals; per-obshtina shards carry the rates. Drives the local-taxes feature + the Governance tax tile (update-local-taxes skill). |
municipal_transparency/index.json |
Transparency International Bulgaria Local Integrity System Index (LISI) annual composite scores for the 27 oblast-center municipalities (scoresByObshtina + per-pillar breakdown + national average). Drives the municipal-transparency tile (update-transparency-lisi skill). |
council/index.json + council/<obshtina>/… |
Municipal-council (общински съвет) resolutions + aggregate за/против/въздържал vote tallies (per-councillor named votes where available), one folder per wired município (~16 oblast centres). Drives the council-activity tile on the Governance / local dashboards + the AI councilResolutions tool (update-council-minutes skill). |
prices/ (Postgres-only — product_slugs.json + product_overrides.json only) |
КЗП "Колко струва" euro-adoption retail-price monitoring (kolkostruva.bg open data), served from Postgres since migration 048 — no static JSON serving tree. load_day.ts loads ~1.4M store rows/day as an SCD-2 delta (price_facts — one row per store×SKU price-run, ~25–40k inserts/day not 1.4M — plus price_current and the price_grid_days aggregate written from each day's own observations, so a reporting gap reads as a gap not a price change). rebuild_catalog.ts clusters the ~95k daily SKU names into a ~118k-product canonical catalogue (price_products — no EAN, identity is name-derived). build_payloads.ts runs the unchanged Jevons-index maths over price_grid_days into price_payloads (the same index/ranking/chains/dict/place/chains-muni shapes the old JSON served, byte-for-byte). Served via /api/db/price-*. Drives the "Цени / Prices" section on the Governance + Consumption dashboards, the /consumption/products browser, the /product/:slug pages, and the euro-verdict tile. product_slugs.json (the top ~3k by chain-count, committed — drives prerender + sitemap) and product_overrides.json (hand-authored merge/split corrections) are the only committed files. _cache/daily/* grids stay local (parity harness); raw_data/prices/*.zip are gitignored + cold-archived to gs://naiasno-archive-prices. A monitoring index, NOT official CPI. |
grao_population.json + grao/<obshtina>.json |
ГРАО settlement-level registered population (permanent + current address), refreshed quarterly. Full bundle plus per-municipality ~1 KB slices — settlement pages fetch only their own slice |
postcode_ekatte.json |
BG Post postal-code → EKATTE settlement map ({ byPostcode: { "1000": { ekatte: ["68134"], names: ["София"] }, "2932": { ekatte: ["00014"], names: ["Абланица"] }, … } }). 4,249 distinct postcodes covering 5,221 / 5,293 settlement rows (98.6% resolution). Joined to data/settlements.json from the data.egov.bg BG Post register; built by scripts/parliament/build_postcode_ekatte.ts, watched monthly by scripts/watch/sources/bgpost_postcodes.ts. Consumed by scripts/declarations/parse_registered_office.ts for HQ-resolution on MP declarations |
parliament/companies-by-ekatte/ (retired) |
RETIRED — served live from Cloud SQL by place_mp_companies() (migration 151, /api/db/place-mp-companies) since mp-tr-edges-pg-v1. The shards matched an MP NAME against Commerce-Registry officers with no people-per-name guard; the route reads the gated person layer instead, covering 1,332 settlements against the shards' 176. Gitignored, still on disk for a data-test comparison, still present on the bucket until an operator runs the gsutil -m rm -r in scripts/bucket_sync_paths.ts |
YYYY_MM_DD/dashboard/demographic_scatter.json |
Per-municipality vote totals for the /demographics vote↔demographics scatter (joined client-side to the census municipalities) |
procurement/ |
Public-procurement contracts from АОП via data.egov.bg (plus a ЦАИС ЕОП storage.eop.bg gap-fill for the ~900 small authorities — mostly schools — the OCDS feed omits): per-month Contract[] shards under contracts/<YYYY>/<YYYY-MM>.json, per-contractor and per-awarder rollups (each awarder rollup carries an enriched address + geo: {ekatte, confidence, tier, isLocalHQ} block — see scripts/procurement/resolve_ekatte.ts + awarder_tier.ts), an MP cross-reference (derived/mp_connected.json chamber-wide aggregate, derived/top_contractors.json, derived/flow.json, derived/awarder_concentration.json), the slim derived/contractors_search.json ({eik,name} for all ~26k contractors — powers the dashboard's company-name search box), per-MP shards under derived/per-mp/<mpId>.json (each carries the MP's connected entries + a pre-computed scorecard: { value, rank, cohortSize, cohortMedian } so the candidate page never loads the chamber-wide aggregate), derived/per-mp/index.json (manifest of MP ids with shards + cohort stats for the no-connections case), the АОП debarred-suppliers register snapshot (debarred.json, merge-on-write so historical entries persist after the upstream purges them), by_settlement/ (per-EKATTE rollups of local-tier procurement plus _national.json for central buyers and index.json for the landing page), plus index.json + bundles.json. The shared data/ekatte_index.json (5,267 settlements with postal codes, distinct from the election-driven settlements.json) feeds the buyer-HQ resolver. |
funds/ |
EU-funds beneficiary register from ИСУН 2020 (2020.eufunds.bg): index.json (corpus totals, by-organisation-type and by-public/private-law breakdowns, top beneficiaries, MP crossReference summary), beneficiaries/<0-9>.json + _x.json shards keyed on EIK last digit — one row per organisation with contracts signed, funds contracted and funds actually paid, all EUR — derived/mp_connected.json, the MP cross-reference (beneficiaries tied to sitting MPs by a declared stake or management role), and per-MP shards under derived/per-mp/<mpId>.json + derived/per-mp/index.json (same pattern as procurement — candidate-page tile reads one tiny shard instead of the chamber-wide aggregate). The sibling projects sub-tree at funds/projects/ is the contract-grain version of the same corpus: index.json (corpus totals, location-kind histogram, per-programme + per-status rollups, shard listings), multi_location.json (region / national / unresolved contracts kept together since they can't be attributed to a single муни), muni-map.json (denormalised one-row-per-муни payload backing the /funds choropleth — with a synthetic SOF00 row aggregating Sofia's S22 + S23xx/S24xx/S25xx obshtinas, per-capita against Census 2021), and per-shard files under by-ekatte/<ekatte>.json / by-muni/<обshtina>.json / by-program/<code>.json / by-eik/<eik>.json (bulky, ~49k files, shipped to the GCS bucket only — same convention as procurement). Alongside every full per-place shard the ingest also emits a slim {place}-summary.json (~3-5 KB) carrying just rollup + top-3 contracts + top-3 programmes + per-capita rank within the oblast — the муни summary backs the My-Area EU-funds tile and the AI placeEuProjects tool so neither loads a 20 MB shard for Sofia (money for a contract spanning N municipalities is split 1/N per муни, so per-place totals never double-count). Each contract carries beneficiary EIK + name, programme, total / grant / paid amounts (EUR), status, duration and a resolved ProjectLocation (kind: settlement / muni / region / national / unresolved). |
budget/ |
State-budget data: kfp.json (consolidated execution time series + monthly snapshots from data.egov.bg КФП), facts/ (per-ministry BudgetFacts at law/amendment/execution stage, admin + program grain), classification/ (administrative + program registries), reconciliation/ (law → amended → executed roll-up), ministries/ (per-spending-unit slices the ministry screen reads one file from), derived/ (admin-flow Sankey, plan-vs-actual variance), documents.json (law + amendment + execution document index), crosswalk-overrides.json, index.json, plus revenue_breakdown/ — itemised revenue-side sub-flows from non-KFP sources: customs/<year>.json (excise + import VAT + customs duties + top-5 country-of-origin from Митническа хроника, 2022-2025; sub-product detail for 2025), vat/<year>.json (declared net domestic VAT by 21 КИД-2008 sectors from НАП Table 3, 2024), pit/<year>.json (PIT by income type + by sector from НАП Tables 8/9/10 + narrative, 2024); noi/funds.json (НОИ social-security fund execution — ДОО / УчПФ / ГВРС, from the monthly B1 files), noi/pensions.json (the /pensions view — per-oblast average pension + cash-vs-bank, the pension size distribution, and the national wage/income/pension series, from the НОИ yearbook ZIP) and kfn/funds.json (КФН private pension funds, pillars 2 & 3 — per-fund net assets + insured); and nzok/ (the НЗОК health-fund pack from nhif.bg: budget.json = annual budget-law breakdown, execution.json + execution_history.json = monthly B1 cash execution since 2022, hospital_payments.json + hospital_reimbursement_by_eik.json = per-facility payments (БМП / drugs / devices streams) joined via the Рег.№→EIK crosswalk hospital_eik.json, hospital_revenue.json = private-hospital annual revenue 2019–2024 recovered from the filed ГФО in the Търговски регистър (portal.registryagency.bg, Gemini OCR; opt-in --revenue — public/state hospitals report ЕЕОФ instead) and public_private.json = the "Частни болници и обществените поръчки" band (ownership split of НЗОК money + the 50%-publicly-funded threshold + who runs ЗОП tenders — the EC-lawsuit view), drug_reimbursement.json = INN/ATC drug reimbursement + full-year YoY growth movers, drug_unit_prices.json = per-hospital drug UNIT prices at pack identity from Справка 5 (gitignored, PG-served via migration 052), hospital_financials.json = ЕЕОФ hospital financials from МЗ, 26 quarters (gitignored, migration 051), activities.json = clinical-pathway activity corpus / cases + ЗОЛ per procedure per hospital (gitignored — the committed activities_overview.json is the compact companion — PG-served via migration 053, with a pathway-internal cases-per-bed outlier + the migration-059 pathway tree "which hospitals bill this КП"), pathway_tariffs.json = НРД clinical-pathway list prices (opt-in --pathway-tariffs, BG-egress; PG-served via migration 059 for the pathway spend tree + the case-mix expected-vs-actual signal — empty until ingested); the report-card + decile-fan (migration 056), reporting-coverage (058) and drug-savings-leaderboard (060) views are PG-only functions over the financials + drug-price corpora above; and vss/budget.json (the судебна власт budget as adopted in each ЗДБРБ, 2018–2025: own revenue incl. съдебни такси + the per-body expenditure split across ВСС / ВКС / ВАС / прокуратурата / съдилищата / НИП / ИВСС — feeds the judiciary sector pack on /awarder/121513231) |
person/ |
The file inputs to the unified person-identity layer: sanctions.json — a hand-curated register of Bulgarian individuals under official OFAC/EU sanctions (program, authority, date, and the OFAC-search URL), attaching to a person ONLY via a stable mpId so a name-ambiguous designee is documented but never publicly attached — and ds.json, the parallel hand-curated register of Комисия по досиетата (comdos.bg) findings of established affiliation to State Security (решение № + date, collaborator category, псевдоними), attaching ONLY via a stable mpId AND an exact birth-date match so a same-named namesake (e.g. the решение-14 Красимир Каракачанов born 1937 vs the current ВМРО MP born 1965) is documented but never publicly attached. Everything else the person layer resolves lives in its source datasets (parliament, candidates, officials, judiciary, financing) and in Postgres (person/person_role/person_alias/person_review_candidate, rebuilt by scripts/person/resolve_persons.ts; PG-only, no serving JSON). |
person/ |
The file inputs to the unified person-identity layer: sanctions.json — a hand-curated register of Bulgarian individuals under official OFAC/EU sanctions (program, authority, date, and the OFAC-search URL), attaching to a person ONLY via a stable mpId so a name-ambiguous designee is documented but never publicly attached — and regulators.json — a hand-curated register of the current members of the independent / regulatory bodies (Конституционен съд, Сметна палата, КФН, БНБ Управителен съвет, СЕМ, КЗК, Омбудсман), each seat attaching ONLY via a stable mpId OR a name confirmed globally-unique, so a name-ambiguous member stays resolved:false and is never mis-attached. Everything else the person layer resolves lives in its source datasets (parliament, candidates, officials, judiciary, financing) and in Postgres (person/person_role/person_alias/person_review_candidate, rebuilt by scripts/person/resolve_persons.ts; PG-only, no serving JSON). |
judiciary/ |
Court caseload, delays and workload from the ВСС annual statistical tables: caseload.json (per year × court tier — filed / resolved / within-3-months / pending / appealed, judge posts, and both official workload measures) and court_load.json (the same measures per INDIVIDUAL named court, geocoded, for the per-court натовареност map; served from Postgres). Plus declarations.json — an index of the ИВСС magistrate asset-declaration register (who filed, when; 46,528 declarations 2017–2025) and the Inspectorate's non-compliance lists — and magistrate_holdings.json, a deeper parse of the same declaration PDFs for magistrates' declared companies and informational financials (loaded into Postgres). Drives the /judiciary dashboard. |
defense/ |
The /defense (Отбрана) national-defence view: gdp_share.json (NATO Bulgaria %GDP series 2014–2025 + the 2%/3.5%/5% targets), category_split.json (equipment/personnel/other, NATO Table 8a), exports.json (Ministry-of-Economy arms-export euro series + cumulative-since-invasion), programs.json (the curated F-16 / Stryker / MMPV / ammo-JV mega-programs, US FMS — not in the procurement register), readiness.json (personnel vacancy + reserve fill + the МО budget personnel/capital split). Small committed JSON; drives the /defense dashboard and the AI defense tools. The МО procurement pack (25-unit group) is separate — it renders off the live contracts corpus on /awarder/000695324. |
energy/ |
The /sector/energy (Енергетика) physics tiles beside the БЕХ procurement pack: generation.json (electricity generation mix by fuel, net electricity trade, CO₂ intensity, 2007–latest, from Ember's Yearly Electricity Data, CC BY 4.0) and prices.json (household electricity price, Eurostat nrg_pc_204) + gas_prices.json (household natural-gas price, Eurostat nrg_pc_202) — each BG vs EU27 plus the RO/GR/HU/HR neighbour peers, all taxes, EUR/kWh, driving the /consumption/electricity & /consumption/gas trend pages — and plants.json (the asset-level power-plant fleet — coal/nuclear/hydro/gas/RES with capacity, owner, ownership state-vs-private and retirement year; CURATED from Global Energy Monitor + the corpus, like defense/programs.json). Small committed JSON (skill update-energy, watchers ember_generation + eurostat_energy_prices; plants curated, not watched). The БЕХ procurement pack (9-EIK state-energy group) is separate — it renders off the live contracts corpus on /sector/energy + /awarder/831373560. |
police/ |
The /sector/security (Сигурност / МВР) outcome data beside the МВР procurement pack: road_safety.json (national road-traffic-death series 2011–latest from Eurostat sdg_11_40, unit=NR/TOTAL — 708 peak 2015 → 478 in 2024, −32%; precomputed latest/peak/change facts). Small committed JSON (script scripts/security/fetch_road_safety.ts, watcher eurostat_road_safety); drives the road-safety tile on /sector/security and the AI securityRoadSafety tool. The МВР procurement pack (~75-EIK group) renders off the live contracts corpus + the awarder-group-top-contracts PG route; the per-oblast crime scatter reuses data/regional.json (no ingest). |
environment/ |
The /sector/environment (Околна среда / МОСВ) outcome data beside the МОСВ procurement pack: waste.json (Eurostat cei_wm011 municipal-recycling rate + env_wasmun waste-per-capita, BG + EU27 + RO/HR/HU) — the recycling-vs-EU-target tile (BG 16.7% in 2023 vs the 55% 2025 Waste Framework Directive target). Tiny committed JSON (scripts/environment/fetch_waste.ts on the eurostat_env watcher; no bucket sync in dev, no PG). The air half of the view reuses data/air/index.json (update-air-quality), the EU-funds tile reads the ИСУН absorption payload from Postgres fund_payloads (kind=absorption, by OP code — not the unmaintained static data/funds/derived/absorption.json), and the GF05 EU-peer strip rides data/cofog.json — so the 27-EIK procurement group + these small outcome series render with no new procurement ingest. |
transport/ |
The /sector/transport (Транспорт) subsidy data beside the state-transport procurement pack: rail_subsidy.json (state rail subsidy — БДЖ PSO + НКЖИ operating/capital — parsed from the cached State Budget Law HTML, 2018–latest, хил.лв→EUR at 1.95583) and rail_ridership.json (national rail passengers + passenger-km from Eurostat rail_pa_total). Small committed JSON (scripts/transport/parse_rail_subsidy.ts on the budget_law watcher + scripts/transport/fetch_rail_ridership.ts on the eurostat_rail watcher); drive the subsidy-per-passenger tile (€5.55/pax 2025) + the AI railSubsidy tool. The transport group's procurement (~€5.9bn, 11 EIKs) + the infrastructure marker map render off the live contracts corpus (transport_project_map PG route — towns named in contract titles); АПИ roads are a separate sector. |
customs/ |
The Агенция „Митници" excise-warehouse register (BACIS licensing feed) behind the Митници pack's register band + the /customs/warehouses page: excise_register.json (one row per licensed operator — excise-goods categories from the CN codes, active warehouse count, status, and public-procurement footprint joined from contracts_list; GCS-served, read by the register table + the exciseRegister AI tool) and excise_warehouses.json (one row per active warehouse, geolocated to its own address's settlement centroid — the input to the bonded-warehouse count map, loaded into the excise_warehouses Postgres table / excise_warehouses_map serving fn, migration 072). Both written by scripts/customs/excise_register.ts (npm run customs:excise-register), watched monthly by customs_excise_register. |
financing/ |
Court of Audit party-financing artifacts: index.json (annual-report year catalogue scraped from bulnao.government.bg), and reports.json + reports-summary.json (per-year, per-party annual-report filing-status catalogue — on time / late / non-compliant / not filed — crawled from the gfopp register; the summary carries per-year counts only, for the governance-page tile) |
census_2021.json, census_2021_settlements.json |
Census aggregates |
problem_sections_stats.json |
Risk-neighborhood summary stats |
Files that stay on Firebase Hosting (under /public/):
| Path | Contents |
|---|---|
articles/ |
Long-form .md content + image attachments (rendered by react-markdown) |
og/ |
Pre-generated Open Graph share images |
fonts/ |
Self-hosted Inter + Fraunces .woff2 |
sitemap_*.xml, robots.txt, llms.txt, llms-full.txt |
Crawler artifacts (must be at site origin) |
favicons, app icons, site.webmanifest |
PWA + browser chrome |
The CLI entry point is scripts/main.ts (cmd-ts). Flags select which stages to run:
| Flag | Stage |
|---|---|
--all / -a |
Run every stage below |
--prod / -p |
Minify output JSON (otherwise pretty-printed for diffability) |
--date / -d |
Restrict to a single election date YYYY_MM_DD |
--election / -e |
Restrict to a single named election |
--reports / -r |
Anomaly reports (concentration, turnout, top-gainers/losers, invalid, recount, problem sections) |
--stats / -s |
National aggregates |
--search / -c |
Full-text search indices |
--financing / -f |
Smetna Palata campaign financing |
--parties |
Per-party regional/municipal/settlement aggregations + vote swings |
--machines / -m |
SUEMG voting-machine flash-memory corrections |
--candidates / -n |
Candidate preferences |
--declarations |
MP financial declarations + Commerce Registry → connections graph |
--flows / -w |
Vote-transition matrices between consecutive elections |
--local-flows |
Local-council vote-transition matrices between consecutive local cycles (manual derived step — not in --all; run after a new local cycle is ingested, mirroring --flows) |
--prevote-flows |
"Pre-vote" flow: estimated vote transition from the most recent parliamentary vote before each local cycle into that cycle's council ballot → data/transitions_prevote/ (national + oblast scope; reuses the vote-flow engine, joining the two cycles' sections on (obshtina, last-7-digits) since parl/local section codes diverge). Manual derived step — not in --all; run after a new local cycle is ingested, mirroring --local-flows |
--local-place-trends |
Per-place cross-cycle trends (council party share + mayoral winner per cycle) for the settlement and район dashboards → data/local_place_trends/{s,r,p}/<key>.json (one small shard per settlement / Plovdiv-Varna район / Sofia район). Reads the per-município section detail files. Manual derived step — not in --all; run after a new local cycle is ingested |
--coords / -g |
Backfill polling-section GPS coordinates |
--local-coords |
Stamp GPS + building address onto the local-election section index from the parliamentary section archive (settlement-name-gated join, ~98% coverage; powers the per-município section map). Idempotent; also folded into --all. Re-run after a new local cycle or a new parliamentary election lands fresh coordinates |
--local-problem-sections |
Flag the curated Roma-neighborhood "problem sections" inside the local council data → data/<cycle>/problem_sections.json per regular _mi cycle (powers the município dashboard's Risk votes / Problem votes by party tile, the council analogue of the parliamentary block). Reuses scripts/reports/problem_sections/neighborhoods.ts; matches local sections by EKATTE + address + section prefix (NOT the parliamentary section codes — МИР vs NSI prefixes diverge). Idempotent; also folded into --all. Must run after --local-coords (the address keyword match needs the stamped address) |
--summary / -u |
Summary-only report regeneration |
npm run prod runs tsx scripts/main.ts --all --prod.
Pipeline subdirectories of note:
parsers/— CIK results, party canonicalization, candidate dedupparliament/— scraper for MP photos (re-encoded to .webp via sharp), bios, term history from parliament.bgparliament/rollcall/+scrape_rollcall.ts— roll-call vote ingest (per-session CSVs from stenogram attachments →data/parliament/votes/sessions/<date>.json). Also emits per-itemitemSlugs(deterministic${itemNo}-${slugified-title}, slug source is the re-vote-normalized title so a motion + itsпрегласуванеshare one URL) anditemTopics(8-tag coarse classifier —budget/zid/zkpo/tax/ratification/personnel/constitution/confidence_vote/other). One-off backfill:backfill_topics_slugs.ts --backfillrewrites both fields onto existing session JSONs without re-scrapingparliament/derived/— recomputed weekly from session JSONs. Emitsloyalty.json,attendance.json(per-MP present-vs-absent counts, byNs envelope ~40 KB gz, drives the /parliament hub most-absent/most-present tiles + the /parliament/attendance screen +mp_scorecard_attendance),similarity.json(withbottomKfor "most-different peers"),cohesion.json,embedding.json,party_correlation.json,topic_index.json(cross-session vote-title index for global search + the/votescontested-feed),dissents.json(per-MP recent defections, capped at 50 most-recent per MP),party_pair_breaks.json(top-20 contested items per unordered party pair, for the/votes/between/:pairdrill-down), plus per-MP shards underper-mp/<ns>/<mpId>.jsonbundling loyalty + attendance + dissents + similarity for one candidate page (idempotent write — file-content compare, prune stale)declarations/— Court-of-Audit property/interest filings + Commerce Registry → MP↔company graph, rankings, per-MP 1-hop subgraphssmetna_palata/— campaign financing parsingpolls/— Wikipedia scrape + accuracy analysis + narrative generationparties/— per-party data bundling, AI-generated campaign retrospects, andbuild_demographics.ts(per-party vote↔demographics Pearson correlations across the 265 municipalities → the scatter, fingerprint tile and cleavages tile)voteFlows/— transition matrices between consecutive electionsmachines_memory/— SUEMG flash-memory correctionsmacro/— Eurostat + World Bank + curated economic / governance indicators (with absolute-floor + 10% regression check per indicator); alsofetch_bnb_auctions.ts, which scrapes the BNB Fiscal Agent auction archive (https://www.bnb.bg/FiscalAgent/FAGSAuctions/FAAuctionResults/) intodata/debt-emissions-domestic.json— one row per auction event, handles both EUR and BGN, flags bids-rejected auctions, no parser failures across 2019-present;fetch_bnb_fdi.ts, which downloads the БНБ monthly FDI-by-investment-type SpreadsheetML export (balance of payments, BPM6) and writesdata/macro_fdi.json— the monthly net inward-FDI flow split into equity / reinvested earnings / debt instruments back to 2010, plus the year-to-date cumulative vs. the same period a year earlier (the figures the euro-adoption coverage cites; powers the FDI tile on/indicators/fiscal); andfetch_fiscal_reserve.ts, the multi-source fiscal-reserve ingest. The Bulgarian Ministry of Finance publishes the фискален резерв figure in three parallel filename series onminfin.bg/upload/—mreport_<Month><YYYY>_bg.pdf(Институт за анализи и прогнози monthly economic review with a КФП summary table carrying ~12 months of rolling values),BULETIN_<MonthName>_<YYYY>.pdf(Информационен бюлетин: Изпълнение на държавния бюджет, narrative press-bulletin with the single end-of-month figure inline), andFRA-MM-YYYY-BG.xlsx(authoritative single-month spreadsheet from the dedicated/bg/statistics/4page). Live minfin.bg is Cloudflare-WAF blocked, so the script reads everything indirectly via Wayback Machineid_URLs; XLSX months past the Wayback cutoff (~Apr 2025) drop manually intodata/_cache/minfin_fr_xlsx/(see the README there). All three sources flow into a per-month MEDIAN-of-votes step so a single misaligned PDF reading can't poison the series. The XLSX parser detects unit ((млн. лв.)vs(млн. евро)) and normalises across the 2026-01-01 euro adoption boundary using the fixed currency-board rate; the round-trip is exact and the chart is continuous across the transition.regional/— oblast-level indicators.fetch_eurostat.tspulls Eurostat NUTS 3 series (per-oblast floor + 10% regression check; includes the recorded-theft ratecrim_gen_regand a derived active-enterprise densitybd_size_r3÷ population);fetch_az_oblast.tsthen merges the АЗ long-term-unemployment share; andfetch_nsi.tsmerges three NSI JSON-stat open-data datasets (FDI per capita id=629, museum visits id=844, hospital beds id=1206), each normalised per-capita against the population series — all into the samedata/regional.json(run all three in order — the skill does). NSI'sNUTSgeo dim reuses the shared NUTS3→oblast map inoblast_map.ts; the generic JSON-stat reader isscripts/lib/jsonstat.ts. Drives the/municipality/<code>drilldown tile and the/demographicsregional choropleth.indicators/— Annual sub-national indicators pulled from multiple BG sources (AZ годишен обзор for registered unemployment, МОН via data.egov.bg for DZI scores, НСИ timeseries XLSX for year-over-year population change andnsi_vital.tsfor natural population change + net migration). Source-pluggable:sources/<source>.tsfiles normalise to a common shape,_name_aliases.jsoncarries the manual code/name overrides,build.tsmerges todata/indicators.jsonplus per-municipality slices. Floor + match-rate safety checks per source.grao/— ГРАО settlement-level registered population.fetch.tsresolves the latest quarterlyt41nmtable from grao.bg, decodes Windows-1251, parses the per-municipality blocks, joins settlement names → EKATTE viasettlements.json, and writesdata/grao_population.json+ per-municipality slices.census/— NSI Census 2021 ingestion (build_census.tsparses the NSI XLSX into country/oblast/municipality JSON + the settlement sidecar + per-entity slices)procurement/— АОП public-procurement ingest.ingest.tswalks the data.egov.bg dataset listing for the АОП org, downloads each fortnight bundle (cached gzipped underraw_data/procurement/), normalizes the OCDS releases into flatContractrows vianormalize.ts, writes month-shards underdata/procurement/contracts/, then rebuilds per-EIK rollups (rollups.ts), MP cross-reference (cross_reference.ts, EIK-keyed against the gated MP↔company link set —scripts/lib/mp_linkage.ts, at itscontractorsscope, since this join's population IS contractors), and the journalism payload (derived.ts— top contractors + sankey-shaped flow).ingest_legacy.tshandles pre-OCDS annual CSV dumps (2011-2023). Canary fixture + diff-cap + amount sanity checks invalidate.ts.rollups.ts(and the per-NSby_ns.ts) excludecontractAmendmentrows from money + count totals — the feed republishes a contract's amended value as a separate release, so summing them double-counts (it had inflated АПИ ~35% / the corpus ~7%); amendments are still kept on the per-contract detail shards. An АПИ road dashboard (/procurement/roads) reads these rollups + parses road references / km-chainage from contract titles client-side (src/data/procurement/roadAttributes.ts); its hero map uses one-off OSM geometry (see One-off backfills below). Tender-stage (procedures, not signed contracts):ingest_tenders.ts(normalizernormalize_eop_tender.ts, raw-record typeeop_tender_types.ts) ingests the ЦАИС ЕОП flat "поръчки" feed atstorage.eop.bg/open-data-<YYYY-MM-DD>/— the same daily buckets as the договори gap-fill, NOT behind the data.egov.bg 403 — into a paralleldata/procurement/tenders/tree (month-shards +by-tender/shard/hash shards keyed on УНП +by-ocid/shard/contract→tender lineage +index.json). OneTenderper УНП with nested lots; the estimated (прогнозна) value is a forecast and is quarantined — it never enters any contracted-spend aggregate. Lineage to the signed contract is free:ocid = ocds-e82gsb-<parentTenderId>(the OCDS contract's own ocid). Incremental run isingest_tenders.ts --apply(last ~30 days); the full 2020→ history is a one-off, flag-gated operator backfill (never in CI):tsx scripts/procurement/ingest_tenders.ts --from 2020-01-01 --to <today> --backfill --apply --upload. Seedocs/plans/procurement-tenders-ingest-v1.md§12.funds/— ИСУН EU-funds ingest.ingest.tsdownloads the public "Бенефициенти" XLSX export from2020.eufunds.bg(cached underdata/_cache/funds/),parse.tslocates the table by header match and flattens the ~53k organisation rows intoFundsBeneficiaryrecords, then the ingest shards them by EIK last digit and rebuildsdata/funds/index.json.cross_reference.tsjoins the beneficiary EIKs against the gated MP↔company link set (scripts/lib/mp_linkage.ts) at its unrestrictedallscope — an MP-linked company that took EU money and never won a public contract is exactly the row to report here, and the contract-restricted scope answers only 43 of this payload's 303 pairs — writing the MP-tied payload toderived/mp_connected.json. The siblingprojects_ingest.tsruns the same flow for the contract-level "Проекти" register:projects_fetch.tspulls the XLSX,projects_parse.tsflattens the ~80k contracts,projects_resolve.tsresolves each row'sМестонахождениеagainstdata/settlements.json+data/municipalities.json(paren-aware comma split,(общ.X, обл.Y)hint parsing, multi-target alias map for the Sofia / Добрич quirks, HQ-oblast tiebreaker for ambiguous settlement names), and the orchestrator emits the per-EKATTE / per-муни / per-EIK / per-programme shards plus the slim per-place summaries andmuni-map.json. Per-capita € usesdata/census_2021_settlements.jsonas the denominator. Full rebuild every run — both exports are full snapshots, not event feeds — with a header-schema guard + row-count floor in lieu of a canary.ngo/— Non-profit legal entities (ЮЛНЦ — сдружения / фондации / читалища). DB-only, no static JSON. The register itself needs no new ingest: it rides the shared ТРРЮЛНЦ open-data feed already pulled by the Commerce Registry watcher, so the TR parser (scripts/declarations/tr/) now also captures NGO governing bodies (управителен съвет, представляващи, читалищни настоятелства, проверителни комисии), objectives + public/private-benefit status and member nationality;db:load:tr:pgderivesentity_class/ngo_type, loads thengo_detailstable + acountrycolumn, and builds the awarder K-Index matview (039_awarder_kindex.sql— share of a buyer's contract value going to politician / NGO-board-linked suppliers, a Hlídač-státu-style scored signal).load_ngo_funding_pg.tsloads the unifiedngo_fundingtable (migration040): EU directly-managed funds from the EC Financial Transparency System per-year XLSX (raw_data/ngo_funding/fts/,ec_ftswatcher) plus curated State-Budget subsidies (data/ngo/budget_subsidies.json), matched to a BG EIK via VAT → exact folded-name → fuzzy-trigram. Surfaced on/company/:eik(NGO tile, external-funding tile, K-Index tile), the/procurement/ngosbrowser, and the conflict-of-interest links when a person in power sits on an NGO board. Run:npm run db:load:ngo-funding:pg. Seedocs/plans/ngo-final-implementation-plan.md.budget/capital_programs/— Per-município annual капиталова програма ingestion. Twenty-six per-município parsers cover the fleet; the seven format-defining ones —sofia.ts(XLSX from sofia.bg, район-tagged in free text viasofia_rayons.ts),plovdiv.ts(PDF from plovdiv.bg, pdfjs positional reader with vertical-text col-A reassembly viaplovdiv_rayons.ts),burgas.ts(XLSX from burgas.bg, 7-column funding-source breakdown, village + Wikipedia-aligned city-quarter extraction),stara_zagora.ts(PDF from starazagora.bg, pdftotext-layout line-based extraction with fragment rejection),ruse.ts(multi-sheet XLSX from obshtinaruse.bg — one sheet per spending unit + dedicated per-kmetstvo sheets, so per-settlement attribution is via workbook structure),varna.ts(two-step:varna_ocr.tsGemini Vision OCR of the rasterized PDF first, then deterministic rollup viavarna_rayons.ts),pleven.ts(two-step: slice the 63-page budget docket down to the 8 capital pages withpypdf, thenpleven_ocr.tsGemini Vision OCR — Прил. №4 general + Прил. №10А EU — then deterministic rollup with per-settlement + per-funding-source dimensions). Each writesdata/budget/capital_programs/{year}/{muni}.jsonwith a common shape (recap total + projects list + per-район or per-settlement rollup) consumed by per-município tiles on the settlement-page "финанси" section. Thecapital_programswatcher (one source covering all 26 municipality URLs) detects re-uploads / new years; each parser runs separately astsx scripts/budget/capital_programs/<muni>.ts --year <year>. The full per-city roster + per-format notes live under "Other government and public sources" above.budget/— Bulgarian state-budget ingest.ingest.tsis the CLI entry point; four pillars share it.kfp.tsparses the data.egov.bg КФП feed (monthly consolidated execution snapshots →kfp.json).law_html.tsparses each year's State Budget Law from Държавен вестник HTML into per-spending-unit appropriations (admin + program grain atstage: "law") plus the Чл. 1 framework totals (planned revenue tree, Section II/III/IV headlines →derived/law_framework.json).execution_pdf.ts/execution_borderless_pdf.ts/execution_xlsx.tsparse each ministry's "Отчет за изпълнението на програмния бюджет" (PDF or XLSX-in-ZIP) — hand-curated inEXECUTION_REPORTSper fiscal year — and emitstage: "amendment"(уточнен план) +stage: "execution"(отчет) facts.headcount.ts/headcount_docx.tsre-parse the same fetched bytes for the per-programme "Численост на щатния персонал" row + Персонал spend (PDF, XLSX, DOCX and DOCX-in-ZIP formats supported viafetchExecutionDocx+fetch_sources_docx.ts);doklad.tsfetches the annual "Доклад за състоянието на администрацията" PDF from iisda.government.bg (years 2017-2025 curated inDOKLAD_FILE_IDS) and extracts the national-aggregate prose totals + Table 1 structure counts + Table II-1 NSI list-headcount via year-tolerant regex;personnel_facts.tsjoins per-ministry headcount summaries with the Доклад aggregates and writes the singledata/budget/personnel.jsonthe frontend reads.reconcile.tsjoins law + amendment + execution at admin and program grain;facts.tsbuilds economic-grain plan-vs-actual variance from the КФП feed;cross_reference.tsjoins spending units to procurement awarders;derived_admin_flow.tsbuilds the Sankey payload (per-ministry planned + framework planned revenue/transfers/EU/balance so the admin view tells a coherent planned-vs-planned story);ministries.tsslices per-ministry rollups so the ministry screen loads one small file. Nine pinned canaries invalidate.ts(КФП resource, law year, four execution-report formats, economic facts, plus the personnel triad — headcount-PDF, headcount-XLSX, Доклад). Two further scripts feed the tax-policy simulator:run_consumption_coicop.tsfetches household consumption by COICOP purpose (Eurostatnama_10_co3_p3+nama_10_gdpP31_S14 totals — each fetch anchor-validated againstmacro.jsonGDP because post-changeover Eurostat re-denominates BG "national currency" series to euros dataset-by-dataset) intodata/budget/revenue_breakdown/consumption.json, andrun_policy_baseline.tsjoins the КФП December snapshots, the НАП PIT split, the Митническа хроника excise split (revenue_breakdown/customs/<year>.json— diesel/petrol/tobacco/alcohol lines that anchor the per-product excise levers; acustoms_revenueflip must re-run this script), a livegov_10a_taxagcontributions fetch and the consumption slices intodata/budget/derived/policy_baseline.json— self-validating (calibration-factor drift gate ≤12% + an engine round-trip guard againstsrc/lib/bgTaxPolicy.ts, the shared COICOP→VAT-regime map). Both run together asnpm run budget:policy-baseline.run_policy_baseline.tsalso emits anexpendituresection powering the simulator's spending levers — pension mass + Swiss-rule indexation inputs (НОИfunds.json+macro.jsonCPI/wage growth), administration positions/vacancies (personnel.json), КФП Персонал + Капиталови lines, a curated NATO-definition defense %, the НОИ quarterly STATB pension-size distribution (минимална-пенсия lever, parsed with SheetJS and cached underraw_data/budget/), and the teachers' 125%-peg anchors (Eurostateduc_uoe_perp01headcount + NSI open-data id=612 wages) — plus a projected next-year GDP for the deficit-as-%-of-GDP panel. Theeurostat_policywatcher fingerprints the five upstream Eurostat datasets (incl.educ_uoe_perp01); thepolicy_baseline_localwatcher covers the НОИ STATB bulletin (flagging when a new quarter publishes so the curatedNOI_STATB_URLgets bumped) and the NSI id=612 wage release.financing/— Сметна палата party-financing scrapers.scrape_index.tsparses the annual-report year catalogue off bulnao.government.bg →data/financing/index.json.scrape_reports.tscrawls the gfopp WebForms register — opening an ASP.NET session per year, paginating each of the four per-status GridViews — for the per-party annual-report filing-status catalogue →data/financing/reports.json+reports-summary.json. Fails loud on truncation (empty newest year, identical per-year counts, sub-floor totals).watch/— Tier-1 daily watcher (63 upstream sources fingerprint-diffed → daily markdown report underdata-reports/+ per-source state understate/watch/, see "Continuous data refresh" below; includesiisda_dokladfor the annual administration report, the extendedministry_execution_reportscovering PDF/XLSX/DOCX formats,minfin_program_otchetpolling Wayback for МФ programme-budget execution PDFs, andmfa_program_otchetdoing the same for МВнР programmatic ZIPs — both cover JS-rendered ministry listing pages that direct HEAD probes can't enumerate)lib/upload.ts— shared GCS upload helpers (gzipped text viagsutil cp -Z, binaries as-is)lib/ingest-state.ts— per-skill ingest-marker helpers consumed byscripts/stamp-ingest.tsand the/process-watch-reportorchestratorfonts/fetch-fonts.mjs— one-shot fetcher for self-hosted Inter + Frauncesreports/,party_stats/,preferences/,search/,stats/,recount/— analytical and aggregation stagesog/,prerender/,sitemap/,images/,llms/— build-time output (run frompostbuild)
Some narrative content is generated with LLMs:
polls:gen-analysiscalls Anthropic Claude (requiresANTHROPIC_API_KEY)party:gen-retrospectcalls Google Gemini (requiresGEMINI_API_KEY)
Both are written to JSON consumed by the SPA — there are no LLM calls at runtime.
For contributors using Claude Code, the repo includes project-specific skills under .claude/skills/ for the recurring data-refresh workflows:
| Skill | What it does |
|---|---|
process-watch-report |
Orchestrator. Compares state/watch/*.json against state/ingest/*.json and runs every tier-2 skill whose mapped sources have changed since its last successful ingest. Survives multi-day gaps. |
update-connections |
Refresh MP declarations + Commerce Registry, rebuild the connections graph, flag suspicious declared values. |
update-officials |
Refresh non-MP declarations (cabinet, state-agency heads, regional governors) from the same Court-of-Audit register; reuses the MP declarations parser, writes per-slug files + index.json + assets-rankings.json under data/officials/. Step 1b additionally ingests the municipal tier (mayors, councillors, etc.) into data/officials/municipal/. |
update-polls |
Scrape new polls from Wikipedia, recompute accuracy, write the per-election narrative. |
update-rollcall |
Ingest new parliament.bg roll-call vote sessions. Validates against a canary fixture; tracks unresolved MP ids without dropping them. |
update-financing |
Refresh the Сметна палата party-financing data: scrape_index.ts for the annual-report year index (data/financing/index.json), and scrape_reports.ts for the per-party filing-status catalogue (data/financing/reports.json — on time / late / non-compliant / not filed, 2011 onward, crawled from the gfopp register). |
update-macro |
Refresh data/macro.json from Eurostat + World Bank + curated tables. |
update-regional |
Refresh data/regional.json — Eurostat NUTS 3 per-oblast GDP/capita, population, net migration, recorded-theft rate and derived active-enterprise density, plus long-term-unemployment share from Агенция по заетостта and three НСИ JSON-stat open-data series (FDI per capita, museum visits, hospital beds). Runs the Eurostat fetch, then the АЗ merge, then the NSI merge. |
update-indicators |
Refresh data/indicators.json — annual per-municipality indicators: registered unemployment (Агенция по заетостта), DZI matura scores (МОН via data.egov.bg), and natural population change + net migration (НСИ vital statistics). Source-pluggable; new annual sub-national indicators slot in with one source file. |
update-landuse |
Refresh data/landuse/index.json — per-oblast land-use composition (8 categories + population density) parsed from НСИ's annual "Баланс на територията" press-release annex, computed off АГКК's digital cadastral map. Operator pastes the new year's PDF URL into LANDUSE_REPORTS when a new release lands. |
update-census |
Rebuild the NSI Census 2021 JSON (census_2021.json, the settlement sidecar, per-entity slices) from the raw NSI XLSX. Event-driven — fresh clone, an NSI re-release, a new dimension, or the 2031 census. |
update-prices |
Refresh the КЗП "Колко струва" euro-adoption retail prices — Postgres-only (migration 048), no data/prices/*.json serving tree. npm run prices fetches each daily ZIP from kolkostruva.bg/opendata, loads ~1.4M store rows into Postgres as an SCD-2 delta (price_facts/price_current/price_grid_days), then rebuilds the ~118k-product canonical catalogue (price_products) and the price_payloads serving blobs (same index / ranking / chains / place shapes the old JSON served) + the top-3k product_slugs.json. Feeds the Consumption/Governance price tiles, the /consumption/products browser, /product/:slug and the euro-verdict tile. Self-reports its /data/updates row (skip the generic append). Daily watcher source kzp_prices. One-off replay to euro-day: npm run prices -- --backfill --from 2026-01-02 (flag-gated operator step, never in the watcher). NOT official CPI — a monitoring basket index. |
update-grao |
Refresh data/grao_population.json + per-municipality slices — quarterly ГРАО settlement-level registered population (permanent + current address), joined to EKATTE. |
update-procurement |
Ingest АОП fortnight OCDS bundles from data.egov.bg into data/procurement/. Normalizes releases into per-month Contract shards, rebuilds per-contractor / per-awarder rollups, runs the MP cross-reference (EIK-joined against the gated MP↔company link set, scripts/lib/mp_linkage.ts) to surface contracts going to companies tied to sitting MPs, derives the awarder→contractor concentration index for the risk-score, and (Step 5) refreshes the АОП debarred-suppliers snapshot via scripts/procurement/debarred.ts. Canary-pinned with a diff-cap; pre-OCDS years (2011-2023) are backfilled via the sibling procurement:ingest-legacy script. EOP gap-fill — the АОП OCDS "обявления" export is a strict subset of what ЦАИС ЕОП itself publishes; the daily flat "договори" feed at storage.eop.bg/open-data-<YYYY-MM-DD>/ lists ~900 small contracting authorities (overwhelmingly schools & kindergartens) the OCDS export omits. scripts/procurement/ingest_eop.ts (normalizer normalize_eop.ts) fetches that flat feed and gap-fills ONLY buyers absent from our corpus (an absent buyer has zero OCDS rows, so no double-count). Watched by the eop_procurement source (fingerprints the latest storage.eop.bg publication day); the incremental run (last ~30 days, ingest_eop.ts --apply) is part of Step 1b of the skill, then procurement:ingest rebuilds rollups/derived from the new shards. The full 2020→ history is a one-off, flag-gated operator backfill (never in CI): tsx scripts/procurement/ingest_eop.ts --from 2020-01-01 --to <today> --backfill --apply. Raw days cache to raw_data/procurement/eop/. Buyer geo-enrichment (Step 1c) — the flat feed carries no buyer address, so gap-fill + legacy-only buyers have no geo and drop out of by_settlement / the my-area place tiles. An EKATTE override map (awarder_geo_map.ts → awarder_geo_overrides.json, applied fill-missing by buildRollups) backfills them from, in order: Tier B МОН school register (data.egov.bg, authoritative schools/kindergartens), Tier E OCDS обявления party addresses harvested by EIK across all parties (build_ocds_party_geo.ts, the biggest reachable lever), Tier D поръчки executionPlaceNuts modal oblast (build_tender_oblast_map.ts, disambiguates the name parse), Tier A awarder-name suffix. Tiers D/E read the same storage.eop.bg buckets as the gap-fill (so eop_procurement covers their freshness); the reachable tiers lift by_settlement local-tier pins 712 → 1,836. See docs/plans/procurement-awarder-geo-v2.md. Risk + feed indices (auto, no new source): the same run also emits derived/cpv_competition.json (per-CPV single-bid baseline that gates the single-bidder flag), derived/pep_connected.json + pep-by-eik/ (officials→contractor links read from company_politicians at kind='official' — the gated person layer — so the trigger is db:load:tr:pg, not an update-officials run), and the slim derived/risk_feed.json + derived/person_procurement_index.json that back the /procurement/flags page, the /procurement/people scanner and the procurementRedFlags AI tool. The single-bidder signal reads release.bids.statistics[]; new fortnights pick it up automatically, and procurement:ingest --renormalize re-applies the parser to every cached bundle to back-fill bid counts on already-ingested rows (manual/periodic — skips the diff-cap, never in CI). Tender stage (Step 1f): the same eop_procurement watcher also covers the ЦАИС ЕОП поръчки feed; scripts/procurement/ingest_tenders.ts --apply refreshes the parallel data/procurement/tenders/ tree (procedures with estimated value/lots/status — a forecast, quarantined from contracted spend) that powers the /procurement/tenders search, the /tenders/:unp detail page and the openTenders/tenderLookup AI tools. Self-contained (no rollup rebuild); full history is a flag-gated --backfill one-off. |
update-funds |
Ingest the ИСУН 2020 EU-funds registers from 2020.eufunds.bg. Step 1 — beneficiary register → data/funds/ (all-time per-organisation rollups: contracts signed, funds contracted, funds paid); runs the MP cross-reference (EIK-joined against the gated MP↔company link set, scripts/lib/mp_linkage.ts, at its unrestricted scope) to surface EU-funds beneficiaries tied to sitting MPs. Step 2 — project register → data/funds/projects/ (per-contract, with implementation location resolved to EKATTE / муни / NUTS-region / national; per-EKATTE / per-муни / per-EIK / per-programme shards). Both paths: full re-export each run, header-schema guard + row-count floor in lieu of a canary. |
update-budget |
Ingest state-budget data into data/budget/. Two ingest paths share one CLI: the data.egov.bg КФП feed (consolidated execution time series + monthly snapshots) and per-ministry "Отчет за изпълнението на програмния бюджет" reports (admin + program grain reconciled against the State Budget Law). Six pinned canaries; admin-grain sanity checks per ministry. |
update-local-elections |
Refresh local-elections (общински избори) data — download ЦИК csv.zip bundles + per-município HTML and rebuild data/<cycle>/. Bypasses Cloudflare via a warmed cf_clearance Playwright session. Run for a new regular cycle or a chmi partial. |
update-local-taxes |
Refresh data/local_taxes/ — five ИПИ municipal-tax indicators across all 265 общини plus per-наредба blocks (residential ТБО, individual property tax, tourist + dog tax) for the oblast capitals. Watchers ipi_local_taxes + municipal_naredba. |
update-council-minutes |
Ingest municipal-council (общински съвет) resolutions + за/против/въздържал tallies (per-councillor named votes where available) into data/council/. ~16 municipalities wired. Watcher council_minutes. |
update-municipal-contacts |
Refresh per-município mayor + deputy-mayor email contacts (data/officials/municipal_contacts/) scraped from the iisda.government.bg registry. Watcher iisda_mayors. |
update-transparency-lisi |
Refresh Transparency International Bulgaria's LISI municipal-integrity scores (data/municipal_transparency/) for the 27 oblast-center municipalities. Watcher ti_bg_lisi. |
update-air-quality |
Refresh ИАОС air-quality monitoring-station data (data/air/index.json) — latest quarterly PM10 + PM2.5 CSVs via data.egov.bg. Watcher iaos_air_quality. |
update-noi |
Refresh NOI social-security data — the B1 fund execution (data/budget/noi/funds.json, ДОО / УчПФ / ГВРС), the pension yearbook (data/budget/noi/pensions.json — the /pensions view: oblast, distribution, cash-vs-bank, national series), and the КФН private-pension quarterly (data/budget/kfn/funds.json, pillars 2 & 3). Watchers nssi_b1, nssi_yearbook, kfn_pensions. |
update-culture |
Refresh the Култура (culture) data behind /culture — the НФЦ film-subsidy corpus (films.json + overview.json), НФК grant success rates (grants.json), state cultural institutes by oblast (oblast.json), the artistic-commission compositions / "кой решава" (commissions.json), and the Sofia + читалища municipal streams (municipal.json). Also the МК ДКИ register (dki_register.json — the ministry's own listing of the държавни културни институти it is the principal of, with each institute's director and seat, reconciled against the four-list culture allowlist). Watchers nfc_film_register, ncf_grant_results, nfc_commissions, mc_dki_register. |
update-energy |
Refresh the Енергетика (energy) physics tiles behind /sector/energy — generation.json (electricity generation mix / net trade / CO₂ intensity from Ember, CC BY 4.0) and, from one run of fetch_prices.ts, both prices.json (household electricity, Eurostat nrg_pc_204) and gas_prices.json (household natural gas, nrg_pc_202) — each BG vs EU27 + RO/GR/HU/HR peers, driving /consumption/electricity & /consumption/gas. Watchers ember_generation, eurostat_energy_prices (fingerprints both cubes). The БЕХ procurement pack renders off the contracts corpus (no ingest). |
parliament-scrape |
Scrape MP photos/bios/seat data from parliament.bg (run after a new parliament is seated). |
party-retrospect |
Generate per-party campaign retrospects. |
Every tier-2 ingest skill has a "Data-integrity contract" section in its SKILL.md enumerating fail-loud surfaces (HTTP errors, schema drift, canary mismatch, count-floor / regression breaches) and intentional non-fatal skips. The orchestrator halts on first downstream failure and refuses to stamp state/ingest/<skill>.json until a clean run.
These can also be run by hand via the npm scripts and the scripts/ CLI flags listed above.
Two-tier model.
Tier 1 — daily watcher. npm run watch (scripts/watch/index.ts) fingerprint-diffs 63 upstream sources (parliament.bg MPs + votes, BG Wikipedia polls, register.cacbg.bg declarations — MP slice, register.cacbg.bg declarations — executive officials slice, register.cacbg.bg declarations — municipal slice (mayors & councillors), Сметна палата party financing, Сметна палата annual-report year index, data.egov.bg Commerce Registry, data.egov.bg АОП procurement, АОП debarred-suppliers register, ИСУН EU-funds public beneficiary register, ИСУН EU-funds public project register, data.egov.bg КФП state-budget execution, per-ministry "Отчет за изпълнението на програмния бюджет", Агенция "Митници" — Митническа хроника (excise + import VAT + customs duties annual PDFs), НАП — Годишен отчет за дейността (domestic VAT by sector + PIT by income type), НОИ — месечни B1 отчети по фондове (per-fund cash-execution XLS for ДОО / УчПФ / ГВРС — drives the social-security-funds drill-down), ДВ — Инвестиционна програма за общински проекти (per-project capital allocations PDF annex — drives the capital-expenditure drill-down), Общински капиталови програми (per-município annual капиталова програма files from sofia.bg + plovdiv.bg + burgas.bg + starazagora.bg — drives the per-район / per-village tiles on the settlement-page "финанси" section), Eurostat macro (22 datasets including FDI flows, intentional-homicide rate, and prisoners per 100K), Eurostat regional NUTS 3 (5 datasets — GDP, population, migration, recorded theft, active-enterprise density), НСИ regional open-data (JSON-stat — FDI per capita, museum visits, hospital beds per oblast), EC "EU spending and revenue" per-Member-State XLSX, BNB domestic ДЦК auctions, minfin.bg КФП monthly bulletins via Wayback (mreport + BULETIN PDFs + FRA-MM-YYYY-(BG|EN).xlsx — feeds the fiscal-reserve series), minfin.bg programme-budget execution reports via Wayback (МФ *ProgOtchet*.pdf annual + semi-annual captures — covers the Cloudflare-blocked manual-pdf gap in EXECUTION_REPORTS), mfa.bg programmatic execution reports via Wayback (МВнР *програмен отчет*.zip annual + quarterly captures — covers the JS-rendered ministry-listing gap), Агенция по заетостта годишен обзор, МОН ДЗИ via data.egov.bg, НСИ population timeseries, НСИ births/deaths/migration timeseries, НСИ Баланс на територията (annual per-oblast land-use composition from АГКК cadastral map — drives the My-Area "Имотен фонд" tile), ГРАО settlement registered population, Transparency International CPI, World Bank WGI) and writes:
data-reports/<YYYY-MM-DD>.md+data-reports/latest.md— human-readable daily snapshotstate/watch/<source>.json— per-sourcelastChanged+lastChecked
Each source declares a cadence (daily / weekly / monthly); the runner honours it by skipping a source whose lastChecked is younger than the cadence window. The watcher itself still runs daily, but a "monthly" source like Transparency International CPI is only actually fingerprinted once every ~29 days — handy for annual-release upstreams that don't move often.
Scheduled via a local Claude Desktop routine — runs from the contributor's machine so source-blocking on cloud-runner IPs (data.egov.bg in particular) doesn't apply. CIK is omitted in v1; its endpoint sits behind Cloudflare and needs a Playwright-based fetch.
Tier 2 — on-demand ingest. Tell Claude Code process-watch-report (or "sync data based on the watcher"). The orchestrator compares state/watch/*.json against state/ingest/<skill>.json and runs only the skills whose mapped sources have advanced since their last successful ingest. Multi-day gaps are handled correctly — the decision is state-driven, not based on the latest report file alone.
.github/workflows/ keeps a workflow_dispatch-only ingest job for the heavier roll-call path that needs the bucket service account, plus the PR test job:
| Workflow | Trigger | What it does |
|---|---|---|
ingest-rollcall.yml |
workflow_dispatch + repository_dispatch |
Runs scrape_rollcall.ts end-to-end (validates against the canary fixture, rebuilds the derived loyalty / attendance / similarity / cohesion metrics in-process, uploads to the bucket). Same skill as /update-rollcall but from CI. |
test.yml |
on PRs | Lint + Playwright. |
See .claude/skills/process-watch-report/SKILL.md for the orchestrator's full source→skill mapping and per-skill data-integrity contracts.
The Tier-1 watcher and /update-rollcall skill both run incrementally — they pick up where the last successful ingest left off. They do not sweep historical id ranges. If you need to backfill old parliaments (or recover from a wiped data/parliament/votes/), use the explicit backfill mode of scrape_rollcall.ts:
# Historical roll-call backfill: 45th-49th NA (Oct 2020 – Jun 2024).
# Walks every pl-sten id in the range, tries CSV → falls back to XLSX
# (incl. the COVID-era "+online" layout), skips misuploaded sessions.
npx tsx scripts/parliament/scrape_rollcall.ts \
--from-id 10500 --to-id 10900 \
--backfill --skip-canary --seated-tolerance 50
# Then regenerate per-MP loyalty / attendance / similarity / cohesion across all NAs.
npm run derived:rebuildNotes:
--backfillbypasses the 5% diff-size guard (which exists to protect the daily watcher from a runaway re-ingest). Use it only when you mean to materially grow the session set.--from-id/--to-idwalk an arbitrary range; the script auto-detects the format per session (modern CSV, modern XLSX, or COVID-era+onlineXLSX) and infers the NS folder from the stenogram subject or the date-range table inscripts/parliament/rollcall/ns.ts.--skip-canaryis required for backfill — the canary stenogram (id 11120, Apr 2026) is outside the historical range and re-fetching it on every batch is wasted work.--seated-tolerance 50relaxes the per-item seated-count check; older sessions occasionally publish fewer than the standard 240 rows.- COVID-era "+online" XLSX files (Oct 2020 – Apr 2021) omit the mp_id column. The backfill resolves ids by name against
data/parliament/profiles/, so the parliament-scrape skill must have run first. - The backfill is a one-off developer action. It is intentionally not wired into the daily watcher,
npm run watch, or any CI workflow.
Expected runtime: ~6-10 minutes over network for the full 10500-10900 range (~250 sessions). Re-runs are safe — existing session files are overwritten with the deterministic canonical JSON.
Another one-off is the road-network geometry for the АПИ road dashboard (/procurement/roads). The Bulgarian motorway + funded republican-road LineStrings are pulled once from OpenStreetMap (Overpass), clipped to Bulgaria, decimated, and joined to the funded contract references — roads change rarely, so this is operator-initiated, not a watcher source:
# Reads the cached Overpass response by default; --fetch re-pulls from OSM.
# Writes data/procurement/roads.json (gitignored, bucket-served). ODbL.
npx tsx scripts/procurement/ingest_osm_roads.ts # from cache
npx tsx scripts/procurement/ingest_osm_roads.ts --fetch # re-pull OSMNotes:
- Coverage is motorways (A1–A6) + republican roads (Път I/II) whose corridor is referenced by an АПИ contract. OSM tags republican roads with bare numbers, so the class is derived from the highway type (trunk → I, primary → II) and a segment is kept only when its corridor matches a funded contract.
- It depends on
data/procurement/awarder_contracts/000695089.json(АПИ) being present to compute the funded-corridor set, so run a procurement ingest first. - After running,
npm run bucket:syncto publishroads.json.
.env.local (gitignored — secrets):
ANTHROPIC_API_KEY=... # only for npm run polls:gen-analysis
GEMINI_API_KEY=... # only for npm run party:gen-retrospect
Both keys are optional unless you're regenerating the AI-written narratives. (vite.config.ts historically injected GEMINI_API_KEY into the frontend bundle as process.env.API_KEY, but no src/ code currently consumes it.)
.env.production (committed — public bucket URL):
VITE_DATA_BASE_URL=https://storage.googleapis.com/data-electionsbg-com
Empty in dev so the Vite middleware serves data from local /data/. The dataUrl() helper handles both cases transparently.
The SPA shell deploys to Firebase Hosting; the data layer syncs to the GCS bucket.
Firebase Hosting (SPA shell):
npm run deploy→ production (elections-bg)npm run staging→ staging (electionsbg-staging)npm run deploy:fast/npm run staging:fast→ skip the predeploy data pipeline (SKIP_PREDEPLOY=1)
Both run the full data pipeline as predeploy unless skipped. SPA rewrites and per-route prerendering are configured in firebase.json; prerendered HTML (~445k files: per-candidate, per-section, per-settlement) is generated by scripts/prerender/ during postbuild so crawlers see populated <meta> tags.
GCS bucket (data layer):
npm run bucket:sync→ incremental rsync ofdata/(text gzipped via-j json,svg,xml,txt,html,css,md, binaries as-is)npm run bucket:sync:dry→ preview without uploading
After most data updates (new polls, scraped roll-calls, refreshed declarations) only the bucket needs to update — no Firebase deploy. Deploy time for SPA changes alone is ~20 min; data-only updates are seconds.
Bucket conventions:
- Cache-Control:
public, max-age=3600, stale-while-revalidate=604800 - Content-Encoding:
gzipfor text viagsutil cp -Z/rsync -j - CORS:
GET, HEADfrom the allowlisted SPA origins —scripts/bucket_cors.json, applied withnpm run bucket:cors. It is the ONLY CORS config; an origin missing from it makes the site render blank from that origin with every page otherwise correct, soscripts/lib/siteOrigin.test.tsgates it.
- Regions, municipalities, and settlements — modified to split Sofia city into its 3 electoral regions and to carve out the Plovdiv city region.
- Sofia city districts — optimized and merged into the region maps.
- World countries — grouped by continent.
- Continents — grouped into a world map and simplified with Mapshaper and geojson.io.
- EKATTE catalog — settlement names in English and Bulgarian.
- Settlement locations.
- World capitals.
- Bulgarian Post — postal code register (data.egov.bg, CC0) — joined to
data/settlements.jsonintodata/postcode_ekatte.json(postcode → EKATTE map, 5,221 / 5,293 rows = 98.6% resolution). Used byscripts/declarations/parse_registered_office.tsto disambiguate village name collisions (Лозен exists 5×, Лясково 6×, …) when resolving the free-textregisteredOfficefield on cacbg.bg property declarations to a specific settlement. Sofia city (1000) collapses to the synthetic EKATTE68134— seescripts/lib/oblast_names.ts. Watched monthly byscripts/watch/sources/bgpost_postcodes.ts.
- 19.04.2026
- 27.10.2024
- 09.06.2024
- 02.04.2023
- 02.10.2022
- 14.11.2021
- 11.07.2021
- 04.04.2021
- 26.03.2017
- 05.10.2014
- 12.05.2013
- 05.07.2009
- 25.06.2005
- parliament.bg API — MP profiles, photos, term history.
- register.cacbg.bg — Court of Audit MP property and interest declarations.
- Сметна палата party financing — Court of Audit register of political-party finances. The annual-report year catalogue is parsed off
bulnao.government.bg; the per-party annual-report filing-status lists (filed on time / late / non-compliant / not filed, 2011 onward) are crawled from thegfopp.bulnao.government.bgWebForms register. - data.egov.bg Commerce Registry dataset — daily Trade Register filings (companies, officers, status).
- data.egov.bg АОП open-data feed — Агенция за обществени поръчки (АОП) fortnightly OCDS-standard public-procurement bundles (since 2026-01-01) and annual CSV dumps for prior years (2011-2023).
- ИСУН 2020 — публичен модул, Бенефициенти — the EU-funds Management & Monitoring Information System. The public beneficiary register lists every organisation that has signed an EU-funds contract (2014-2020 + 2021-2027 + Recovery Plan), with contracts signed, funds contracted and funds actually paid; offered as an XLSX export and ingested into
data/funds/. - data.egov.bg КФП feed — Министерство на финансите Консолидирана фискална програма: monthly consolidated state-budget execution snapshots (revenue + expenditure by economic + functional grain).
- Държавен вестник — State Budget Law — annual Закон за държавния бюджет (HTML) parsed for per-spending-unit appropriations at admin + program grain. The same source also carries Чл. 53 (Чл. 51 in older years) — the per-municipality transfer table, allocating the annual state-to-municipal envelope across 265 общини × 5 transfer categories (delegated state activities, equalization, winter road maintenance, capital subsidy, other targeted local activities). Parsed into
data/budget/municipal_transfers/and surfaced on/budget(Sankey drill-down on Общини), the per-region dashboard tile, the per-municipality dashboard tile, and the Sofia city dashboard. 2025 totals reconcile to the lead-paragraph envelope at €0 delta across all five types. - Държавен вестник — Инвестиционна програма за общински проекти (Приложение № 3) — annual PDF annex to the State Budget Law (Чл. 113 in 2025 / Чл. 107 in 2024). Per-project capital allocations to municipalities with stable
OP-YY.NNN-NNNNproject IDs, responsible institution asОбщина X, област Y, and cost in хил. лв. The 2025 annex has 3066 projects totalling ~€3.6B. Parser atscripts/budget/investment_program/parse_annex_pdf.tshandles the borderless layout via pdfjs-dist text positioning, with fallbacks for justified per-glyph rendering, hyphenated line-breaks, and Sofia capital variants. Output underdata/budget/investment_program/{year}.json; drives the "Капиталови разходи" drill-down on/budgetwith a 9-category classification (roads / water-sewage / education / social / sports / culture / buildings / energy / other), 14-oblast list and top-10 projects by cost. - МРРБ — Инвестиционна програма за общински проекти (ИПОП), execution feed — the execution-side companion to the State Budget Law's Annex III investment programme above. Ministry of Regional Development publishes a daily-refreshed CSV at
ipop.mrrb.bg/reports_projects_export.phpcovering 3,492 projects across 264 municipalities (~all 265 общини minus a handful with zero МРРБ-funded projects), with per-project agreement value + paid + submitted-in-review + approved-awaiting-payment amounts in EUR. Same OP-YY.NNN-NNNN project IDs as the State Budget Law annex, so the two datasets are joinable. National totals as of late May 2026: €2.98B committed, €991M paid → 33.2% execution, €1.23B requested-in-review, €158M approved-awaiting. Parser atscripts/budget/ipop/ingest.tsreads the BOM-prefixed semicolon-delimited CSV, matches CSV oblast names (Cyrillic) + município names to our 3-letter oblast codes + obshtina codes (with manual overrides for Sofia capital "Столична" → syntheticSOF22, Plovdiv città "Пловдив" →PDV22— itsmunicipalities.jsonrecord is keyed under the synthetic oblastPDV-00, so the oblast-name lookup misses — and 4 spelling/case fixes like "Вълчидол" → "Вълчи дол"), flags stalled projects (agreement ≥ €100k AND paid < 5% — 769 projects in 2025), and emits a national summary atdata/budget/ipop/{year}.json(~80 KB, includes per-município and per-oblast aggregates) plus 264 per-município shards atdata/budget/ipop/municipalities/{obshtinaCode}.json(5-50 KB each, with the full project list sorted by agreement value DESC). Drives the new IPOP execution tile on every município settlement page — shows total agreement, paid percentage with progress bar, pending pipeline (submitted + awaiting), stalled-project warning chip, and top-5 projects with per-project execution bars (amber for stalled, emerald for normal). Watcher:ipop_mrrb, daily cadence, HEAD-probes the CSV's content-length + last-modified. - Per-município capital programmes (Sofia, Plovdiv, Burgas, Stara Zagora, Ruse, Varna, Pleven, Sliven, Dobrich, Asenovgrad, Shumen, Vidin, Veliko Tarnovo, Pernik, Haskovo, Gabrovo, Yambol, Kardzhali, Lovech, Dupnitsa, Velingrad, Samokov, Karlovo, Kazanlak, Kyustendil, Montana) — each община publishes its own annual капиталова програма (поименен списък на обектите за капиталови разходи) as a board-decision appendix, but the format varies per city: Sofia ships Приложение №3 as a clean XLSX with paragraph / function / activity hierarchy and 24 райони tagged in free text; Plovdiv ships a borderless PDF with vertical-text "Разпоредител с бюджет" column and 6 райони explicitly tagged; Burgas ships the капиталова програма as one sheet inside the city draft-budget XLSX workbook with 7 funding-source columns (state subsidy / own / debt / EU / other / 2× carry-overs) and 11 villages + named city quarters from Wikipedia категория КварталинаБургас; Stara Zagora ships Приложение №4 as a born-digital PDF inside a council-decision ZIP, tagging 51 villages of obshtina СЗР31 via the "с." prefix; Ruse ships a multi-sheet XLSX with the highest data quality of the bunch — one sheet per second-level spending unit + one dedicated sheet per kmetstvo for the 12 villages, so per-settlement attribution is via workbook structure rather than free-text regex; Varna ships only 71-page rasterized scans, so the parser pipeline has an OCR pre-step via Gemini Vision (
scripts/budget/capital_programs/varna_ocr.ts) that runs once per fiscal year and caches the extracted JSON underraw_data/, then the rollup parser is deterministic; Pleven ships a single 63-page budget docket PDF with the capital programme split across Приложение №4 (general capital, 7.59M BGN) and Приложение №10А (EU projects, 11.00M BGN) — text is technically extractable but the layout is heavily fragmented (rotated funding-source labels, multi-line descriptions, sparse columns), so the pipeline slices those 8 pages withpypdfand OCRs them via Gemini Vision (pleven_ocr.ts), then rolls up by settlement + funding source; Sliven — first Tier-2 oblast capital (SLV20, 45 settlements) — publishes a 23-page rasterized PDF at an opaque-hash URL, ingested through the Gemini Vision OCR pipeline (sliven_ocr.ts→sliven.ts). 60% per-village localisation; Dobrich is a single-settlement município (DOB28 — the city; surrounding villages live in a separate DOB15 община) where the capital programme ships as an inline HTML table on dobrich.bg —dobrich.tsscrapes it server-side viafetch+ regex (no OCR, ~$0 ingest cost); Asenovgrad (PDV01, 29 settlements in Plovdiv oblast) publishes a clean 10-page born-digital PDF on the council site — the same OCR pipeline as Sliven for robustness, with another 60% per-village localisation tier; Shumen (SHU30, 27 settlements) — first harvester-discovered município whose budget portal is JS-rendered; published as Приложение №6 (15-page born-digital PDF, URL surfaced via the Playwright-basedharvest.ts); Vidin (VID09, 34 settlements) is the first execution-report ingest in the fleet — vidin.bg packages the year-end "Отчет за капиталови разходи" as a.docinside an annual RAR archive; operator extracts withunar, converts to text via macOStextutil, thenvidin.tsparses bullets via Cyrillic regex (90% per-village localisation — the bullets always explicitly tag the settlement). 2022 + 2023 ingested as execution; 2025 plan documented as ЦС-subsidy-only (see parser header); Veliko Tarnovo (VTR04, 89 settlements) — Tier-2 oblast capital. The council's "Приложения 1-22" master XLSX contains sheet "Pril15" = Инвестиционна програма, parsed directly with a dynamic header-row resolver since the anchor row drifts between years. 2024 + 2025 ingested; 2023 and earlier are not on the rebuilt CMS; Pernik (PER32, 24 settlements) — oblast capital. 2024 + 2025 ship as BGN XLS files (pre-euro); 2026 ships asPoimenen-spisak-EURO.xlswith figures already in EUR (back-converted from BGN at the 1.95583 peg, fractional cents preserved) — the parser handles both via aSOURCE_CURRENCYmap. Single-sheet XLS, 114-159 itemised projects per year; Haskovo (HKV34, 37 settlements) — oblast capital, 19-page born-digital landscape PDF (Прил. №7, MINFIN B3 template) with project descriptions wrapping across multiple lines and amounts spread across many narrow funding-source columns; Gemini Vision OCR handles the multi-line joins and column disambiguation. 2024: 268 projects, ~€21.3M itemised (matching the published recap exactly); Gabrovo (GAB05, 134 settlements — the largest village count in the fleet) — oblast capital. The municipal portal is JS-rendered and the harvester can't surface the budget appendix, but Google indexes the file directly atgabrovo.bg/files/budjet2025/izmenenia/20.5.pdf(Прил. №5 to the December 2025 budget actualisation). Born-digital 9-page landscape PDF; OCR via Gemini Vision. 2025: 299 projects, ~€8.1M (small per-project amounts because Gabrovo's actualisation only tracks year-end adjustments, not the full annual programme); Yambol (JAM26, single-settlement) — oblast capital where the whole community fits in the city; surrounding villages live in separate общини. The annual budget ships as a RAR archive (2024+) or ZIP (2022-2023) packaged with Прил. 4 (Прил. 5 in 2025) — capital expenditure list. ZIP files use CP866 for Cyrillic filenames so macOSunziperrors out; the operator falls back to Python'szipfilewithcp437→cp866decode. Born-digital Excel-rendered PDF inside; OCR via Gemini Vision. 4 fiscal years (2022-2025) ingested, all matching the published recap at ratio 1.000. Per-município parsers underscripts/budget/capital_programs/{muni}.tswrite a common-shapeddata/budget/capital_programs/{year}/{muni}.json(recap total + per-project list + per-район or per-village rollup), and twenty-six matching frontend tiles surface the data inside the "финанси на общината" section on each city's settlement and município page — each tile silent-no-ops for non-matching obshtina codes. Thecapital_programswatcher tracks all twenty-six URLs under one fingerprint; new years require adding the URL to bothCAPITAL_PROGRAM_URLSin the watcher ANDSOURCE_URLSin each parser. Historical coverage: 4 fiscal years (2022-2025) are ingested for Sofia, Ruse, Stara Zagora, Varna, Pleven, Burgas, Asenovgrad, and Yambol; Kardzhali carries 2024-2025 (where the 2024 file uses било/става amendment-pair columns — parser captures the post-amendment "СТАВА" value); Pernik carries 3 years (2024-2026, with the BGN→EUR currency switch handled per-year); Vidin (2022-2023), Veliko Tarnovo (2024-2025) and Dobrich (2024-2025) carry 2 years apiece — each multi-year tile has a year picker. Plovdiv and the Tier-2 общини without a discoverable back-year archive (Sliven, Shumen, Haskovo, Gabrovo) stay at a single year because their back-year capital programmes either ship inside multi-document budget bundles where the capital sub-section isn't easily isolatable, or the CMS no longer hosts older budget pages. Adding the next município follows one of four recipes by source format: clean XLSX → SheetJS direct parse (Sofia / Veliko Tarnovo / Pernik path), born-digital PDF → Gemini Vision OCR (Sliven / Asenovgrad / Shumen / Haskovo / Gabrovo path), inline HTML table → fetch + regex (Dobrich path), or.doc-inside-RAR →unar+textutil+ regex (Vidin path). For общини with JS-rendered budget portals where the harvester misses the file (Gabrovo), Google's site-indexed search is a useful fallback to discover the direct PDF URL. Karlovo (PDV13, 27 settlements: 4 towns Карлово/Калофер/Клисура/Баня + 23 villages) — Plovdiv-oblast município. The capital file ships as a clean XLSX (Приложение № 7) served via karlovo.bg'sservice-download-file.phpendpoint (Referer header required to bypass anti-hotlink check). Workbook has two sheets —Общо (2)is the MINFIN B3 multi-year отчет, and the standalone2025sheet is the actual annual plan with carryover + plan-2025 source breakdown in 12 columns. Parsed bykarlovo.ts(no OCR), 2025: 136 projects, ~€15.0M itemised matching the published ОБЩО total exactly; 81% settlement localisation (untagged are generic equipment lines like printers/computers/drone). URL discovered by the user via Safari + the council's currentNews-9070 page. Kazanlak (SZR12, 20 settlements: 3 towns Казанлък/Крън/Шипка + 17 villages) — Stara Zagora-oblast município. The site is Nuxt-rendered, so neither the Playwright harvester nor direct curl surfaces budget files; the workaround is to fetch the page's_payload.json(Nuxt pre-renders content into JSON at/{slug}/_payload.jsonnext to the HTML shell). That payload reveals the URL of the council's "Приложения" PDF (born-digital, ABBYY-produced, 17 pages, with the investment programme as Приложение №4 on pages 9-17). The accompanyingBudget_2025_7404.xlsis password-protected and unusable. OCR via Gemini Vision. 2025: 201 projects, ~€7.9M, perfect recap match with "Общо за Общината". Settlement localisation tier is the lowest in the fleet (35%) because Kazanlak's programme is heavy on centrally-purchased equipment (computers, climatics, ОП КДиПИС vehicles) that don't carry settlement tags. Kyustendil (KNL29, 72 settlements: city + 71 villages — the second-largest village count after Gabrovo's 134) — Kyustendil-oblast capital. The municipal portal doesn't publish a standalone capital programme PDF; the only public source is Приложение №6 inside the council's "Окончателен годишен план" docket on obs.kyustendil.bg (41-page mixed scan + born-digital PDF in the council's session-30DnevenRedfolder). Operator slices pages 30-40 with pypdf into a focused capital-pages PDF then runs Gemini Vision OCR. 2025 final plan: 246 projects, ~€11.0M, perfect recap match with "ОБЩО Капиталови разходи". Montana (MON29, 24 settlements: 1 town + 23 villages) — Montana-oblast capital. Source is a 5-page rasterized Konica Minolta scan posted at /свали/бюджет/32. Pages 1-4 contain per-function sub-appendices (Прил. 7а/7б/7в/7д, funding-source breakdowns) that itemise the SAME projects shown on page 5; including them would double-count. Parser uses ONLY page 5 (the consolidated 9-project summary, with row 7 the 30M bul. Трети март rehab + a separate 3M театър ремонт row sitting below the ВСИЧКО recap). Headline ≈€29.1M including the theater. OCR via Gemini Vision; 78% settlement localisation. - НОИ — Национален осигурителен институт — monthly B1 per-fund cash-execution reports (legacy BIFF8 XLS, CP1251). Three funds: 5500 ДОО (main social-security fund), 5591 Учителски пенсионен фонд, 5592 Гарантирани вземания на работниците и служителите. The "OTCHET-agregirani pokazateli" sheet carries the standard expenditure roll-up (Personnel / Operations / Social expenses / Subsidies / Capital); the more detailed "OTCHET" sheet has the §4100 "Пенсии" vs §4200 "Текущи трансфери, обезщетения и помощи" split. 2024 verified: revenue €6.66B, expenditure €12.64B, pensions €11.13B (88% of DOO), short-term benefits €1.41B. Output at
data/budget/noi/funds.json; drives the "Социалноосигурителни фондове" drill-down on/budget. Auto-fetch unreliable (NSSI 302→homepage on GET despite 200 on HEAD), so the watcher (nssi_b1) HEAD-probes the year-end files and the operator manually downloads new B1 XLSes intoraw_data/budget/noi/before runningtsx scripts/budget/noi/__write_funds.ts. - Съдебна власт — бюджет по органи — parsed from the „Бюджет на съдебната власт" article of each State Budget Law (the same cached
raw_data/budget/law-{year}.html.gzthe budget ingest already fetches, so no new source). Two tables per year: the judiciary's own revenue (съдебни такси, глоби, приходи от собственост, други) and its expenditure split across the eight spending bodies (ВСС, ВКС, ВАС, Прокуратурата, Съдилищата, НИП, ИВСС, резерв). Both are reconciled at ingest (Σ bodies == total expenditure; Σ revenue == total revenue). 2025 verified: expenditure €707.8M, own revenue €77.7M (11% self-financing, of which €70M court fees), courts €360.9M (51%) + prosecution €251.6M (35.6%). Caveat: the per-body table is paragraph (2) up to 2024 and (3) from 2025, when a functional-area („програмен бюджет") table was inserted before it. Output atdata/budget/vss/budget.jsonviatsx scripts/budget/__write_judiciary.ts; drives the judiciary sector pack on/awarder/121513231and the AIjudiciaryBudgettool. If any year fails its reconciliation the script throws and writes nothing — pass--allow-partialto write the years that did parse and exit non-zero, which is a debugging aid, not a routine flag (a partial artifact silently regresseslatestYear). - Per-ministry "Отчет за изпълнението на програмния бюджет" — each first-level spending unit publishes its annual execution report (PDF, XLSX-in-ZIP, DOCX or DOCX-in-ZIP) on its own site, hand-curated per ministry in
EXECUTION_REPORTS. Drives the law → amended → executed reconciliation. The same source also carries the per-programme "Численост на щатния персонал" row, paired with the programme's Персонал spend to derive average annual cost per FTE — surfaced as a Personnel layer on/budget, a Sankey drill-down, and a per-programme table on each ministry page. Cloudflare-blocked sites (minfin.bg, mvr.bg) go through amanual-pdfcache path atraw_data/budget/exec-<adminId>-<fy>.pdf— МФ FY2023 was backfilled this way via the Internet Archive (theminfin_program_otchetwatcher flips when a newer МФ programme-budget PDF lands in Wayback). JS-rendered ministry sites whose listing pages can't be enumerated by curl/HEAD are covered by the same Wayback-CDX pattern —mfa_program_otchetsurfaces new МВнР programmatic ZIPs, and the Playwright-basedscripts/budget/discover_execution_reports.tsis a one-off developer tool that drives chromium across the rest of the budget-section pages (МОН, МРРБ, МК, МЕ, МС, МТС) to surface candidate URLs. Current personnel-data coverage: ~30% of total first-level FY2024 expenditure (€2.7B of €8.9B), plus МФ and МВнР for FY2023; the rest of the gap is documented with survey notes inscripts/budget/fetch_sources.ts. - ВСС — Обобщени статистически таблици за дейността на съдилищата — the annual court statistics, published only as ~170-page PDFs with irregular filenames (so
VSS_ANNUAL_TABLESinscripts/judiciary/sources.tsis a curated map, not a URL pattern). The PDFs carry a real text layer, so parsing is deterministic — pdfjs text positioning, rows bucketed by y, cells merged by x-gap; no OCR. Приложение № 1 yields, per court tier (apelativni / voenni / okrazhni+СГС / RS in oblast centres+СРС / RS outside / administrativni): cases pending at start, filed, resolved, resolved within the statutory 3-month deadline, decided on the merits, terminated, pending at end, appealed — plus judge posts and BOTH official workload measures, „по щат" (per allocated post) and „действителна" (per person-month actually worked). Reconciled at ingest: Σ tiers == total for every column, and the stock-flow identitypendingEnd == pendingStart + filed − resolved. 2018–2025 verified; 2025 = 544,541 filed / 544,035 resolved / 129,536 pending / 81% within deadline / 2,260 judge posts. Gotchas: the decimal separator is a dot up to 2021 and a comma from 2022; rows are keyed by ORDER, never by label (the "Районни съдилища извън областните центрове" label wraps and leaves its data row label-less, and "Окръжни съдилища + СГС" carried "+ СНС" until the specialised criminal court closed in 2022). Coverage excludes ВКС, ВАС and the prosecution, which report separately — the totals are not "the whole judiciary". Output atdata/judiciary/caseload.jsonviatsx scripts/judiciary/__write_caseload.ts(skillupdate-judiciary, watchervss_court_statistics); drives the/judiciarydashboard and the AIjudiciaryCaseload/judiciaryWorkloadtools. Приложение № 2 of the same PDFs carries the per-INDIVIDUAL-court натовареност (each named court's judge posts, person-months worked, and both workload measures), parsed bytsx scripts/judiciary/__write_court_load.ts→data/judiciary/court_load.json(178 courts, 2018–2025). It is anchored on the ВСС identityload = count ÷ person-months(rejecting admin-ratio mis-anchors), tiered by name prefix, geocoded viasettlements.json, and hard-gated so Σ person-months and the pm-weighted load per tier reconcile back tocaseload.json. It drives the per-court map on/judiciary(served one year at a time from Postgres via/api/db/court-load, schema069_court_load.sql) and the AIjudiciaryCourtLoadtool. The PDFs are cached underraw_data/judiciary/;--refetchre-downloads them (use when the ВСС republishes a year in place, since the cache is keyed by URL). As with the budget writer, a failed assert throws and writes nothing unless--allow-partialis passed. - ИВСС — регистър на имуществените декларации на магистратите — the Inspectorate to the Supreme Judicial Council publishes every magistrate's asset declaration (чл. 175а, ал. 1 ЗСВ) as a PDF, indexed by year × first letter of the given name on a Joomla site at a bare IP over HTTP (
http://62.176.124.194, linked as „Публикувани декларации"). There is no TLS on that host, so a network-position attacker chooses the names, входящи номера and PDF paths this repo commits about named individuals — run the ingest from a trusted network and read the diff before committing (see.claude/skills/update-judiciary/SKILL.md). We index what was filed and when — never the contents: 261 HTML pages → 46,528 declarations from 5,556 magistrates, 2017–2025. Each year has two filing batches:/declaracii/<year>/(the annual declaration, due 15 May) and/declaracii/<year>-1/(change declarations under чл. 175в, ал. 5, filed through the autumn). The measured filing calendar shows 65.8% of annual declarations land in May, peaking on the 14th and 15th — the deadline itself. Alongside it we reproduce the ИВСС's own four non-compliance lists (late annual filers, late change filers, left-office-without-filing, and the чл. 175ж „несъответствие, неотстранено в срок" list), verbatim and with the legal reference; an empty list is shown as empty. Asserts: ≥8 years, ≥3,000 magistrates and both batches per closed year, dedupe accounting balances onto distinct PDF paths, the page heading's year agrees with the year in the PDF's own path (bar a bounded handful — the ИВСС files January change declarations into the prior cycle's directory; any other offset throws), and >40% May clustering (its loss means the входящ-номер date parsing broke). Framing: magistrates are not elected officials, filing gaps mostly reflect entering/leaving the corps and are NOT surfaced as a compliance score. Extracting the declarations' contents is feasible (12-page form, real text layer) but is 46k PDFs / ~37 GB and a separate project; the full per-declaration index with PDF paths is written toraw_data/judiciary/declarations_index.json(gitignored) as its input. Output atdata/judiciary/declarations.jsonviatsx scripts/judiciary/__write_declarations.ts(skillupdate-judiciary, watcherivss_declarations); drives the integrity section of/judiciaryand the AIjudiciaryDeclarationstool. A separate, deeper parse of the SAME declaration PDFs —tsx scripts/judiciary/__write_magistrate_holdings.ts→data/judiciary/magistrate_holdings.json— reads the latest annual declaration of each magistrate for declared companies (дялове/акции/участие; company names harvested by legal-form token, then resolved name→EIK against the TRstate.sqlite, UIC only on a unique match) and a small set of informational financials (bank/cash, securities, count of owned real estate). Since the move to Postgres, the parser emits the full latest-year roster (3,113 magistrates — every declaration it parses, not only the holders), so the person page and the procurement search cover everyone while the tiles stay holder-focused. Of those, 208 hold a declared company (363 companies, ~67% EIK-resolved — companies stay sparse since magistrates are barred from management), and 2,797 carry non-zero informational financials. Loaded into Postgres (magistrate+magistrate_companytables, schema070_magistrates.sql; per-entity lookups are index-served in <1 ms, the full search roster is ~392 KB / ~43 KB gzipped) and surfaced on/judiciary(holdings tile —WHERE company_count > 0, still the 208, with a "виж всички" link to the standalone/judiciary/magistratesbrowse table served through the generic/api/db/tableengine, resourcemagistrate_holdingsover themagistrate_holdings_tableview),/company/:eik(declared-by-magistrate tile),/person/:name(per-magistrate declaration tile with the labelled financials — shown as specific amounts, never a net-worth total or a cross-magistrate ranking), and the procurement combined search. Financials caveat: they are best-effort auto-extractions validated only on a small hand-checked sample; the median is ~78k лв but the high tail (a handful over ~1M лв, up to a near-certain ~19.7M лв parse artifact) is unreliable, so figures are framed „ориентировъчни; следа, не доказателство" and never ranked. Framing: magistrates are not elected officials; a decla