Skip to content

The data pipeline ​

Futuros does not query live APIs when someone opens a page. The entire corpus is ingested, baked and frozen as static JSON before deployment; the SPA only reads files. That makes the platform fast, cacheable, offline-capable, and, above all, auditable: every figure you see was verified at build time against its source record. This pipeline directly serves Pillar 1 — Data Trust: a corpus with provenance and a clean license is the one asset a regional model cannot copy.

GCF: only complete calendar years; the current year is not published as a comparable figure.

The source registry ​

Every external source is a SourceRecord declared in scripts/sources/registry/. It defines where the data comes from and under which rules it may be published:

  • AdapterEngine: the connector type: series, bulk, sdmx, ckan, ocds, event, entity, microdata, provider or meta. Every external API has exactly one adapter in adapters.ts (WHO GHO, UNESCO UIS, OpenAlex, Climate Watch, UN SDG, UNHCR, OWID, IMF DataMapper, generic SDMX/DBnomics…). Later waves live under scripts/sources/gap/ (one file per source). Wave F (wave-f/ + src/lib/gap-sources-f/) is the full-geo pass: JME wasting, UIS tertiary GER, DHS nutrition, CIVICUS civic space, Ember/IRENA fill, plus honest-gap probes (RICYT, ILIA/GIRAI/GDB, Chile INE.Stat health/innovation, ENDI) that re-check and return []. Refresh: scripts/refresh-wave-f.ts. Wave H (wave-h/ + src/lib/gap-sources-h/) uses the same cut. Its first source is the sovereign-loan register of the China Development Bank and the Export-Import Bank of China compiled by Myers and Gallagher (Boston University Global Development Policy Center and the Inter-American Dialogue): a stock of commitments, not outstanding debt, with neutral direction, so it stays out of the composite index. The second source is NOAA NCEI IBTrACS v04r01: unique tropical cyclones per complete season from 1980, assigned to the 25 countries with Natural Earth coastlines. It is a hazard count and stays neutral in the composite. The third source is UN Tourism inbound statistics (arrivals and receipts, World partner): absolute levels that scale with country size, so they stay neutral in the composite, and a country whose newest point is older than the vintage floor is omitted rather than filled in. The fourth source is BIS statistics (real residential property prices, total and household credit as % of GDP, credit-to-GDP gap): only six of the 25 countries publish (Argentina, Brazil, Chile, Colombia, Mexico and Peru; Argentina without property prices, Peru without credit), the rest are omitted rather than imputed, and they stay neutral in the composite. The fifth source is IATI via d-portal (IATI Secretariat): disbursements and commitments in current USD by recipient country and complete calendar year, weighted by each activity's declared country share, plus a disbursement series that excludes nine multilateral development banks; pass-through flows can be double-counted and publisher coverage grew over time; the running year is omitted, a missing year is a gap not a zero; they stay neutral in the composite.
  • LicensePosture: kind / redistribute / aggregates_only: what may be republished and what may only be cited.
  • Probe verification: sources.ts is SOURCE_REGISTRY_V2, with probe-tested endpoints; regulation-sources.ts is the sibling registry for AI-law tracking. The nso/*.json files are 27 source lists, one per national statistics office.
  • Verification anatomy: each record also declares its deep_link (the per-record URL stamped into citations.json), its cadence (cadence_days), a verify spec for sample-based re-verification (refetch-compare: re-request a sample and compare against what was stored) and an expected_volume sized for a ±50% volume sentinel. Note: the sentinel and the re-verifier are today a declared schema contract, no ingester runs them yet; the guard that does run on every refresh is the freshness guard.

The full schema of a SourceRecord (scripts/sources/registry/schema.ts), each field with the rule validateRegistry() applies to it:

FieldType / valuesRule in validateRegistry()
keystable UPPER_SNAKEregex ^[A-Z0-9_]+$; duplicate or invalid = error
tier · priority · statusprimary/secondary/curated · P0/P1/P2 · wired/ready/blocked/researchthe daemon's ingest:due tick drains ready rows
engineone of the 10 AdapterEngine valuesunknown engine = error
licensekind (open/cc-by/non-commercial/share-alike/cite-only) + redistribute + aggregates_only?the license gate (below); aggregates_only + microdata engine = reminder warn: the adapter aggregates, never dumps rows
authnone/env_key/registration/approval + envenv = environment variable the adapter reads
endpointURL pattern with {iso3}/{code} placeholdersmissing = error
deep_linkper-record URL patternthis is what gets stamped into citations.json
cadence_daysnumberthe freshness ledger compares against this
geos · geo_levelISO3[] or "ALL" · country/adm1/adm2/point/supranationalgeo outside the 26-strong set (25 countries + LATAM) = warn
pillarsdimension slugsempty with engine ≠ provider = warn
expected_volumenumber ≥ 0negative or non-numeric = error; sizes the ±50% sentinel
verifysample_n + method (refetch-compare/liveness-only/checksum)missing block or sample_n < 0 = error

The adapters are failure-isolated: a dead endpoint returns [] instead of breaking the ingestion. Cadence (how many months between refreshes for each indicator) lives next to the source and materializes in the freshness ledger.

The license gate (at compile time) ​

The most important rule in the registry is not a runtime rule: it is a compile-time rule. validateRegistry() rejects any source whose license says redistribute: false while being wired to an engine that is not provider. The exact condition is threefold: redistribute === false and engine ≠ provider and status === "wired": a cite-only source may stay declared as research or blocked without breaking the build; what it cannot do is be wired to an engine that produces values. Republishing values from a source that only allows citing is a compile error (assertRegistryValid() throws with the list of offending keys), not a warning someone could ignore. A source's license posture decides whether its data appears as a republished indicator, only as an aggregate, or only as a citation, and it does so before the build produces a single file.

The same gate applies to data contributed by third parties via /contribuir: nothing is redistributed beyond what its license allows. It is the mechanism that keeps the Data Trust honest.

The ingestion stages ​

scripts/ingest-all.ts orchestrates three stages, each failure-isolated (a failing stage does not drag down the others):

  1. runMultisource() → parameter-cache/ cells + citations. This is the bulk: each indicator's value per country, with its citation_id. When two adapters emit the same id, mergeSeriesByYear unions them by year: the curated overlay wins on each shared year, and history the overlay does not cover is kept. FORCE_REPLACE_IDS opts out of that union: those ids are replaced wholesale when the methodology changes or the tip must move backwards.
  2. runDocuments() → documents/ and news/: documents and news by dimension and country.
  3. runSignals() → cited signals, LLM-extracted, in signals/.

When finished, it writes refresh-meta.json. There are additional domain ingesters for open budget, economic freedom, digital freedom, authoritarian drift, OGP, human freedom, press freedom, regulation, the observatory, the ADM1 subnational level and the city grain (ingest:city: deterministic and network-free, values verified twice against the cited official endpoints and pinned as constants with their formula declared, the same pattern as the ADM1 homicides ingester).

There is also a fourth, cadenced path: the social pulse. ingest-social.ts (GDELT events + GKG + social RSS) and fetch-trends.ts feed the social/, social-trends/, attention/ and civic/ families, with their own bakes (bake-trends, bake-civic, bake-attention). It appears in refresh-meta.json as the source social:gdelt, with a post count.

And a deterministic signals path: Earth observation. bake-earthobs-signals.ts (no LLM and no keys) queries two sources. Climate TRACE v7: monthly GHG emissions by country for the 25 geographies, in a single GET per month whose URL encodes the queried month; CC BY 4.0 license, with the inventory's record on Zenodo (concept DOI 10.5281/zenodo.15014790). And INPE's TerraBrasilis (CC-BY-SA 4.0): DETER (aggregated monthly deforestation and mining alerts in the Amazon, updated_date as the date of the latest alert; the last complete month is signaled because the current month is always partial) and PRODES (consolidated annual rate for the Legal Amazon). It emits signals of type earth_obs in signals/medio-ambiente-clima__<ISO3>.json, alongside the cell's LLM signals but under a different provenance contract (see Provenance). Honesty rules: every signal cites the exact GET that produced the value; a country absent or at zero in the response is omitted, never invented; a downed source leaves its cells intact (the upsert replaces by source, not by cell); aggregate statistics only, no imagery, no geometries, no satellite tasking, no imagery resale. It runs in the docs tier after the signal pruning, which also applies a per-source age cap so a downed upstream cannot leave its last reading on screen indefinitely.

And a fifth path: high-frequency (HF). ingest-hf.ts runs NASA POWER daily (one capital point per ISO3) and, in the same run, adapters for FIRMS, ClimateSERV CHIRPS, monthly Ember, Wikimedia, GHS, FAO ASIS, GFW and Meta Movement. As of 30 September 2026 seven more behavioural feeds are registered and still unpublished: Banxico SIE (monthly remittances and daily SPEI, plus the CSV for FIX, the policy rate, 28-day TIIE and reserves), WFP market food prices via HDX, CAMMESA monthly electricity demand (SSPM mirror), ChileCompra monthly purchase-order aggregates, Ember's monthly generation mix (renewables, fossil and coal shares; total generation stays on EMBER_MONTHLY) and the OpenAQ daily archive (country means of PM2.5 and PM10). The publication rule is strict: only nasa-power.json is committed and served. An empty store (observations: []) or a feed with no publishable rows is not written under public/data/hf-observations/: a hollow 200 is a defect, not a "coming soon". check-hf-publish.ts blocks the build if any extra JSON appears or POWER comes back empty. POWER does not enter parameter-cache or observations.csv (that CSV already exceeds GitHub's 100 MB ceiling). The hf lane does not move generated_at.

The bake ​

scripts/bake-all.ts transforms the raw cache into the derived datasets the pages consume: narratives → frontier indicators → subnational → pulse briefs and contradictions → forecasts → shadows → people graph → composite scores → markets index → _index.json. There are 41 bake-*.ts scripts plus 10 build-* generators: close to 50 bake-and-generate scripts: scores, markets, V-Dem, exploration, insights, data health, signals, coalitions, ontology, trends, civic, attention, feeds, contributions, compute sovereignty, regional convergence, instability nowcast, triangulation, narratives and the people graph, plus generators like build-democracy-index and build-search-index.

explore/, insights/ and ontology/ are re-baked on every scheduled data refresh (the corpus tier of data-refresh.yml, next to split-citations.ts).

Two domain generators index families you already saw in the dataset list: build-cases-index.ts regenerates the _index.json of cases/ (the per-country precedents and lessons, authored file by file) and gen-elections-index.mjs regenerates that of elections/ (the regional election calendar).

No fact is "baked" into a model: facts live in the data with their deterministic citation; the bake only precomputes what is derivable from them.

The baked public API ​

The pipeline has a fourth product besides the SPA's data: a static read-only API at public/api/v1, re-baked on every deploy by bake-api.ts — citations.csv/.json, freshness.json, contributions.json, futuros-data.xlsx and per-country and per-indicator records with M49/SDMX interoperability. It has its own gate: check-api.ts runs after the bake and verifies that the API mirrors the current data (for example, that its index's citation count matches citations.json). By construction, the API is never out of sync with the corpus. Full reference in Public API.

The dataset families (public/data/**) ​

The result is 69 families of static data. Among the main ones:

  • parameters.json (the 10 axes), geographies.json (25 countries + LATAM, with ISO3; M49 codes do not live here, they are joined from an ISO3→M49 mapping owned by bake-api.ts when baking the public API).

  • parameter-cache/ (indicator×country cells) and parameter-cache-adm1/ (thousands of subnational cells).

  • Behavioural ADM1 series (2026-10-15, batch 03): Anatel (fixed/mobile accesses and density by UF, monthly/annual, BRA, open CC BY); BCB Pix (transactions per individual payer per month, monthly rolled to complete years, BRA, ODbL); Chile SII (firms and female worker share by region, annual, CHL, open); Conab (grain yield/production/area by UF, in-season monthly, BRA, cite-only with attribution); CNJ Justiça em Números (congestion/IAD by state court, annual, BRA, LAI; complements CNJ DataJud); INEGI ENOE microdata (TIL1 labour informality by entity, quarterly, MEX, free-use terms; preferred over BIE ENOE). Each cell cites the concrete ZIP or record; no imputation.

  • Behavioural ADM1 and country series (2026-10-15, batch 04): ICFES Saber 11 (department mean punt_global, calendar-A 2014-2022, COL, CC BY-SA 4.0 via datos.gov.co; family parameter-cache-adm1-series-icfes/); IMSS insured jobs (jobs, insured, contribution wage and temporary share by entity, monthly, MEX, MX free use; sibling family parameter-cache-adm1-series-icfes/); PNCP Pregão Eletrônico (annual process counts by UF, BRA, open data); MapBiomas LULC (native vegetation by ADM1, annual, BRA/ARG/CHL, CC BY 4.0); INPE PRODES (annual Legal Amazon and Cerrado deforestation by UF, km², BRA, CC BY-SA TerraBrasilis; cells *__inpe_prodes.json under parameter-cache-adm1-series-icfes/ so they do not collide with MapBiomas or orphan_series); OpenStreetMap amenities via Geofabrik taginfo (hospital/clinic/school/bank per 100,000 people, year-end cut, 22 countries, ODbL). Each cell cites the concrete record; no imputation.

  • SESNSP state series 2015-2025 + RNID 2026: snapshot sesnsp-delitos-estatal.json; scripts/sources/adm1/bake-sesnsp-delitos.ts writes parameter-cache-adm1-series/seguridad__MEX__* (intentional homicide headline, five delitos in detail[], separate monthly_rnid block). Series break in 2026 (RNID): the new methodology is never spliced onto the annual series.

  • Behavioural ADM1 series (2026-10-15, batch 05): SECOP II (direct-award share by department, annual 2022-2025, COL, CC BY-SA 4.0 via datos.gov.co); SIAP agricultural close (loss rate, value, planted area and grain maize by entity, annual, MEX, CC BY 4.0 SADER); TSE electoral turnout (presidential first round by UF, biennial, BRA, CC BY; family parameter-cache-adm1-series-icfes/ so it does not overwrite PNCP); IBGE PNAD Contínua (unemployment, informality and real earnings by UF, quarterly, BRA, open SIDRA; family parameter-cache-adm1-series-icfes/). Each cell cites the concrete record; no imputation.

  • Country behaviour series (2026-10-15, batch 06): CEPALSTAT time use and unpaid care (OIG; care-related inactivity, country, annual, CC BY via CEPALSTAT; ECLAC citations); UNESCO LLECE ERCE 2019 / TERCE 2013 via UIS (grade-6 minimum proficiency in reading/math, overall and poorest quintile, 17 countries, UIS share-alike); GBIF occurrences (annual record counts and domestic publishing, 25 countries, CC BY aggregates); Global Forest Watch / UMD Hansen tree cover loss (ha, primary forest, % of 2000 extent and gross emissions, annual, 25 countries, CC BY 4.0; wave-H GFW API kept over the wave-I OWID mirror); GitHub Innovation Graph expanded (organizations, repositories, LATAM/US collaboration, Python pushers and MIT license, quarterly, 25 countries, CC0). Every cell cites the concrete record; no imputation.

  • Country behavioural series (2026-10-15, batch 07): Glottolog CLDF v5.3 (living/endangered/extinct languages and endangered share, country, by release, CC BY 4.0 Max Planck); UN Comtrade HS export composition (extractive, agri-food, machinery shares, country, annual, cite-only aggregates); Digital Policy Alert / SGEPT (digital-policy events by country-year, 16 countries, CC BY-NC 4.0); EIA International Energy Statistics (per-capita use, PPP intensity and grid losses, country, annual, US public domain); Ember power-sector emissions (gCO2/kWh intensity, MtCO2 and gas % of generation, country, annual, CC BY 4.0; deep links fixed); EM-DAT country profiles via HDX (events, climate events, deaths and affected, country, annual, CRED/UCLouvain non-commercial); GEM Adult Population Survey (eight key entrepreneurship indicators, 10 countries with fresh data ≥2024, cite-only aggregates). Every cell cites the concrete record; no imputation.

  • Country behavioural series (2026-10-15, batch 08): HRMI Rights Tracker ESR (SERF scores for education/food/health/housing/work, 23 countries, 2022 release / 2019 data, CC BY-NC 4.0); ITU DataHub v3 (mobile ownership, IMT spectrum and mobile broadband per 100, country, annual, CC BY-NC-SA 3.0 IGO); IUCN Red List Index / SDG 15.5.1 via UN (25 countries, annual, non-commercial); LAPOP AmericasBarometer (democracy support, trust, victimization and cohesion, 24 countries, 2025/26 waves, weighted aggregates, non-commercial); Latinobarómetro (support for and satisfaction with democracy, 17 countries, 2024 wave, cite-only aggregates); Media Cloud via Internet Archive Wayback (AI news attention per 10,000 stories on national ccTLDs, 20 countries, annual, cite-only aggregates). GFW TCL already shipped in batch 06 (WRI API); the wave-I OWID mirror was skipped. Every cell cites the specific record; no imputation.

  • Country behavioural series (2026-10-15, batch 09): ReliefWeb disasters list via HDX (annual disaster and climate-disaster counts, 25 countries, daily export / annual publish, CC BY 4.0 OCHA); UCDP GED by violence type (non-state, one-sided and civilian deaths, 25 countries, annual, CC BY 4.0; reuses the UCDP key); WDPA protected areas via WDI (terrestrial and marine, 25 countries, annual, CC BY 4.0 UNEP-WCMC/IUCN); OpenAlex facets (open-access share and SDG 3/4/7/10/13/16 tagged works, 25 countries, monthly, CC0; OPENALEX_FACETS key distinct from OPENALEX); Overture Maps Places (place density per 1,000 people and by category, 25 countries, per release, CC0 1.0). IATI and IBTrACS were already on main (#1244, #1234) and were skipped. Every cell cites the specific record; no imputation.

  • Country behavioural series (2026-10-15, batch 10): OECD PISA 2022 (math, reading and science means, 14 countries, triennial, CC BY-NC-SA 3.0 IGO); PSMSL RLR mean sea level (annual anomaly, 9 countries, annual, cite Holgate et al. / PSMSL); IMF IFS via DBnomics (FX depreciation and reserves ex-gold, 23 countries, annual, cited aggregates); WIPO IP Statistics (patents and trademarks by origin, 23 countries, annual, CC BY 4.0). UN Tourism was already on main (#1237) and was skipped. Every cell cites the specific record; no imputation.

  • parameter-cache-city/: the city grain, a first cohort of five anchor cities (CABA, CDMX, São Paulo municipality, Bogotá D.C., Santiago/RM) carrying measured-only data from their national statistical offices via keyless machine-readable routes (SIDRA N6, GAIA/ITER, Series de Tiempo, datos.gov.co SODA, INE SDMX). It is a sibling store to ADM1, not one more row: three of the five cities coincide with an ADM1 unit, and the municipality of São Paulo would collide with the slug of the state of the same name. The canonical registry lives in src/data/city-units.ts; each indicator declares its real grain per cell (for example, Santiago's labour indicators only exist at region grain). Whatever the source does not serve, such as IBGE's municipal Gini, INEGI's ENOE (token-gated) or DANE's annexes behind its WAF, is omitted and documented, never imputed.

  • hf-observations/: only nasa-power.json is published (non-empty NASA POWER). Other HF feeds, including Banxico SIE, WFP prices, CAMMESA, ChileCompra, the Ember mix and the OpenAQ archive, are ingested but not committed or served; an empty shell is a defect.

  • citations.json: the central citation registry (several MB).

  • documents/, news/, signals/, social/, social-trends/, attention/, shadows/.

  • personas/ (profiles + _index + _graph), state-history/, country-history/.

  • scores.json (composite), data-health.json, frontier-indicators/, compute-sovereignty/, agi-economy/, dependency/, climate-action/.

  • pulse/, regional-convergence/, subnational-convergence/, instability-nowcast/, triangulation/, uncertainty/, insights/.

  • pilots/, pilot-archetypes/, financing-instruments/, coalitions/, positions/, cases/.

  • regulation/, constitutional/, elections/, democracy/, transparency/, ai-governance/.

  • markets/, metrics/, ontology/, civic/, gender-gap/, trends/.

  • search-index.json (Voyage index for the assistant), catalog.json, sdg-crosswalk.json, falsifications.json.

Freshness ​

The platform rule: no published observation may be more than two years old. The threshold lives exactly once, in STALE_AFTER_YEARS in src/lib/vintage.ts, and the three surfaces that apply it all derive from there: the vintage badge the visitor sees (amber past the rule), check-vintage at individual-figure level and check-freshness at dataset-family level (730 days). When the publisher's own frontier is older than the rule, nothing is dressed up and no number is invented: either the data is refreshed from a current source covering the same concept, or that frontier is declared with its evidence and its re-verification date. As of 2026-09-09 there are six such fronts, every one checked live that day: the five families derived from Global Data Lab's Subnational HDI v10.2 (13 May 2026, coverage to 2023, the newest release GDL publishes) and Cuba's annual return to UN Comtrade, which records no filing after 2022. Costa Rica's Programa Estado de la Nación indicators are the same case one scale down, at figure level: the eight oldest — health investment per capita (2015), total road network and public health and education spending (2019), forest cover, total CO₂ emissions and primary and secondary dropout rates (2020) — are exactly the last observation PEN publishes for Costa Rica, verified against its API on 2026-09-09. They ship with their year visible and in amber, never as if they were current.

public/data/refresh-meta.json is the freshness ledger, with two granularities: the indicators map tracks the World Bank / WGI subset per indicator (49 codes); everything else (884 series in the baked source ledger) is tracked at feed level in the sources map (~200 entries such as social:gdelt or doc:exa, with kind, last_refreshed, next_refresh_due and counts like posts; the map itself carries the live count, which grows with every new source). series:* rows also carry cadence: cadence_months with cadence_basis: "observed" (median spacing between observations in the series we hold) or cadence_basis: "stated" when the cadence comes from the institution's official release calendar. Seven sources are audited against their calendars today (IBGE, INEGI, DANE, INDEC and BCB monthly; Banxico and CEPALSTAT annual, because we only pull their annual closes); they keep the observed spacing in cadence_observed_months and link the calendar in cadence_calendar_url. The shape of a per-indicator entry:

FieldExample (SI.POV.GINI)What it answers
source / source_urlWorld Bank Open Datawhere did it come from?
cadence_months / next_refresh_due12 / 2027-03-02when is a refresh due?
last_refreshed2026-07-28when did we last request it?
last_observation_year2025how new is the data itself?
countries_with_data / total_countries22 / 25how much coverage?
source_frontier_year / source_frontier_checked_at2025 / 2026-07-23is there anything newer upstream?
freshness_status / freshness_notecurrentverdict: current, source_not_updated or no_data

The distinction that carries the weight is last_refreshed vs last_observation_year: yesterday's pull with 2023 data is not a pipeline failure but provider lag — and source_frontier_year proves it, because it records the newest year the upstream publishes. That is what makes it possible to show a figure's vintage instead of pretending everything is up to date. The /confianza surface reads this ledger; /incertidumbre is something else, it exposes V-Dem confidence bands from uncertainty/vdem-ci.json, not this file.

The ledger also has an active guard, not just a screen. The multisource runner prunes and rewrites cells while ingesting, so a run with a downed or rate-limited adapter could silently erase cells that used to exist, the "my fetch failed, written to disk as there is nothing there" defect. freshness-guard.py prevents it with three subcommands over parameter-cache/:

snapshot <out.json>          # before the run
  for each parameter-cache/<pillar>__<ISO3>.json, for each indicator:
    snap[id][cell] = { vintage: vintage_year, value, obj: <full indicator> }

compare <before.json>        # after the run
  per indicator:  gone       (cells that disappeared)
                  regressed  (vintage went backwards: after < before)
                  advanced   (vintage moved forward)  ·  added (new cells)
  reports the total of lost + regressed cells

repair <before.json> --apply
  restores ONLY gone ∪ regressed, from the obj stored in the snapshot
  advanced and added are left intact — a genuine advance is never reverted
  without --apply it is a dry run: lists what it would restore, touches no disk

Subtle invariant of the --only=<substr> filter: the guard filters the baseline with the same terms as the live snapshot. Without that, every indicator outside the filter would look gone and repair would restore the entire platform to the baseline, including later deliberate fixes. Refresh runs are non-destructive by construction.

In the prebuild, bake-source-ledger.ts bakes the source ledger that feeds /confianza and stamps the top-level fields source_ledger_baked_at and source_ledger_series into refresh-meta.json; on data refreshes, recheck-wb-freshness.ts re-verifies World Bank vintages against the upstream and stamps wb_freshness_checked_at.

The prebuild gates ​

The prebuild is a chain of gates linked with && in package.json; if any of them fails, the build fails and nothing gets deployed:

* = advisory (--warn): reports and exits 0.

First the groundwork: two typechecks (tsc -p tsconfig.scripts.json for the ingestion and bake scripts, tsc -p tsconfig.server.json for the assistant server and the functions), the full test suite (bun test --timeout 15000: 2,443 tests across 186 files, including the assistant's server, the MCP handler and the bakers) and build-positions-index --check. Then twenty-three named check-* checks, plus two gates in --check mode over scripts that normally write (build-positions-index, bake-trends-index). Two are advisory (--warn: they report and exit 0, check-search-index, check-adm1-coverage); the rest fail the build, including check-vintage, which stopped being advisory on 2026-09-09, and the new check-freshness.

Corpus provenance and coherence:

  1. check-traced: every figure has its trace back to a source.
  2. check-signals: the signals are well-formed and cited. LLM signals must cite a corpus document of the cell with the figure verbatim from its excerpt; earth_obs signals (deterministic, no sibling document) pass a structural gate: an allowlisted https host (only api.climatetrace.org and terrabrasilis.dpi.inpe.br), fixed cell and source, full ISO date and the figure verbatim inside its own claim_es (with the same digits in claim_en/claim_pt).
  3. check-source-links: citation links point to the specific country + indicator record. The only exception is the LANDING_OK list inside the gate itself: a documented per-source opt-out (curated and internal layers that resolve to /metodologia, documentary layers EXA/GDELT/RSS/gazettes/signals and a few sources with no stable per-record URL whose honest destination is their own dataset page), each with its justification in the code. Everything else carries a deep link or the build fails.
  4. check-pilots: pilot coherence.
  5. check-persona-scope: no profile may be unreachable. The /personas country <select> is built from the canonical 25, so a profile filed under an iso3 outside that set was a complete, sourced record no visitor could filter to (audit D-5 found 50 of 1,384 that way: Barbados, the Bahamas and one Miami-based columnist under USA). They were not deleted, they were tiered; this gate makes the tiering binding.
  6. check-citations: integrity of the citation registry.
  7. check-parameter-cache-citations: every citation_id in parameter-cache/ resolves in citations.json, with no synthetic-citation fallback. The public API was already clean; this gate keeps the cache it is baked from clean too.
  8. check-scores: coherence of the composite scores (see The Futuros Index).
  9. check-provenance: rejects fabricated hashes or digests (truncated sha256 stubs) anywhere in public/data; born from the July 2026 data-accuracy audit. 9b. check-frontier-provenance: fails the build if any row under public/data/frontier-indicators/** carries provenance: "synthesised", simulated: true, an unresolvable citation_id, or a value under a frontier catalog key marked redistribute: false. It is the gate that retired the fabricated eight-indicator corpus and keeps the cited corpus (97 indicators × 25 countries) honest.
  10. colombia/check-real-data: separates real from synthetic in the Colombia corpus, so no demo figure ships without its banner.

Style, taxonomy and coverage:

  1. check-emdash-drift: style drift (em dashes) does not reappear in already-swept content.

  2. check-landing-stats: every number printed on public/landing.html is still the number the data says. The landing is a hand-edited artifact, the highest-traffic page and the only one with no component, dictionary or test behind it; its statistics were free to rot, and they did (the 2026-08-13 audit found it advertising 6,726 citations against a corpus of 6,855, and "300+ pilots" against an index of 337). The gate recomputes each figure from public/data/** the way the matching surface does, and prints the literal to paste. It checks six today: distinct primary sources, countries, axes, citations, personas and instruments.

  3. check-docs-citations: every hand-written citation-corpus figure in docs/ is still the one citations.json says. It is check-landing-stats's twin for the documentation site: the citation and primary-source counts live by hand in the ES/EN/PT index, in Provenance and citations, in the Spanish edition of this page and in the /api/v1/meta examples in dev/public-api, with no component, dictionary or test behind them, and they rotted the same way: through August 2026 the docs advertised ~6,850 citations (the July figure) while the live corpus was already at 15,098, a 2.2x understatement that no automated audit caught because nothing connected the two. The figures were corrected on 2026-09-04; this gate is what keeps them from drifting apart again. Beyond the declared slots it sweeps all of docs/ for count-shaped claims, so a new page carrying a stale figure fails too. Historical figures are exempt if they say which audit and when: narrating the past is honest, repeating it as the present is not.

  4. check-basedosdados: the Brazilian municipal-grain store is held to the same contract as parameter-cache: every citation_id resolves both in the store's own citations and in the global citations.json; every citation points to a specific Base dos Dados table (?table=<uuid>) and also to the record of the agency that produced the figure, because BD is a mirror and not the producer; every indicator declares its own vintage, so no blended "latest figure" can be published; and no declared column may come back empty across every row. That last rule exists because it happened: SICONFI state revenue was selected by label (Receitas Orçamentárias), a line item that does not exist at that accounting stage, and produced 27 nulls that looked like a data gap and were a selector bug. The store covers Brazil only (municipality/UF grain), so the gate does not require coverage of any other country; if the store has never been ingested, it passes green: a clean checkout still builds.

  5. check-catalog: the dataset catalog is complete and consistent.

  6. check-search-index (advisory), the semantic index covers the corpus.

  7. check-law-term-index: the legal term index covers 100% of the regulation corpus (instrument_count from regulation/_index.json). Local index only: Voyage stays opt-in and fail-closed.

  8. bake-trends-index --check: the baked trends index matches its inputs.

  9. check-vintage: how many published figures break the two-year vintage rule. The threshold and the year-resolution order come from src/lib/vintage.ts, the same module the headline strips render from, so the gate measures what the visitor sees. It exists because bake-data-health scores a cell by the median age of its indicators: the platform reported 2 stale cells out of 260 while 27% of individual figures broke the rule. It was advisory until 2026-09-09 — it measured the rule without being able to enforce it — and now fails the build above its ratchet of 8,978 stale figures, which may only go down. The one exception is the year boundary: oldestAllowed rolls with the calendar, so on 1 January a whole annual cohort turns stale at once and the count leaps thousands above a ceiling measured in the previous year. The ratchet therefore records the year it was measured in, and outside that year the gate reports loudly and exits 0 until someone re-measures and commits the new ceiling. A broken build nobody caused erodes a gate faster than the drift it watches for.

  10. check-freshness: no live platform dataset is older than two years. Where check-vintage counts individual figures inside parameter-cache/, this gate asks what that one structurally cannot: whether a whole feed is stuck. It walks the fifteen quantitative dataset families declared in scripts/lib/freshness.ts and compares each one's newest observation against the 730-day limit derived from the same STALE_AFTER_YEARS. Families with one file per country are judged record by record, the only grain that catches the defect that produced the gate: the 2026-07-23 UN Comtrade bake died after six countries and left 19 of 25 at trade-year 2023 while the six it had refreshed held the family's headline at 2025. It never counts a generated_at or a last_refreshed — those say when we ran, not when the world was measured — and it discards projections (IMF WEO runs to 2031, CCKP SSP scenarios to 2080): a forecast is not an observation. A year is dated to the end of that year, never to 1 January, because rounding in the flattering direction is precisely the error the gate is looking for. When the frontier is the publisher's rather than ours, it is declared as an expiring exemption: who publishes, which release, which year, the URL that proves it, when it was verified live and by when it must be verified again. The gate fails if the exemption lapses, if the declared frontier stops matching what the files measure, or if it covers data that has become current again — which is what stops today's honest exemption from becoming a permanent allowlist.

  11. check-adm1-coverage (advisory), the honesty gate for the subnational layer. "432 of 456 ADM1 units carry real data" was true only of the GDL universal overlay; at eje level no unit carried more than 5 of the 10 ejes on a real measurement, five ejes were 100% modelled, and 26 units had their cells written under slugs the app can never request. Nothing detected any of it, so it drifted silently for months.

  12. check-city-cells: the honesty gate for the city grain (parameter-cache-city/), strict from birth (no ratchet: the store was born clean). The contract is measured-only: modelled coverage levels are structurally impossible in this store; every indicator requires a finite value, a current year, a citation_id ending in -<year> that resolves in citations.json and whose row carries exactly the same url as the cell's source_url (a cell and its citation cannot point to different places), a source_url with a deep link (never the source's landing page) and a slug that resolves in src/data/city-units.ts. The registry is the coverage claim: a city with no data is a city that does not get registered.

  13. check-i18n: ES/EN/PT coverage does not regress.

  14. check-taxonomy: UI copy does not revert to the retired nouns for the ten sectoral measures (diez parámetros, ten dimensions, dez dimensões) nor point nav at /governance. The ten are ejes / axes / eixos.

  15. check-hf-publish: only nasa-power.json may live under public/data/hf-observations/, and it must be a non-empty NASA POWER store. Empty shells (observations: []) and unpublished feeds (CHIRPS, FIRMS, Ember…) are a defect: they serve a 200 that looks live.

  16. check-api: runs last, after the prebuild bakes (bake-data-health, bake-source-ledger, bake-api, bake-feeds, bake-research-library), and verifies with seven assertion groups that the static public API mirrors the current data. bake-research-library does not feed the API: it writes public/data/research-library.json and public/research/<slug>.json for /biblioteca.

The provenance gates in the strict sense are the subset check-traced, check-signals, check-source-links, check-citations, check-parameter-cache-citations, check-provenance and check-frontier-provenance: but they all share the same consequence: they are the reason "every figure with its source" is not a slogan but a compile-time invariant. How a citation resolves on screen is explained in Provenance and citations.

A pattern repeats across the newer gates (check-landing-stats, check-docs-citations, check-vintage, check-freshness, check-adm1-coverage, check-persona-scope): each was born from an audit that found a claim true when it was written and false when it was read, with nothing connecting it to the data. The gate is that connection. It is the same reason this documentation exists, and the reason it is audited against the code.

Every figure with its source — traceability is the contract.