Skip to content

Optimización de la procedencia: hoja de ruta técnica

La procedencia de Futuros hoy es derivable: el deep link se reconstruye del citation_id, la batería de compuertas del prebuild (trazabilidad, deep links, citas, puntajes, procedencia, catálogo, API — más la suite completa de tests) rechaza cualquier cifra sin rastro, y el recibo del fideicomiso es un hash reproducible sobre un cuerpo canónico (Procedencia y citas, El pipeline de datos). Esta página documenta la siguiente frontera: pasar de "cada cifra tiene su fuente" a "cada cifra lleva una prueba de origen verificable por máquina, que un tercero puede comprobar sin confiar en nosotros".

Todo lo que sigue es hoja de ruta, no capacidad en vivo, salvo donde se marca lo contrario. El orden es deliberado: cada fase reutiliza costuras que ya existen en el código (citation_id, stableStringify, receipt_hash, chain_anchor, refresh-meta.json, las compuertas check-*), de modo que ninguna fase exige una reescritura.

El modelo objetivo: cuatro propiedades

Una cifra con procedencia optimizada satisface cuatro propiedades independientes, cada una con su mecanismo:

PropiedadPregunta que respondeMecanismo
Integridad¿Estos bytes son los que se ingirieron?Direccionamiento por contenido (SHA-256) de las respuestas upstream
Autenticidad¿Quién produjo este artefacto?Firmas Ed25519 sobre cuerpos canónicos + atestaciones de build
Reproducibilidad¿Puedo recomputarlo y obtener lo mismo?Horneado hermético y determinista, insumos fijados por digest
Transparencia¿Pudo reescribirse la historia después?Log append-only estilo Merkle con anclaje externo periódico

La disciplina actual (citas deterministas + compuertas de build) da integridad interna. Las fases siguientes la vuelven verificable desde afuera.

Fase A — Capa de adquisición direccionada por contenido

Hoy la ingesta escribe celdas derivadas; los bytes crudos de la respuesta upstream se descartan. Eso hace imposible re-auditar una transformación después del hecho: si el Banco Mundial revisa una serie, no podemos probar qué versión vimos.

El diseño: cada fetch de adaptador persiste su respuesta cruda como un blob direccionado por contenido:

snapshots/
  sha256/ab/cd/abcd…ef        # bytes crudos de la respuesta (gzip)
  manifests/wb-sp-dyn-le00-in-2026-07-30.json

El manifiesto de fetch registra { url, method, request_headers_relevantes, etag, last_modified, fetched_at, digest, adapter, source_key }. Cada celda de parameter-cache/ gana un campo derived_from: [digest…] — la lista de blobs exactos de los que se derivó. El conjunto forma un DAG de Merkle: celdas → manifiestos → blobs; el digest raíz de un horneado resume todo el corpus de entrada en 32 bytes.

Consecuencias inmediatas:

  • Re-auditoría retroactiva. Cualquier defecto de transformación se puede reproducir contra los bytes originales, no contra lo que el upstream sirva hoy.
  • Detección de revisiones upstream. Un etag/digest distinto en la misma URL distingue "la fuente revisó su serie" de "nuestro pipeline cambió" — dos eventos que hoy son indistinguibles.
  • Deduplicación gratuita. El direccionamiento por contenido colapsa respuestas idénticas entre corridas; el costo de almacenamiento crece con el cambio real, no con la cadencia de refresco.

La costura existente: scripts/freshness-guard.py ya implementa snapshot→compare→repair para auditorías puntuales; la fase A lo generaliza de herramienta de auditoría a invariante del pipeline.

Fase B — Grafo de linaje PROV-O consultable

/linaje narra el origen de cada serie; la fase B lo vuelve un grafo formal usando el vocabulario W3C PROV-O:

  • Cada blob de la fase A es una prov:Entity; cada corrida de adaptador o script bake-* es una prov:Activity (prov:used → insumos, prov:generated → salidas); el pipeline y los operadores son prov:Agent.
  • Cada celda horneada exporta su cadena como JSON-LD bajo la API pública: GET /api/v1/prov/{indicator}/{iso3} devuelve el subgrafo mínimo que conecta la cifra en pantalla con los digests de sus blobs de origen.
  • El grafo es derivado, no mantenido a mano: se emite como subproducto de la instrumentación de los ~50 scripts de bake, con un wrapper común que registra (insumos leídos, salidas escritas, digest de cada una, versión del script).

El criterio de éxito es falsable: para cualquier cifra visible en la plataforma, un cliente sin acceso al repo puede recorrer el JSON-LD desde el citation_id hasta un digest de bytes crudos, y verificar cada arista con hashes.

Fase C — Horneado reproducible + atestaciones de build

La reproducibilidad convierte el linaje en algo comprobable, no solo declarado:

  1. Bakes herméticos. Cada bake-*.ts declara sus insumos por digest (fase A) y queda prohibido de tocar la red; las fuentes de no-determinismo (timestamps, orden de iteración de objetos, floats) se canonicalizan — stableStringify ya existe para los recibos del fideicomiso (server/trust/ledger.ts: serialización recursiva con claves ordenadas, sobre la que receipt_hash = sha256(cuerpo canónico); el recibo es determinista dado el cuádruple contribución + issued_at + nonce + salt, y por eso reproducible de forma independiente) y se promueve a serializador de todo artefacto horneado. Meta: scores.json bit-a-bit idéntico dado el mismo conjunto de insumos.
  2. Atestaciones estilo in-toto/SLSA. Cada artefacto de public/data/** y public/api/v1/** se acompaña de una atestación firmada: { subject: digest_del_artefacto, materials: [digests_de_insumos], builder: {repo, commit, script, versión}, invocation: {argv, env_permitido} }, firmada Ed25519 con una clave de build publicada. Se publican bajo public/api/v1/attestations/.
  3. Verificación de terceros. Con (1) + (2), un auditor externo — una universidad, un banco multilateral, un gobierno que corre el mirror soberano — recomputa el bake sobre los mismos blobs y compara digests. La confianza deja de ser "creemos en Futuros" y pasa a ser "lo recomputamos".

Nota de honestidad: la autenticación de emisor por HMAC del recibo del fideicomiso ya está construida y se activa por entorno (TRUST_RECEIPT_HMAC_KEY): con la clave configurada, el recibo pasa a esquema v2 y lleva una firma HMAC-SHA256 sobre su receipt_hash. Eso autentica al emisor pero no es verificable por terceros, porque verificar exige la clave secreta; la verificabilidad pública con Ed25519 sigue en hoja de ruta, y la clave de firma de build de esta fase es su prerequisito.

Fase D — Log de transparencia con anclaje externo

Un ledger append-only que opera el mismo actor que escribe en él prueba poco: podríamos reescribir la historia y re-servirla. La fase D lo hace económicamente imposible de falsificar:

  • Árbol de Merkle estilo RFC 6962 (Certificate Transparency) sobre tres series: el registro de citas, el ledger del fideicomiso y las atestaciones de build. Cada entrada nueva produce un checkpoint firmado {tree_size, root_hash, timestamp}.
  • Pruebas de inclusión y consistencia. Un recibo de contribución gana una prueba de inclusión de ~log₂(n) hashes; dos checkpoints cualesquiera admiten prueba de consistencia (nada se borró ni se reescribió entre ellos). El receipt_hash existente es exactamente la hoja que el árbol necesita — cero migración de esquema. (Y el recibo ya sella salt_version — una etiqueta pública one-way de la sal que produjo el contributor_id — de modo que una rotación futura de TRUST_ID_SALT no invalida la verificabilidad ni la revocabilidad de recibos antiguos.)
  • Anclaje externo periódico. El root se ancla fuera de nuestra infraestructura con cadencia fija: timestamping RFC 3161 y/o OpenTimestamps (agregación en Bitcoin, costo ~cero), con la ranura chain_anchor del recibo — reservada desde el día uno — recibiendo el identificador de anclaje. Falsificar la historia pasada exigiría falsificar también el anclaje externo con fecha.
  • Monitores. El checkpoint firmado se publica en la API pública; cualquier tercero puede correr un monitor que verifique consistencia entre checkpoints — el mismo modelo de auditoría distribuida de CT.

La forma del log, en esquema:

Fase E — Credenciales de contenido en los artefactos exportados

La procedencia hoy viaja con el embed vía la cita sintética (Procedencia); se pierde en el momento en que alguien exporta un PNG/SVG/CSV y lo pega en un informe. La fase E adjunta C2PA Content Credentials a cada export:

  • El manifiesto C2PA embebido declara: emisor (Futuros), citation_ids de cada serie graficada, digest del dataset de origen, timestamp firmado, y la aserción de que el render se produjo desde datos citados sin edición manual.
  • Los CSV/XLSX de /datos-abiertos ganan un sidecar de atestación (.csv + .csv.att.json) con el mismo contenido — el formato tabular no admite manifiesto embebido.
  • Resultado: una gráfica de Futuros que circula por WhatsApp o un deck ministerial sigue siendo verificable — cualquier verificador C2PA recupera las citas y el digest, y detecta ediciones.

Fase F — SLOs de frescura, deriva y ausencia tipada

Las fases A–E prueban de dónde vino una cifra; la fase F gobierna su calidad temporal — y codifica la lección más cara de las auditorías de datos de este proyecto.

SLOs de frescura. refresh-meta.json ya registra cadencia y vencimiento por fuente — hoy con last_refreshed / next_refresh_due a nivel de feed (social:gdelt, doc:perplexity, …) y seguimiento por-indicador solo para el subconjunto WB/WGI (49 códigos); el resto del corpus (884 series en el ledger de fuentes) se rastrea a nivel de feed, con el rollup de vintage en data-health.json. La fase F los convierte en objetivos de nivel de servicio con presupuesto de staleness: cada fuente declara {cadencia, gracia, severidad}; el gate pasa de binario (vencida/no) a un presupuesto agregado que puede fallar el build cuando el corpus en conjunto envejece más allá del umbral, aunque ninguna fuente individual esté crítica.

Detección estadística de deriva. Sobre los blobs de la fase A: cuando un refresh trae una serie revisada, un test de deriva distribucional (PSI / Kolmogorov–Smirnov sobre la serie solapada) clasifica el cambio como revisión menor (re-bake silencioso con nota), revisión estructural (requiere entrada en el changelog de la fuente y re-verificación de las celdas derivadas) o anomalía de transporte (bloquea el refresh — probablemente es un error nuestro o del upstream, no un dato nuevo). Complementa la detección de anomalías año-a-año ya publicada en la capa de salud del dato (|z| > 4 + piso de 10 pp, ver Metodología).

Ausencia tipada. El defecto recurrente más peligroso encontrado en las auditorías internas del pipeline: un fetch fallido escrito a disco como "no hay dato". Una celda vacía debe declarar por qué está vacía, con un enum cerrado:

absence: "no_data"        # la fuente afirma que no existe medición
       | "not_collected"  # fuera del alcance declarado de la fuente
       | "fetch_failed"   # nuestro transporte falló — NO se sabe nada del valor
       | "license_gated"  # existe, pero la licencia solo permite cita
       | "revoked"        # existió, el aportante revocó el consentimiento

Las compuertas de build tratan fetch_failed como error de refresh, jamás como dato: un valor previo nunca se sobreescribe con una ausencia de transporte. La distinción license_gated/revoked conecta con la puerta de licencia y hace la revocación visible en el corpus en vez de silenciosa.

Fase G — Prueba ZK de registro

Las fases C y D dejan al fideicomiso con dos piezas: un compromiso determinista por registro y un log público donde ese compromiso queda incluido. La fase G añade la tercera: pruebas de conocimiento cero que demuestran un predicado sobre el contenido de un registro sin revelar el registro.

La cadena de dependencia, eslabón por eslabón:

  1. Cuerpo canónico + receipt_hash (en vivo hoy; verificable por hash, sin firma). stableStringify produce una serialización determinista del recibo y receipt_hash = sha256(cuerpo canónico). Es el único eslabón que ya existe: un compromiso de 32 bytes por contribución.
  2. Prueba de inclusión de Merkle (fase D, hoja de ruta). Sitúa ese compromiso en un log append-only con anclaje externo. Sin D, una prueba ZK sobre un recibo no dice nada sobre el corpus: solo habla de un blob que nadie más vio.
  3. Prueba ZK de registro (esta fase). Un circuito toma el cuerpo canónico como testigo privado, el receipt_hash más la raíz del log como entradas públicas, y demuestra afirmaciones del tipo "existe un registro incluido en el checkpoint N cuyo privacy es aggregates_only y que no ha sido revocado", sin exponer aportante, dataset ni ningún otro campo.

El diseño existente hace el circuito tratable: la hoja del árbol es exactamente el receipt_hash, y el cuerpo canónico ya es una serialización estable con claves ordenadas, es decir, un testigo bien definido. La fase G no pide rediseñar el recibo; pide que D opere primero.

Qué desbloquea: divulgación selectiva (un auditor verifica una propiedad de un registro sensible sin verlo), agregados demostrables (los conteos públicos por modo de privacidad pasan de afirmación a prueba) y el prerequisito directo de la hoja de ruta ZK del pilar 2 (ver abajo).

Estado honesto: no existe circuito, ni prototipo, ni elección de esquema de prueba. Es la última fase de la cadena criptográfica y no se inicia antes de que D esté en producción.

Fase H — Niveles de sensibilidad y cifrado por registro

El punto de partida, verificable en server/trust/ledger.ts:

  • PrivacyMode es metadato declarado, no un control técnico. El aportante elige open | aggregates_only | clean_room | private_compute; el modo gobierna el puntaje y cómo se agrega la contribución, pero hoy no activa cifrado ni control de acceso criptográfico alguno.
  • El almacenamiento es en claro. El ledger persiste los registros sin cifrar en el backend del operador.
  • La pseudonimia viene de TRUST_ID_SALT. El contributor_id es un hash salado del email, con salt_version sellado en el recibo para que la rotación de sal no rompa la revocación. Es pseudonimia, no cifrado: protege la identidad frente a lectores del ledger, no el contenido frente al operador.

La fase H cierra la distancia entre lo declarado y lo aplicado con dos movimientos:

  1. Niveles de sensibilidad por registro. Cada registro se clasifica en un nivel cerrado: T0 público, T1 pseudonimizado (el estado actual), T2 sensible, cifrado en reposo, T3 nunca sale del aportante (el "computar sin poseer" que private_compute ya declara). El nivel por defecto se deriva del PrivacyMode declarado, que deja de ser solo una etiqueta.
  2. Cifrado por registro con claves por aportante. Los niveles T2 y T3 usan cifrado de sobre: una clave de datos por registro, envuelta por una clave por aportante. La revocación gana un mecanismo criptográfico: destruir la clave (crypto-shredding) vuelve el registro ilegible sin reescribir el log, compatible con el árbol append-only de la fase D, donde el receipt_hash permanece como constancia de que el registro existió.

Dependencias: el cifrado en reposo no requiere ni a G ni a D, pero la constancia verificable de una revocación (probar que se destruyó la capacidad de descifrar) sí se apoya en el log de la fase D. Y H es el prerequisito de datos de la inferencia ZK del pilar 2: sin registros cifrados no hay "inferencia sobre registros cifrados".

Un solo hilo con el pilar 2: inferencia ZK sobre registros cifrados

La hoja de ruta del pilar 2, Modelo Soberano incluye la inferencia con conocimiento cero sobre registros cifrados del fideicomiso. Ese ítem y las fases G y H de esta página son una sola cadena de dependencia, no promesas paralelas:

receipt_hash sobre cuerpo canónico (en vivo) → prueba de inclusión de Merkle (fase D) → prueba ZK de registro (fase G) + registros cifrados por nivel (fase H) → inferencia ZK sobre registros cifrados (pilar 2).

Ningún eslabón posterior al primero existe hoy. Si alguna página de esta documentación llegara a presentar la inferencia ZK del pilar 2 como capacidad sin que G y H operen aquí, eso sería un defecto reportable en el mismo espíritu del log de falsificaciones.

Secuencia y dependencias

FaseDepende deCosto dominanteQué desbloquea
A — CAS de adquisiciónAlmacenamiento de blobs (crece con el cambio, no con la cadencia)Re-auditoría, detección de revisiones, el DAG
B — Linaje PROV-OAInstrumentar los ~50 scripts de bake con un wrapper comúnProcedencia consultable por máquina, por celda
C — Bakes reproducibles + atestacionesACanonicalizar no-determinismo; gestión de la clave de buildVerificación por terceros; la firma del recibo
D — Log de transparencia + anclajeC (clave)Operar el log + monitoresHistoria no-reescribible; chain_anchor deja de ser null
E — Credenciales C2PACTooling de manifiestos en el pipeline de exportProcedencia que sobrevive fuera de la plataforma
F — SLOs + deriva + ausencia tipadaA (para deriva)Migración del esquema de celdas al enum de ausenciaFrescura gobernada; fin de la clase de defecto "fetch fallido = no hay dato"
G — Prueba ZK de registroDDiseño del circuito y elección del esquema de pruebaDivulgación selectiva; agregados demostrables; el prerequisito ZK del pilar 2
H — Sensibilidad + cifrado por registroD (para constancia de revocación)Gestión de claves por aportante; migración del almacenamientoCifrado en reposo por nivel; revocación por destrucción de clave

Qué vuelve verificable cada fase — y quién puede comprobarlo sin confiar en Futuros:

FaseQué se vuelve verificableVerificadorArtefacto que lo prueba
AQue los bytes upstream son exactamente los ingeridosAuditor con acceso a los blobsDigest SHA-256 + manifiesto de fetch
BLa cadena completa celda → blob de origenCualquier cliente HTTP, sin acceso al repoJSON-LD bajo /api/v1/prov/…
CQue el bake se recomputa bit a bitTercero que corre el mirror soberanoAtestación firmada Ed25519
DQue la historia no se reescribióMonitor externo continuo, estilo CTPruebas de inclusión/consistencia + anclaje fechado
ELa procedencia de un export fuera de la plataformaCualquier verificador C2PA estándarManifiesto embebido o sidecar .att.json
FPor qué una celda está vacía y cuánto envejeció el corpusLas compuertas de build + el lectorEnum de ausencia + presupuesto de staleness
GUn predicado sobre un registro, sin revelar el registroCualquier verificador con el checkpoint del logPrueba ZK + prueba de inclusión
HQue una revocación destruyó la capacidad de descifrarAuditor con el log y el registro de clavesEvento de destrucción de clave anclado en el log

A y F-ausencia-tipada son los puntos de partida correctos: A es el prerequisito de todo lo criptográfico, y la ausencia tipada elimina hoy la clase de defecto que más veces ha producido datos falsos-por-omisión. D y E son los de mayor visibilidad externa pero solo tienen sentido sobre C. G y H cierran la cadena: G solo existe sobre D, y la inferencia ZK del pilar 2 solo sobre G y H.


La regla que gobierna toda la hoja de ruta es la misma del resto de la plataforma: ninguna fase se anuncia como capacidad hasta que su verificación sea ejecutable por un tercero. Mientras tanto, esta página es el compromiso público de hacia dónde se optimiza — en el mismo espíritu falsable que el log de falsificaciones.

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