Procedencia y citas
La procedencia es el foso competitivo de Futuros. Cualquiera puede mostrar cifras; lo raro es que cada cifra se remonte a un registro de fuente primaria específico en dos clics, y que el sistema falle en compilación si no puede. Este documento explica la maquinaria que sostiene ese contrato. Sirve tanto al Pilar 1 — Fideicomiso de Datos (la procedencia es lo que hace verificable el corpus) como al Pilar 2 — Modelo Soberano (los hechos viven en las citas, nunca precalculados en pesos).
El contrato de dos clics
Toda cifra en pantalla lleva su cita en la propia cifra: un subrayado ligero, no un chip [WB] después del número. Un clic abre la referencia (título, editor, año); un segundo clic te lleva al registro exacto de la fuente, el país y el indicador precisos, no una landing genérica. Esa especificidad es una regla de build: la compuerta check-source-links hace fallar el build si un enlace apunta a una fuente genérica en vez del registro país + indicador.
La excepción es explícita, no tácita: la propia compuerta lleva una lista LANDING_OK de fuentes cuyas citas resuelven legítimamente a una página de landing o metodología, capas curadas e internas (gov-, event-, calc-, agg-, idx-), capas documentales (EXA, GDELT, RSS, gacetas, señales) y unas pocas fuentes sin URL por registro (OCDE benchmarks, IDEA, UNODC, V-Dem, FSI, INFORM, IMF PortWatch, FAOSTAT), cada una con su justificación comentada en el código. Es un opt-out consciente y auditable, no un agujero: todo lo que no está en esa lista debe llevar deep link o el build no compila. Si un dato no puede rastrearse así, no se publica como valor.
Dos redes más cierran los resquicios. GENERIC_URLS enumera páginas "genéricas de dataset" que difieren de la raíz solo por un path o un fragmento (el dashboard de FSI, el formulario de búsqueda vacío de USGS…): una cita no exenta sobre una de ellas es violación igual que una landing desnuda. Y los agregados regionales (-latam, -world, -subregion, -oecd en el id) quedan exentos porque no existe un registro país+indicador para una media supranacional. El diseño es fail-closed: una fuente nueva sin plantilla de deep link cae a su url_root y el build muere en prebuild; el mensaje de error imprime las dos salidas válidas, escribir la url_template y correr fix-source-deeplinks.ts, o documentar la exención en LANDING_OK con justificación de una línea.
Convenciones de citation_id
El registro central de citas es public/data/citations.json: 22.291 registros con la forma {id, source, title, url, retrieved_at, year}, sobre 491 fuentes primarias distintas. Las celdas de datos y los componentes apuntan a un registro por ese id (el campo que las celdas llaman citation_id). El id lleva un prefijo que codifica su origen: wb- (Banco Mundial, el más numeroso), oecd-, who-, gov- (fuente gubernamental), event-, web- (búsqueda web externa), etc.; corr- (correlación) existe como convención reservada en el código, hoy sin citas vivas. El prefijo no es cosmético: es lo que permite resolver una cita a su fuente aunque la cita no esté en el registro en memoria (ver reconstrucción sintética).
La gramática del id es mecánica, y por eso parseable: <prefijo>-<geo>-<año>, consumida de derecha a izquierda por parseMultiId(): si el último token son 4 dígitos, es el año; si el siguiente es un ISO3 en minúsculas (o latam, la media regional derivada por Futuros), es la geografía; todo lo que queda es el prefijo. extractYear() aplica la misma convención con la regex -(\d{4})(?:[-_]|$). Ejemplo real del registro: wb-si-pov-gini-arg-2024 → fuente WB, indicador SI.POV.GINI, Argentina, 2024.
Una serie fusionada por año puede resolver a dos registros. El citation_id del indicador es el del tip. Los años que solo aporta la serie anterior llevan su citation_id en el punto, y ambos ids quedan en las citas de la celda. En pobreza monetaria del Perú (inei_per_pobreza), 2016-2025 resuelven al informe gob.pe inei-per-pobreza-per-2025 y 2014-2015 al cubo INEI inei-per-pobreza-per-2024.
El registro de fuentes
src/lib/source-registry.ts mapea más de 200 SourceKey (más los que registran las olas de fuentes en módulos aparte) (acrónimos como WB, WHO, IMF) a su SourceMeta: acrónimo, nombre completo, agencia, jurisdicción, raíz de URL, plantilla de URL, cadencia, metodología (con variantes _en / _pt), tier y licencia. Las piezas clave:
inferSourceKey()resuelve uncitation_ida su fuente por el prefijo.deepLinkFor()construye el enlace al registro específico.- 43 funciones
url_templatereconstruyen el deep link por-indicador y por-país a partir del id solo: es decir, dado uncitation_idel sistema puede regenerar la URL exacta de la fuente sin depender de que alguien la haya guardado a mano.
Esto es lo que garantiza el segundo clic del contrato: el deep link no se almacena, se deriva, y por eso no puede quedar desactualizado respecto al id. Dos detalles del resolutor importan en la práctica:
- El orden de los prefijos es semántico.
inferSourceKey()es una cadena de reglasstartsWithdonde gana la primera coincidencia, así que los prefijos más largos van antes (mepyd-debe preceder amep-; el comentario en el código lo advierte). Los bancos centrales ilustran la disciplina de nombres:bcb-es Brasil,bcbol-Bolivia,bch-Honduras,bcch-Chile,bccr-Costa Rica, tokens únicos y terminados en guion para que ninguno ensombrezca a otro. Un prefijo no mapeado devuelveUNKNOWN, que deliberadamente no está exento en la compuerta de deep links: un prefijo nuevo sin registrar es una regresión, no un caso permitido. - La licencia viaja en la fuente.
SourceMeta.license(open,cc-by,non-commercial,share-alike,cite-only; ausente ⇒ abierta/permisiva) compuertea el precálculo: una fuentecite-onlypuede citarse sin redistribuir sus valores. - Las olas de fuentes registran aparte. Un frente de ingesta que añade decenas de fuentes no edita las ~5.000 líneas de
source-registry.ts: declara sus entradas en un módulo propio (src/lib/gap-sources.tspara la ola de brechas; a partir de la ola E, un archivo por fuente bajosrc/lib/gap-sources-e/y, en la ola F, bajosrc/lib/gap-sources-f/, cada uno exportando su propioSOURCESyPREFIXES) y el registro central lo fusiona:GAP_PREFIX_RULESse consulta antes que la cadena de prefijos incorporada, ylookupSourceMeta()busca en ambos registros. Las reglas de prefijo se ordenan de más largo a más corto en el momento de la fusión, no a mano: así un prefijo corto nuevo no puede tapar en silencio a uno largo ya existente, que es el modo de fallo que un orden manual invita. Así dos streams paralelos no compiten por las mismas líneas, ymetaForKey()garantiza que ninguna clave nueva devuelvaundefineden los chips. - No toda fuente puede tener
url_template. Cuando el editor no publica un permalink por registro (PAHO, IDEA), el adaptador estampa comosource_urlel archivo versionado exacto que contiene la fila, un CSV o XLSX descargable, no la portada del portal. La compuertacheck-source-linkssigue satisfecha porque esa URL difiere delurl_root, y la cita sigue siendo verificable: quien la abre obtiene el dato citado.
El vintage viaja con la cifra: la regla de dos años
La procedencia contesta de dónde salió este número. La regla de vintage contesta la otra mitad: de cuándo es. Ninguna observación publicada puede tener más de dos años, y el umbral vive una sola vez —STALE_AFTER_YEARS en src/lib/vintage.ts— para que la insignia que el visitante ve y las compuertas que fallan el build no puedan discrepar sobre qué significa "dos años".
vintageOf() resuelve el año de una celda en orden decreciente de confianza: vintage_year explícito → year explícito → última observación de la series → el año codificado en el citation_id. Un registro sin año en ninguno de esos lugares devuelve null y no se renderiza como rancio: "sin fecha" es un defecto distinto de "viejo", y afirmar una obsolescencia que no podemos sustentar sería tan deshonesto como esconderla. VintageBadge imprime ese año junto a la cifra, en ámbar cuando pasa la regla, con el año en el tooltip.
En el build la misma regla se aplica en dos escalas: check-vintage cuenta cifras individuales de parameter-cache/ bajo un trinquete que solo puede bajar, y check-freshness mide la observación más reciente de cada familia de datasets contra 730 días. Ver las compuertas del prebuild.
Cuando el propio publicador no tiene nada más nuevo, la respuesta nunca es inventar un número ni maquillar la fecha. Se refresca desde una fuente actual que cubra el mismo concepto, o se declara la frontera del publicador con su evidencia y una fecha de re-verificación que hace fallar el build al vencer. Los ocho indicadores más viejos de Costa Rica —del Programa Estado de la Nación, entre 2015 y 2020— son exactamente eso: la última observación que el PEN publica para el país, comprobada contra su API el 9 de septiembre de 2026, servida con su año a la vista.
Enlaces retirados: cuando el deep link es específico pero está muerto
Un deep link puede ser perfectamente específico y aun así no llevar a ninguna parte. check-source-links tiene por eso una segunda compuerta, independiente de la primera: una lista curada de formas de URL retiradas (scripts/lib/dead-links.ts) que no pueden aparecer en ningún archivo de datos publicado. No es un rastreador de enlaces, y esa decisión tiene una razón medida.
El código HTTP miente en las dos direcciones. La auditoría del 4 de septiembre de 2026 encontró un caso de cada una:
- 200 pero muerto. El Banco Mundial archivó los seis Worldwide Governance Indicators (
CC.EST,GE.EST,PV.EST,RL.EST,RQ.EST,VA.EST) de su ruta de indicadores heredada.data.worldbank.org/indicator/CC.EST?locations=BRsigue respondiendo 200; la API responde «The indicator was not found. It may have been deleted or archived.» Unas 2.400 URLs citadas apuntaban ahí. Ningún rastreo por código de estado lo habría detectado. - 404 pero vivo.
www.climatewatchdata.org/ghg-emissions?regions=ARGresponde 404 y un armazón SPA vacío a un cliente automatizado, y 200 con la página real a un navegador, la misma URL. Un rastreador ingenuo habría declarado muertas 26 citas sanas.
De ahí la división de trabajo. La compuerta que corre en prebuild es offline y determinista: coteja las formas ya confirmadas contra los ~11.700 archivos de public/data/**: no solo citations.json, porque la misma URL se copia a las celdas de parameter-cache, a los fragmentos por país de citations/, a los índices derivados y a refresh-meta.json, y un arreglo que se salte cualquiera de ellos sigue publicando un enlace roto. La verificación en vivo es opt-in y no forma parte del build:
bun run check:links:liveEse modo hace dos pasadas. Contra el Banco Mundial consulta el endpoint autoritativo de datos (api.worldbank.org/v2/country/<iso>/indicator/<code>) y no la página HTML ni el endpoint de metadatos /v2/indicator/<code>: ambos devuelven 200 para un código archivado; solo el de datos dice la verdad. Un código muerto falla. Contra el resto de los hosts de alto volumen toma una muestra de estado que es solo informe, nunca compuerta: sobre 210 hosts heterogéneos un código de estado es una pista para verificar a mano, no un veredicto. El ciclo es deliberado: el modo en vivo produce pistas, una persona confirma una contra el endpoint autoritativo del editor, y la forma confirmada entra en DEAD_URL_PATTERNS: desde donde el build se niega a publicarla nunca más.
Los ids de cita no cambian cuando cambia la URL. Los WGI conservan su raíz histórica wb-cc-est-<iso>-<año>: un citation_id es una clave opaca referenciada desde parameter-cache, los payloads precalculados y la API pública, no una URL. El remapeo ocurre donde se construye el enlace, liveWbIndicatorCode() en src/lib/source-registry.ts, aplicada tanto por la url_template del Banco Mundial (la ruta de deepLinkFor()) como por los adaptadores de ingesta, de modo que ninguna refresca futura reintroduzca la forma archivada.
Brechas residuales conocidas. Dos editores bloquean clientes automatizados en su front-end HTML mientras sirven bien a un navegador: IMF DataMapper y OpenAlex responden 403 a la muestra en vivo. En ambos casos el registro citado existe, se verifica contra sus APIs (imf.org/external/datamapper/api/v1/…, api.openalex.org), así que aparecen como pistas en el informe y no como fallos. GDACS agota el tiempo de forma intermitente por lentitud del portal, no por enlace roto.
Marcadores CiteRef
src/components/citations/CiteRef.tsx es el ancla de cita de toda la plataforma: la cifra o el sintagma citado es el enlace, con un subrayado ligero. No hay chips [CEPAL] / [INEC] / [WB] después del número. Al pasar el cursor (o al enfocar / tocar) muestra el mismo popover de siempre (título, editor, año, y la fecha de consulta: el retrieved_at de la cita o, en su defecto, el last_refreshed de la fuente en el ledger de frescura; si ninguno existe, no se muestra fecha en vez de inventarla); al hacer clic lleva a la fuente externa. Si un tramo tiene varias fuentes, el popover las lista. La resolución sigue este orden:
Resolución (letter F / procedencia reproducible): solo cuenta un registro real del catálogo registrado en memoria. Ya no se fabrica un syntheticCitation(id) en el render — un id sin registro se pinta como marcador suelto («Referencia no disponible en los datos actuales») en vez de inventar título/URL/retrieved_at. La reconstrucción sintética sigue existiendo en src/lib/source-registry.ts para embeds y herramientas que la invocan explícitamente, nunca como fallback silencioso del chip.
El chat registra las citas que el servidor emite durante el turno. Tras un reload, chatStore rehidrata turns + citas desde sessionStorage, y ChatPanel vuelve a volcar esas citas en el CitationRegistryProvider (el mismo camino que un enlace compartido #ask=). Sin ese paso, el corpus estaba en la sesión y el tooltip decía «no disponible». Al cerrar un turno, el servidor además rescata contra citations.json cualquier [[cite:id]] citado que aún no se hubiera emitido como registro.
Qué tramo lleva el subrayado
Que el subrayado caiga sobre la cifra, y no sobre cualquier número cercano ni sobre la prosa vecina, lo decide src/lib/cite-claim.ts (attachCiteSpans), el único punto por el que pasan todas las superficies de prosa citada: chat (prosa y tablas), reseña de país, historia subnacional y MarkdownWithCitations. La regla es el estadístico más cercano que precede al token [[cite:id]], con estas precisiones:
- El tramo es el número y su unidad, nada más. Qué palabra puede acompañar a la cifra lo fija una lista de permitidos de unidades ES/PT/EN (
UNIT_WORDS): magnitudes (mil,millones,bilhões,M), lo que se cuenta (habitantes,casos,municipios,empresas), superficie y longitud (km²,hectáreas,metros), energía y cómputo (MW,GWh,PFLOPs,GPUs), moneda, tiempo y puntos de índice. Cualquier otra palabra corta el tramo. - Es una lista de permitidos, no una de prohibidos, y eso es el diseño. La regla anterior era la inversa —cualquier palabra valía como unidad salvo que una lista de verbos la atrapara— y perdía: un barrido de los ~14.000 tramos citados de
country-history,state-historyycasesencontró 4.122 palabras distintas dentro de los subrayados, casi todas prosa. Así es como «la Revolución de Mayo de 1810 depuso al virrey» acabó subrayando1810 depuso. Tras el cambio quedan 128, todas unidades reales. Una unidad que falte en la lista falla de forma segura: el tramo se encoge al número pelado, que es justo lo prometido; una palabra admitida de más subraya prosa, que es el defecto. En la duda, se deja fuera. - Una unidad nunca casa como el principio de otra palabra. Sin ese límite,
m(metros) casaba con la «m» de «mayo» ykcon la de cualquier apellido. Los nombres propios que empiezan por una unidad (Estados Unidos,Ciudad de México,New York Times) quedan fuera por la misma razón. - Un año de edición nunca roba el ancla. Un año de cuatro dígitos que sigue a una cifra real, o que vive dentro de un paréntesis de fuente, es una añada, no un dato: el subrayado se queda en la cifra. Antes,
99,4 (International IDEA · Global State of Democracy, 2025)subrayaba2025y dejaba99,4sin enlace. - Un año sí es el ancla cuando la fecha ES la afirmación («la ley fue promulgada en 2019»): no compite con ninguna otra cifra y no es una añada. Las fechas completas («May 8, 2026») anclan en su año, nunca en el día.
- El ancla no cruza una frase. Si entre la cifra y el token empieza una cláusula nueva, la cita es sobre esa cláusula y no sobre el número de la frase anterior:
En 1991 se promulgó la constitución. El régimen cerró el espacio cívico [[cite:x]]no subraya nada. Un token que cae justo tras el punto de su propia frase (creció 3,2%. [[cite:x]]) sí ancla. - El ancla no cruza al interior de un paréntesis de fuente, y arrastra consigo los marcadores de negrita ajustados (
**99,4**), de modo que la cifra se renderiza en negrita y subrayada. Una negrita de frase (**52% del ingreso antes de impuestos**) ya no se absorbe entera: la negrita se pinta igual —el escáner de realce la lleva a través del corte— y el subrayado se queda en52%.
En tablas: la cita va bajo cada número medido
src/lib/cite-table.ts (normalizeTableCites) decide dónde aterriza la cita de una fila antes de que la tabla se renderice. Dos formas llegan del modelo y las dos anclaban mal:
- una columna «Fuente» al final, redundante con la cita en línea: se elimina y sus tokens pasan a las cifras de la fila;
- un token aparcado en la celda de nombre (
Democracia liberal (V-Dem, 0–1)[[cite:vdem]]): el subrayado caía sobre el1del rango de la escala, un número que no mide nada.
La regla es la cita va bajo los números medidos, y bajo todos ellos. Una fila que compara 2020 / 2023 / 2025 subraya los tres valores, porque la fuente respalda cada uno. Quedan fuera, por definición:
| Fuera del ancla | Por qué |
|---|---|
La columna de nombre (Indicador, País, Ley, Sector…) | sus dígitos son cotas de escala, números de norma o versiones de modelo, no mediciones |
Celdas que son solo un año (2023) | es la añada de la medición, no la medición |
La columna Δ / tendencia (−17.6%, +2,0%) | es derivada de los valores ya citados |
| Celdas que el modelo ya citó una por una | su procedencia propia manda |
Si la fila no tiene nada medible (una comparación cualitativa: «en trámite»), la procedencia se conserva en la última celda en vez de perderse.
Ningún asterisco llega a la pantalla
Los tokens de cita parten el párrafo en tramos, así que un realce de negrita abre rutinariamente en un tramo y cierra en otro (**60.1/100[[cite:x]]**). Un emparejador por expresión regular ve un delimitador desbalanceado a cada lado y lo imprime: de ahí los ** sueltos que aparecían a media frase, y la cifra que perdía su cita cuando el ** de cierre se colaba dentro del tramo citado.
src/lib/inline-emphasis.ts es un escáner con estado que atraviesa esas fronteras: el delimitador siempre se consume, nunca se pinta. Un ** sin cierre da estilo al resto del párrafo; no dibuja un asterisco. Las dos excepciones son deliberadas porque no son realce: la aritmética (5 * 3, espacio a ambos lados) y los guiones bajos de un identificador (wb_gdp_pc, …__SLV). Es la única implementación de realce en línea para chat, reseña de país, historia subnacional y MarkdownWithCitations.
Sin atribución en línea
El modelo a veces escribe una atribución en el propio texto — 99,4 (International IDEA · Global State of Democracy, 2025) o 51% [[cite:id]] (ILO, 2021) — pese a que la regla 2 del prompt ya lo prohíbe («un paréntesis suelto es una afirmación sin enlace»). stripSourceAttribution() la retira antes de renderizar (tanto antes como después del token [[cite:…]]): editor, título, añada y fecha de recuperación ya viven en la tarjeta de procedencia que abre la cifra subrayada, así que el paréntesis solo añadía ruido y su año competía por el subrayado.
El filtro es deliberadamente estrecho, porque un falso positivo borra palabras que el lector necesita: solo actúa pegado a un token [[cite:id]], y solo si el paréntesis lleva una añada o un separador · y ningún número que no sea ese año. Así desaparece (International IDEA · GSoD, 2025) mientras sobreviven (81,4 años), (Ley N° 26.743 · 2012) y (excluye zonas rurales). El mismo filtro se aplica a la exportación (texto copiado y briefing numerado), donde las fuentes ya aparecen en la lista de «Fuentes».
Letter F retiró el fallback sintético del render: un miss ya no se maquilla con un título/URL inventados. Las superficies que saben que sus citas fueron registradas (página de indicador con strict) y el chat (tras rehidratar la sesión) dependen del registro real; un id huérfano sin fila en citations.json se declara como tal. Cuando un id resuelve a fuente pero no a URL externa útil, el clic abre el cajón SourceTrace con la metodología. Cada clic sobre un marcador emite el evento de telemetría source_click con {cita, fuente, año}.
Alrededor de CiteRef viven CitationChip (un envoltorio que exige el texto a subrayar), CitationDrawer y SourceTrace (un cajón de respaldo con metodología y el log de falsificaciones). La marca de evidencia externa no es un componente aparte: vive dentro del propio CiteRef, que pinta el subrayado en oro discontinuo cuando el citation_id lleva el prefijo web-. El recorrido de extremo a extremo es: valor precalculado → citation_id → CiteRef → registro → deep link.
El cajón SourceTrace lleva dos sellos de fecha distintos, nunca intercambiables. extraído es el retrieved_at de esa cita concreta, juzgado contra un reloj plano. fuente consultada es el last_refreshed de la fuente en el ledger de frescura (sources["series:<CLAVE>"] de refresh-meta.json), y se juzga contra la cadencia de la propia fuente, no contra ese reloj: next_refresh_due del ledger, o cadence_months si hay que derivarlo. Un reloj plano era la regla equivocada para este sello en las dos direcciones: CEPALSTAT y Banxico publican anualmente, así que una descarga de hace 100 días está exactamente en fecha y llamarla stale inventaba una alarma; IBGE, INEGI, DANE, INDEC y BCB publican mensualmente, así que esa misma descarga lleva tres ciclos perdidos y el reloj plano no mostraba nada. El veredicto es al día mientras no venza, toca refrescar dentro de un ciclo de gracia y refresco vencido pasado ese ciclo; si la fila no declara cadencia (cadence_basis: "unavailable") se muestran la fecha y la antigüedad sin veredicto, porque no hay regla contra la cual juzgarla. Ninguno de los dos sellos habla de la antigüedad del dato en sí: eso es la regla de vintage, con su propio umbral (STALE_AFTER_YEARS) y sus propias compuertas (check-vintage, check-freshness).
Grado del enlace
linkGradeFor(citationId, recordUrl) en src/lib/cite-link-grade.ts clasifica el enlace que se puede ofrecer sin disfrazar una portada de registro:
record: el registro trae una URL propia, no vacía y distinta de#.deep: no hay URL de registro, pero laurl_templatede la fuente declara parámetros (Function.length > 0) ydeepLinkForarma un enlace por id.portal: lo reconstruible es una página de aterrizaje (url_root, o una plantilla constante del estilo() => "https://…").
En resolveCiteView (CiteRef.tsx) una vista suelta, sin registro de catálogo, queda en grado portal. drawerOnly también es verdadero cuando el grado es portal: el clic abre el cajón SourceTrace en vez de una portada en una pestaña nueva.
resolveTraceSource en SourceTrace.tsx arma la sección Fuente del cajón. La URL es cite?.url || deepLinkFor(id): un url vacío cae al enlace reconstruido y no se queda como href="". Si no hay registro, el cajón muestra la marca sin registro y la nota de que los metadatos se reconstruyen del identificador. Un enlace de grado portal lleva encima la etiqueta de portal. El editor mostrado es el source del propio registro cuando la clave del registro de fuentes es un cubo genérico (CURATED, INTERNAL, UNKNOWN) o CURATED_LAW; en los demás casos es el nombre institucional de la fuente.
Señales: dos linajes, una honestidad
La capa de señales (public/data/signals/) tiene dos linajes con contratos de procedencia distintos, y la compuerta check-signals distingue entre ambos por el campo type:
- Señales extraídas por LLM (seis tipos:
policy_announcement,funding,incident,milestone,risk,contradiction). Su procedencia es un documento del corpus:source_urldebe existir en el archivo hermanodocuments/<eje>__<ISO3>.jsony todafiguredebe ser subcadena verbatim del extracto de ese documento. Cuando el documento rota fuera de la ventana de 365 días, la señal se poda (con asiento enstate/pruned-signals.jsonl): una señal nunca sobrevive a su documento. - Señales
earth_obs(deterministas, sin LLM). Las emitebake-earthobs-signals.tsconsultando fuentes de observación de la Tierra legibles por máquina: Climate TRACE v7 (emisiones mensuales por país; CC BY 4.0 según sus términos y el registro Zenodo del inventario, DOI conceptual10.5281/zenodo.15014790) e INPE / TerraBrasilis (DETER mensual y PRODES anual; CC-BY-SA 4.0 según su página de citas y licencia). Aquísource_urles el GET exacto que produjo el valor (el mes consultado viaja en el propio URL) y no existe documento hermano, así que la compuerta les aplica un contrato estructural: host https en lista blanca (exactamente los que emite el generador:api.climatetrace.orgyterrabrasilis.dpi.inpe.br; Zenodo/DOI quedan fuera adrede, porque Zenodo aloja subidas arbitrarias y admitirlo dejaría citar un registro autopublicado), celda fija (medio-ambiente-clima, con el archivo coincidiendo conparameter×geography_id),source_namedentro del conjunto conocido, fecha ISO completa yfigureverbatim dentro de su propioclaim_es, con la misma secuencia de dígitos presente enclaim_en/claim_pt(ninguna lengua puede afirmar otro número). La atribución obligatoria de ambas licencias se satisface en pantalla:source_name(«Climate TRACE», «INPE / DETER», «INPE / PRODES») es el ancla del enlace a la fuente.
El tipo earth_obs no puede nacer de un modelo: la lista VALID_TYPES del extractor LLM lo excluye, de modo que un modelo que alucine ese tipo ve su señal descartada por el validador determinista. Y las pasadas de poda (la poda de huérfanas del bake y prune-stale-signals) eximen a earth_obs de la regla del documento hermano (sin esa exención, cada refresco del corpus documental las borraría), pero las podan igualmente si dejan de pasar la compuerta estructural.
Esa exención está acotada por un tope de antigüedad por fuente (EARTH_OBS_MAX_AGE_DAYS), porque el documento hermano es lo que hace expirar a una señal LLM y sin tope la vía earth-obs sería la única que nunca expira: un upstream caído dejaría su última lectura en pantalla para siempre. El tope es por fuente porque es la cadencia de cada fuente, no un número de la casa: Climate TRACE y DETER son mensuales (365 días; el propio generador solo busca 8 meses hacia atrás un mes publicado), mientras que en PRODES la date es el fin del período de referencia (31 de julio) y la tasa consolidada se publica hacia noviembre y rige hasta que llega su sucesora un año después, así que una señal PRODES vigente llega legítimamente a ~16 meses (550 días de tope). Un tope plano de ~400 días habría borrado dato vivo y correcto: el 8 de septiembre de 2026 la señal PRODES vigente tenía 404 días. Añadir una fuente nueva sin declarar su cadencia no compila: la tabla de topes está indexada por la propia lista blanca de source_name, así que una fuente sin tope es un error de tipos, no una herencia silenciosa del valor por defecto (365 días, seguro solo para un feed mensual y letal para uno anual). El tope se aplica en prune-stale-signals (con asiento en el ledger), no en check-signals: la compuerta de build no depende del reloj, para que ningún build se ponga en rojo porque el dato envejeció sin que nadie lo tocara. En la interfaz, la franja climática de cada país muestra la señal earth_obs más reciente como «última alerta», con su fecha y su fuente enlazada.
Reconstrucción sintética para embeds
Las superficies incrustables (/embed/stat, /embed/chart, /embed/board) no siempre cargan el registro completo de citas, pesaría demasiado. Para eso existe syntheticCitation(): reconstruye la cita a partir del citation_id y el registro de fuentes (prefijo → SourceKey → url_template). Resultado: una tarjeta incrustada en un sitio de terceros conserva su cita y su deep link, sin cargar los megabytes del registro. La procedencia viaja con el dato, no con la página.
Tres invariantes de honestidad en la sintética: devuelve null si el prefijo resuelve a UNKNOWN (nunca inventa una institución); retrieved_at queda vacío (no fabrica una fecha de recuperación que no ocurrió); y el año solo se incluye si es derivable del propio id.
Badging de citas web externas
No todo hecho viene del corpus precalculado. Cuando el asistente usa búsqueda web como último recurso (ver El asistente), esas citas llevan el prefijo web- y se marcan visiblemente como externas: el subrayado de la cifra es oro y discontinuo, y la tarjeta de hover antepone la etiqueta de dato no verificado. La razón es honestidad: un hecho traído de la web abierta nunca debe confundirse con un dato del corpus verificado. El badging separa las dos clases de evidencia en la propia interfaz, para que el lector sepa siempre qué tan firme es el piso bajo cada cifra.
El marcado visual no basta por sí solo cuando la respuesta se copia a un memo: por eso el asistente además empareja en el texto cada primer uso de una cita con una etiqueta legible de la fuente y su año, y para un id web- exige en cada uso el nombre de la publicación (o URL) con su fecha, más la etiqueta "fuente externa" en línea cuando una celda de tabla solo se apoya en un id web-. Un puntero que la herramienta no puede respaldar con publicación y fecha se marca [?] o se elimina (ver la regla completa en El asistente).
Desde la remedición post-#826 ese emparejamiento de primer uso también se aplica en el servidor, no solo en el prompt (server/chat/cite-label-assist.ts): cuando el modelo deja un primer [[cite:id]] desnudo y el registro de cita que la herramienta devolvió en ese mismo turno trae fuente y año, el post-procesado antepone la etiqueta "(fuente, año)" junto al token. Tres límites lo mantienen honesto: la etiqueta sale solo de campos del lado del servidor — el campo source del registro real, con un único respaldo para registros del corpus cuya fuente llegó vacía: el hostname de su propia URL; nunca del título de una página web, que es texto no confiable, y nunca se inventa para un registro externo sin fuente: ese id queda para el mecanismo [?] —; el emparejamiento del propio modelo gana, y desde la remedición post-#828 "propio" se mide estricto: un introductor o nombre de familia de fuente cuenta solo si está a unos 40 caracteres antes del token (una palabra suelta tres cláusulas atrás ya no suprime la etiqueta), mientras las palabras distintivas de la fuente concreta cuentan en toda la oración o celda; y solo se toca el primer uso de cada id, con tope por respuesta.
El marcador [?]: una cita inválida no se borra en silencio
Cuando la auto-corrección del asistente detecta un token [[cite:id]] cuyo id ninguna herramienta devolvió en la conversación (y que ningún turno previo del asistente había citado, la reutilización honesta de una cita de un turno anterior está licenciada), la reescritura de salvamento no elimina el token en silencio: lo reemplaza por el marcador visible [?]. Un borrado silencioso convertiría "cifra con cita inventada" en "cifra sin fuente con más confianza aparente"; el marcador deja el hueco de procedencia a la vista, en el punto exacto de la afirmación. En los bloques de visualización el campo "cite" inválido sí se elimina (un marcador rompería el JSON del gráfico), y el punto de datos conserva su valor como cifra sin cita. Las respuestas que se entregan sin pasar por el salvamento no se editan: viajan con su señal de verificación (unverified_ids) intacta.
Log de falsificaciones
public/data/falsifications.json es el registro de afirmaciones que fueron puestas a prueba, el reverso de "cada cita es una fuente": cada falsificación es un compromiso auditable de que la plataforma corrige lo que resulta no sostenerse. SourceTrace lo expone junto a la metodología de la fuente. Es parte del contrato de honestidad que se detalla en Metodología y honestidad.
La procedencia en Futuros es derivable, no prometida: el deep link se reconstruye del id, la cita sintética viaja al embed, y el build se niega a compilar si algún enlace fuera del opt-out documentado (LANDING_OK) apunta a una fuente genérica. Sigue con El asistente para ver cómo el chat hereda esta misma disciplina, cada figura anclada a un [[cite:id]]. Y para la siguiente frontera (de procedencia derivable a procedencia criptográficamente verificable por terceros), ver la hoja de ruta técnica de optimización.