Skip to content

El pipeline de datos

Futuros no consulta APIs en vivo cuando alguien abre una página. Todo el corpus se ingiere, se hornea y se congela como JSON estático antes del despliegue; la SPA solo lee archivos. Eso hace la plataforma rápida, cacheable, offline-capaz — y, sobre todo, auditable: cada cifra que ves fue verificada en tiempo de build contra su registro de fuente. Este pipeline sirve directamente al Pilar 1 — Fideicomiso de Datos: un corpus con procedencia y licencia limpia es el único activo que un modelo regional no puede copiar.

Transportes meta: una API, muchas agencias

Dos conectores no traen una fuente sino decenas, y por eso rinden más por línea de código que cualquier adaptador individual:

  • DBnomics (makeDbnomicsAdapter) — 93 proveedores tras una sola API abierta; es el transporte de FMI WEO, UNCTAD y OMC.
  • World Bank Data360 (scripts/sources/gap/data360.ts) — ~35 bases estadísticas del Banco y de agencias asociadas (UIT, WIPO, WEF, Yale) tras una API sin clave con código ISO3 en cada fila. El motor descarga primero el CSV masivo de cada base (data360files.worldbank.org, sin límite de tasa) y lo filtra en streaming; solo cae a la API paginada cuando el archivo supera el tope de tamaño. La API responde con HTTP 417 cuando limita el tráfico, así que el motor lleva su propio backoff de 3 a 60 segundos.

Dos disciplinas que estos transportes imponen y que valen para cualquier fuente nueva:

  • Fijar los miembros de cada dimensión. Una misma serie puede traer el presupuesto aprobado y el ejecutado, en moneda local y como porcentaje del gasto, bajo un único código. Si no se fija la dimensión, la serie mezcla dos cosas distintas sin avisar. Los codelists se leen del endpoint /portal/v1/dimensions, no se adivinan.
  • Cortar las series de flujo en el último año completo. Los agregados anuales de aprobaciones (Banco Mundial, GCF, CFI, MIGA) tendrían un año parcial al final, y la celda lo leería como un desplome. Se corta en el último año calendario cerrado.

El registro de fuentes

Cada fuente externa es un SourceRecord declarado en scripts/sources/registry/. Define de dónde sale el dato y bajo qué reglas puede publicarse:

  • AdapterEngine — el tipo de conector: series, bulk, sdmx, ckan, ocds, event, entity, microdata, provider o meta. Cada API externa tiene un solo adaptador en adapters.ts (WHO GHO, UNESCO UIS, OpenAlex, Climate Watch, UN SDG, UNHCR, OWID, IMF DataMapper, SDMX/DBnomics genérico…). Las fuentes añadidas por olas posteriores viven en scripts/sources/gap/, un archivo por fuente, y entran a ALL_ADAPTERS como un solo ...GAP_ADAPTERS: añadir una fuente es un archivo nuevo más una línea de import, nunca una edición dentro del adapters.ts de 4.400 líneas. La ola E lleva ese mismo principio un nivel más abajo: sus adaptadores viven en scripts/sources/gap/wave-e/<fuente>.ts con su propio índice, porque la ola D había concentrado sus 19 fuentes en un solo gap/index.ts y eso recreaba, un nivel más adentro, el punto de colisión que este diseño existe para evitar — cada persona que añade una fuente quiere la última línea del mismo archivo. La ola F (granularidad plena, wave-f/ + src/lib/gap-sources-f/) usa el mismo corte: un archivo por fuente para emaciación JME, matrícula terciaria UIS, nutrición DHS, espacio cívico CIVICUS, relleno Ember/IRENA, y sondas de hueco honesto (RICYT, ILIA/GIRAI/GDB, INE.Stat salud/innovación de Chile, ENDI Ecuador) que re-verifican y devuelven [] en vez de inventar un número. scripts/refresh-wave-f.ts refresca solo las series que producen valores (mensual). Con el corte por archivo, dos personas que añaden dos fuentes tocan dos archivos distintos. scripts/sources/gap/wave-e/probe-file.ts completa el patrón: ejecuta los adaptadores de un módulo sin escribir nada, de modo que una fuente se construye y se verifica entera antes de que su línea entre en ningún índice compartido.
  • LicensePosturekind / redistribute / aggregates_only: qué se puede republicar y qué solo se puede citar.
  • Verificación por sondasources.ts es SOURCE_REGISTRY_V2, con endpoints probados; regulation-sources.ts es el registro hermano para el rastreo de leyes de IA. Los nso/*.json son 27 listas de fuentes por oficina nacional de estadística.
  • Anatomía de verificación — cada registro declara además su deep_link (la URL por-registro que se estampa en citations.json), su cadencia (cadence_days), un spec verify de re-verificación por muestreo (refetch-compare: volver a pedir una muestra y comparar contra lo guardado) y un expected_volume dimensionado para un centinela de volumen de ±50%. Ojo: el centinela y el re-verificador son hoy contrato declarado del esquema — ningún ingestor los ejecuta todavía; el guardián que sí corre en cada refresco es el guardián de frescura.

El esquema completo de un SourceRecord (scripts/sources/registry/schema.ts) — cada campo con la regla que validateRegistry() le aplica:

CampoTipo / valoresRegla en validateRegistry()
keyUPPER_SNAKE estableregex ^[A-Z0-9_]+$; duplicado o inválido = error
tier · priority · statusprimary/secondary/curated · P0/P1/P2 · wired/ready/blocked/researchel tick ingest:due del daemon drena filas ready
engineuno de los 10 AdapterEnginemotor desconocido = error
licensekind (open/cc-by/non-commercial/share-alike/cite-only) + redistribute + aggregates_only?la compuerta de licencia (abajo); aggregates_only + motor microdata = warn recordatorio: el adaptador agrega, nunca vuelca filas
authnone/env_key/registration/approval + envenv = variable de entorno que lee el adaptador
endpointpatrón URL con placeholders {iso3}/{code}faltante = error
deep_linkpatrón URL por-registroes lo que se estampa en citations.json
cadence_daysnúmeroel ledger de frescura compara contra esto
geos · geo_levelISO3[] o "ALL" · country/adm1/adm2/point/supranationalgeo fuera del set de 26 (25 países + LATAM) = warn
pillarsslugs de dimensiónvacío con motor ≠ provider = warn
expected_volumenúmero ≥ 0negativo o no-numérico = error; dimensiona el centinela ±50%
verifysample_n + method (refetch-compare/liveness-only/checksum)bloque faltante o sample_n < 0 = error

Los adaptadores están aislados por fallo: un endpoint muerto devuelve [] en vez de romper la ingesta. La cadencia (cada cuántos meses se refresca cada indicador) vive junto a la fuente y se materializa en el ledger de frescura.

La compuerta de licencia (en compilación)

La regla más importante del registro no es de runtime: es de compilación. validateRegistry() rechaza cualquier fuente cuya licencia diga redistribute: false pero esté cableada a un motor que no sea provider. La condición exacta es triple: redistribute === false y motor ≠ provider y status === "wired" — una fuente cite-only puede quedar declarada como research o blocked sin romper el build; lo que no puede es estar cableada a un motor que produce valores. Republicar valores de una fuente que solo permite citar es un error de compilación (assertRegistryValid() lanza con la lista de claves ofensoras), no un aviso que alguien podría ignorar. La postura de licencia de una fuente decide si sus datos aparecen como indicador republicado, solo como agregado, o solo como cita — y lo hace antes de que el build produzca un solo archivo.

Esta misma compuerta aplica a los datos aportados por terceros vía /contribuir: nada se redistribuye más allá de lo que su licencia permite. Es el mecanismo que mantiene honesto al Fideicomiso de Datos.

Las etapas de ingesta

scripts/ingest-all.ts orquesta tres etapas, cada una aislada por fallo (una etapa que falla no arrastra a las otras):

  1. runMultisource() → celdas de parameter-cache/ + citas. Es el grueso: el valor de cada indicador por país, con su citation_id.
  2. runDocuments()documents/ y news/: documentos y noticias por eje y país.
  3. runSignals() → señales citadas, extraídas por LLM, en signals/.

Al terminar, escribe refresh-meta.json. Hay ingestores de dominio adicionales para presupuesto abierto, libertad económica, libertad digital, deriva autoritaria, OGP, libertad humana, libertad de prensa, regulación, observatorio y el nivel subnacional ADM1.

Existe además una cuarta vía cadenciada: el pulso social. ingest-social.ts (eventos GDELT + GKG + RSS social) y fetch-trends.ts alimentan las familias social/, social-trends/, attention/ y civic/, con sus propios horneados (bake-trends, bake-civic, bake-attention). Aparece en refresh-meta.json como la fuente social:gdelt, con conteo de posts.

Y una quinta vía: alta frecuencia (HF). ingest-hf.ts corre NASA POWER diario (un punto capital por ISO3) y, en la misma corrida, adapters de FIRMS, ClimateSERV CHIRPS, Ember mensual, Wikimedia, GHS, FAO ASIS, GFW y Meta Movement. La regla de publicación es estricta: solo nasa-power.json se commitea y se sirve. Un almacén vacío (observations: []) o un feed sin filas publicables no se escribe bajo public/data/hf-observations/ — un 200 hueco es un defecto, no un “próximamente”. check-hf-publish.ts bloquea el build si aparece cualquier JSON extra o un POWER vacío. POWER no entra en parameter-cache ni en observations.csv (ese CSV ya rebasa el tope de 100 MB de GitHub). El lane hf no mueve generated_at. No se listan en la superficie pública de Fuentes: el mismo criterio que las olas E/F. No se adaptan Advan, Veraset, Spectus, rasters de luz nocturna, Open-Meteo, rastros de dispositivos, GDB, GIRAI ni el lakehouse B0.5.

Cadencia recurrente

Desde agosto de 2026 el refresco ya no depende de corridas manuales: tres workflows de GitHub Actions ejecutan la ingesta en cadencia y publican por el mismo camino verificado (compuerta verde con el prebuild completo → PR rodante → squash-merge automático; nunca un push directo a main):

Workflow / tierCadenciaQué refresca
social-ingest.ymlcada 2 horaspulso social, cívico y de atención (GDELT + Wikimedia + RSS, sin clave)
data-refresh.yml · docslun/mié/viecorpus de noticias y documentos (GDELT + RSS + gacetas sin clave; Exa si su secret existe) + extracción de señales citadas (si ANTHROPIC_API_KEY está configurada; si no, ingesta de docs sin señales LLM) + poda de señales huérfanas + snapshot Polymarket
data-refresh.yml · derivedmartessonda de frontera WB (recheck-wb-freshness) + horneados derivados que rezagan de sus insumos: puntajes compuestos, resiliencia democrática, screener de mercados, brief de pulso, manifiesto _index.json
data-refresh.yml · corpusdía 1 de cada mescorpus numérico completo: Banco Mundial/WGI (refresh-from-live-apis) + series multisource (ingest-all --skip-docs --skip-signals) + shards de citas + todos los derivados + ingesta y horneado del observatorio regulatorio (ingest-regulationbake-regulation)
data-refresh-hf.yml · dailycada día 06:17 UTCNASA POWER (punto capital) + FIRMS (bbox; FIRMS_MAP_KEY solo si el secret existe) + CHIRPS (aislado; HF_SKIP_CHIRPS=1 lo sostiene) + GFW alertas integradas (GFW_API_KEY opcional; sin clave usa download/csv)
data-refresh-hf.yml · emberdías 1 y 15Ember mensual, PASS-PARTIAL (12 ISO3 en el CSV; no se inventan celdas)
data-refresh-hf.yml · wikidías 3–5 de cada mesWikimedia pageviews top-by-country (ISO2→ISO3; CU/HN/NI/VE ausentes en 2026-07 se omiten)
data-refresh-hf.yml · ghsdía 1JRC GHS-COUNTRY-STATS (superficie construida km², épocas 1975–2030; el ZIP no se commitea)
data-refresh-hf.yml · asisdía 1FAO ASIS anual nacional (PROVINCE=ALL; 403 a UA de Python, 200 con UA de navegador)
data-refresh-hf.yml · metadías 1 y 15Meta Movement Distribution (HDX; rollup país×día; CSV crudo 48–195 MB se descarta)

Las corridas del tier corpus van envueltas en el guardián de frescura (snapshot → repair) y en INGEST_GUARD=1, de modo que un adapter estrangulado no puede borrar celdas en silencio. Los lanes con clave de pago (Exa, Voyage, señales LLM vía ANTHROPIC_API_KEY) y los índices anuales que requieren un bump de URL por edición (V-Dem, RSF, HFI, WEO oct/abr) siguen siendo corridas manuales deliberadas cuando la clave falta — el detalle vive en la auditoría DATA_AUDIT_2026-08-22.md del repo.

Ampliaciones de corpus (2026-08-24). Sobre los adaptadores de las olas E/F ya cableados, se hornearon series nuevas y verificadas en vivo: expansión CEPALSTAT (femicidios en número, maternidad adolescente, participación laboral, ocupación carcelaria, primaria completa 15–19); la tasa de política monetaria del BPI (BIS_CBPOL / WS_CBPOL) para los seis bancos centrales de ALC que publican la serie; profundidad WHO GHO (MCV2, hipertensión, cobertura ART) y UNESCO UIS (finalización secundaria, alfabetización adulta); RSF y ND-GAIN llevados a parameter-cache (libertad de prensa 2013–2026 e índice/vulnerabilidad/preparación climática 1995–2024); DesInventar/Sendai (DESINVENTAR) con el conteo anual de eventos de desastre reportados para 22 geografías (BRA/CUB/SUR sin perfil publicado, omitidos); Freedom House FIW (FIW) con Total/PR/CL 2013-2024 para las 25 geografías; lactancia materna exclusiva UNICEF (NT_BF_EXBF) sobre el adaptador SDMX ya cableado; inseguridad alimentaria severa OWID/FAO; anomalía térmica CMIP6 SSP2-4.5 vía CCKP (cuatro ventanas 2020-2099); e iniciativas STI iniciadas (OCDE STIP) para siete geografías del panel. Cada cifra lleva cita con deep-link al registro, CSV o ZIP concreto; los países sin serie se omiten, nunca se imputan.

El horneado (bake)

scripts/bake-all.ts transforma el cache crudo en los datasets derivados que consumen las páginas: narrativas → indicadores de frontera → subnacional → briefs y contradicciones de pulso → pronósticos → shadows → grafo de personas → puntajes compuestos → índice de mercados → _index.json. Son 44 scripts bake-*.ts más 10 generadores build-* — 54 scripts de horneado y generación: puntajes, mercados, V-Dem, exploración, insights, salud del dato, señales, coaliciones, ontología, tendencias, cívica, atención, feeds, contribuciones, soberanía de cómputo, convergencia regional, nowcast de inestabilidad, triangulación, narrativas y grafo de personas, más generadores como build-democracy-index y build-search-index.

Dos generadores de dominio indexan familias que ya viste en la lista de datasets: build-cases-index.ts regenera el _index.json de cases/ (los precedentes y aprendizajes por país, autorados archivo a archivo) y gen-elections-index.mjs el de elections/ (el calendario electoral regional).

Ningún hecho se "hornea" dentro de un modelo: los hechos viven en los datos con su cita determinista; el horneado solo precalcula lo derivable de ellos.

La API pública horneada

El pipeline tiene un cuarto producto además de los datos de la SPA: una API estática de solo lectura en public/api/v1, re-horneada en cada despliegue por bake-api.tscitations.csv/.json, freshness.json, contributions.json, futuros-data.xlsx y registros por país y por indicador con interoperabilidad M49/SDMX. Tiene su propia compuerta: check-api.ts corre después del horneado y verifica que la API espeja los datos actuales (por ejemplo, que el conteo de citas de su índice coincide con citations.json). Por construcción, la API nunca queda desfasada del corpus. Referencia completa en Public API.

Las familias de datasets (public/data/**)

El resultado son 68 familias de datos estáticos. Entre las principales:

  • parameters.json (los 10 ejes), geographies.json (25 países + LATAM, con ISO3; los códigos M49 no viven aquí — se unen desde un mapeo ISO3→M49 propio de bake-api.ts al hornear la API pública).
  • parameter-cache/ (celdas indicador×país) y las tres capas subnacionales: parameter-cache-adm1/ (miles de celdas ADM1), parameter-cache-adm1-series/ (sus series temporales) y parameter-cache-adm1-universal/ (la superposición universal GDL — esperanza de vida, INB per cápita, escolaridad esperada — que cubre unidades donde los ejes no llegan).
  • hf-observations/ — el único JSON publicado es nasa-power.json (NASA POWER, punto capital, no vacío). El resto de feeds HF se ingiere pero no se commitea ni se sirve; un archivo hueco es un defecto.
  • citations.json — el registro central de citas (15.098 registros, varios MB) y citations/ — sus shards, para que un consumidor no tenga que traer el registro entero.
  • documents/, news/, signals/, social/, social-trends/, attention/, shadows/.
  • personas/ (perfiles + _index + _graph), state-history/, country-history/.
  • scores.json (compuesto), 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/ (índice horneado + _instruments.json), regulation-raw/ (lo ingerido antes de hornear), legislation/, policies/, constitutional/, elections/, democracy/, transparency/, governance/, ai-governance/.
  • markets/, metrics/, ontology/, civic/, gender-gap/, trends/, explore/ (el banco de 215 métricas de /explorar), polymarket/ (snapshot de mercados de predicción), biotech/.
  • colombia/ — el corpus de la respuesta de Colombia, cuyo nivel de persona y operación es sintético y va con banner de demostración (ver Colombia).
  • search-index.json (índice Voyage para el asistente), catalog.json, sdg-crosswalk.json, falsifications.json, ai-runs.json, personas-images.json.

Frescura

public/data/refresh-meta.json es el ledger de frescura, con dos granularidades: el mapa indicators rastrea por-indicador el subconjunto Banco Mundial / WGI (49 códigos); todo lo demás (884 series en el ledger de fuentes horneado) se rastrea a nivel de feed en el mapa sources (197 entradas como social:gdelt o doc:exa, con kind, last_refreshed, next_refresh_due y conteos como posts). El campo de nivel superior generated_at significa el último ingest no-social (series / documentos / pull del Banco Mundial): el pulso social de cada 2 h estampa sources.social:* y lanes.social, y no mueve generated_at — si lo hiciera, un corpus de noticias rancio de 25 días se leería como actual. El mapa lanes (docs / social / hf / derived) es la lectura honesta por tier. Junto a ellos, el nivel superior lleva total_indicators, scope, vintage_note, generated_at_note, wave_f_refreshed_at y los tres sellos que estampan las compuertas y los refrescos (source_ledger_baked_at, source_ledger_series, wb_freshness_checked_at). La forma de una entrada por-indicador:

CampoEjemplo (SI.POV.GINI)Qué responde
source / source_urlWorld Bank Open Data¿de dónde salió?
cadence_months / next_refresh_due12 / 2027-03-02¿cuándo toca refrescar?
last_refreshed2026-07-28¿cuándo lo pedimos por última vez?
last_observation_year2025¿qué tan nuevo es el dato en sí?
countries_with_data / total_countries22 / 25¿cuánta cobertura?
source_frontier_year / source_frontier_checked_at2025 / 2026-07-23¿existe algo más nuevo en el upstream?
freshness_status / freshness_notecurrentveredicto: current, source_not_updated o no_data

La distinción que carga el peso es last_refreshed vs last_observation_year: un pull de ayer con dato de 2023 no es un fallo del pipeline sino rezago del proveedor — y source_frontier_year lo demuestra, porque registra el año más nuevo que el upstream publica. Es lo que hace posible mostrar el vintage de una cifra en vez de fingir que todo está al día. La superficie /confianza lee este ledger; /incertidumbre es otra cosa — expone bandas de confianza de V-Dem desde uncertainty/vdem-ci.json, no este archivo.

El ledger tiene además un guardián activo, no solo una pantalla. El runner multisource poda y reescribe celdas al ingerir, así que una corrida con un adaptador caído o limitado podría borrar en silencio celdas que existían — el defecto "mi fetch falló, escrito a disco como no hay nada". freshness-guard.py lo impide con tres subcomandos sobre parameter-cache/:

snapshot <out.json>          # antes de la corrida
  por cada parameter-cache/<pilar>__<ISO3>.json, por cada indicador:
    snap[id][celda] = { vintage: vintage_year, value, obj: <indicador completo> }

compare <before.json>        # después de la corrida
  por indicador:  gone       (celdas que desaparecieron)
                  regressed  (vintage retrocedió: after < before)
                  advanced   (vintage avanzó)  ·  added (celdas nuevas)
  reporta el total de celdas perdidas + retrocedidas

repair <before.json> --apply
  restaura SOLO gone ∪ regressed, desde el obj guardado en el snapshot
  advanced y added se dejan intactos — un avance genuino nunca se revierte
  sin --apply es dry run: lista qué restauraría, no toca disco

Invariante sutil del filtro --only=<substr>: el guardián filtra la línea base con los mismos términos que el snapshot vivo. Sin eso, todo indicador fuera del filtro parecería gone y repair restauraría la plataforma entera a la línea base — incluidas correcciones deliberadas posteriores. Las corridas de refresco son no destructivas por construcción.

En el prebuild, bake-source-ledger.ts hornea el ledger de fuentes que alimenta /confianza y estampa en refresh-meta.json los campos de nivel superior source_ledger_baked_at y source_ledger_series; en los refrescos de datos, recheck-wb-freshness.ts re-verifica los vintages del Banco Mundial contra el upstream y estampa wb_freshness_checked_at.

Las compuertas del prebuild

El prebuild es una cadena de compuertas encadenadas con && en package.json; si cualquiera falla, el build falla y nada se despliega:

* = consultiva (--warn): reporta y sale 0.

Primero el terreno: dos typechecks (tsc -p tsconfig.scripts.json para los scripts de ingesta y horneado, tsc -p tsconfig.server.json para el servidor del asistente y las funciones), la suite completa de tests (bun test --timeout 15000 — 2.076 tests en 175 archivos, que incluyen el servidor del asistente, el handler MCP y los horneadores) y build-positions-index --check. Después, veintiuna comprobaciones check-* con nombre propio, más dos gates en modo --check sobre scripts que normalmente escriben (build-positions-index, bake-trends-index). Tres son consultivas (--warn: reportan y salen 0 — check-search-index, check-vintage, check-adm1-coverage); las demás hacen fallar el build.

Procedencia y coherencia del corpus:

  1. check-traced — toda cifra tiene su rastro a fuente.
  2. check-signals — las señales están bien formadas y citadas.
  3. check-source-links — los enlaces de cita apuntan al registro específico de país + indicador. La única excepción es la lista LANDING_OK dentro de la propia compuerta: un opt-out documentado por fuente (capas curadas e internas que resuelven a /metodologia, capas documentales EXA/GDELT/RSS/gacetas/señales y unas pocas fuentes sin URL estable por registro cuyo destino honesto es su propia página de dataset), cada uno con su justificación en el código. Todo lo demás lleva deep link o el build falla.
  4. check-pilots — coherencia de los pilotos.
  5. check-persona-scopeningún perfil puede quedar inalcanzable. El <select> de país de /personas se construye desde los 25 canónicos, así que un perfil archivado bajo un iso3 fuera de ese conjunto era una ficha completa y con fuentes que ningún visitante podía filtrar (la auditoría D-5 encontró 50 de 1.384 así: Barbados, Bahamas y un columnista radicado en Miami bajo USA). No se borraron, se escalonaron por tier; esta compuerta hace vinculante el escalonamiento.
  6. check-citations — integridad del registro de citas.
  7. check-parameter-cache-citations — cada citation_id de parameter-cache/ resuelve en citations.json, sin recurso a una cita sintética de respaldo. La API pública ya estaba limpia; esta compuerta mantiene limpia la caché desde la que se hornea.
  8. check-scores — coherencia de los puntajes compuestos (ver El Índice Futuros).
  9. check-provenance — rechaza hashes o digests fabricados (stubs de sha256 truncados) en cualquier parte de public/data; nació de la auditoría de precisión de datos de julio de 2026.
  10. colombia/check-real-data — separa lo real de lo sintético en el corpus de Colombia, para que ninguna cifra de demostración se sirva sin su banner.

Estilo, taxonomía y cobertura:

  1. check-emdash-drift — la deriva de estilo (guiones largos) no reaparece en el contenido ya barrido.
  2. check-landing-stats — cada número impreso en public/landing.html sigue siendo el número que dicen los datos. La landing es un artefacto editado a mano, la página de más tráfico y la única sin componente, diccionario ni test detrás; sus estadísticas eran libres de pudrirse y lo hicieron (la auditoría del 13 de agosto de 2026 la encontró anunciando 6.726 citas contra un corpus de 6.855, y "300+ pilotos" contra un índice de 337). La compuerta recomputa cada cifra desde public/data/** del mismo modo que la superficie correspondiente, e imprime el literal que hay que pegar. Hoy verifica seis: fuentes primarias distintas, países, ejes, citas, personas e instrumentos.
  3. check-catalog — el catálogo de datasets está completo y consistente.
  4. check-search-index (consultiva) — el índice semántico cubre el corpus.
  5. check-law-term-index — el índice de términos legales cubre el 100% del corpus regulatorio (instrument_count de regulation/_index.json). Índice local: Voyage sigue siendo opt-in y falla cerrado.
  6. bake-trends-index --check — el índice de tendencias horneado coincide con sus insumos.
  7. check-vintage (consultiva) — cuántas cifras publicadas rompen la regla de vintage de dos años. El umbral y el orden de resolución de año salen de src/lib/vintage.ts, el mismo módulo desde el que renderizan las tiras de titular, así que la compuerta mide lo que el visitante ve. Existe porque bake-data-health puntúa una celda por la edad mediana de sus indicadores: la plataforma reportaba 2 celdas rancias de 260 mientras el 27% de las cifras individuales violaba la regla.
  8. check-adm1-coverage (consultiva) — la compuerta de honestidad de la capa subnacional. "432 de 456 unidades ADM1 con dato real" era cierto solo de la superposición universal GDL; a nivel de eje ninguna unidad llevaba más de 5 de los 10 ejes medidos, cinco ejes estaban 100% modelados y 26 unidades tenían sus celdas escritas bajo slugs que la app nunca puede pedir. Nada lo detectaba, así que derivó en silencio durante meses.
  9. check-i18n — la cobertura ES/EN/PT no retrocede.
  10. check-taxonomy — la copia de interfaz no vuelve a los sustantivos retirados para las diez medidas sectoriales (diez parámetros, ten dimensions, dez dimensões) ni apunta la navegación a /governance. Los diez son ejes / axes / eixos.
  11. check-hf-publish — bajo public/data/hf-observations/ solo puede vivir nasa-power.json, y debe ser un almacén NASA POWER no vacío. Cascarones vacíos (observations: []) y feeds no publicados (CHIRPS, FIRMS, Ember…) son un defecto: sirven un 200 que parece vivo.
  12. check-api — corre al final, después de los cuatro horneados de prebuild (bake-data-health, bake-source-ledger, bake-api, bake-feeds), y verifica con siete grupos de aserciones que la API pública estática espeja los datos actuales.

Las compuertas de procedencia en sentido estricto son el subconjunto check-traced, check-signals, check-source-links, check-citations, check-parameter-cache-citations y check-provenance — pero todas comparten la misma consecuencia: son la razón por la que "toda cifra con su fuente" no es un eslogan sino una invariante de compilación. Cómo se resuelve una cita en pantalla se explica en Procedencia y citas.

Un patrón se repite en las compuertas más nuevas (check-landing-stats, check-vintage, check-adm1-coverage, check-persona-scope): cada una nació de una auditoría que encontró una afirmación cierta cuando se escribió y falsa cuando se leyó, sin que nada la conectara con el dato. La compuerta es esa conexión. Es la misma razón por la que existe esta documentación y por la que se audita contra el código.

Cada cifra con su fuente — la trazabilidad es el contrato.