# E2E Scrape/Load Tracker Objetivo: **una sola lista** de trabajo operativo para completar el extremo a extremo (extract -> transform -> load -> publish) por tipo de dato. ## Fundación de escala y accountability (`2026-08-13`) Donde estamos ahora: - snapshot HF `2026-08-12` publicado y verificado: `127` tablas / `134` Parquet en el bundle tabular, con `latest.json`, manifest y README coherentes; `vote_gate_passed=false` e `initiative_gate_passed=false` permanecen visibles. Evidencia: `docs/etl/sprints/SCALE-FOUNDATION-20260810/evidence/hf-public-origin-quality-20260812.json`, - origin analítico scale v2 publicado, firmado y verificado: release content-addressed `5d9ce557...`, `7` corpora, `5,414,326` filas, `8,597` data files y `500,714,815` data bytes, conservando toda identidad pública. Pointer, manifest, stable artifact contract `d7de5544...6565`, registry/readiness y attestation SSH Ed25519 mantienen parity exacta sin errores ni warnings. El restore clean-room demostrado sobre las seis lanes anteriores permanece como prueba histórica; la nueva lane `candidate_occurrences` requiere su propio restore independiente antes de marcar `clean_room_restore=true`. Siguiente gate: segundo rebuild/attestation por mantenedor externo. Evidencia actual: `docs/etl/sprints/REPRODUCIBILITY-20260819/evidence/live-signed-snapshot-verification.json`, `live-scale-origin-parity.json`, `published-scale-manifest-diff.json` y `snapshot-reproducibility-validation.json`, - Evidence API v1 live: `/api/v1/index.json` fija release inmutable `feb61b39...` sobre el origin scale firmado. Cuatro colecciones publican `5,830` objetos reales en `26` páginas (`1,147` entidades, `1,105` cadenas, `48` fuentes, `3,530` respuestas) con cursor seek, hashes, provenance, gaps explícitos y retención exacta `1,147/1,147` de labels públicos. El verifier live descarga `32` objetos / `11,452,986` bytes y reconcilia fingerprint, manifests, páginas, cursor chain y policy sin errores. Dirección: añadir vertical slices sin romper v1. Siguiente: `TODO 20/21`, spending/procurement real. Evidencia: `docs/etl/sprints/EVIDENCE-API-20260819/evidence/evidence-api-v1-validation.json` y `live-evidence-api-v1-verification.json`, - Promesas BNG 2023 publicadas y verificadas live: el PDF oficial checksum-pinned (`b8994dee...`, `396.842` bytes, `68` páginas) alimenta un modelo aditivo directo a partido, sin persona proxy. El pipeline page-aware produce `266` claims / `266` anclas exactas / `91` promesas / `323` enlaces topic; `6` fragmentos incompletos quedan `needs_context`, `26` compromisos blandos siguen pendientes y solo `20` acciones completas rule-confirmed entran en el sample. Replay devuelve los mismos IDs, conteos, release `a3864cf...` y JSON SHA-256 `08a35fc3...`; FK, offsets/citas y real-only pasan. `/promises/`, el JSON público y el PDF oficial devuelven HTTP `200`; los bytes/hashes live son exactos y fulfillment permanece `not_assessed`. Dirección: encadenar promesa -> decisión -> dinero -> resultado sin reescribir la claim. Siguiente: segundo manifiesto + revisión humana estratificada y `TODO 20/21`. Evidencia: `docs/etl/sprints/PROMISES-20260819/evidence/promises-bng-2023-ingest.json`, `promises-bng-2023-validation.json` y `live-promises-bng-2023-verification.json`, - spending/procurement canónico publicado y verificado live: `docs/data-model/spending-spec-v1.md` separa `14` fases y fija Decimal/identidad/provenance/scale. El materializador toma latest versions de la adquisición PLACSP real (`330.577` registros, `262.558` awards, `3.190.620` documentos) y publica un sample determinista de `20` awards / `40` actores / `38` evidencias, con `10` personas jurídicas, `5` posibles personas físicas y `5` tipos no resueltos retenidos exactamente. Importe award `4.835.494,71 EUR`, payable `5.845.384,70 EUR`; todos los pagos/entregas quedan desconocidos. El archive fuente de `146.382.691` bytes rehasha a `bda70aa0...`; release `7abe9ac6...`, JSON `b09990e1...`, replay, FK e integridad pasan. `/spending/` y JSON live devuelven HTTP `200`; tres expedientes PLACSP, uno por estrato de identidad, también devuelven HTTP `200`. Dirección: materialización canónica completa particionada y primer enlace award -> contrato/pago/entrega solo con evidencia oficial. Evidencia: `docs/etl/sprints/SPENDING-20260819/evidence/placsp-spending-materialization.json`, `placsp-spending-validation.json` y `live-placsp-spending-verification.json`, - agreements/meetings canónico publicado y verificado live: `docs/data-model/agreements-influence-spec-v1.md` separa hechos de meeting/agreement, topics derivados e influence assertions. El materializador usa la captura oficial real `moncloa_referencias` de `21.234` bytes / SHA-256 `ab08b7dc...`: de `20` referencias selecciona los `11` resúmenes que dicen explícitamente `acuerdo` y produce `11` sesiones, `11` acuerdos adopted, `22` relaciones de convener/disclosing body, `37` parties explícitas, `20` topics rule-derived unreviewed y `22` evidencias exactas. Asistencia personal `0`; influence assertions `0`; release `6f75ec32...`, JSON `8d8ce219...`, replay, FK, integridad, real-only y browser pasan. `/agreements/` y JSON live devuelven HTTP `200` con bytes/hash exactos; `3` referencias oficiales Moncloa también devuelven HTTP `200`. Dirección: fuente de agendas/lobbying con personas/cargos públicos, segunda captura/history y primer enlace agreement -> implementation, sin anonimizar identidad pública. Evidencia: `docs/etl/sprints/AGREEMENTS-20260819/evidence/moncloa-agreements-materialization.json`, `moncloa-agreements-validation.json` y `live-moncloa-agreements-verification.json`, - geopolitics canónico publicado y verificado live: `docs/data-model/geopolitics-spec-v1.md` separa acciones verificables, clasificaciones derivadas y futuras inferencias para tratados, resoluciones, sanciones, ayuda, defensa, votos internacionales y posiciones temporales. Sobre la misma captura oficial `21.234` bytes / SHA `ab08b7dc...`, reglas exactas seleccionan `2/20` referencias internacionales y producen `2` acciones `treaty_process`, `11` actores, `2` evidencias, `4` topics derivados y `2` links al acuerdo fuente. Binding, entrada en vigor, implementación, outcomes y posiciones quedan `not_assessed`; filas de posición/observación e inferencias son `0`. Release `6683fd44...`, JSON `ea8c3eb1...`, replay, FK, integridad, real-only e identidad pública pasan. `/geopolitics/`, JSON exacto y las `2` fuentes Moncloa devuelven HTTP `200`; browser desktop/móvil no presenta overflow, controles cortados ni errores, y el audit global actual pasa `44` rutas / `48` assets / `119` evidencias. Dirección: conectar BOE para etapa jurídica posterior y una fuente oficial de votos o sanciones. Evidencia: `docs/etl/sprints/GEOPOLITICS-20260819/evidence/moncloa-geopolitics-materialization.json`, `moncloa-geopolitics-validation.json` y `live-moncloa-geopolitics-verification.json`, - indicadores Eurostat canónicos publicados y verificados live: `docs/data-model/indicators-spec-v1.md` separa definición, release, serie territorial, versión de observación y evidencia exacta. La captura oficial JSON-stat `nama_10r_3gdp` tiene `4.763.587` bytes / SHA `8584b2c1...`; el slice exhaustivo de geografías `ES*` produce `86` series (`1` país, `7` NUTS 1, `19` NUTS 2, `59` NUTS 3), `2.091` observaciones `2000..2024`, `2.091` pointers de celda, `113` marcas provisionales, uncertainty `not_provided` y revision state `baseline`. Release `a5d27828...`, JSON `8a878b41...`, replay exacto de todas las celdas, FK, integridad, real-only e identidad pública pasan. `/indicators/`, JSON exacto, dataset, metodología y reuse Eurostat devuelven `200`; browser desktop/móvil no presenta overflow, controles cortados ni errores; audit global `44` rutas / `48` assets / `119` evidencias, `0` fallos. No hay claim causal, mérito ni culpa. Dirección: segundo snapshot real con delta `new/unchanged/changed/deleted` y codebook representativa antes de promotion. Evidencia: `docs/etl/sprints/INDICATORS-20260820/evidence/eurostat-gdp-materialization.json`, `eurostat-gdp-validation.json`, `live-eurostat-gdp-verification.json` y `live-route-audit.json`, - recovery de release inmutable explícita: `HF_SCALE_RESTORE_SNAPSHOT_PATH` permite fijar `scale/snapshots//` sin consultar `scale/latest.json`. El drill real sobre la release publicada `623b4a5a...` y cache vacío descargó `114` ficheros / `9,688,787` bytes de `actor_mandates`; checksum `114/114` y full validation `88,031` filas / `108` Parquet pasan. Esto cierra recuperación explícita de una release; el plan de pointer/supersession también está preparado, mientras activación autorizada y RPO/RTO medidos siguen abiertos. Evidencia: `docs/etl/sprints/SCALE-FOUNDATION-20260810/evidence/hf-scale-origin-explicit-release-restore-actor-20260812.json` y `hf-scale-origin-explicit-release-actor-validation-20260812.json`, - rebuild SQLite desde release restaurada: el nuevo importer batch/atomic reconstruye el corpus actor dos veces desde los `108` Parquet checksum-verificados. Ambas salidas contienen `88,031` filas y `88,031` mandate IDs distintos, comparten hash lógico `eb7fdb8e...` y son byte-idénticas (`71,168,000` bytes; SHA-256 `61cfdf8e...`); `integrity_check`, row/file/byte reconciliation, identity retention y RSS pasan. Sigue abierto reconstruir el esquema normalizado de producción con relaciones/FKs reales. Evidencia: `docs/etl/sprints/SCALE-FOUNDATION-20260810/evidence/hf-scale-origin-sqlite-rebuild-actor-20260812.json` y `hf-scale-origin-sqlite-rebuild-actor-replay-20260812.json`, - raw-object CAS escalable en local: uploader y restorer ahora usan batches bounded y `16` workers. Sobre datos reales, dos replays procesan `6,792` objetos / `133,219,457` bytes, deduplican `100%` y emiten el mismo manifest determinista (`2,733,754` bytes; SHA-256 `6e496f35...`); el restore `full_manifest` hace preflight de bytes + reserva `10 GiB` y recupera/checksum-verifica `6,792/6,792`. Es prueba local warm-path, no origin durable: la auditoría de configuración encuentra bucket, endpoint, region/profile y credenciales S3 ausentes, por lo que no se intentó un upload remoto. Siguen abiertos S3 remoto, versioning/retention y ampliar de los `6,792` textos enlazados a todo el inventario elegible `21,373`. Evidencia: `docs/etl/sprints/SCALE-FOUNDATION-20260810/evidence/content-object-parallel-replication-20260812.json`, `content-object-parallel-replication-replay-20260812.json`, `content-object-full-local-restore-20260812.json`, `raw-object-remote-origin-config-audit-20260812.json`, - Docker context/storage: un temporal de packaging no excluido infló el contexto a `514.20 MB` y Docker Desktop falló al extraer la imagen con `input/output error`. Se eliminó exactamente el temporal de `514 MB`, el packaging temporal se movió fuera del repo y `.dockerignore` ahora excluye `.scale-origin-*` y `etl/data/restored`; el filesystem recuperó `5,697,978,368` bytes libres. El tracker host pasa (`38` tracker / `48` DB, `0` mismatch/waiver/expired/DONE-zero-real), pero containerd continúa devolviendo I/O error al leer/eliminar la imagen desechable y su container huérfano. El gate Docker queda bloqueado hasta reparar/reiniciar Docker Desktop y repetir la medición del contexto; no se reinició porque hay containers ajenos activos. No se borró ningún corpus ni artifact Docker ajeno. Evidencia: `docs/etl/sprints/SCALE-FOUNDATION-20260810/evidence/docker-context-storage-blocker-20260812.json`, - estado global: `real_foundation_ready_scale_incomplete`, - política vigente: solo corpora capturados de fuentes oficiales cuentan para coverage, capacidad y readiness; synthetic/mock está prohibido; todo dato personal publicado por la fuente oficial se conserva con provenance y la clasificación nunca lo suprime; secretos, trazas workstation y datos no públicos quedan bloqueados, - corrección de integridad `2026-08-12`: los fallbacks silenciosos habían cargado `12` source records no reales y derivado `10` policy events, `10` issues y `10` ledger entries. La purga real-only terminó con FK `0` y `remaining_non_real_source_records=0`; se retiraron también fixtures y artifacts derivados identificados. Los conectores ya no hacen fallback implícito, `programas_partidos` exige `--from-file`, y `just real-data-only-check` bloquea raw/derived/published/UI. Evidencia: `docs/etl/sprints/SCALE-FOUNDATION-20260810/evidence/non-real-record-purge-20260812.json` y `real-data-only-validation.json`. Reportes históricos que dependían de esos fixtures quedan invalidados como evidencia actual, - registry canónico: `docs/etl/real-corpus-registry.json`; reporte: `etl/data/published/scale-readiness-latest.json`; comando: `just etl-scale-readiness`. El gap documental usa contrato machine-checked: registry, inventario, audit, cola, extracción nativa, validación independiente, OCR, validación OCR y CSV humano deben balancear o readiness falla; el payload público incluye métricas y hashes de toda evidencia, - actualización documental vigente, que supersede las cifras pre-BOE conservadas más abajo como historia operativa: `111,422` descubiertos; `111,408` oficiales elegibles / `109,558` contenidos / `5,093,218,479` bytes; `111,357` con checksum + URL (`99.9542%`); `51` gaps históricos sin cambio. BOE contiene `1,034` resúmenes y `89,001` XML; la expansión histórica añadió `18` fechas exactas y `21` documentos exactos, todos terminales y con manifest. Inventario reutiliza `111,369` filas y sólo inspecciona `48`. Extracción v5: `111,408/111,408`, `109,558/109,558`, `1,999,902,681` chars, `108,139` CAS, `0` retry/dead/failure/truncation; full validation pasa. OCR sigue `856/856`, `849` text, `7` no-text, `89,632` chars, `17` gains, `131` CAS; human review `0/40`. Gate `100k` pasa; diversidad, coste, review, origin raw remoto y `1M` siguen abiertos, - auditoría actual: `7/7` corpora registrados validan todos sus ficheros, bytes, SHA-256, filas y allowlists de `source_id`/host; `3` lanes superan un millón de filas reales; `0` lanes están promocionadas, - cierre técnico actual: `1,346` tests Docker pasan (`11` skips contextuales), `92/92` SQLite staging pasan el audit enforced con `0` incidencias, el último real-only escanea `124,583` ficheros con `0` findings, publication hygiene escanea `16,842` artefactos con `0` secretos/sesiones/paths locales, el build genera `1,433/1,433` rutas y el audit live verifica `44` rutas / `48` assets / `119` evidencias sin fallos. La higiene publicable no elimina nombres, identificadores ni otros datos personales publicados oficialmente, - votos nominales actuales: `1,809,222` filas / `8,373` shards / `170,990,631` bytes; source-record y URL pública `1,809,222/1,809,222` (`100%`). Un sidecar verificado aplica la URL oficial Congreso, hash oficial, hash capturado y event ID estable al único evento legacy y repara sus `350` votos; `0` URLs quedan nulas. El audit disk-backed clasifica las `102,172` filas HTTP / `1,166` URLs, todas de Senado legislaturas 10 y 12: `33,683` filas / `484` URLs tienen captura checksum local y `68,489` filas / `682` URLs no. Dos probes HTTPS content-equivalence acotados devolvieron `HTTP 403` HTML (`417/418` bytes); no se reescribió ninguna URL y no habrá retry sin palanca nueva. Evidencia: `docs/etl/sprints/SCALE-FOUNDATION-20260810/evidence/member-vote-source-url-lineage-20260812.json`, - Eurostat actual: corpus de escala `1,755,809` observaciones / `155,435` series / `37` Parquet / `253,373,860` bytes; URL HTTPS `100%`; replay unchanged `26/26` particiones mediante `37/37` hardlinks; durable-origin y clean restore pasan. Primer slice canónico público: `86` series territoriales españolas / `2,091` observaciones / `2,091` evidencias / `113` marcas provisionales, con JSON live exacto. No promotion por mix no representativo, ausencia de segundo snapshot y correction delta, - PLACSP v5 actual registrado: `263,302` facts / `50` Parquet / `20,803,781` bytes; URL HTTPS y lineage `100%`; los `128,849/128,849` nombres e identificadores de contraparte presentes en source se retienen exactamente (`117,531` legal entity, `8,260` natural person, `3,058` unclassified); `12,898` rows no traen identidad en source; replay unchanged `50/50`; durable-origin y clean restore pasan; no promotion por historia incompleta, semántica award != payment e identity resolution, - BDNS v7 actual registrado: `1,360,382` facts / `14` Parquet / `42,955,289` bytes; capacidad corregida y verificada como `s2_1m`; adquisición `1,419/1,419` páginas y `1,080,788,680` raw bytes, sin retries/dead; `89/89` ventanas diarias completas; URL HTTPS, lineage, amount y publication-state `100%`; nombres `1,360,382/1,360,382` e identificadores source `163,270/163,270` retenidos exactamente; replay unchanged `1/1` partición mediante `14/14` hardlinks. Durable-origin parity contract-bearing y clean-room restore pasan. No promotion porque faltan historia representativa y segundo snapshot. El worker ejecuta preflight antes del claim y reserva CAS + SQLite/WAL. Un check intermedio llegó a verde tras liberar temporales, pero el preflight de adquisición capturado más reciente vuelve a `blocked_storage` con `5,685,862,400` bytes libres, `10,863,247,360` requeridos y headroom `-5,177,384,960`; no se reclama más trabajo. Evidencia histórica bloqueada: `docs/etl/sprints/SCALE-FOUNDATION-20260810/evidence/bdns-concessions-partitioned-real-s3-storage-preflight-blocked-20260812.json`; evidencia actual: `docs/etl/sprints/SCALE-FOUNDATION-20260810/evidence/bdns-concessions-partitioned-real-s3-storage-preflight-20260812.json`, - salud de storage publicable: `etl/data/published/storage-health-latest.json` agrega únicamente los preflights reales BDNS y PLACSP, verifica aritmética de reserva/headroom y enlaza evidencia por SHA-256 sin copiar roots, credenciales, sesiones ni paths de workstation. Estado actual `blocked_storage`: BDNS requiere `5,177,384,960` bytes adicionales y PLACSP `66,776,911,872`; la publicación remota del artifact queda pendiente de autorización, - rollback analytical candidate: la release inmutable anterior `623b4a5a...` se restauró por path explícito sin consultar/mutar `scale/latest.json`: `8,619/8,619` ficheros / `504,093,303` bytes checksum-valid, `1,199.614s`, `0` errores. Full validation posterior pasa los seis corpora, `5,403,506` filas, `8,595` data files, identidad pública intacta y peak RSS `1,007.453 MB`. Un plan GET-only posterior revalida el pointer live `5872efaf...`, ambos manifests, restore y validation; todos los checks pasan, delta de contrato es `0` filas / `0` ficheros / `0` bytes, compare-and-swap queda fijado, ambas releases se preservan y `authorized=false` / `mutation_performed=false`. El cache desechable se borró; quedan abiertos rollback real del pointer con autorización, registro de supersession, RPO formal y activation RTO. Evidencia: `docs/etl/sprints/SCALE-FOUNDATION-20260810/evidence/hf-scale-origin-rollback-candidate-full-restore-20260812.json`, `hf-scale-origin-rollback-candidate-full-validation-20260812.json`, `hf-scale-origin-live-verification-20260812.json` y `hf-scale-origin-rollback-plan-20260812.json`, - ledger actual limpio: `126,760` facts / `13` Parquet / `1,271,649` bytes; URL HTTPS y lineage `100%`; `126,734` source-record links; `126,757` actor states resueltos y `3` unresolved explícitos; `26` source IDs inferidos desde URL BOE oficial con scope explícito; replay unchanged `13/13`; durable-origin y clean restore pasan; no promotion por mix/volumen, - actores actuales: `88,031` mandatos / `79,023` personas / `108` Parquet / `9,236,064` bytes; URL HTTPS `100%`; replay unchanged `108/108`; durable-origin y clean restore pasan; no promotion por volumen, mix e identidad externa, - candidaturas BOE actuales: siete proclamaciones inmutables cargan `42,056` ocurrencias base / `4,901` candidaturas (`25,077` titulares, `16,979` suplentes, `185` candidaturas no proclamadas) para seis fechas electorales entre 2015 y 2024. Once correcciones conservan `956` párrafos exactos. El subconjunto revisado de tres proclamaciones conserva `10,815` base / `1,188` candidaturas; tres correcciones aplicadas contienen `180` párrafos, `127` referenciados por `17` operaciones y `53` de contexto, para `10,820` efectivas. Ocho correcciones / `776` párrafos quedan `captured_unapplied`; sus cuatro proclamaciones / `3,713` candidaturas / `31,241` candidatos están excluidos del efectivo. La nueva cola machine-checked exporta `776/776` tareas exactas dual-review (`0` decisiones inventadas) y un JSONL deduplicado con `3,713/3,713` candidaturas y `31,241/31,241` candidatos públicos; hashes, schema, URL, texto, contexto y decisiones se validan en readiness. Noviembre 2019 está capturado pero falla cerrado porque una cabecera oficial omite el número de candidatura; no se infiere. Todas las filas base retienen nombre, párrafo, URL, checksum, orden, distrito, cámara, condición, source record y document lineage; las efectivas conservan person/party/source/document lineage, `10,795` enlaces base, `25` identidades source-scoped nuevas/cambiadas y `88` source records corregidos. FK `0`, `quick_check=ok`. Infoelectoral sigue bloqueado; no se infiere identidad cross-source ni outcome. Evidencia: `docs/etl/sprints/SCALE-FOUNDATION-20260810/evidence/boe-election-candidates-latest.json`, `boe-election-candidate-correction-materialization.json` y `boe-candidate-correction-review-queue.json`, - analytics bounded real para candidatos: el contrato unificado v3 exporta las `10,820` ocurrencias BOE efectivas a `2` particiones/ficheros Parquet Zstd (`2,083,541` bytes). Conserva `10,820/10,820` nombres públicos exactos, `10,820/10,820` párrafos oficiales, URL HTTPS, checksum y source/document lineage; `88` filas mantienen evidencia de corrección y `is_elected` queda null en las `10,820` porque una proclamación no prueba resultado. El validator independiente reconstruye IDs y contratos source-specific, rehasha cada fichero y falla ante drift/tamper. Replay unchanged reutiliza `2/2` particiones mediante `2/2` hardlinks y reconstruye `0`. El corpus entra como séptimo registry real, pasa su gate local `10k`, pero no representatividad/promotion/origin/restore. El dry-run HF separado empaqueta las tablas normalizadas sin mutación ni supresión de identidad; publicación y restore remoto requieren autorización explícita. Evidencia: `etl/data/published/candidate-occurrence-semantic-partition-manifest-latest.json`, validation, incremental manifest e incremental validation, - documentos actuales: el inventario real-only descubre `21,387` ficheros, excluye `5` fixtures/samples y `9` capturas reales de denegación/error de la métrica de éxito, preserva las últimas como evidencia de obstrucción y balancea `21,373/21,373` instancias elegibles / `19,523` hashes. `3` páginas de programas explícitamente sintéticas y `8` capturas Europarliament que copiaban exactamente un fallback inventado se retiraron físicamente; el gate posterior bloquea fingerprints explícitamente no reales sin rechazar payloads oficiales repetidos. El audit verifica checksum + URL pública para `21,322` (`99.7614%`) y `6,792/6,792` textos previos, sin conflictos. Además de `10,326` URLs exactas embebidas, recupera `50` relaciones PDF desde hrefs capturados y GETs idénticos; `4,516` relaciones desde SQLite archivados sobre `3,757` ficheros (`3` gaps cerrados); `14` PDFs por identificador de publicación más GET idéntico; `56/57` XML agregados Senado por proyección de path más GET idéntico (`318,209,909` bytes); un RSS BOE mediante sidecar oficial explícito; `24` relaciones de manifests de programas; `530` del manifest de votos; y `124` edges JEC/Parlamento de Andalucía. La lane Wayback disk-backed usa sólo endpoints exactos respaldados por el repo, agrupa `26` gaps en `3` targets, indexa/rehasha `5` captures históricas distintas, rechaza `5/5` nonmatches, checkpointa `7` variantes inconclusas y publica `0` relaciones falsas. El checkpoint JSONL atómico contiene `43` registros: schema, `3` targets, `26` paths, `8` consultas y `5` captures; el límite del índice falla cerrado. URL pattern sin igualdad de bytes no cuenta: el único XML histórico Senado que hoy difiere sigue abierto. La cola contiene `51` gaps / `8,315,544` bytes: `25` Europarliament, `25` programas de partido y ese XML Senado; no se inventan URLs ni se suprime información personal oficial. La nueva extracción v2 procesa `21,373/21,373` instancias y `19,523/19,523` contenidos, conserva `444,668,899` caracteres públicos en `18,104` CAS, y termina con `0` retries/dead/truncation; el validator independiente rehasha cada raw y CAS. OCR aditivo procesa `856/856` páginas candidatas de `58` PDFs: `849` text, `7` no-text, `89,632` chars, `17` mejoras y `131` CAS; validation vuelve a verificar los `58` PDFs y todos los CAS. Un packet humano determinista incluye `40` páginas reales, todos los no-text y gains, ambas fuentes/razones, y `0/40` reviews: ningún label se inventa. Recovery exacto siguiente: completar review `40/40`; retry sólo de las `7` variantes Wayback fallidas o refresh con capture nueva; obtener manifest exacto para Europarliament, sidecar/manifest primario o respuesta archivada idéntica para programas, y captura histórica idéntica para Senado; después crecer `78,627` instancias hasta `100k`. Evidencia: `docs/etl/sprints/SCALE-FOUNDATION-20260810/evidence/real-document-format-inventory.json`, `real-data-only-validation.json`, `real-document-provenance-audit.json`, `document-provenance-gap-queue-20260812.json`, `wayback-document-lineage.json`, `wayback-document-checkpoint.jsonl`, `archived-document-source-lineage.json`, `captured-link-document-lineage.json`, `official-document-identifier-lineage.json`, `official-path-document-lineage.json`, `real-document-text-extraction.json`, `real-document-text-validation.json`, `real-document-page-ocr.json`, `real-document-page-ocr-validation.json` y `real-document-page-ocr-human-review-queue.json`, - gaps de escala/promoción abiertos: candidatos nominales efectivos `10,820/1M` vía BOE, con base capturada `42,056`, base revisada `10,815`, `17` operaciones y `31,241` candidatos pendientes excluidos; artifact semántico validado/replayable y `8,926` resultados electos como hecho separado. Faltan revisar ocho correcciones, resolver el defecto fuente de noviembre 2019, más elecciones, link de resultados, origin y restore. BDNS v7 `1,360,382/1M` pasa capacidad y ventanas pero no historia/promoción; documentos `111,408/1M`; integrity representative `0/1M`, - corpus real inspeccionado: un snapshot legacy contiene `1,809,222` votos nominales reales, pero queda `observed_not_promoted`; el audit v3 separa correctamente `AUSENTE` de `no_vote` y mide `5,437/6,947` eventos reconciliados (`78.2640%`), `1,510` discrepancias y `34,364` ausencias observadas sin total de ausencia en ese artefacto; falta publicar los shards en origin/CDN, lineage efectivo por evento padre y URL pública son `100%`, y las demás lanes siguen por debajo del millón, - repair Senado offline sobre DB desechable: el resolver ya no cruza archivos `ses_*.xml` entre legislaturas y `--local-cache-only` bloquea HTTP en el loader; `4,313` eventos cacheados procesados, `4,205` con member votes, `1,113,152` rows cargadas/upserted, `5,319` stale rows retiradas, `1,221` eventos sin cache omitidos, `17` no-match, `0` fetch failures. Resultado medido: `1,877,033` member votes, FK `44,646 -> 0`, `integrity_check=ok`, `5,830/6,952` eventos reconciliados (`83.8608%`) y person link técnico `100%` tras optimizar el bridge de etiqueta exacta a un solo scan; delta de entidades `+948` personas-label y `+3,360` mandatos observados. El audit v2 hace accionable el residual: `1,115/1,122` eventos tienen menos filas observadas que categorías oficiales, `7/1,122` conservan población comparable con distribución distinta y `0` tienen exceso; por fuente son `1,111` Senado y `11` Congreso. Evidencia: `docs/etl/sprints/SCALE-FOUNDATION-20260810/evidence/senado-local-cache-repair-audit.json`. Sigue `PARTIAL`: etiqueta exacta no prueba identidad externa, la DB es desechable/no publicada y las filas faltantes requieren captura/fuente autoritativa, - delivery bounded directo desde la DB reparada: `8,357` shards gzip con `1,877,033` votos / `179,993,722` bytes, index `4,528,365` bytes, max shard `31,761` bytes / `350` votos. El validator independiente confirma `8,357/8,357` archivos, checksums y payloads, totales exactos, lineage/public URL `100%` y `0` tokens privados. Evidencia: `docs/etl/sprints/SCALE-FOUNDATION-20260810/evidence/senado-repaired-db-shard-manifest.json` y `docs/etl/sprints/SCALE-FOUNDATION-20260810/evidence/senado-repaired-db-shard-validation.json`. Sigue sin promotion porque `publication_status=local_generated_not_published`, - analytics bounded real para member votes: `1,877,033` rows exportadas a `20` particiones semánticas snapshot/source/jurisdiction/year y `33` Parquet Zstd (`10,795,673` bytes), con schema hash, content hash por partición, rows/min-max/SHA por fichero, lineage y URL pública `100%`, `0` tokens privados, RSS export `122.625 MB` y RSS validator full-corpus `226.078 MB`. La URL se conserva con scope explícito: `732,975` record-specific y `1,144,058` source-default oficial. Rebuild next-snapshot sin cambios: `20/20` particiones reutilizadas, `33/33` hardlinks, `0` recomputadas, RSS `30.531 MB`; validator incremental completo `status=ok`. Evidencia: `docs/etl/sprints/SCALE-FOUNDATION-20260810/evidence/member-vote-semantic-partition-manifest.json`, `member-vote-semantic-partition-validation.json`, `member-vote-semantic-partition-incremental-manifest.json` y `member-vote-semantic-partition-incremental-validation.json`. Sigue `PARTIAL`: durable-origin contract parity pasa; promotion bloqueada por reconciliación `83.8608%`, identidad externa y restore limpio desechable, - analytics bounded real para accountability ledger: `126,760` facts exportados a `13` particiones/ficheros Parquet Zstd (`1,271,649` bytes), con balance exacto DB/join/distinct-ID, lineage/URL pública `100%`, `126,734` source-record links, `126,757` actor states resueltos, `3` unresolved explícitos y `0` tokens privados. `126,398` URLs son record-specific; `362` referencias legacy hacen fallback a URL oficial source-default sin publicar paths privados. Incremental unchanged reutiliza `13/13` particiones mediante `13/13` hardlinks y `0` recomputadas. Evidencia: `docs/etl/sprints/SCALE-FOUNDATION-20260810/evidence/accountability-ledger-semantic-partition-manifest.json`, `accountability-ledger-semantic-partition-validation.json`, `accountability-ledger-semantic-partition-incremental-manifest.json` y `accountability-ledger-semantic-partition-incremental-validation.json`. Sigue `PARTIAL`: pasa `S1` analítica, durable-origin y clean restore; no pasa `S2/promotion` y está dominado por acciones parlamentarias, - actor real Infoelectoral + analytics bounded: los XLSX oficiales de cargos electos cargan `8,926` outcomes (`5,600` Congreso + `3,326` Senado), `16` elecciones entre `1977-06-15` y `2023-07-23`, en CAS content-addressed con SHA/fila/source record. Parser bounded, staging set-based `5,000`, replay idempotente y reconciliación exacta facts/source-records/personas-occurrence/mandatos; DB `quick_check=ok`, FK `0`, RSS ingest `49.246 MB`. El transporte queda marcado `tls_verified=false` por cadena incompleta del host oficial. El actor Parquet actualizado contiene `88,031` mandatos / `79,023` personas distintas en `108` particiones/ficheros Zstd (`9,236,064` bytes); balance, lineage y URL `100%`, source-record `87,106/88,031` (`98.949234%`), `79,104` source-identifier rows, `8,926` source-record identity rows, `1` alias-only, `0` unknown-year/private tokens. RSS export `150.375 MB`; validator full-row `193.879 MB`; incremental unchanged reutiliza `108/108` particiones por `108/108` hardlinks. Evidencia: `docs/etl/sprints/SCALE-FOUNDATION-20260810/evidence/infoelectoral-elected-officials-real-20260811.json`, `etl/data/published/actor-mandate-semantic-partition-manifest-latest.json`, `actor-mandate-semantic-partition-incremental-latest.json` y `actor-mandate-semantic-partition-validation-incremental-latest.json`. Sigue `PARTIAL`: durable-origin y clean restore pasan; faltan `11,969` filas representativas para `S1` e identidad externa, - durabilidad/reconciliación Infoelectoral v2: `8,926` facts present, `0` absent y `8,926` observations por fact+snapshot+hash; links directos person/mandate/party/institution/role/territory/source-record completos. `first_seen`, `last_seen` e `is_present` evitan borrado silencioso; presence finaliza solo tras stream completo. Drift gate pre-mutation por cámara compara con snapshot anterior, bloquea cambio absoluto `>15%` salvo override explícito y reporta `0%/0%` en el live replay. Tests prueban rerun idempotente, desaparición preservada como absent y override revisado. Run `v2`: `quick_check=ok`, FK `0`, RSS `49.246 MB`, - candidaturas Infoelectoral: carril productivo implementado, pero la fuente real está bloqueada antes del primer archive. Catálogo canónico: `60` archivos soportados / `36` election IDs; queue `60 pending`, `0 attempts`, `0 leased/succeeded/dead`. El schema, ingest y Parquet v2 conservan DNI, fecha de nacimiento raw/normalizada y demás identidad publicada por el archive oficial con lineage; los fallbacks de personas inventadas fueron eliminados. No existe artifact real vacío y no se cuenta ninguna fila. Probe actual: API sin bytes y archive 2023 `Connection reset by peer`; `no_new_lever`, no repetir este sprint. Evidencia: `infoelectoral-candidate-archive-queue-latest.json` e `infoelectoral-candidate-origin-access-20260811.json`, - analytics `S0` fallback para dinero público: `RETIRED_NON_EVIDENCE`. Sus `10` filas generated/fallback y el publisher v3 que suprimía contrapartes no cumplen las políticas real-only ni public-domain-retention; no cuentan para coverage, capacidad, readiness ni publicación. El contrato vigente es v5 y se valida solo contra corpora oficiales reales, - adquisición PLACSP real aislada H1 2025: loader ZIP/Atom bounded con queues separadas archive/member completa `6/6` archivos (`1,037,792,505` compressed; `10,077,339,637` member bytes) y `667/667` miembros, `0` pending/leased/dead. Reconcilia `330,577` versiones, `121,555` stable contracts, `1,014` tombstone sightings, `262,558` award-result versions, `3,190,620` document sightings y `935` duplicate sightings. El máximo real de `3,250` documentos/entry expuso el cap inicial `1,000`; se elevó de forma explícita a `10,000`, se reencolaron solo nueve errores matching y terminaron todos. Evidencia Q1: `placsp-real-s1-enqueue.json`, `placsp-real-s1-archive-worker.json`, `placsp-real-s1-run.json`; Q2: `placsp-real-2025q2-enqueue.json`, `placsp-real-2025q2-archive-worker.json`, `placsp-real-2025q2-run.json`, - analytics PLACSP v5 H1: última versión por contrato, `121,555` notice facts + `141,747` award facts = `263,302` rows en `50` particiones / `50` Parquet; `249,875` amounts suman `271,195,575,502.570000 EUR` con semántica notice/award separada y nunca payment. URL/lineage `100%`, `0` private tokens. Retención exacta source-to-Parquet: `128,849/128,849` nombres y `128,849/128,849` identificadores, desglosados en `117,531` legal entities, `8,260` natural persons y `3,058` unclassified; `12,898` rows no traen identidad en source. Full validator pasa; unchanged replay reutiliza `50/50` particiones/ficheros por hardlink. Evidencia: `placsp-real-s1-v5-semantic-manifest.json`, validation, incremental manifest y validation. Gate: money-fact y stable-contract `S1` `DONE`; representatividad histórica y `S2/promotion` `PENDING`, - documentos PLACSP: `3,190,620` sightings -> `998,392` unique URL items / `256` queue partitions. Muestra real deliberadamente bounded: `20/20` success, `21,055,060` bytes, `20` SHA, `66` sightings enlazados, `0` retry/dead; `998,372` quedan pending. Evidencia: `placsp-real-2025h1-document-fetch-enqueue.json` y `placsp-document-fetch-sample.json`. No drenar sin budget por host, origin/restore, telemetría y cohortes `100 -> 1k -> 10k -> 100k`, - señales PLACSP internas: detector v4 version-aware prefiere award results, excluye versiones superseded y fingerprinta el conjunto de evidencia; `2,036` review signals / `6,788` evidence links materializados desde `17,585` candidate rows al umbral analítico `15,000 EUR`, mínimo `3`. Todas quedan `review_signal`, `internal`, `public_by_default=false`, `human_review_required=true`, `corruption_finding=false`. Quedan auditables y withdrawn `12` v2 + `1,041` v3 superseded; replay v4 conserva `2,036/6,788` y supersede `0` adicional. Evidencia: `placsp-real-2025h1-integrity-signal-run.json` e idempotence. Siguiente gate: gold set, double review, adjudication, false-positive/reversal/drift, right-of-reply y correction antes de publicación, - historia PLACSP preparada: discovery bounded y TLS-verificado sobre catálogo oficial descubre `35` archive links y selecciona `22/22` gap-free (`14` annual 2012-2025 + `8` monthly 2026), con hash de catálogo y archive-contract SHA-256. Queue durable: `22` pending / `0` leased/dead; replay de enqueue `22` existing / `0` inserted. Guard de storage ejecutado antes de claim/network: `blocked_storage`, `41,134,141,440` bytes libres frente a `107,911,053,312` requeridos, `0` attempts/leases/network. Probe 2024 separado: tres resets verificados + HTML `HTTP/1.1 200 Error`; incidente intermitente en name-and-shame, no repetir sin señal fresca y budget remoto/local. Evidencia: `placsp-official-archive-catalog-20260811.json`, `placsp-official-history-enqueue-20260811.json`, `placsp-official-history-storage-preflight-20260811.json`, `placsp-history-archive-access-probe-20260811.md`, - informe humano PLACSP: `docs/etl/sprints/SCALE-FOUNDATION-20260810/reports/placsp-real-s1-contracts-documents-integrity.md` reúne scope, comandos, reconciliación, privacy/money semantics, fallos cerrados y siguientes gates sin promover la lane, - adquisición bulk BDNS live `S1` previa (`2026-08-12`): el endpoint oficial descubre `28,676,987` concesiones / `28,677` páginas de `1,000`. La cohorte termina `100/100` páginas y `100,000` rows sin retries/dead, con identidad pública exacta. Queda como historia válida, pero el registry/readiness vigente es `S3` abajo, - adquisición particionada BDNS `S3` completada (`2026-08-12`): `enqueue-daily` evita paginación global profunda mediante ventanas oficiales diarias, registra lineage en `money_bulk_partitions`, conserva `source_page_number` y valida que cada fila pertenezca al intervalo solicitado. `expand-daily` revalida el contrato oficial y añade páginas append-only sin renumerar las previas. La cohorte bounded cubre `89/89` particiones completas: queue `1,419/1,419` succeeded, `0` retries/dead/unfinished, `1,360,382` records vistos/cargados/distintos, `1,360,382` sightings/version links/raw-page lineage y `1,080,788,680` bytes. Todos los checks de adquisición y reconciliación pasan. El gate analítico, durable-origin contract parity y clean-room restore pasan. `promotion_gate_passed=false` y `SCALE-036` permanece `PARTIAL`: es un snapshot bounded, aún no historia completa. Siguiente gate: segundo snapshot/delta real y cohortes históricas. Evidencia: `bdns-concessions-partitioned-real-s3-enqueue-20260812.json`, `bdns-concessions-partitioned-real-s3-expand-20260812.json` y `bdns-concessions-partitioned-real-s3-run-20260812.json`, - analytics BDNS v7 `S3`: `1,360,382` filas oficiales en `14` Parquet Zstd / `42,955,289` bytes; capacity contract `s2_1m`; `100%` source URL, lineage, amount EUR no negativo, nombre y publication-state. Retención exacta source-to-Parquet: `1,360,382/1,360,382` nombres y `163,270/163,270` identificadores; `163,270` legal entities y `1,197,112` unclassified, sin suprimir campos oficiales. Importe publicado total `10,121,196,195.270000 EUR`, semántica subsidy award, no desembolso/outcome. Full validator exacto usa índice SQLite temporal y pasa con `0` private tokens; unchanged replay reutiliza `1/1` partición por `14/14` hardlinks con `0` rebuild y pasa full validation. Registry/readiness valida la tercera lane real de más de un millón; durable-origin contract parity y clean-room restore pasan; `analytical_partition_gate_passed=true`, `promotion_gate_passed=false`. Siguiente gate: historia oficial, resolver identidades y capturar segundo snapshot/delta. Evidencia: manifest, validation, incremental manifest e incremental validation `bdns-public-money-real-s3-v7-*`, - analytics live BDNS v5 `S1`: `100,000` filas oficiales en `1` Parquet Zstd / `3,464,007` bytes; `100%` source URL, lineage, amount EUR no negativo y publication-state. Retención exacta source-to-Parquet: nombres `100,000/100,000`, identificadores `39,539/39,539`; `39,539` filas clasificadas legal entity y `60,461` unclassified, sin suprimir ningún campo. Importe publicado total `3,259,528,635.280000 EUR`, semántica subsidy award, no desembolso/outcome. Full validator pasa con `0` private tokens y RSS `211.453 MB`; unchanged replay reutiliza `1/1` partición/fichero por hardlink con RSS `31.074 MB`. El corpus entra en registry como sexto corpus real y pasa `R1`; `promotion_gate_passed=false`. Evidencia: `bdns-public-money-real-s1-v5-manifest-20260812.json`, validation, incremental manifest y incremental validation. El artifact v3 que suprimía `58,136` contrapartidas queda `RETIRED_NONCOMPLIANT`, - checkpoint BDNS `S2` histórico: `1,000` páginas se encolaron para `1M`; quedaron `146/1,000`, `146,000` filas, `114,558,806` bytes, `146,000` version sightings, `0` dead y `854` pending. Se corrigió el circuit per-claim con ventana rolling de `20`; el perfil paró al observar `16` fallos upstream en `24` intentos. Esa DB/root no está disponible como artifact actual y no cuenta en readiness. La cohorte fresca `S1` demuestra señal oficial verde sin errores; tras preflight debe arrancarse una nueva cohorte `S2` durable y v5-compatible en vez de atribuir continuidad al checkpoint ausente. Evidencia histórica: `bdns-concessions-s2-enqueue.json` y `bdns-concessions-s2-partial-run.json`, - analytics BDNS `S2` parcial: el artifact v3 de `146,000` filas queda `RETIRED_NONCOMPLIANT` porque suprimía `102,417` contrapartidas oficiales no clasificadas. Debe regenerarse desde la adquisición oficial con publisher v5; no cuenta como artifact-ready hasta validar retención exacta, source totals, revisions, identity resolution y origin, - pipeline documental real: `4,035` iniciativas escaneadas, `9,084` links Senado materializados sin red, `6,792` documentos XML/HTML descargados y extraídos (`29,728,661` caracteres, cero truncaciones), `198` fallos permanentes y `2,094` diferidos; las `3,607` iniciativas con links tienen al menos un documento descargado. El origin local contiene `6,792` objetos / `133,219,457` bytes y un restore checksum de `100` objetos pasa. Evidencia: `docs/etl/sprints/SCALE-FOUNDATION-20260810/reports/million-scale-foundation-and-real-document-run.md`, - el full live publish `31356749319` de `2026-08-10` terminó correctamente, pero los runs fallidos recientes impiden tratar una corrida verde como SLO sostenido, - revisión comunitaria del recibo de agua de Andalucía: checkpoint abierto; `0/5` revisores y `0/5` tracks completados al verificar el issue `#20`. - gate de higiene de publicación comprimida: el scanner ya inspecciona `.gz` en streaming; el snapshot tracked `votaciones-es-2026-02-12.json.gz` tenía `351` referencias locales de workstation, fue saneado sin cambiar filas (`183,811` votos nominales/`6,035` referencias de evento preservadas), y ahora devuelve `0` findings. El gate permite emails e identidad de fuentes públicas y bloquea secretos, rutas locales y estado no público. Qué sigue: mantener el test gzip y bloquear cualquier publish con findings. Hacia donde vamos: - promocionar cada lane desde captura oficial `R0 -> S1 100k real -> S2 1M real`, con restore, reconciliation, calidad, coste y publicación; fixtures/generated nunca cuentan, - separar control plane SQLite, object origin content-addressed, hechos Parquet particionados y JSON público bounded, - convertir anomalías en review signals con corroboración/counterevidence/corrección; nunca en acusaciones automáticas. Qué sigue: 1. mantener en CI la fundación cerrada (`SCALE-001..014`) y conectar alerting operativo, 2. completar las `40/40` decisiones OCR reales, configurar origin remoto y llevar documentos reales por cohortes hasta `100k`, reutilizando la extracción/OCR completa y añadiendo presupuesto por host (`SCALE-015..023`), 3. extender PLACSP desde H1/`121,555` contratos estables a historia oficial completa y luego `1M`; capturar el segundo snapshot BDNS; cerrar mix/delta/correcciones de indicadores (`SCALE-027..031`, `SCALE-035..037`), 4. ejecutar pruebas reales por lane (`SCALE-032..039`), 5. cerrar delivery/review/ops/community (`SCALE-040..052`). | ID | Estado | Evidencia actual | Siguiente gate | | --- | --- | --- | --- | | `SCALE-001` queue states + attempts | `DONE` | `publicdata_ops/work_queue.py`, schema y tests | mantener compatibilidad en todos los workers | | `SCALE-002` atomic indexed claim | `DONE` | índice `idx_pipeline_work_claim_v2`; query plan en benchmark | CI `50k` estable | | `SCALE-003` expired lease recovery | `DONE` | primitive + unit contract | fault-injection multiprocess real | | `SCALE-004` long-job heartbeat | `DONE` | heartbeat común usado por fetch/extraction + test de future lento | fault-injection multiprocess real | | `SCALE-005` retry taxonomy/circuit breaker | `DONE` | error classes, permanent dead-letter, Retry-After/jitter y host breaker 401/403/429 | calibrar umbrales con corpus real | | `SCALE-006` queue observability | `DONE` | `report_pipeline_work_queue.py`: states, attempts, retries, dead rate, overdue leases, workers, partitions y errores | conectar a run heartbeat/alerting | | `SCALE-007` durable queue at real scale | `PARTIAL` | primitives de queue implementadas; la corrida documental real reconcilia estados, pero ninguna queue oficial de `1M` items ha cerrado con recovery y coste | ejecutar por cohorts reales y cerrar `1M` sin silent loss | | `SCALE-008` bounded streaming download | `PARTIAL` | `publicdata_core/blobstore.py`; corrida real conserva `6,792` objetos / `133,219,457` bytes | validar truncation/oversize/retry y cleanup en cohort real estratificada | | `SCALE-009` content-addressed atomic local object | `DONE` | SHA-256/sharded path/atomic replace | remote checksum parity | | `SCALE-010` resumable document fetch worker | `PARTIAL` | corrida real `6,792` success / `198` dead / `2,094` deferred | reconciliar failures por host/clase y completar `100k` real con recovery | | `SCALE-011` resumable text worker | `DONE` | extracción v5 content-deduplicated `111,408/111,408` instancias / `109,558/109,558` contenidos, `1,999,902,681` chars, `108,139` CAS, `0` retry/dead/failure/truncation; validation independiente rehasha todo | mantener el contrato al crecer a `1M` y añadir lineage page/section | | `SCALE-012` large seen-set reconciliation | `DONE` | temp-table/`NOT EXISTS`; `40,001`-row test | million-row real identity/mandate run | | `SCALE-013` honest readiness artifact | `DONE` | `scripts/report_scale_readiness.py`, `scale-readiness-latest.json`; valida siete corpora más inventario/provenance/queue/BOE/Wayback/native/OCR/review, `111,408/111,408` native, `108,139` CAS, cero truncation, `856/856` OCR, `131` OCR CAS; candidato BOE base capturado `42,056`, revisado `10,815`, efectivo `10,820`, pending efectivo `0`, `17` operaciones, replay `2/2`; review real `0/40` | extender contratos machine-checked a gaps restantes con evidencia estructurada | | `SCALE-014` integrity policy contract | `DONE` | `docs/method/integrity-signal-policy.md`, templates, CI test | exercise complete false-positive/correction workflow | | `SCALE-015` document format inventory | `PARTIAL` | `111,422` descubiertos; `111,408` elegibles / `109,558` contenidos / `5,093,218,479` bytes; numeric `100k` pasa; incremental reutiliza `111,369` y sólo inspecciona `48`; `1,437` PDFs / `44,927` páginas; `0` fallos | cerrar estratos representativos scan/office/idioma/densidad, coste y origin remoto | | `SCALE-016` S3-compatible origin contract | `DONE` | optional boto3 adapter, content key, checksum/size verification, no embedded credentials | select/configure actual bucket policy | | `SCALE-017..018` remote replication + disposable cache | `PARTIAL` | streaming contract tests + `6,792` real objects / `133,219,457` bytes replicated to local filesystem origin, `0` missing | run against configured versioned remote origin | | `SCALE-019` restore drill | `PARTIAL` | deterministic real `100`-object local restore sample passes checksum | remote sample/full-manifest drill with RPO/RTO evidence | | `SCALE-020..023` format routing/OCR/100k real docs | `PARTIAL` | numeric `100k` pasa con `111,408`; native y OCR completos/validados; PDF density `44,927`; packet `40` real / `0` labels inventados / `0` reviews; assignment hash inmutable y prep anti-overwrite | finish `40/40`, quarantine/diversity/cost gates; numeric 100k no equivale a representatividad | | `SCALE-024..031` Parquet schemas/manifests/incremental | `PARTIAL` | registry actual valida member votes `1,809,222`, Eurostat `1,755,809`, PLACSP v5 `263,302`, BDNS v7 `1,360,382`, accountability ledger limpio `126,760`, actor mandates `88,031` y candidatos BOE `10,820`; replays aplicables pasan | añadir nuevas lanes/cohortes solo tras full validation | | `SCALE-032` actor/candidate/mandate `1M real` | `PARTIAL` | `88,031` mandates / `79,023` people; actor origin/restore pasan; `8,926` outcomes separados. BOE conserva `42,056` base / `4,901` candidaturas; el subset revisado `10,815` aplica `17` operaciones para `10,820` efectivas. Ocho correcciones / `776` párrafos y `31,241` candidatos quedan pendientes y excluidos. Artifact `2` Parquet valida identidad/texto/lineage, outcome null y replay `2/2` | revisar correcciones históricas, resolver noviembre 2019 sin inferencia, ampliar elecciones BOE, conectar resultados como facts separados, desbloquear `60/60` Interior, origin/restore autorizados, identity gold set y `1M` real | | `SCALE-033` member-vote/action `1M real` | `PARTIAL` | artifact actual `1,809,222` rows / `8,373` shards validado; URL/source-record `100%`; `102,172` filas / `1,166` URLs HTTP clasificadas; captura checksum `33,683` filas / `484` URLs; gap inmutable `68,489` / `682`; dos probes HTTPS `403`; durable-origin y clean restore pasan | obtener captures oficiales inmutables para las `682` URLs restantes o verificar equivalencia HTTPS con palanca nueva; reconciliar totales oficiales e identidad externa | | `SCALE-034` accountability facts `1M real` | `PARTIAL` | root actual real-only recuperado/validado: `126,760` rows / `13` Parquet, replay `13/13`, origin/restore pasan | ampliar a `1M` real representativo | | `SCALE-035` contracts `1M real` | `PARTIAL` | artifact registrado PLACSP v5 `263,302` facts / `50` Parquet; identidad oficial retenida `128,849/128,849`, replay `50/50`, origin/restore pasan | completar historia, identity resolution/payment semantics | | `SCALE-036` subsidies `1M real` | `PARTIAL` | adquisición particionada `1,419/1,419` páginas y root v7 validado: `1,360,382` rows / `14` Parquet; `89/89` ventanas completas; identidad oficial exacta `1,360,382` nombres + `163,270` IDs; `0` retries/dead/private tokens; clean-room restore `20/20` y origin contract parity pasan | segundo snapshot/delta, historia oficial, resolución de identidad y promoción | | `SCALE-037` indicators `1M real` | `PARTIAL` | `1,755,809` Eurostat oficiales, `26` particiones / `37` Parquet, full validation, replay y origin/restore pasan | segundo snapshot, mix representativo y corrections | | `SCALE-038..039` reconciliation + drift | `TODO` | source gates exist but not stage-balanced globally | manifest balance and source contract drift blockers | | `SCALE-040..042` high-cardinality public delivery | `PARTIAL` | legacy: `1,809,222` votos / `8,373` shards; repair SQLite-direct: `1,877,033` votos / `8,357` shards / `179,993,722` bytes, index `4,528,365`, max `31,761` bytes / `350` miembros; validación total, lineage/URL `100%`, `0` tokens privados y sin monolito intermedio | publicar origin/CDN, verificar cache/old links y retirar monolito de rutas | | `SCALE-043` durable integrity review/correction states | `DONE` | additive signal/evidence/review/transition/response/correction schema + gated API; PLACSP materializa `2,036` v4 internal review items / `6,788` evidence links; revisions supersede+withdraw prior items | connect review UI, claims, leases and reviewer ownership | | `SCALE-044` reviewer calibration/adjudication | `TODO` | real PLACSP queue exists but `0` items are calibrated/adjudicated/published | stratified gold set, double review, disagreement, reversal, false-positive and drift metrics | | `SCALE-045` false-positive/reply/correction drill | `PARTIAL` | state machine existe; PLACSP real permanece interno y sin items calibrados/adjudicados | ejecutar workflow completo sobre casos históricos con findings oficiales | | `SCALE-046..049` ops/cost/DR/fault injection | `PARTIAL` | queue health reports states/attempts/partitions/errors; real doc run has `0` overdue leases y bounded circuit evidence; Docker ETL context medido cae de `87.80 MB` a `1.97 MB` al excluir corpora, sprint evidence, run logs, screenshots y generated docs locales; el tracker rebuilt conserva `0` mismatches/waivers/DONE-zero-real | unified telemetry, cost, remote RPO/RTO and unattended fault-injection run | | `SCALE-050..052` contributor throughput | `PARTIAL` | contribution/governance/security/conduct/citation scaffolding | three-maintainer proof and published scorecard | Reconciliación de verdad de indicadores (`2026-08-11`): la nota histórica de `37,431` puntos describe una corrida anterior cuyo SQLite ya no estaba disponible. Se inspeccionaron `82` ficheros `*.db` de staging; `30` exponían `indicator_points` y los `30/30` sumaban `0`, igual que los tres baselines designados (`politicos-es.db`, `politicos-es.e2e19.db`, `politicos-es.recovered.db`). Ese baseline canónico sigue en `0`; no se reescribe ni se presenta la cifra histórica como vigente. En una DB aislada nueva, el registry bounded Eurostat adquirió y normalizó `1,755,809` observaciones oficiales / `155,435` series con reconciliación exacta y luego pasó Parquet/full validation/replay incremental. Por eso `SCALE-037-REAL-S1` y `SCALE-037-REAL-S2-ROWS` están cerrados, mientras `SCALE-037` global permanece `PARTIAL`: faltan representatividad, segundo snapshot/delta, origin público, restore y promoción. Evidencia: `docs/etl/sprints/SCALE-FOUNDATION-20260810/evidence/eurostat-indicator-real-s2-acquisition.json`, `docs/etl/sprints/SCALE-FOUNDATION-20260810/evidence/eurostat-indicator-real-s2-semantic-manifest.json`, `docs/etl/sprints/SCALE-FOUNDATION-20260810/evidence/eurostat-indicator-real-s2-semantic-validation.json`, `docs/etl/sprints/SCALE-FOUNDATION-20260810/evidence/eurostat-indicator-real-s2-incremental-manifest.json`, `docs/etl/sprints/SCALE-FOUNDATION-20260810/evidence/eurostat-indicator-real-s2-incremental-validation.json`. Artefactos y comandos: - readiness: `just etl-scale-readiness`, - Eurostat real durable: `just etl-scale-eurostat-indicators-enqueue` / `just etl-scale-eurostat-indicators-work` / `just etl-scale-eurostat-indicators-backfill` / `just etl-scale-eurostat-indicators-report`, - Eurostat real Parquet/replay: `just etl-scale-eurostat-indicators-export` / `just etl-scale-eurostat-indicators-validate` / `just etl-scale-eurostat-indicators-replay` / `just etl-scale-eurostat-indicators-replay-validate`, - PLACSP catalog/history/archive/member durable: `just etl-scale-placsp-history-discover` / `just etl-scale-placsp-history-enqueue` / `just etl-scale-placsp-archives-enqueue` / `just etl-scale-placsp-archives-work` / `just etl-scale-placsp-members-work` / `just etl-scale-placsp-report` / `just etl-scale-placsp-corpus-report`, - PLACSP Parquet/replay: `just etl-scale-placsp-export` / `just etl-scale-placsp-validate` / `just etl-scale-placsp-replay` / `just etl-scale-placsp-replay-validate`, - PLACSP documents/integrity: `just etl-scale-placsp-documents-enqueue` / `just etl-scale-placsp-documents-work` / `just etl-scale-placsp-integrity-review`, - gate combinado: `just etl-scale-gate`, - documento end-to-end: `just parl-document-pipeline-scale`, - refresh Senado estrictamente desde cache: `just parl-refresh-senado-local-cache`, - audit de SQLite de votos: `just etl-scale-audit-vote-db`, - export bounded directo desde SQLite: `just etl-scale-export-vote-db-shards`, - validación de shards SQLite-direct: `just etl-scale-validate-vote-db-shards`, - export/validación actor-mandate Parquet: `just etl-scale-export-semantic-actor-mandates` / `just etl-scale-validate-semantic-actor-mandates`, - export/validación candidate-occurrence Parquet: `just etl-scale-export-semantic-candidate-occurrences` / `just etl-scale-validate-semantic-candidate-occurrences`, - ingesta oficial de cargos electos históricos: `just etl-extract-infoelectoral-elected-officials`, - export/validación dinero público Parquet: `just etl-scale-export-semantic-public-money` / `just etl-scale-validate-semantic-public-money`, - BDNS bulk durable: `just etl-scale-bdns-bulk-enqueue-daily` / `just etl-scale-bdns-bulk-work` / `just etl-scale-bdns-bulk-report`; el enqueue global queda solo para cohorts pequeños porque el offset profundo degrada, - política de señales: `docs/method/integrity-signal-policy.md`, - dirección completa y DoD: `ROADMAP.md` y `docs/roadmap-tecnico.md`. - **Andalucia 2026 auto-safe execution reviews** (`2026-05-17`): se habilita la via automatizada segura para promotion de drafts de evidencia de ejecucion. Donde estamos ahora: `scripts/apply_andalucia_2026_execution_review_drafts.py` trata `--auto-safe` como alias de `--all --allow-machine-drafts`, conserva gates de seguridad (`NO_MERIT_CLAIM_STATUSES`, `SAFE_REVIEW_STATUSES`, `SAFE_INTERPRETATION_STATUSES`, `merit_blame_not_scored`, `causal_impact_not_claimed` y bloqueo de campos de score), y separa trazabilidad por origen (`promoted_from_machine_draft_explicit_selection` en seleccion manual y `promoted_from_machine_draft_auto_safe` en mode repetible). Hacia donde vamos: correr `just etl-andalucia-2026-accountability-apply-safe` y medir el delta en `etl/data/published/andalucia-2026-execution-evidence-review-apply-report.json` y `etl/data/published/andalucia-2026-accountability.json`. Evidencia esperada: `scripts/apply_andalucia_2026_execution_review_drafts.py`, `justfile`, `etl/data/published/andalucia-2026-execution-evidence-review-apply-dry-run.json`, `etl/data/published/andalucia-2026-execution-evidence-review-apply-report.json`, `etl/data/published/andalucia-2026-accountability.json`. Reglas: - Marcar `DONE` solo con evidencia (comando/SQL/snapshot) y `records_loaded > 0` en red real (cuando aplique). - No duplicar este backlog en otros docs (para planificación: `docs/roadmap.md` y `docs/roadmap-tecnico.md`). - Foco operativo vigente: priorizar lanes `Derechos` (restricciones de libertad) y no abrir nuevos lanes fuera de ese bloque mientras no se cumpla el gate de cobertura definido en la fila `Gate de foco: libertad ciudadana primero`. - Excepción operativa: trabajo en lanes no-`Derechos` solo para mantenimiento crítico (rotura de pipeline, corrupción de datos o incumplimiento de integridad). Actualización de prioridad vigente (`2026-03-05`): - **Andalucia 2026 promocion issue-review y ejecucion inicial** (`2026-05-17`): se aplican reviews conservadoras y se baja el siguiente bloqueo real sin scoring. Donde estamos ahora: los 3 drafts issue-level de `sanidad`, `seguridad_libertades` y `vivienda` se promueven a `etl/data/seeds/andalucia_2026_issue_reviews.json` como direccion/actor parlamentario parcial, manteniendo `execution_owner_not_reviewed`, `budget_execution_not_linked`, `outcome_not_linked`, `causal_impact_not_claimed` y `merit_blame_not_scored`. Se amplian planes de fuentes de ejecucion para `sanidad`, `vivienda`, `fiscalidad` y `seguridad_libertades`; la cola sube de `8` a `19` items en `8` temas, `13` fuentes oficiales verificadas y `204.635` filas candidatas. Se promueven solo 3 drafts de ejecucion suficientemente concretos: contrato SAS de envio de medicacion hospitalaria a domicilio (`10.331` EUR), pago agregado AVRA (`1.158.219` EUR) y pago agregado Agencia de Seguridad y Gestion de Emergencias (`138.725` EUR). El snapshot queda con `8` issue reviews, `72` filas de ejecucion revisadas, `published_merit_blame_claims_total=0`, `execution_source_plan_missing=0`, `budget_or_execution_review_missing=1` (`fiscalidad`) y `delivery_or_beneficiary_missing=7`. Hacia donde vamos: revisar fiscalidad con una evidencia mas especifica o dejarla abierta, y bajar los 7 temas con ejecucion parcial a entrega/beneficiario/outcome posterior. Que sigue: no aplicar el resto de drafts genericos; priorizar fuentes de entrega final/beneficiario y outcomes post-2026 antes de merito/culpa. Evidencia: `etl/data/seeds/andalucia_2026_issue_reviews.json`, `etl/data/seeds/andalucia_2026_execution_evidence_reviews.json`, `etl/data/published/andalucia-2026-execution-evidence-review-apply-report.json`, `etl/data/published/andalucia-2026-accountability.json`. - **Andalucia 2026 planes de fuentes para topics vote-only** (`2026-05-17`): se elimina el bloqueo `execution_source_plan_missing` para topics con voto revisado pero sin BOJA. Donde estamos ahora: `ISSUE_EXECUTION_EVIDENCE_PLANS` cubre ahora `sanidad`, `vivienda`, `fiscalidad` y `seguridad_libertades` con rutas separadas para organo ejecutor, presupuesto/contratos/subvenciones/tesoreria e indicadores/outcomes, usando solo fuentes oficiales ya registradas o el registro oficial de contratacion. La cola resultante expone candidatos y preguntas de revision sin publicar ninguna conclusion de impacto. Hacia donde vamos: convertir la cola en evidence reviews solo cuando el row oficial sea suficientemente especifico. Que sigue: crear loaders/outcome series especificos para SAS, vivienda y seguridad cuando los indicadores presupuestarios no basten. Evidencia: `scripts/export_andalucia_2026_accountability_snapshot.py`, `tests/test_export_andalucia_2026_accountability_snapshot.py`, `ui/gh-pages-next/public/elecciones/andalucia-2026/data/execution-evidence-queue.csv`. - **Andalucia 2026 issue-review drafts automaticos** (`2026-05-17`): se automatiza el siguiente cuello de botella de direccion/actor sin aplicar reviews a ciegas. Donde estamos ahora: `scripts/generate_andalucia_2026_issue_review_drafts.py` lee los paquetes de `etl/data/published/andalucia-2026-accountability.json`, exige voto revisado + claims observados + gaps de direccion/actor y salta topics ya revisados. La primera corrida genero `3` borradores seguros (`sanidad`, `seguridad_libertades`, `vivienda`), ya promovidos; la corrida vigente queda `drafts=0` porque los `8` topics con direccion/actor ya tienen review o no tienen voto revisado. Cada draft permitido queda como `machine_draft_needs_human_review`, con direccion parcial y actor parlamentario parcial, pero mantiene `execution_owner_not_reviewed`, `budget_execution_not_linked`, `outcome_not_linked`, `causal_impact_not_claimed` y `merit_blame_not_scored`. `scripts/apply_andalucia_2026_issue_review_drafts.py` conserva dry-run de promocion con hard gate contra scoring, duplicados por topic y aplicacion sin seleccion. Hacia donde vamos: usarlo para nuevos topics cuando aparezca voto revisado, no para reabrir scoring. Que sigue: buscar voto revisado para `empleo` y `transparencia_corrupcion`; no publicar merito/culpa hasta tener ejecucion, outcome posterior y causalidad. Evidencia: `scripts/generate_andalucia_2026_issue_review_drafts.py`, `scripts/apply_andalucia_2026_issue_review_drafts.py`, `tests/test_generate_andalucia_2026_issue_review_drafts.py`, `tests/test_apply_andalucia_2026_issue_review_drafts.py`, `etl/data/published/andalucia-2026-issue-review-drafts.json`, `etl/data/published/andalucia-2026-issue-review-apply-dry-run.json`. - **Andalucia 2026 clasificador de expectativa BOJA** (`2026-05-17`): se elimina trabajo fantasma en BOJA sin relajar los casos que si deben tener publicacion legal. Donde estamos ahora: `scripts/export_andalucia_2026_accountability_snapshot.py` distingue votos revisados que pueden producir cambio BOJA (`law_final_approval_vote_passed`, `decree_law_validation_vote_passed`, `decree_law_vote_passed`) de votos rechazados/no vinculantes/comisiones que solo documentan posicion parlamentaria. El snapshot publica por issue `reviewed_vote_boja_expected_total`, `reviewed_vote_boja_not_expected_total`, `reviewed_vote_boja_expectation_status` y `reviewed_vote_legal_effect_counts`; `sanidad` pasa a `boja_not_expected_for_reviewed_vote_effects` con `1` voto revisado no-BOJA, `seguridad_libertades` con `5`, y `vivienda` con `2`. Ese paso bajo `reviewed_boja_missing` a `0` y evito perseguir BOJA en votos que no la generan; los bloqueos actuales quedan descritos en las entradas posteriores de issue-review/ejecucion. Hacia donde vamos: usar la cola para revisar direccion/actor/ejecucion real, no buscar BOJA donde el efecto parlamentario no la genera. Que sigue: mantener BOJA solo para aprobaciones legales/decretos y abrir ejecucion/outcomes para los topics con voto revisado. Evidencia: `scripts/export_andalucia_2026_accountability_snapshot.py`, `tests/test_export_andalucia_2026_accountability_snapshot.py`, `etl/data/published/andalucia-2026-accountability.json`, `ui/gh-pages-next/public/elecciones/andalucia-2026/data/accountability.json`. - **Andalucia 2026 promocion vote-review drafts** (`2026-05-17`): se cierran los 8 drafts legislativos verificables sin abrir merito/culpa. Donde estamos ahora: tras contrastar el texto oficial cacheado de los PDFs del Parlamento andaluz, se promueven a seed los votos `12-25/COM-000012` (`50 si / 56 no`), `12-25/M-000017` (`37/69`, `7/69/30`, `7/99`), `12-26/PNLP-000026` (`30/70/4`, `27/71/7`) y `12-26/PNLP-000013` (`13/93`, `14/92`). La seed `etl/data/seeds/andalucia_2026_parliament_vote_reviews.json` sube de `12` a `20` reviews; el snapshot publica `115` claims observados de responsabilidad (`100` partido, `15` candidato foco), `8` issues con voto revisado y `published_merit_blame_claims_total=0`. La cola de drafts queda en `0`, con `20` reviews existentes saltados y solo `empleo`/`transparencia_corrupcion` sin cola de voto elegible. Hacia donde vamos: resolver esos dos temas por scraping/clasificacion legislativa y bajar `sanidad`/`seguridad_libertades` a direccion/actor/ejecucion antes de scoring. Que sigue: buscar senal parlamentaria para `empleo` y `transparencia_corrupcion`, y revisar direccion ciudadana/actor para `sanidad`, `seguridad_libertades` y `vivienda`; BOJA queda reservado para aprobaciones legales/decretos. Evidencia: `etl/data/seeds/andalucia_2026_parliament_vote_reviews.json`, `etl/data/published/andalucia-2026-parliament-vote-review-apply-report.json`, `etl/data/published/andalucia-2026-accountability.json`, `ui/gh-pages-next/public/elecciones/andalucia-2026/data/accountability.json`. - **Andalucia 2026 vote-review drafts automaticos** (`2026-05-17`): se automatiza el cuello de botella `reviewed_vote_missing` sin convertir votos en merito/culpa. Donde estamos ahora: `scripts/generate_andalucia_2026_parliament_vote_review_drafts.py` lee la cola oficial de votos del Parlamento andaluz, salta los `12` reviews ya versionados y genera `etl/data/published/andalucia-2026-parliament-vote-review-drafts.json` con `8` borradores seguros (`sanidad=1`, `seguridad_libertades=5`, `vivienda=2`). Cada draft queda como `machine_draft_needs_human_review`, `reviewed_vote_result_only`, `official_vote_result_review_no_merit_or_blame`, con `impact_status=outcome_not_reviewed`, `responsibility_status=party_positions_observed` y limitaciones explicitas de no causalidad/no scoring. `scripts/apply_andalucia_2026_parliament_vote_review_drafts.py` anade promocion dry-run con hard gate: seleccion explicita o `human_approved`, bloqueo de duplicados por voto, bloqueo de score fields y plan en `etl/data/published/andalucia-2026-parliament-vote-review-apply-dry-run.json`; `just etl-andalucia-2026-accountability-assist` lo ejecuta junto al assist existente. Hacia donde vamos: promover solo drafts revisados a seed y usar la salida `blocked_vote_topics_without_drafts` para abrir busqueda legislativa en `empleo` y `transparencia_corrupcion`. Que sigue: revisar/aprobar los 8 drafts o ampliar scraping/clasificacion de votos para los 2 temas sin cola elegible. Evidencia: `scripts/generate_andalucia_2026_parliament_vote_review_drafts.py`, `scripts/apply_andalucia_2026_parliament_vote_review_drafts.py`, `tests/test_generate_andalucia_2026_parliament_vote_review_drafts.py`, `tests/test_apply_andalucia_2026_parliament_vote_review_drafts.py`, `etl/data/published/andalucia-2026-parliament-vote-review-drafts.json`, `etl/data/published/andalucia-2026-parliament-vote-review-apply-dry-run.json`. - **Andalucia 2026 auto-hunt entrega/beneficiario** (`2026-05-17`, refrescado `2026-08-12`): se convierte el bloqueo `delivery_or_beneficiary_missing` en busqueda oficial ejecutable sin abrir scoring. Donde estamos ahora: `scripts/export_andalucia_2026_accountability_snapshot.py` genera `delivery_evidence_hunts` desde filas revisadas de presupuesto, contrato, subvencion y Tesoreria; `scripts/run_andalucia_2026_delivery_evidence_hunts.py` ejecuta el lote reproducible contra BOJA, Datos Abiertos, BDNS/SNPSAP y el Registro de Contratacion de la Junta; y `scripts/generate_andalucia_2026_delivery_review_drafts.py` clasifica los resultados machine-readable en confirmaciones de reviews existentes o borradores nuevos. El reporte vigente `etl/data/published/andalucia-2026-delivery-evidence-hunt-results.json` cubre `40` targets (`40` ejecutados, `0` manuales), encuentra `23` candidatos oficiales en `11` hunts de `4` temas, sube a `14` candidatos machine-readable y registra `15` targets con error (`4` intentos de consulta fallidos), sin sustituirlos por fixtures. La capa de revision `etl/data/published/andalucia-2026-delivery-evidence-review-drafts.json` confirma automaticamente `9` reviews existentes, genera `1` draft nuevo y aparta `4` resultados insuficientes. La pagina `/elecciones/andalucia-2026/` muestra Auto-hunts, targets ejecutados, resultados oficiales, confirmaciones y drafts de revision; la identidad de beneficiarios se conserva como la publica la fuente. El snapshot mantiene `publishable_merit_blame_topics_total=0`, `topics_with_post_change_outcome_total=0` y `published_merit_blame_claims_total=0`. Hacia donde vamos: convertir candidatos buenos en evidencia revisada de recepcion/finalizacion, justificacion de subvencion, pagos desagregados o beneficiario final. Que sigue: resolver errores de endpoint con evidencia oficial nueva y buscar entrega final/outcome post-cambio; no publicar merito/culpa hasta causalidad y outcome posterior. - **Andalucia 2026 automatizacion source-discovery** (`2026-05-17`): se reduce el cuello de botella de buscar fuentes oficiales a mano sin abrir scoring. Donde estamos ahora: `scripts/discover_andalucia_2026_execution_sources.py` consulta el CKAN oficial del Portal de Datos Abiertos de la Junta, cruza paquetes/recurso con los gaps de ejecucion/outcomes ya definidos, detecta fuentes ya integradas y genera `etl/data/published/andalucia-2026-execution-source-discovery.json` con next actions (`wire_source_loader`, `fix_resource_access`, `already_integrated`). `just etl-andalucia-2026-accountability-assist` encadena discovery, export, draft generation y apply dry-run conservador. Hacia donde vamos: usar el reporte para elegir el siguiente loader oficial con mejor ratio evidencia/esfuerzo. Que sigue: priorizar datasets de movimientos/pagos de Tesoreria si el recurso descarga de forma reproducible; si falla acceso, registrar blocker y pasar al siguiente candidato machine-readable. - **Andalucia 2026 Tesoreria pagos agregados** (`2026-05-17`): se anade ejecucion de caja agregada sin convertir pagos agregados en entrega final ni outcome. Donde estamos ahora: el exporter incorpora el dataset oficial `Movimientos de la Tesoreria General de la Junta de Andalucia 2025`, resuelve el enlace alternativo oficial de descarga en `www.juntadeandalucia.es`, cachea el archivo `.7z`, lee solo el miembro pequeno `2025T4_PAGOS_4.CSV` y evita el fichero masivo de detalle/ofuscado. Se promueven 4 filas seguras `treasury_payment_aggregate`: `campo_agua` Consejeria de Agricultura/Pesca/Agua `5.022.333` EUR enero 2025; `cultura_patrimonio` Consejeria de Cultura y Deporte `2.872.430` EUR; `educacion` Consejeria de Desarrollo Educativo y FP `24.171.067` EUR; `energia_clima` Consejeria de Sostenibilidad/Medio Ambiente `5.043.448` EUR. La seed sube a `65` execution reviews, `reviewed_treasury_payment_rows_total=4`, `source_files_total=11`, `source_file_errors_total=0`, `treasury_payment_candidate_rows_total=132` y `published_merit_blame_claims_total=0`. Hacia donde vamos: enlazar pagos agregados con expedientes/beneficiarios/entrega y outcome posterior antes de cualquier merito/culpa. Que sigue: si se necesita mas precision, usar `PAGOS_3_OFUS` solo con redaccion/filtrado fuerte y sin publicar NIF/datos personales. - **Andalucia 2026 cierre cola cultura outcomes** (`2026-05-17`): se agota la cola automatizada de indicadores oficiales pendientes sin abrir scoring. Donde estamos ahora: se promueven 4 drafts seguros de `cultura_patrimonio/missing_outcomes` desde `objetivos-actuaciones-e-indicadores.xlsx` programa `45H` (`Expedientes de adquisiciones y cesiones tramitados`, `Ejemplares de publicaciones editadas en papel`, `Publicaciones digitales frente al total publicaciones`, `Actividades de difusion`). Todos quedan como `indicator_target_only_observed_outcome_series_pending`, con `causal_impact_not_claimed` y `merit_blame_not_scored`. La seed sube a `69` execution reviews, `reviewed_indicator_target_rows_total=28`, la cola de drafts queda en `0` y `published_merit_blame_claims_total=0`. Ademas, discovery normaliza URLs equivalentes entre `gdc-pdpopendata-ckan.paas.junta-andalucia.es` y `www.juntadeandalucia.es` para no volver a proponer dumps ya integrados por diferencia de host. Hacia donde vamos: buscar outcome observado posterior o ejecucion final real, no mas targets. Que sigue: priorizar series oficiales post-cambio o documentos de ejecucion/beneficiarios finales antes de cualquier atribucion de merito o culpa. - **Andalucia 2026 Fiscalidad BOJA y ayudas** (`2026-05-16`): se cierran los huecos `missing_reviewed_boja_legal_change` y `missing_execution_owner` para el paquete `fiscalidad` sin publicar merito/culpa. Donde estamos ahora: el exporter fuerza cuatro disposiciones oficiales por id BOJA (`disposition.2026.203802.1`, `disposition.2026.56.1`, `disposition.2026.74.1`, `disposition.2026.86.3`) para Decreto-ley 1/2026, su convalidacion, Decreto-ley 4/2026 y su convalidacion; la review issue-level añade 4 refs BOJA de organos/ejecucion parcial y 4 refs BOJA de techo presupuestario/ayudas (`500.000.000`, `110.000.000`, `75.000.000` y `50.000.000` EUR) para Agricultura/Ganaderia y Empleo. El paquete `fiscalidad` queda `program_vote_boja_reviewed` con `4` cambios BOJA revisados, `4` refs de ejecucion, `4` refs presupuestarias, `13` claims observados, `published_merit_blame_claims_total=0` y gaps restantes `missing_budget_execution`, `missing_outcomes`. Hacia donde vamos: bajar de techo/convocatoria a pagos ejecutados, beneficiarios finales y outcomes por danos de borrascas. Que sigue: localizar fuente oficial de pagos/beneficiarios o series post-2026 antes de cualquier atribucion de merito o culpa. Evidencia: `scripts/export_andalucia_2026_accountability_snapshot.py`, `etl/data/seeds/andalucia_2026_boja_impact_reviews.json`, `etl/data/seeds/andalucia_2026_issue_reviews.json`, `etl/data/published/andalucia-2026-accountability.json`. - **Andalucia 2026 Cultura BOJA promulgada** (`2026-05-16`): se cierra `missing_reviewed_boja_legal_change` para `cultura_patrimonio` sin publicar merito/culpa. Donde estamos ahora: el exporter fuerza `disposition.2026.65.1`, Ley 4/2026 de Patrimonio Cultural de Andalucia, y evita que resultados de `patrimonio natural` contaminen el bloque cultural salvo que tambien traigan señal cultural/historica. El paquete `cultura_patrimonio` queda `program_vote_boja_reviewed` con `1` cambio BOJA revisado, `32` claims observados, review issue-level `reviewed_issue_vote_boja_direction_actor_execution_owner_budget_and_contract_partial`, `published_merit_blame_claims_total=0` y gaps restantes `missing_budget_execution`, `missing_outcomes`. Hacia donde vamos: bajar de aprobacion/promulgacion, asignaciones y contratos candidatos a ejecucion final, beneficiarios y outcomes culturales observados. Que sigue: localizar fuente oficial de ejecucion/beneficiarios/resultados de la Ley 4/2026 antes de cualquier atribucion de merito o culpa. Evidencia: `scripts/export_andalucia_2026_accountability_snapshot.py`, `etl/data/seeds/andalucia_2026_boja_impact_reviews.json`, `etl/data/seeds/andalucia_2026_issue_reviews.json`, `etl/data/published/andalucia-2026-accountability.json`. - **Andalucia 2026 Energia/clima BOJA promulgada** (`2026-05-16`): se cierra `missing_reviewed_boja_legal_change`, `missing_citizen_direction` y `missing_responsible_actor` para `energia_clima` sin publicar merito/culpa. Donde estamos ahora: el exporter fuerza `disposition.2026.55.1`, Ley 2/2026 para la Gestion Ambiental de Andalucia, y la review issue-level enlaza 2 votos oficiales sobre `12-25/PL-000009`, 1 publicacion BOJA revisada y 10 claims observados de responsabilidad legislativa. El paquete `energia_clima` queda `program_vote_boja_reviewed`, review `reviewed_issue_vote_boja_direction_and_actor_partial`, `published_merit_blame_claims_total=0` y gaps restantes `missing_execution_owner`, `missing_budget_execution`, `missing_outcomes`. Hacia donde vamos: bajar de aprobacion/promulgacion a organo ejecutor, expedientes ambientales, presupuesto ejecutado y outcomes observados. Que sigue: localizar fuente oficial de autorizaciones/licencias/inspecciones o ejecucion presupuestaria ligada a la Ley 2/2026 antes de cualquier atribucion de merito o culpa. Evidencia: `scripts/export_andalucia_2026_accountability_snapshot.py`, `etl/data/seeds/andalucia_2026_boja_impact_reviews.json`, `etl/data/seeds/andalucia_2026_issue_reviews.json`, `etl/data/published/andalucia-2026-accountability.json`. - **Andalucia 2026 Educacion BOJA promulgada** (`2026-05-16`): se cierra `missing_reviewed_boja_legal_change`, `missing_citizen_direction` y `missing_responsible_actor` para `educacion` sin publicar merito/culpa. Donde estamos ahora: el exporter fuerza `disposition.2026.45.1`, Ley 1/2026 Universitaria para Andalucia, y la review issue-level enlaza 1 voto oficial sobre `12-25/PL-000007`, 1 publicacion BOJA revisada y 5 claims observados de responsabilidad legislativa. El paquete `educacion` queda `program_vote_boja_reviewed`, review `reviewed_issue_vote_boja_direction_and_actor_partial`, `published_merit_blame_claims_total=0` y gaps restantes `missing_execution_owner`, `missing_budget_execution`, `missing_outcomes`. Hacia donde vamos: bajar de aprobacion/promulgacion a organo ejecutor, desarrollo reglamentario, financiacion efectiva, aplicacion universitaria y outcomes observados. Que sigue: localizar fuente oficial de financiacion/ejecucion universitaria o actos de desarrollo de la Ley 1/2026 antes de cualquier atribucion de merito o culpa. Evidencia: `scripts/export_andalucia_2026_accountability_snapshot.py`, `etl/data/seeds/andalucia_2026_boja_impact_reviews.json`, `etl/data/seeds/andalucia_2026_issue_reviews.json`, `etl/data/published/andalucia-2026-accountability.json`. - **Andalucia 2026 Educacion y energia organos competentes** (`2026-05-17`): se cierran los huecos `missing_execution_owner` de `educacion` y `energia_clima` sin convertir organo competente en ejecucion final. Donde estamos ahora: la review issue-level de `educacion` enlaza 3 refs BOJA de la Ley 1/2026 para Consejeria de Universidad, Investigacion e Innovacion, inspeccion universitaria y potestad sancionadora; la de `energia_clima` enlaza 2 refs BOJA de la Ley 2/2026 para Consejeria competente en medio ambiente y ayuntamientos en licencia ambiental. Los dos paquetes quedan `program_vote_boja_reviewed`, con `execution_owner_partially_observed`, `published_merit_blame_claims_total=0` y gaps restantes `missing_budget_execution`, `missing_outcomes`. Hacia donde vamos: bajar de competencia legal parcial a expedientes, financiacion/ejecucion y outcomes. Que sigue: localizar actos de desarrollo, autorizaciones/licencias, inspecciones, ejecucion presupuestaria o datos de resultado antes de cualquier atribucion de merito o culpa. Evidencia: `etl/data/seeds/andalucia_2026_issue_reviews.json`, `ui/gh-pages-next/public/elecciones/andalucia-2026/data/accountability.json`, `etl/data/published/andalucia-2026-accountability.json`. - **Andalucia 2026 Educacion y energia presupuesto 2026** (`2026-05-17`): se anade evidencia oficial de asignacion presupuestaria e indicadores objetivo para `educacion` y `energia_clima` sin publicar ejecucion, outcome ni merito/culpa. Donde estamos ahora: `educacion` enlaza partidas 42J de `191.560.745` EUR para ajustes de financiacion de universidades publicas andaluzas y `2.000.000` EUR para compensacion de becas ministeriales, mas indicador objetivo de creditos aprobados en primera matricula; `energia_clima` enlaza partidas 44B de `1.295.724` EUR para cambio climatico/calidad del aire y `4.266.816` EUR para mejora de calidad del aire/reduccion del ruido, mas indicadores objetivo de planes de calidad del aire y autorizaciones de emisiones. Los dos paquetes quedan con `budget_allocation_linked_execution_pending`, `issue_budget_allocation_reviews_total=5`, `published_merit_blame_claims_total=0` y gaps restantes `missing_budget_execution`, `missing_outcomes`. Hacia donde vamos: automatizar draft reviews conservadoras desde fuentes oficiales para reducir trabajo manual. Que sigue: construir generador de borradores con hard gate de no merito/culpa salvo ejecucion, outcome observado y actor enlazado. Evidencia: `etl/data/seeds/andalucia_2026_execution_evidence_reviews.json`, `etl/data/seeds/andalucia_2026_issue_reviews.json`, `scripts/export_andalucia_2026_accountability_snapshot.py`, `ui/gh-pages-next/public/elecciones/andalucia-2026/data/execution-evidence-queue.csv`, `etl/data/published/andalucia-2026-accountability.json`. - **Andalucia 2026 draft reviews automaticas** (`2026-05-17`): se reduce el cuello de botella manual sin abrir claims publicos nuevos. Donde estamos ahora: `scripts/generate_andalucia_2026_execution_review_drafts.py` lee `etl/data/published/andalucia-2026-accountability.json`, reutiliza la cola oficial `issue_execution_evidence_queue`, salta locators ya revisados en `etl/data/seeds/andalucia_2026_execution_evidence_reviews.json` y genera `etl/data/published/andalucia-2026-execution-evidence-review-drafts.json` con `23` borradores conservadores (`campo_agua=5`, `cultura_patrimonio=6`, `educacion=6`, `energia_clima=6`). Todos quedan como `machine_draft_needs_human_review`, con `claim_status` de `no_merit_or_blame`, `review_confidence=low` y limitaciones explicitas; no se publican como seed final ni scoring. Hacia donde vamos: promover los borradores buenos a semilla tras revision humana y despues automatizar tambien issue-level summaries. Que sigue: anadir un aplicador con modo `--dry-run` que convierta borradores revisados a seed, manteniendo el hard gate de no merito/culpa hasta que existan ejecucion, outcome observado y actor enlazado. Evidencia: `scripts/generate_andalucia_2026_execution_review_drafts.py`, `tests/test_generate_andalucia_2026_execution_review_drafts.py`, `etl/data/published/andalucia-2026-execution-evidence-review-drafts.json`. - **Andalucia 2026 aplicador dry-run de drafts** (`2026-05-17`): se anade promocion automatizable pero no ciega de borradores de ejecucion. Donde estamos ahora: `scripts/apply_andalucia_2026_execution_review_drafts.py` lee drafts y seed de reviews, exige seleccion explicita por `--review-item-id` o drafts `human_approved` con `--all`, bloquea duplicados por locator, bloquea cualquier fila sin `no_merit_or_blame`, sin `merit_blame_not_scored` o con campos de scoring, y por defecto solo genera plan. El dry-run vigente en `etl/data/published/andalucia-2026-execution-evidence-review-apply-dry-run.json` queda limpio tras las promociones (`status=no_selection`, `selected_total=0`, `would_apply_total=0`, `blocked_unsafe_total=0`, `applied_total=0`). Hacia donde vamos: usar el plan para nuevas tandas si aparecen mas fuentes, no para scoring automatico. Que sigue: buscar outcomes oficiales post-cambio antes de publicar merito/culpa. Evidencia: `scripts/apply_andalucia_2026_execution_review_drafts.py`, `tests/test_apply_andalucia_2026_execution_review_drafts.py`, `etl/data/published/andalucia-2026-execution-evidence-review-apply-dry-run.json`. - **Andalucia 2026 promocion batch de ejecucion** (`2026-05-17`): se cierran las dos tandas iniciales de borradores conservadores y se regenera el snapshot publico sin abrir scoring. Donde estamos ahora: la primera tanda ya habia subido la seed de `22` a `45` reviews; el reporte vigente `etl/data/published/andalucia-2026-execution-evidence-review-apply-report.json` registra la segunda tanda con `existing_reviews_total=45`, `selected_total=8`, `would_apply_total=8`, `blocked_unsafe_total=0`, `skipped_duplicate_total=0`, `applied_total=8`, `apply_changed=true`; la seed queda con `53` reviews y el snapshot publica `reviewed_evidence_rows_total=53` (`21` budget-plan, `7` contract-award, `24` indicator-target, `1` observed baseline), con `published_merit_blame_claims_total=0`. La cola de drafts queda `drafts_total=0`, `skipped_existing_reviews_total=44`, `skipped_unsupported_candidates_total=4`. Hacia donde vamos: pasar de presupuesto/contrato/indicador objetivo a series oficiales de outcome y expedientes de ejecucion final. Que sigue: ampliar fuentes IECA/ministeriales/Junta para outcomes por cultura, educacion y energia, y solo despues evaluar merito/culpa. Evidencia: `etl/data/seeds/andalucia_2026_execution_evidence_reviews.json`, `etl/data/published/andalucia-2026-accountability.json`, `ui/gh-pages-next/public/elecciones/andalucia-2026/data/accountability.json`, `etl/data/published/andalucia-2026-execution-evidence-review-apply-report.json`, `etl/data/published/andalucia-2026-execution-evidence-review-drafts.json`. - **Andalucia 2026 IECA outcome baselines** (`2026-05-17`): se automatiza otra capa de fuentes oficiales de resultado sin abrir merito/culpa. Donde estamos ahora: el exporter descarga y cachea 4 consultas IECA ODS nuevas (`educacion_abandono_115550`, `cultura_patrimonio_gasto_85763`, `clima_gei_60509`, `aire_pm10_85172`), parsea dimensiones variables y medidas con `Estado` desplazado, y sube la cola a `9` source files, `6471` outcome candidates y `0` errores. El generador prioriza `observed_outcome_series` por encima de indicadores objetivo cuando el hueco es `missing_outcomes`, para que una fuente observada no quede enterrada por previsions presupuestarias. Se promueven solo 4 observed baselines explicitos (`educacion` abandono temprano 2025 `14,5`; `cultura_patrimonio` gasto per capita patrimonio cultural 2023 `23,23`; `energia_clima` GEI 2024 `-51,44`; PM10 2024 `23,80`), dejando los indicadores no seleccionados como drafts. La seed queda con `57` execution reviews y el snapshot publica `reviewed_observed_outcome_rows_total=5`, `published_merit_blame_claims_total=0`. Hacia donde vamos: pasar de baselines pre/post recientes a outcomes post-cambio enlazados con ejecucion y actor. Que sigue: localizar series posteriores, expedientes de aplicacion y beneficiarios antes de cualquier atribucion de merito o culpa. Evidencia: `scripts/export_andalucia_2026_accountability_snapshot.py`, `scripts/generate_andalucia_2026_execution_review_drafts.py`, `tests/test_export_andalucia_2026_accountability_snapshot.py`, `tests/test_generate_andalucia_2026_execution_review_drafts.py`, `etl/data/seeds/andalucia_2026_execution_evidence_reviews.json`, `etl/data/published/andalucia-2026-accountability.json`, `etl/data/published/andalucia-2026-execution-evidence-review-apply-report.json`. - **Andalucia 2026 subvenciones beneficiario/importe** (`2026-05-17`, politica de identidad corregida `2026-08-12`): se anade una fuente oficial de ejecucion/beneficiarios sin convertir concesion en impacto. Donde estamos ahora: el exporter consulta la API oficial `subventions/search` del Portal de Datos Abiertos para 6 programas prioritarios (`42J`, `44B`, `44E`, `44F`, `45E`, `51D`) con muestra acotada de `80` filas por programa, cachea `480` concesiones en `junta_subvenciones_programas_prioritarios.json`, conserva nombres de personas fisicas y el NIF enmascarado exactamente como los publica la fuente oficial, y clasifica `grant_award` como evidencia distinta de presupuesto, contrato e outcome. Se promueven 4 grants seguros: `campo_agua` digitalizacion control usos del agua urbana `100000` EUR; `cultura_patrimonio` fomento/promocion gestion cultural `3000000` EUR; `educacion` enseñanzas universitarias `196124` EUR; `energia_clima` proyectos cambio climatico `26113` EUR. La seed sube a `61` execution reviews, el snapshot publica `reviewed_grant_rows_total=4`, `source_files_total=10`, `source_file_errors_total=0` y `published_merit_blame_claims_total=0`. Hacia donde vamos: pasar de concesion/beneficiario a entrega final, expediente de aplicacion y outcome observado. Que sigue: enlazar pagos/justificacion/estado de ejecucion o serie posterior antes de cualquier atribucion de merito o culpa. Evidencia: `scripts/export_andalucia_2026_accountability_snapshot.py`, `scripts/generate_andalucia_2026_execution_review_drafts.py`, `scripts/apply_andalucia_2026_execution_review_drafts.py`, `etl/data/raw/elections/andalucia_2026/execution_evidence/junta_subvenciones_programas_prioritarios.json`, `etl/data/seeds/andalucia_2026_execution_evidence_reviews.json`, `etl/data/published/andalucia-2026-accountability.json`. - **Contributor source onboarding scaffold** (`2026-05-11`, endurecido `2026-08-13`): se cierra la siguiente slice controlable para que añadir datasets sea aburrido y verificable sin inventar datos. Donde estamos ahora: `just add-source` exige payload oficial HTTPS no vacío, SHA-256, fecha de captura y base de reutilización; copia los bytes exactos, crea sidecar de procedencia, parser, test strict y docs hook. `just etl-audit-official-samples` fija path/tamaño/hash/URL de las `25` capturas configuradas, rechaza marcadores inventados y valida `9` sidecars contra capturas archivadas; los cinco fixtures heredados con personas inventadas fueron sustituidos por registros públicos exactos de Congreso, Senado, Europarlamento, RED SARA y Asamblea de Madrid, conservando todas sus identidades públicas. Las fuentes scaffolded entran por `publicdata_connectors_es/contrib` como `source_records_only`. Hacia donde vamos: exigir la misma cadena de custodia a toda fuente nueva y usar el catálogo público como front door con estados `available/blocked/stale/missing`. Que sigue: ejecutar `just etl-contributor-gates` en cada PR y convertir `source_records_only` a normalización/backfill cuando el contrato oficial sea estable. - **Contrato estructural de respuestas publicas accountability** (`2026-05-12`): se endurece el gate para que la Evidence API no pueda pasar solo por conteos. Donde estamos ahora: `scripts/validate_accountability_artifacts.py` valida arrays reales contra `coverage`, exige que actor/issue answers tengan rutas de dossier, particion completa de dimensiones (`promises` -> `outcomes`), estado coherente `answerable/partial/unanswerable`, confianza/frescura, caveats, y muestras de evidencia con `entry_id`, rol, tipo, tier, fuente y resumen/cita; tambien valida actor-issue refs, clusters, gap answers, Q&A routes y paridad de status counts. El exporter rellena `summary` desde `title`/issue/source cuando la fila BOE no trae cita textual, y los JSON publicados/static quedan regenerados con `1164` samples validadas. Hacia donde vamos: tratar este validator como contrato minimo para todo widget o Q&A publico. Que sigue: subir floors por familia de fuente y no publicar desde DB local si difiere de `etl/data/published` validado. - **Bloqueos como respuestas publicas Evidence API** (`2026-05-12`): se deja de esconder la obstruccion solo en docs/tracker. Donde estamos ahora: `scripts/export_accountability_evidence_api_snapshot.py` lee `source-catalog-2026-02-12.json` y emite `3` `blocker_answers` con estado `blocked`, tipo de bloqueo (`http_403`, `contract_or_token`, `anti_bot_or_waf`), rutas al catalogo, evidencia refs bajo `docs/etl/sprints/...` y siguiente comando reproducible; la API sube a `6` templates y `30` Q&A, y `scripts/validate_accountability_artifacts.py` valida conteos, evidencia refs, comandos y paridad de `blocker_kind_counts`. La pagina `/accountability-evidence/` muestra la seccion `Fuentes bloqueadas`. Hacia donde vamos: que "desconocido" y "bloqueado" sean estados auditables de producto, no notas laterales. Que sigue: enlazar cada blocker a fuente/dimension afectada cuando exista mapping fuerte y elevar floors por dominio. - **Deploy publico sin drift de DB local** (`2026-05-12`): se separa el deploy manual de la corrida ETL completa. Donde estamos ahora: `just cloudflare-pages-deploy` ejecuta `just cloudflare-pages-build` antes de Wrangler, por lo que accountability se primea desde `etl/data/published/*-latest.json` validado y no desde un re-export accidental del SQLite local con conteos divergentes. Hacia donde vamos: que deploy publique solo artifacts que ya pasaron validator, build y privacy gate; la regeneracion desde DB queda para flujos ETL intencionales. Que sigue: si se necesita refresh live, correrlo como lane explicito con sus gates y evidencias antes de desplegar. - **Calendario electoral publico** (`2026-05-16`): se abre la opcion top-level para proximas elecciones con un artifact estatico propio. Donde estamos ahora: `scripts/generar_proximas_elecciones_espana.py` ya descarga/calcula el calendario, verifica el PDF oficial `jec_andalucia_2026_calendario`, publica `etl/data/published/proximas-elecciones-espana.json` y alimenta `/calendario-electoral/` con `10` eventos (`1` oficial scrapeado, `9` ciclos legales/condicionales), incluida la cita `Parlamento de Andalucia` del `2026-05-17`; live queda publicado en `gh-pages` commit `75e3da73` y `https://votaconlachola.org/calendario-electoral/` hidrata `10|1|2026-05-16`. Hacia donde vamos: sustituir filas condicionales por convocatorias oficiales a medida que JEC/organismos autonomicos publiquen calendarios reutilizables. Que sigue: ampliar el manifest de fuentes autonomicas y registrar bloqueo si una administracion solo publica HTML/PDF no reutilizable o queda inaccesible. Evidencia: `docs/proximas-elecciones-espana.md`, `etl/data/published/proximas-elecciones-espana.json`, `ui/gh-pages-next/public/calendario-electoral/data/election-calendar.json`, `ui/gh-pages-next/app/calendario-electoral/page.js`. - **Andalucia 2026 accountability page** (`2026-05-16`): se abre una superficie dedicada para no mezclar identidad electoral con conclusiones de merito/culpa no sustentadas. Donde estamos ahora: `scripts/export_andalucia_2026_accountability_snapshot.py` descarga/parses el PDF oficial de candidaturas proclamadas, publica `etl/data/published/andalucia-2026-accountability.json` y alimenta `/elecciones/andalucia-2026/` con `125` listas, `27` claves de partido, `1715` titulares, `500` suplentes, `337` candidatos enlazados a actor DB y `0` valoraciones publicadas de culpa/merito; ademas enlaza conservadoramente el ledger publicado de Evidence API (`126767` entradas, `929` respuestas de actor) a `2` candidatos, `1` candidato foco y `2` rollups nacionales de partido (`PP`, `PSOE`) sin fingir que son historiales regionales completos. `etl/data/seeds/andalucia_2026_program_sources.json` aporta una capa repetible `programas_2026` con `6/6` PDFs descargados, texto extraido y verificado por terminos (`PP`, `PSOE-A`, `VOX`, `PorA`, `ADELANTE ANDALUCIA`, `MUNDO+JUSTO`), marcando las copias alojadas por prensa como `press_hosted_copy` y no como fuente primaria perfecta. La misma exportacion extrae `120` medidas programaticas declaradas, agrupadas en `10` bloques y `5` partidos con programa accionable; cada medida queda como `tier_3_declared`, snippet corto, fuente, pagina/linea aproximada, `claim_status=declared_program_measure_not_assessed` y `interpretation_status=needs_review_before_impact_claim`. Nueva capa BOJA: el exporter consulta la API oficial `datos.juntadeandalucia.es/api/v0/boja/get/search_pagination`, cachea `10` consultas por bloque y detalles por disposicion en `etl/data/raw/elections/andalucia_2026/boja_normas/`, y publica `53` muestras tema-registro de `Disposiciones generales` (`2022-06-19` a `2026-05-16`) con `105` fragmentos oficiales cortos, `tier_1_primary`, PDF BOJA, `action_kind` conservador, y `interpretation_status=needs_direction_and_impact_review`; ademas deriva una cola de impacto BOJA de `105` items con `5` revisiones `reviewed_legal_change_only` (`candidate_direction=unknown`, outcome pendiente, sin merito/culpa) y preguntas de cambio legal, direccion ciudadana, actor responsable y evidencia de impacto faltante. La cola queda exportada como CSV revisable en `etl/data/published/andalucia-2026-boja-impact-review-queue.csv` y `ui/gh-pages-next/public/elecciones/andalucia-2026/data/boja-impact-review-queue.csv` (`106` lineas con cabecera). El total bruto de hits API queda visible (`64831`) pero acotado a muestras por bloque para no inflar el artifact, con primer filtro para no mezclar `sanidad vegetal` agricola con sanidad publica. Hacia donde vamos: convertir programas 2026, BOJA, actividad del Parlamento andaluz, dinero/ejecucion y outcomes en evidencia por issue antes de puntuar responsabilidades. Que sigue: resolver direccion/impacto ciudadano, actor, presupuesto/ejecucion y outcome de la cola BOJA antes de atribuir valores o responsabilidad. Evidencia: `etl/data/raw/elections/andalucia_2026/boja_normas/`, `etl/data/seeds/andalucia_2026_program_sources.json`, `etl/data/seeds/andalucia_2026_boja_impact_reviews.json`, `ui/gh-pages-next/app/elecciones/andalucia-2026/page.js`, `ui/gh-pages-next/public/elecciones/andalucia-2026/data/accountability.json`, `ui/gh-pages-next/public/elecciones/andalucia-2026/data/boja-impact-review-queue.csv`, `tests/test_export_andalucia_2026_accountability_snapshot.py`. - **Andalucia 2026 BOJA review packet** (`2026-05-16`): se convierte la cola plana de impacto BOJA en trabajo revisable. Donde estamos ahora: los `105` fragmentos BOJA siguen sin atribuir merito/culpa (`published_merit_blame_claims_total=0`), pero el snapshot y el CSV anaden `priority_rank`, `priority_score`, `review_batch_id` y `priority_reason`; quedan `9` lotes de revision de `12` items maximo y `24` items destacados para triage humano por accion normativa, bloque ciudadano y fecha. La semilla `etl/data/seeds/andalucia_2026_boja_impact_reviews.json` aplica `5` revisiones `reviewed_legal_change_only`: documentan cambio legal y publicador oficial, mantienen `candidate_direction=unknown` y `impact_status=legal_change_documented_outcome_pending`, y usan `claim_status=reviewed_boja_legal_change_no_merit_claim`. Hacia donde vamos: revisar primero direccion ciudadana, actor responsable, presupuesto/ejecucion y outcome de los lotes altos antes de publicar valoraciones. Que sigue: ampliar revisiones BOJA como datos versionados, no texto libre, y mantener la prioridad como orden de trabajo, no como ranking politico. Evidencia: `scripts/export_andalucia_2026_accountability_snapshot.py`, `etl/data/seeds/andalucia_2026_boja_impact_reviews.json`, `ui/gh-pages-next/app/elecciones/andalucia-2026/page.js`, `ui/gh-pages-next/public/elecciones/andalucia-2026/data/accountability.json`, `ui/gh-pages-next/public/elecciones/andalucia-2026/data/boja-impact-review-queue.csv`. - **Andalucia 2026 Parlamento activity index** (`2026-05-16`): se cubre el primer hueco de responsabilidad legislativa sin convertir conteos brutos en responsabilidad final. Donde estamos ahora: `scripts/export_andalucia_2026_accountability_snapshot.py` descarga/cachea HTML oficial de `parlamentodeandalucia.es` para iniciativas legislativas de la XII legislatura y la pagina de `Sentido del voto`; el snapshot publica `109` iniciativas legislativas oficiales, `4` documentos recientes de resultado/sentido del voto, `81` eventos de votacion parseados desde PDF oficial, `80` con expediente detectado, `25` enlazados a expediente oficial del indice de iniciativas, `80` con triaje conservador de efecto legal (`11` clases), `25` filas de resumen partido-tema, conteos brutos por grupo parlamentario (`PP`, `PSOE-A`, `VOX`, `PorA`, `Adelante`), `8827` votos nominales de diputado, `5588` votos nominales enlazados a candidatos proclamados, `69` candidatos con resumen nominal y `3` candidatos foco con historial visible; ademas mantiene `32` iniciativas con grupo parlamentario explicito segun texto fuente. La nueva cola voto-impacto convierte los `81` eventos de votacion en `81` items revisables, `7` lotes y `24` prioridades, exportados en `etl/data/published/andalucia-2026-parliament-vote-impact-review-queue.csv` y `ui/gh-pages-next/public/elecciones/andalucia-2026/data/parliament-vote-impact-review-queue.csv`. Cada expediente queda como `claim_status=official_parliamentary_initiative_not_assessed`; cada votacion queda como `claim_status=official_vote_count_not_interpreted`, `review_status=needs_legal_effect_actor_and_impact_review` y `position_status=raw_group_tally_not_interpreted`; cada triaje legal queda como regla por tipo/titulo/mayoria, no como impacto; cada item de cola queda como `parliament_vote_review_queue_only_no_public_claim`; cada resumen nominal queda como `official_member_vote_history_not_interpreted`; `published_merit_blame_claims_total` sigue en `0`. Hacia donde vamos: completar enlace expediente->voto->actor, confirmar efecto legal y cruzar BOJA/dinero/outcomes antes de publicar comparaciones de responsabilidad. Que sigue: aplicar revisiones como datos versionados para efecto legal, direccion ciudadana, actor, presupuesto/ejecucion y outcomes antes de puntuar merito o culpa. Evidencia: `etl/data/raw/elections/andalucia_2026/parlamento_andalucia/`, `ui/gh-pages-next/public/elecciones/andalucia-2026/data/accountability.json`, `ui/gh-pages-next/public/elecciones/andalucia-2026/data/parliament-vote-impact-review-queue.csv`, `ui/gh-pages-next/app/elecciones/andalucia-2026/page.js`, `tests/test_export_andalucia_2026_accountability_snapshot.py`. - **Andalucia 2026 first vote-result reviews** (`2026-05-16`): se aplica la primera semilla versionada de revision legislativa sin publicar scoring. Donde estamos ahora: `etl/data/seeds/andalucia_2026_parliament_vote_reviews.json` revisa 12 resultados oficiales desde PDFs del Parlamento andaluz (`PL-000011`, `PL-000010`, `PL-000009`, `PL-000007`, `DL-000001`), marca el packet voto-impacto como `partially_reviewed`, conserva `published_merit_blame_claims_total=0`, y publica resumen por 5 partidos y 69 candidatos enlazados. La UI anade un comparador dedicado por partido/candidato, separa apoyo/oposicion/abstencion ante efectos aprobados y efectos rechazados, y muestra `con resultado` / `contra resultado` sin convertirlo en merito, culpa ni impacto ciudadano. Hacia donde vamos: revisar direccion ciudadana, actor concreto, presupuesto/ejecucion y outcomes antes de convertir senal legislativa en responsabilidad. Que sigue: extender la semilla a los siguientes lotes prioritarios y mantener cada revision como dato estructurado. Evidencia: `etl/data/seeds/andalucia_2026_parliament_vote_reviews.json`, `ui/gh-pages-next/app/elecciones/andalucia-2026/page.js`, `ui/gh-pages-next/public/elecciones/andalucia-2026/data/accountability.json`, `tests/test_export_andalucia_2026_accountability_snapshot.py`. - **Andalucia 2026 observed responsibility claims** (`2026-05-16`): se abre el primer claim publico de responsabilidad sin cruzar la linea de merito/culpa. Donde estamos ahora: `published_accountability_claims` publica `75` claims observados con `claim_status=published_observed_responsibility_no_merit_or_blame`: `60` claims partido-voto y `15` claims candidato-voto para `8` actores y `5` temas (`cultura_patrimonio`, `campo_agua`, `fiscalidad`, `energia_clima`, `educacion`). Cada claim enlaza PDF oficial, resultado revisado, posicion (`si`/`no`/abstencion), relacion con resultado (`34` con resultado, `37` contra resultado, `4` abstencion) y limitacion explicita: no merito, no culpa, no impacto ciudadano, no dinero/ejecucion ni outcome final. La UI anade "Responsabilidad legislativa observada" y mantiene `published_merit_blame_claims_total=0`. Hacia donde vamos: convertir estos claims observados en responsabilidad sustantiva solo cuando se cierre direccion ciudadana + actor + BOJA/dinero/outcomes. Que sigue: extender reviews issue-level a los siguientes paquetes con tres capas sin publicar scoring. Evidencia: `scripts/export_andalucia_2026_accountability_snapshot.py`, `ui/gh-pages-next/app/elecciones/andalucia-2026/page.js`, `ui/gh-pages-next/public/elecciones/andalucia-2026/data/accountability.json`, `tests/test_export_andalucia_2026_accountability_snapshot.py`. - **Andalucia 2026 Campo y agua issue review** (`2026-05-16`): se aplica la primera revision issue-level sobre un paquete con programa, voto y BOJA. Donde estamos ahora: `etl/data/seeds/andalucia_2026_issue_reviews.json` aporta una review para `campo_agua` dentro de `3` reviews issue-level totales; el snapshot conserva `published_merit_blame_claims_total=0`. La review documenta cambio legal en montes/agua, senal parcial de actor parlamentario/publicador oficial, un ejecutor administrativo observado en BOJA y una partida candidata del Presupuesto 2026 (`partidas-de-gastos.xlsx:fila 10741`, programa `44E`, `APOYO REDACCIÓN LEY DE MONTES`, `50000` EUR). El paquete ya no marca `missing_execution_owner`; mantiene `missing_budget_execution` y `missing_outcomes` porque la partida revisada es asignacion presupuestaria, no importe ejecutado, beneficiarios ni resultado ciudadano enlazado. La UI anade "Ejecutor enlazado" y "Partida" y muestra evidencia BOJA de ejecucion separada de presupuesto/outcome. Hacia donde vamos: convertir issue packets en responsabilidad sustantiva solo cuando haya presupuesto/ejecucion y outcomes trazables. Que sigue: enlazar ejecucion, beneficiarios o resultados para `campo_agua`. Evidencia: `etl/data/seeds/andalucia_2026_issue_reviews.json`, `scripts/export_andalucia_2026_accountability_snapshot.py`, `ui/gh-pages-next/app/elecciones/andalucia-2026/page.js`, `ui/gh-pages-next/public/elecciones/andalucia-2026/data/accountability.json`, `tests/test_export_andalucia_2026_accountability_snapshot.py`. - **Andalucia 2026 Cultura y patrimonio issue review** (`2026-05-16`): se abre una segunda revision issue-level sin esperar a BOJA/dinero. Donde estamos ahora: `etl/data/seeds/andalucia_2026_issue_reviews.json` revisa `cultura_patrimonio` desde 4 resultados oficiales del Pleno sobre `12-25/PL-000011` (`58/50` en aprobacion final y tres votaciones previas rechazadas), enlaza `32` claims observados de responsabilidad legislativa y `8` perfiles actor-tema, y marca la review como `reviewed_issue_vote_direction_and_actor_partial`. El paquete ya no marca `missing_citizen_direction` ni `missing_responsible_actor`; mantiene `missing_reviewed_boja_legal_change`, `missing_execution_owner`, `missing_budget_execution` y `missing_outcomes`. Hacia donde vamos: cerrar BOJA/promulgacion, unidad ejecutora, dinero y outcomes culturales antes de cualquier merito/culpa. Que sigue: localizar el BOJA o expediente final de la Ley de Patrimonio Cultural y despues sus partidas/ejecucion. Evidencia: `etl/data/seeds/andalucia_2026_issue_reviews.json`, `ui/gh-pages-next/public/elecciones/andalucia-2026/data/accountability.json`, `ui/gh-pages-next/app/elecciones/andalucia-2026/page.js`, `tests/test_export_andalucia_2026_accountability_snapshot.py`. - **Andalucia 2026 Fiscalidad issue review** (`2026-05-16`): se abre una tercera revision issue-level desde los votos del Decreto-ley 1/2026. Donde estamos ahora: `etl/data/seeds/andalucia_2026_issue_reviews.json` revisa `fiscalidad` desde 2 resultados oficiales del Pleno sobre `12-26/DL-000001` (`72/0` en convalidacion y `36/72` en votacion posterior), enlaza `13` claims observados de responsabilidad legislativa y marca la review como `reviewed_issue_vote_direction_and_actor_partial`; el clasificador tambien asigna el expediente oficial `12-26/DL-000001` a `fiscalidad`, de modo que expediente, votos y claims observados quedan en el mismo bloque ciudadano. El paquete ya no marca `missing_citizen_direction` ni `missing_responsible_actor`; mantiene `missing_reviewed_boja_legal_change`, `missing_execution_owner`, `missing_budget_execution` y `missing_outcomes`. Hacia donde vamos: cerrar BOJA/expediente, unidad ejecutora, presupuesto/ejecucion, beneficiarios y outcomes por danos de borrascas antes de cualquier merito/culpa. Que sigue: localizar publicacion/expediente del Decreto-ley 1/2026 y sus datos de ejecucion. Evidencia: `scripts/export_andalucia_2026_accountability_snapshot.py`, `etl/data/seeds/andalucia_2026_issue_reviews.json`, `ui/gh-pages-next/public/elecciones/andalucia-2026/data/accountability.json`, `ui/gh-pages-next/app/elecciones/andalucia-2026/page.js`, `tests/test_export_andalucia_2026_accountability_snapshot.py`. - **Andalucia 2026 execution evidence candidates** (`2026-05-16`): se baja el siguiente hueco de `campo_agua` desde "buscar fuentes" a "revisar filas oficiales". Donde estamos ahora: el exporter lee los XLSX oficiales de Presupuesto 2026, los JSON oficiales de contratos menores 2024/2025 y el JSON REST oficial IECA/BADEA ODS 6.3.1 cacheados en `etl/data/raw/elections/andalucia_2026/execution_evidence/`, adjunta `5401` filas oficiales candidatas a la cola de ejecucion (`4112` presupuesto/partidas/contratos y `1289` indicadores/outcomes), e incorpora `2693` contratos candidatos (`1454` de 2024 y `1239` de 2025) y `12` filas de serie observada IECA sin convertirlas en prueba cerrada. Hay `9` filas revisadas: `3` partidas presupuestarias como plan/asignacion (`138832165`, `193585086` y `50000` EUR), `2` contratos menores oficiales de depuracion/tratamiento de aguas (`36449` y `37614` EUR), `3` indicadores como objetivo/prevision (`4,97%`, `162138` beneficiarios previstos y `49,4 km` de redes) y `1` baseline observado IECA ODS 6.3.1 (`Andalucia 2012: 72,15%`, `2022: 77,81%`). Todas mantienen limitacion explicita: no entrega final, no resultado post-cambio 2026, no causalidad y no merito/culpa. El CSV publico `ui/gh-pages-next/public/elecciones/andalucia-2026/data/execution-evidence-queue.csv` queda con conteo por fuente, top filas candidatas y top filas revisadas por item. Hacia donde vamos: revisar esas filas contra ejecucion real, expedientes/beneficiarios y resultados posteriores antes de publicar merito, culpa o impacto. Que sigue: enlazar documento de ejecucion/beneficiarios/outcome post-cambio o descartar cada fila con motivo. Evidencia: `scripts/export_andalucia_2026_accountability_snapshot.py`, `etl/data/seeds/andalucia_2026_execution_evidence_reviews.json`, `etl/data/raw/elections/andalucia_2026/execution_evidence/`, `ui/gh-pages-next/public/elecciones/andalucia-2026/data/accountability.json`, `ui/gh-pages-next/public/elecciones/andalucia-2026/data/execution-evidence-queue.csv`, `tests/test_export_andalucia_2026_accountability_snapshot.py`. - **Andalucia 2026 responsibility comparison matrix** (`2026-05-16`): se anade una capa de comparacion antes del scoring. Donde estamos ahora: `responsibility_comparison` publica `27` perfiles de partido y `5` candidatos foco; `5` partidos y `3` candidatos foco tienen senales legislativas revisadas, cada fila cruza candidatura, programa, voto parlamentario, iniciativas de grupo, historial Evidence API y hueco principal. La UI muestra la matriz como "Responsabilidad verificable" y cada fila queda con `claim_status=responsibility_evidence_comparison_only_no_merit_or_blame_claim`. Hacia donde vamos: usar estos huecos para decidir el siguiente review por issue antes de emitir merito, culpa o impacto. Que sigue: convertir los perfiles con programa+voto en revisiones por issue que unan BOJA, dinero/ejecucion y outcomes. Evidencia: `scripts/export_andalucia_2026_accountability_snapshot.py`, `ui/gh-pages-next/app/elecciones/andalucia-2026/page.js`, `ui/gh-pages-next/public/elecciones/andalucia-2026/data/accountability.json`, `tests/test_export_andalucia_2026_accountability_snapshot.py`. - **Andalucia 2026 issue packets** (`2026-05-16`): se cruza el trabajo por bloque ciudadano sin abrir scoring. Donde estamos ahora: `issue_accountability_packets` publica `10` paquetes por tema, `5` con votos revisados, `3` con cambios BOJA revisados, `5` con claims observados de responsabilidad legislativa y `1` con programa+voto+BOJA (`campo_agua`); los paquetes integran los `75` claims observados en `31` perfiles actor-tema, y `campo_agua` queda como primer paquete de tres capas con `15` claims y `5` actores observados. Las 12 revisiones parlamentarias ya salen clasificadas como `cultura_patrimonio`, `campo_agua`, `energia_clima`, `fiscalidad` y `educacion`, en vez de quedar en `sin_tema`; `campo_agua`, `cultura_patrimonio` y `fiscalidad` tienen review issue-level. La UI anade una seccion "Promesa · voto · BOJA" que muestra conteos por bloque, perfiles partido-tema, actores observados, muestras de voto/responsabilidad/BOJA y huecos abiertos (`direccion ciudadana`, `actor ejecucion/BOJA` cuando aplica, `presupuesto/ejecucion`, `outcomes`). Cada paquete usa `claim_status=issue_evidence_packet_only_no_merit_or_blame_claim`. Hacia donde vamos: usar estos paquetes como cola priorizada de revision por issue antes de publicar responsabilidad sustantiva. Que sigue: cerrar BOJA para `cultura_patrimonio`, `energia_clima`, `fiscalidad` y `educacion`, y despues dinero/outcomes. Evidencia: `scripts/export_andalucia_2026_accountability_snapshot.py`, `ui/gh-pages-next/app/elecciones/andalucia-2026/page.js`, `ui/gh-pages-next/public/elecciones/andalucia-2026/data/accountability.json`, `tests/test_export_andalucia_2026_accountability_snapshot.py`. - **Live ETL comunitario** (`2026-05-11`): se cierra la brecha de operativa pública mínima. Donde estamos ahora: existe workflow programado `Live ETL Publish` para ejecutar ingest, tracker gate, privacy/publish dry-run, build estático y publicación HF/Cloudflare cuando haya secretos; el tracker ya no trata `PARTIAL` como falso mismatch y baja de `DONE` las fuentes sin evidencia live-clean vigente. Hacia donde vamos: el catálogo público será la puerta de entrada para reclamar fuentes y el workflow será el pulso recurrente de verdad. Que sigue: cada nueva fuente entra por `Data Source` + `just add-source` + `just etl-contributor-gates`, y cada `DONE` exige red real o bloqueo reproducible. - **Columna vertebral generica de accountability** (`2026-05-10`): se abre la primera slice de implementacion directa del roadmap de extraccion D0-D4. Donde estamos ahora: el repo tenia ledgers especificos por caso (`responsibility_explainer_*`) y responsabilidades normativas puntuales (`legal_fragment_responsibilities`), pero faltaba una tabla comun para preguntar por issue/actor/rol/fuente sin reconstruir cada caso a mano. Se anade schema aditivo `accountability_issues` + `accountability_ledger_entries`; el aplicador de reviews de responsibility explainer materializa los `responsibility_link` aprobados tambien en esa capa generica; y `scripts/export_accountability_ledger_snapshot.py` publica un JSON issue-led con conteos por rol. Hacia donde vamos: usar esta capa como spine comun para actor dossiers, issue ledgers, BOE/appointments, votos, dinero publico, enforcement y Q&A. Que sigue: backfillear entradas desde votos/iniciativas/BOE appointments y enlazar `person_id/party_id/institution_id` cuando la resolucion de identidad este disponible. - **D1 parlamento -> accountability ledger** (`2026-05-10`): primer backfill automatico sobre la spine generica. Donde estamos ahora: `scripts/backfill_accountability_ledger_from_parliament.py` convierte `parl_vote_member_votes` + `parl_vote_events` + `parl_vote_event_initiatives` en entradas `parliamentary_action`, creando issues por iniciativa (`parl-initiative:*`) o por voto fallback (`parl-vote:*`) y resolviendo `mandate_id/party_id/institution_id` cuando hay mandato datado. Hacia donde vamos: que actor/persona/partido tenga historial navegable por issue desde votos nominales reales antes de sumar BOE, nombramientos y dinero publico. Que sigue: ejecutar el backfill sobre el DB parlamentario publicado y luego publicar `scripts/export_accountability_ledger_snapshot.py --snapshot-date `. - **D1 party rollups source-backed** (`2026-05-10`): se anade agregacion de partido sin usar grupo parlamentario como proxy. Donde estamos ahora: `scripts/backfill_accountability_ledger_from_parliament.py` emite entradas `actor_kind='party'` por `issue/vote/party_id/role` solo cuando el mandato datado del votante trae `party_id`; el payload marca `basis=dated mandate.party_id` y mantiene separado el rollup de grupo parlamentario. Corrida rica: `party_entries=569`, `entries_with_party_id=1255`, `entries_by_actor_kind.party=569`, partidos con rollup `5` (`EH Bildu`, `ERC/ESQUERRA`, `JxCat`, `PSOE`, `PP`). Hacia donde vamos: convertir afiliacion formal de partido en cobertura nacional, no inferida por grupo. Que sigue: enriquecer mandatos con partido oficial para mas camaras/periodos y anadir gate de `ACCOUNTABILITY_MIN_PARTY_ID_ENTRIES`. - **Jerarquia de evidencia en accountability ledger** (`2026-05-10`): los registros oficiales primarios quedan clasificados como `evidence_tier=1` porque son evidencia con efecto legal/administrativo, no leads. Donde estamos ahora: `scripts/accountability_evidence_tiers.py` centraliza la inferencia; votos/iniciativas parlamentarias, BOE, PLACSP, BDNS/SNPSAP y nombramientos oficiales publican tier `1`; Moncloa/comunicacion oficial queda tier `3`; fuentes oficiales estructuradas sin efecto directo quedan tier `2`. Las regresiones focales cubren parliament, `policy_events`, `legal_fragment_responsibilities`, BOE appointments y reviews aplicadas; tras regenerar el corte default, `accountability_ledger_entries` queda `362/362` en tier `1` y el JSON publico refleja `evidence_tiers={"1": ...}`. Hacia donde vamos: mantener la jerarquia de evidencia del roadmap dentro de cada backfill, no solo en docs. Que sigue: anadir gate explicito de tier por familia de fuente cuando el snapshot incluya BOE/PLACSP/BDNS reales no vacios. - **D2 normativa/responsabilidad -> accountability ledger** (`2026-05-10`): se conecta la responsabilidad legal existente a la spine generica. Donde estamos ahora: `scripts/backfill_accountability_ledger_from_legal_responsibilities.py` convierte `legal_fragment_responsibilities` en entradas `rule`/`implementation`/`enforcement`/`audit` con roles normalizados (`proposed`, `approved`, `delegated_to`, `enforced`, `audited`) e issues por norma (`legal-norm:*`); cuando una responsabilidad legal trae `actor_label` institucional sin ID, crea un stub conservador en `institutions` usando match exacto y lo enlaza a la ledger. La corrida rica publica 8 issues `legal-norm:*` y 15 filas D2 junto a los votos parlamentarios. Hacia donde vamos: que normas, fragmentos, organismos competentes y responsables puedan compararse contra votos, nombramientos y dinero publico dentro de la misma ledger. Que sigue: extender el mismo patron a BOE appointments y `policy_events`. - **Policy events -> accountability ledger** (`2026-05-10`): se conecta la capa `policy_events` a la spine generica con mapeo conservador por instrumento. Donde estamos ahora: `scripts/backfill_accountability_ledger_from_policy_events.py` crea issues `policy-event:*` y entradas para BOE (`published`/`rule`), contratacion (`contracted`/`money`), subvenciones (`subsidized`/`money`) y referencias ejecutivas (`proposed`/`implementation`); el lane de dinero ahora se auto-siembra desde los organo nombres a `domains` + `institutions`, asi que las 10 filas money de la corrida rica (`5` `placsp_contratacion`, `5` `bdns_subvenciones`) y las `85` filas BOE entran con `policy_events_with_domain_id=10`, `policy_events_with_institution_id=10` y alimentan el mismo ledger con actor-resolucion completa. `just etl-backfill-accountability-ledger` ejecuta primero `scripts/ingestar_politicos_es.py backfill-policy-events-money` y `backfill-policy-events-boe` antes de la ledger. Hacia donde vamos: usar este puente para que BOE, PLACSP, BDNS y Moncloa entren en el mismo ledger que votos y responsabilidades legales. Que sigue: mejorar agrupacion issue-led para que eventos relacionados caigan bajo un issue ciudadano comun en vez de solo `policy-event:*`. - **D3 catalogo sancionador -> current owner ledger** (`2026-05-10`): se conecta el catalogo de normas sancionadoras a la spine generica como evidencia de owner/competencia actual. Donde estamos ahora: `scripts/backfill_accountability_ledger_from_sanction_norm_catalog.py` convierte `sanction_norm_catalog` + `sanction_norm_fragment_links` en entradas `enforcement/current_owner` por fragmento, crea stubs conservadores en `institutions` para el `organismo_competente`, usa evidencia BOE `tier 1` cuando el fragmento apunta a BOE, y `just etl-backfill-accountability-ledger` ahora falla rapido con `set -e` si algun backfill intermedio rompe. Corrida rica: `current_owner=8`, `entries_by_kind.enforcement=12`, `entries_with_resolved_actor_id=126660`, cola de actores sin resolver `0`. Hacia donde vamos: sustituir los competent-body labels compuestos por organigramas oficiales cuando existan y sumar volumen/procedimiento sancionador desde `sanction_volume_observations`/`sanction_procedural_metrics` cuando haya datos. Que sigue: importar o aplicar observaciones oficiales de volumen/procedimiento y convertirlas en evidencia de ejecucion real. - **Actor resolution para ledger generica** (`2026-05-10`): se anade `scripts/backfill_accountability_ledger_actor_ids.py` para resolver `actor_label` contra `persons/person_name_aliases`, `parties/party_aliases`, `institutions`, `government_org_units` y `government_positions`. Regla de seguridad: solo exact match normalizado y unico; las coincidencias ambiguas quedan sin resolver. Hacia donde vamos: maximizar filas con `person_id/party_id/institution_id/org_unit_id/position_id` sin sobreatribucion fuzzy. Que sigue: alimentar alias oficiales y revisar cola de labels ambiguos antes de publicar dossiers por actor. - **BOE sumario oficial + nombramientos/ceses -> accountability ledger** (`2026-05-11`): se anade soporte real para el endpoint oficial BOE OpenData `GET /datosabiertos/api/boe/sumario/{fecha}` en `publicdata_connectors_es/government/boe_legal.py`, con parser JSON/XML/RSS que conserva seccion, departamento, epigrafe, fecha de publicacion y URLs HTML/XML/PDF por documento; `just etl-ingest-boe-sumario-snapshot` ingiere el sumario del `SNAPSHOT_DATE` con `--strict-network`, y `just etl-backfill-accountability-ledger` ejecuta `backfill-policy-events-boe` antes de la extraccion de nombramientos. `backfill-policy-events-boe` ahora crea stubs conservadores en `institutions` desde `department_name` BOE para que las filas `rule/published` queden resueltas, no como actor desconocido. `scripts/backfill_accountability_ledger_from_boe_appointments.py` crea entries `appointment` con rol `appointed` o `dismissed`, evidencia tier `1`, `policy_event_id`, `persons` + `person_name_aliases` por match exacto, `government_positions`, `person_org_memberships` y enlaces `person_id`/`position_id`; el parser de titulo cubre `se nombra a persona como cargo` y `se nombra cargo a persona`, limpiando prefijos profesionales/militares antes de `don/doña`. El cargo solo queda como `political_appointee` para patrones de alto cargo (`director general`, `secretario de estado`, etc.); el resto queda `unknown` para no sobreatribuir. Corrida rica `politicos-es.e2e19.db` para `SNAPSHOT_DATE=2026-02-12`: `85/85` source records BOE validos, `85` BOE policy_events, `85` BOE rule rows, `12` appointment rows, `10` BOE appointment persons, `8` positions, `12` memberships, `12` entries con `position_id`, `0` actor-resolution queue y `0` FK errors; evidencia aislada en `docs/etl/sprints/AI-OPS-ACCOUNTABILITY/evidence/accountability_boe_sumario_open_data_smoke_latest.json`. Regresion focal: `tests.test_boe_connector`, `tests.test_boe_policy_events_mapping`, `tests.test_backfill_accountability_ledger_from_policy_events` y `tests.test_backfill_accountability_ledger_from_boe_appointments` verdes. Hacia donde vamos: cadena de appointees publica sin mezclarla con empleo publico ordinario. Que sigue: el batch BOE de 97 revisiones ya fue aplicado y la cola issue-level queda en `0`. - **Publicacion de ledger generica** (`2026-05-10`): la spine generica ya tiene runbook y artefacto publico. Donde estamos ahora: `just etl-refresh-accountability-ledger` ejecuta los backfills D1/D2/D3 parciales, resuelve actores por match exacto y publica `etl/data/published/accountability-ledger-.json` + `accountability-ledger-latest.json`; `just etl-publish-hf*` regenera ese JSON antes del privacy gate y `publicdata_publish.collect_published_files()` incluye el alias latest en el paquete HF. El JSON incluye vista issue-led y `actors[]` agregado para dossiers por actor. Para publicar cortes ricos sin subir ledgers masivos, `scripts/export_accountability_ledger_snapshot.py` acepta `--max-entries-per-issue` / `ACCOUNTABILITY_LEDGER_MAX_ENTRIES_PER_ISSUE` y `--max-sample-entries-per-actor` / `ACCOUNTABILITY_LEDGER_MAX_SAMPLE_ENTRIES_PER_ACTOR`; mantiene `coverage.entries_total` completo y agrega `entries_exported`, `entries_truncated` y muestras por actor. Corrida publicada local sobre `etl/data/staging/politicos-es.e2e19.db` con defaults acotados y BOE + money: `issues_total=235`, `entries_total=126767`, `entries_exported=1330`, `actors_total=929`, `entries_with_resolved_actor_id=126767`, `entries_with_person_id=119828`, `entries_with_institution_id=126755`, `entries_with_position_id=12`, `entries_with_party_id=1255`, `entries_with_parliamentary_group_id=126068`, `entries_by_actor_kind.institution=118`, `entries_by_actor_kind.party=569`, `ledger_bytes=4877866`. Hacia donde vamos: convertir el artifact en API/surface de actor dossiers e issue ledgers, y sumar BOE/money/enforcement a los mismos issues. Que sigue: enriquecer los stubs de persona con identificadores oficiales y agrupar `parl-vote:*`/`policy-event:*` bajo issues ciudadanos revisados. - **Dossiers compactos de accountability** (`2026-05-10`): se anade una salida publicable para historial por actor/issue sin volcar todas las filas de evidencia. Donde estamos ahora: `scripts/export_accountability_dossier_snapshot.py` genera `etl/data/published/accountability-dossiers-.json` + `accountability-dossiers-latest.json` con `actors[]`, `issues[]`, edges issue-actor agregados, roles, tipos de entrada, rangos de fecha y cobertura de IDs; `just etl-refresh-accountability-ledger` y `just etl-publish-hf*` lo regeneran, y el empaquetado HF incluye el alias latest. El recipe acota listas internas con `ACCOUNTABILITY_DOSSIERS_MAX_ISSUES_PER_ACTOR=12` y `ACCOUNTABILITY_DOSSIERS_MAX_ACTORS_PER_ISSUE=25`, manteniendo totales completos en `coverage`. Corrida publicada sobre `politicos-es.e2e19.db`: `entries_total=126767`, `actors_total=929`, `issues_total=235`, `issue_actor_edges_total=33159`, `entries_with_person_id=119828`, `entries_with_institution_id=126755`, `entries_with_position_id=12`, `entries_with_party_id=1255`, `entries_with_parliamentary_group_id=126068`, tamano `6546768` bytes bajo el limite `10000000`. Hacia donde vamos: usar este artifact como base de actor dossiers e issue ledgers estaticos. Que sigue: revisar cluster BOE y enriquecer appointees con identificadores oficiales. - **Superficie estatica de accountability dossiers** (`2026-05-10`): se conecta el artifact anterior al portal publico. Donde estamos ahora: ruta Next `/accountability-dossiers/`, entradas en catalogo/dataset, y recipe `accountability-dossiers-next-prime` que publica `ui/gh-pages-next/public/accountability-dossiers/data/dossiers.json`, `ledger.json` y `/accountability-evidence/data/evidence-api.json` desde `etl/data/published/*-latest.json` o desde export DB cuando `GH_PAGES_NEXT_PRIME_EXPORT=1`. La ruta muestra cobertura, temas, actores y muestras concretas de evidencia del ledger (`tier`, fuente, fecha, localizador y cita), ademas de enlaces directos a los JSON; enlaza a indices estaticos (`/accountability-dossiers/actors/`, `/accountability-dossiers/issues/`) y a fichas generadas por actor (`/accountability-dossiers/actors/*`) y por issue (`/accountability-dossiers/issues/*`) con identidad, roles, temas implicados y evidencia trazable. Build local `just cloudflare-pages-build` con BOE + money pasa y prerenderiza `1437` paginas (`929` actores + `235` issues + indices + rutas existentes) con `126767` entradas, `929` actores, `235` issues, `119828` person IDs, `126755` institution IDs, `12` position IDs, `1255` party IDs y `126068` group IDs; privacy gate completo limpio (`files_scanned=15882`). HTTP smoke local confirma `/accountability-evidence/` con `source_entries_total=126767`, `source_issues_total=235`, `actor_answers_total=929`, `issue_cluster_assignment_review_needed_total=0`, y ficha BOE `/accountability-dossiers/issues/boe-appointment-boe-boe-api-legal-boe-a-2026-3242-kyd2vf/` con `200`, Miguel Angel Manez Ortiz y enlace BOE primario. `just cloudflare-pages-deploy` sigue pendiente porque Wrangler exige `CLOUDFLARE_API_TOKEN` en entorno no interactivo. Hacia donde vamos: convertir estas fichas en dossiers profundos enlazados al Explorer y al futuro Evidence API. Que sigue: cola BOE cerrada; desplegar desde entorno autenticado cuando haya token. - **Evidence API estatica de accountability** (`2026-05-10`): se cierra la primera slice P6 sin servidor. Donde estamos ahora: `scripts/export_accountability_evidence_api_snapshot.py` genera `etl/data/published/accountability-evidence-api-.json` + `accountability-evidence-api-latest.json` desde ledger+dossiers; aplica `etl/data/seeds/accountability_issue_cluster_reviews_seed_v1.json` para revisar etiquetas publicas de clusters; y aplica `etl/data/seeds/accountability_issue_cluster_issue_reviews_seed_v1.json` para revisar asignaciones source-issue revisadas sin fingir revision de cada actor entry. `just etl-refresh-accountability-ledger` y `just etl-publish-hf*` lo regeneran; el empaquetado HF incluye el alias latest; y `/accountability-evidence/` consume `/accountability-evidence/data/evidence-api.json` con catalogo de preguntas, respuestas narrativas Q&A enlazables, respuestas por issue, respuestas por actor, clusters ciudadanos con etiqueta revisada y primeras asignaciones issue-level revisadas, cola explicita de revision de clusters, cola issue-level de asignaciones heuristicamente agrupadas, gap answers por dimension, muestras de evidencia trazable, estado `partial/unanswerable`, confianza por tier de evidencia, frescura por fecha y cobertura de dimensiones. El exporter escribe JSON compacto por defecto para preservar la misma cobertura dentro del presupuesto publico; `--pretty` queda para inspeccion manual. Corrida rica con BOE + money: `question_templates_total=5`, `qa_answers_total=27`, `qa_answers_with_self_route_total=27`, `qa_answer_status_counts.partial=24`, `qa_answer_status_counts.unanswerable=3`, `actor_answers_total=929`, `issue_answers_total=235`, `actor_issue_refs_total=2351`, `issue_clusters_total=13`, `issue_cluster_links_total=323`, `issue_cluster_review_items_total=13`, `issue_cluster_review_status_counts.reviewed=13`, `issue_cluster_reviews_applied_total=13`, `issue_cluster_issue_reviews_applied_total=235`, `issue_cluster_reviewed_links_total=323`, `issue_cluster_assignment_review_needed_total=0`, `issue_cluster_assignment_review_queue_total=0`, `fallback_issue_cluster_answers_total=15`, `gap_answers_total=9`, `gap_answer_status_counts.unanswerable=3`, `gap_answer_status_counts.partial=6`, `evidence_samples_total=1164`, `confidence_level_counts.high=403`, `confidence_level_counts.medium=761`, `freshness_level_counts.current=512`, `freshness_level_counts.recent=552`, `freshness_level_counts.historical=100`, `evidence_api_bytes=6605804` bajo limite `8000000`. Hacia donde vamos: Evidence API como contrato estable para Q&A, widgets, dossiers y rutas shareables. Que sigue: cola BOE cerrada; mantener el gate de `ACCOUNTABILITY_MAX_EVIDENCE_API_ISSUE_CLUSTER_ASSIGNMENT_REVIEW_NEEDED=0`. - **Cola accionable de revision issue-cluster** (`2026-05-10`): se convierte la deuda heuristica de agrupacion en paquete revisable. Donde estamos ahora: `scripts/export_accountability_issue_cluster_assignment_review_queue.py` lee `accountability-evidence-api-.json` y exporta JSON+CSV en `docs/etl/sprints/AI-OPS-ACCOUNTABILITY/evidence/accountability_issue_cluster_assignment_review_queue_latest.json` y `docs/etl/sprints/AI-OPS-ACCOUNTABILITY/exports/accountability_issue_cluster_assignment_review_queue_latest.csv`; `just etl-export-accountability-issue-cluster-assignment-review-queue` lo regenera y `just etl-refresh-accountability-ledger` lo ejecuta despues de validar artifacts. `scripts/apply_accountability_issue_cluster_assignment_reviews.py` aplica un CSV revisado al seed `etl/data/seeds/accountability_issue_cluster_issue_reviews_seed_v1.json` y escribe reporte en `docs/etl/sprints/AI-OPS-ACCOUNTABILITY/evidence/accountability_issue_cluster_assignment_reviews_apply_report_latest.json`; batch1 aplico `10`, batch2 `25`, batch3 `75`, y batch4 BOE aplico `97`, dejando `225` source issues revisadas y la cola issue-level en `0`. Corrida actual con BOE: `pending_issue_assignments_total=0`, `queue_rows_total=0`, `queue_truncated=false`, `reviewed_issue_assignments_total=225`, `source_issue_answers_total=225`, `source_issue_clusters_total=13`. Hacia donde vamos: mantener esta cola como gate recurrente para nuevas issues sin fingir adjudicacion automatica. Que sigue: nuevos issues solo entran por batch revisado; el gate de cola queda en `0`. - **Gate de artifacts accountability** (`2026-05-10`): se protege la publicacion para no subir una ledger/dossier/Evidence API rota o vacia. Donde estamos ahora: `scripts/validate_accountability_artifacts.py` valida schema versions, `snapshot_date`, paridad `entries_total` ledger/dossier, minimos de entries/actors/issues, cobertura de actor resuelto, floors de `person_id`, `party_id` y `parliamentary_group_id`, limites de tamano, schema `accountability_evidence_api_v1`, minimo de preguntas, minimo de issue clusters, minimo opcional de clusters revisados, minimo opcional de asignaciones issue-level revisadas, minimo y maximo opcional de cola issue-level pendiente, minimo de gap answers, minimo de Q&A answers, byte budget de Evidence API, paridad de conteos de confianza/frescura contra answers, que cada Q&A exportada tenga `routes.self`, que la cola de revision de clusters tenga paridad con los clusters exportados, que los conteos de status de revision cuadren con la cola, y que la cola issue-level no exceda el total pendiente; `just etl-refresh-accountability-ledger` y `just etl-publish-hf*` ejecutan `just etl-validate-accountability-artifacts` antes del privacy gate/HF packaging. Corrida con BOE + money y cola cerrada: `ACCOUNTABILITY_MIN_ENTRIES=126767`, `ACCOUNTABILITY_MIN_ACTORS=929`, `ACCOUNTABILITY_MIN_ISSUES=235`, `ACCOUNTABILITY_MIN_PERSON_ID_ENTRIES=119828`, `ACCOUNTABILITY_MIN_PARTY_ID_ENTRIES=1255`, `ACCOUNTABILITY_MIN_PARLIAMENTARY_GROUP_ID_ENTRIES=126068`, `ACCOUNTABILITY_MIN_EVIDENCE_API_QUESTIONS=5`, `ACCOUNTABILITY_MIN_EVIDENCE_API_ISSUE_CLUSTERS=13`, `ACCOUNTABILITY_MIN_EVIDENCE_API_REVIEWED_ISSUE_CLUSTERS=13`, `ACCOUNTABILITY_MIN_EVIDENCE_API_ISSUE_CLUSTER_ISSUE_REVIEWS=235`, `ACCOUNTABILITY_MIN_EVIDENCE_API_GAP_ANSWERS=9`, `ACCOUNTABILITY_MIN_EVIDENCE_API_QA_ANSWERS=27`, `ledger_resolution_pct=1.0`, `ledger_bytes=4877866`, `dossiers_bytes=6546768`, `evidence_api_bytes=6605804`, `evidence_api_qa_answers_total=27`, `evidence_api_qa_answers_with_self_route_total=27`, `evidence_api_issue_clusters_total=13`, `evidence_api_issue_cluster_links_total=323`, `evidence_api_issue_cluster_review_items_total=13`, `evidence_api_reviewed_issue_clusters_total=13`, `evidence_api_issue_cluster_issue_reviews_applied_total=235`, `evidence_api_issue_cluster_assignment_review_needed_total=0`, `evidence_api_issue_cluster_assignment_review_queue_total=0`, `evidence_api_issue_cluster_reviewed_links_total=323`, `evidence_api_gap_answers_total=9`, `evidence_api_confidence_levels_total=1164`, `evidence_api_freshness_levels_total=1164`. Hacia donde vamos: convertir estos floors en umbrales de release por snapshot cuando se sumen BOE/money/enforcement. Que sigue: cola BOE cerrada; subir floors por familia de fuente. - **HF publish gate para accountability D1+D2+D3+Evidence API** (`2026-05-10`): se intenta el checklist de publicacion HF tras el cambio de datos. Donde estamos ahora: `ACCOUNTABILITY_LEDGER_DB_PATH=etl/data/staging/politicos-es.e2e19.db SNAPSHOT_DATE=2026-02-12 ACCOUNTABILITY_MIN_ENTRIES=126660 ACCOUNTABILITY_MIN_ACTORS=885 ACCOUNTABILITY_MIN_ISSUES=128 ACCOUNTABILITY_MIN_PERSON_ID_ENTRIES=119816 ACCOUNTABILITY_MIN_PARTY_ID_ENTRIES=1255 ACCOUNTABILITY_MIN_PARLIAMENTARY_GROUP_ID_ENTRIES=126068 ACCOUNTABILITY_MIN_EVIDENCE_API_QUESTIONS=5 ACCOUNTABILITY_MIN_EVIDENCE_API_ISSUE_CLUSTERS=12 ACCOUNTABILITY_MIN_EVIDENCE_API_REVIEWED_ISSUE_CLUSTERS=12 ACCOUNTABILITY_MIN_EVIDENCE_API_ISSUE_CLUSTER_ISSUE_REVIEWS=128 ACCOUNTABILITY_MIN_EVIDENCE_API_ISSUE_CLUSTER_ASSIGNMENT_REVIEW_NEEDED=0 ACCOUNTABILITY_MAX_EVIDENCE_API_ISSUE_CLUSTER_ASSIGNMENT_REVIEW_NEEDED=0 ACCOUNTABILITY_MIN_EVIDENCE_API_GAP_ANSWERS=9 ACCOUNTABILITY_MIN_EVIDENCE_API_QA_ANSWERS=27 just etl-publish-hf-dry-run` regenera catalogo, scrape queue, ledger, dossiers y Evidence API; valida artifacts (`evidence_api_bytes=5972776`, `evidence_api_qa_answers_with_self_route_total=27`, `evidence_api_issue_cluster_review_items_total=12`, `evidence_api_reviewed_issue_clusters_total=12`, `evidence_api_issue_cluster_issue_reviews_applied_total=128`, `evidence_api_issue_cluster_assignment_review_needed_total=0`); y pasa privacy gate sobre `etl/data/published` (`files_scanned=62`). El tramo Docker queda bloqueado porque el daemon no esta disponible (`connect: no such file or directory`). Fallback local directo tambien queda bloqueado: `pyarrow` no esta instalado, y con `--require-liberty-atlas-release-latest` el release latest apunta a `2026-04-13` mientras este snapshot es `2026-02-12`. Hacia donde vamos: completar dry-run/publish HF desde Docker activo o entorno ETL con `pyarrow` y politica clara para el release liberty atlas cross-snapshot. Que sigue: arrancar Docker o instalar dependencias ETL y decidir si este publish debe relajar `HF_REQUIRE_LIBERTY_ATLAS_RELEASE_LATEST` para snapshots historicos. - **Queue de resolucion de actores para ledger generica** (`2026-05-10`): se hace visible la deuda D0 que bloquea dossiers por actor. Donde estamos ahora: `scripts/export_accountability_actor_resolution_queue.py` agrupa `accountability_ledger_entries` sin `person_id/party_id/institution_id/org_unit_id/position_id` por `actor_label`, con conteos por rol, tipo de entrada, fechas, issues y muestras; `just etl-export-accountability-actor-resolution-queue` escribe JSON+CSV y acepta `ACCOUNTABILITY_LEDGER_DB_PATH` para usar un DB de accountability mas rico que el DB principal de publish. Artefacto default sobre `politicos-es.db` tras el puente D0: `entries_total=350`, `unresolved_actor_labels_total=0`, `unresolved_entries_total=0`. Primera corrida ampliada sobre `parlamentario-es.db` antes del puente mostro `entries_total=87500`, `unresolved_actor_labels_total=354`, `issues_total=75` en la ledger, todo bajo `parl-initiative:*`; despues del puente D0 el mismo artefacto queda en `unresolved_actor_labels_total=0`, `unresolved_entries_total=0` y privacy gate limpio. Hacia donde vamos: convertir esta queue en seed/review aplicable para `persons`, `person_name_aliases`, `parties` y mandatos datados. Que sigue: enriquecer esos 354 nombres con identificadores oficiales de Congreso/Senado y evidencia primaria por actor. - **D0 roll-call labels -> person aliases + observed mandates** (`2026-05-10`): se cierra la primera deuda masiva de identidad parlamentaria sin fuzzy matching. Donde estamos ahora: `scripts/backfill_persons_from_vote_member_names.py` crea stubs conservadores en `persons` y alias `official_roll_call` desde nombres oficiales de `parl_vote_member_votes`, solo por etiqueta normalizada exacta; `scripts/backfill_mandates_from_vote_member_names.py` materializa mandatos observados por participacion en votaciones, con camara/institucion y rango observado de votos, sin inferir partido desde `group_code`. Ambos se integran en `just etl-backfill-accountability-ledger` antes de reconstruir la ledger. Corrida sobre `etl/data/staging/parlamentario-es.db`: `354` labels distintos, `354` personas/alias, `354` mandatos observados, `87500` votos nominales enlazados con `person_id`; despues de reconstruir la ledger, `accountability_ledger_entries` queda en `87500/87500` filas con `person_id`, `mandate_id` e `institution_id`, `0` filas sin actor resuelto, `PRAGMA foreign_key_check` limpio, y el artefacto ampliado de queue queda en `unresolved_actor_labels_total=0`, `unresolved_entries_total=0`. Hacia donde vamos: enriquecer esos stubs con identificadores oficiales Congreso/Senado, fechas completas de mandato y partidos cuando haya evidencia primaria. Que sigue: resolver IDs externos oficiales y evitar publicar dossiers personales que traten estos stubs como identidad completa. - **D0 grupos parlamentarios observados** (`2026-05-10`): se materializa la afiliacion parlamentaria observable sin convertirla falsamente en partido. Donde estamos ahora: se anaden schema aditivo `parliamentary_groups`, `person_parliamentary_group_memberships`, `parl_vote_member_votes.parliamentary_group_id` y `accountability_ledger_entries.parliamentary_group_id`; `scripts/backfill_parliamentary_groups_from_vote_member_votes.py` crea grupos desde `group_code`, memberships por persona/grupo/rango observado, y actualiza votos nominales; `scripts/backfill_accountability_ledger_from_parliament.py` emite tambien rollups `actor_kind='group'` por issue/voto/grupo/rol para que el ledger responda que grupos parlamentarios tocaron cada issue. Corrida sobre `etl/data/staging/politicos-es.db`: `9` grupos, `350` memberships, `350/350` votos con group id, `362` ledger rows totales (`350` personas + `12` grupos). Corrida sobre `etl/data/staging/parlamentario-es.db`: `9` grupos, `354` memberships, `87500/87500` votos con group id, `90596` ledger rows totales (`87500` personas + `3096` grupos), `0` sin actor resuelto; grupos: `GP`, `GS`, `GVOX`, `GSUMAR`, `GMx`, `GR`, `GJxCAT`, `GEH Bildu`, `GV (EAJ-PNV)`. Hacia donde vamos: enlazar grupos parlamentarios a partidos solo cuando exista fuente primaria reproducible y distinguir grupo mixto/coaliciones de partido unico. Que sigue: resolver afiliacion formal de partido y cargos de partido fuera del Congreso/Senado. - **Prioridad activa**: terminar y estabilizar el bloque `Derechos` (restricciones de libertad) end-to-end. - **No tocar en bloque**: `PLACSP`, `AEMET`, `BDE`, `UE` y otros lanes no-`Derechos` salvo mantenimiento crítico. - **Organigrama oficial como infraestructura de atribución** (`2026-04-25`, revalidado `2026-08-12`): se abre una excepción explícita por petición directa de producto: poder responder quién depende de quién y quién es responsable máximo exige una columna vertebral oficial de unidades, separada de partidos/mandatos. Donde estamos ahora: el repo tenía `persons/mandates/parties/institutions`, pero no una relación `unidad -> unidad superior`; se añade schema aditivo `government_org_units`, `government_org_relationships`, `government_positions` y `person_org_memberships`, más el conector `dir3_unidades_age` que resuelve el XLSX oficial DIR3 de unidades orgánicas AGE desde el catálogo oficial de datos.gob.es y descarga desde administracionelectronica.gob.es. El retry estricto actual carga `0` filas: `HTTP 302` en bucle; el probe directo devuelve `HTTP 200 text/html` con `Request Rejected`, no XLSX ([evidencia](sprints/SCALE-FOUNDATION-20260810/evidence/dir3-age-access-blocker-20260812.json)). No se usa fallback ni captura fabricada. Hacia dónde vamos: usar DIR3 como fuente de verdad estructural, BOE como fuente normativa de competencias/estructura, Portal de Transparencia/PAG como fuente de organigramas y altos cargos, y mantener afiliación política como membresía separada, no como empleo público. Qué sigue: no repetir sin palanca nueva; cuando exista endpoint/captura oficial revisada, ejecutar el ingest estricto y después `python3 scripts/ingestar_politicos_es.py backfill-government-org-units --db ` para materializar el grafo. Bloqueo registrado en `docs/etl/name-and-shame-access-blockers.md`. - **P0 de estabilización**: mantener `AI-OPS-218` como estado de "continuidad de release/coverage alcanzada + heartbeat de publicación parcial", y cerrar `AI-OPS-219` para dejar el lane `Atlas de Restricciones` en estado `ok` estable. - **Mantenimiento crítico citizen/explainability**: `AI-OPS-553` abre una lane controlable para traducir títulos parlamentarios genéricos a implicaciones ciudadanas verificables (`parl_vote_implication_reviews` + export/apply queue + instrucciones). No reabre scope no-`Derechos`: corrige una limitación directa del producto ciudadano sobre votaciones ya publicadas. Primer lote real ya sincronizado sobre `congreso_votaciones`: `2823` filas candidatas, `716` con prioridad `100`, `174` con margen estrecho (`|yes-no| <= 5`), limpieza de filas locales `file:` verificada (`0` restantes). Además, el exportador ya corta batches operativos por `review_reason`, margen y texto; primer seed batch de vida cotidiana (`split_vote_point + margen<=5 + vivienda/movilidad/cliente/transporte/energía/presupuestaria`) exportado con `8` filas y ya tiene paquete de delegación listo (`DelegationPackage.json`, `ExecutionQueue.json`, `AggregationGuide.md`, `cards/WS1-*.md`). En la primera ejecución real de microtask contra la URL oficial por-votación (`WS1-008`), `webpublica/opendata/votaciones/.../VOT_*.json` devolvió `HTTP 403 Access Denied`; el piloto quedó rehecho con fallback local integrado (`local-evidence/WS1-*.json`) y, en el cierre del seed batch (`2026-03-12`), los `8` task_ids quedaron adjudicados como `resolved` tras una segunda pasada con recuperación de evidencia oficial de Congreso (`BOCG`, `Diario de Sesiones`, fichas/actas oficiales). El CSV adjudicado ya se aplicó a SQLite sobre `congreso_votaciones` (`8` updates válidos), esas revisiones quedaron expuestas en `/explorer-votaciones` como bloque visible de `reviewed_implications` (`8` cards revisadas, independiente del top `200` de eventos recientes) y el flujo completo `just privacy-check-public-artifacts` -> `just explorer-gh-pages-build` -> `just explorer-gh-pages-publish` quedó en verde el `2026-03-12`, con push público de `gh-pages` en `dfffb7a`. La deriva más amplia detectada en `citizen` combinado/declarado y en parte de `political-positions` sigue siendo un follow-up real, pero ya no bloquea el cierre de esta slice controlable porque la delta visible bajo control del repo quedó publicada. Evidencia/contrato: `docs/etl/citizen-vote-implication-review-instructions.md`, `docs/etl/sprints/AI-OPS-553/reports/citizen-vote-implication-review-queue-contract-20260308.md`, `docs/etl/sprints/AI-OPS-553/evidence/vote_implication_review_queue_stats_20260308T175054Z.txt`, `docs/etl/sprints/AI-OPS-553/evidence/vote_implication_review_queue_daily_life_seed_20260308T175204Z.csv`, `docs/etl/sprints/AI-OPS-553/delegation/daily-life-seed-20260308/DelegationPackage.json`, `docs/etl/sprints/AI-OPS-553/delegation/daily-life-seed-20260308/results/research-submissions/WS1-001.json`, `docs/etl/sprints/AI-OPS-553/delegation/daily-life-seed-20260308/results/research-submissions/WS1-008.json`, `docs/etl/sprints/AI-OPS-553/delegation/daily-life-seed-20260308/results/adjudicated/decisions_adjudicated.csv`, `docs/etl/sprints/AI-OPS-553/delegation/daily-life-seed-20260308/results/adjudicated/apply_summary.json`, `docs/etl/sprints/AI-OPS-553/evidence/congreso_vote_event_url_403_headers_20260308T180003Z.txt`. - **Continuación controlable en iniciativas oficiales** (`2026-04-12`): la cola separada a nivel iniciativa para normalizar “qué cambia” desde documentos oficiales descargados (`parl_initiative_measure_review_tasks` + `parl_initiative_measure_points`, con export/apply y schema de salida para workers Codex CLI) quedó cerrada end-to-end para el lote controlado sincronizado sobre `congreso_iniciativas` + `senado_iniciativas`. En esta pasada se añadió el reporter operativo `report_initiative_measure_review_queue_status.py`, se corrigió un bug real de batching en `export_initiative_measure_review_queue.py` (`--offset/--limit` aplicado dos veces en runs sin `--contains`) con regresión en `tests/test_export_initiative_measure_review_queue.py`, y se resolvieron/aplicaron los `20` task_ids sincronizados en staging (`19` Congreso, `1` Senado) mediante batches reproducibles de evidencia oficial. Estado final de la cola en `etl/data/staging/parlamentario-es.db`: `tasks_total=20`, `pending_total=0`, `resolved_total=20`, `measure_points_total=85`, `resolved_without_measure_points_total=0`. Se resolvieron tanto los dossiers iniciales de movilidad/clientela/multirreincidencia como el lote completo restante de iniciativas oficiales (espacio público, presupuestaria, menores, Ministerio Fiscal, rural, deslocalización, discapacidad, juventud, dependencia/ELA, DANA, retribuciones, transporte, La Palma, inversión local/autonómica, acuerdo Senado-Consejo Oleícola Internacional y reforma del Estatuto de Castilla-La Mancha). Delta visible publicada bajo control del repo: `export_parliamentary_accountability_snapshot.py` ahora puede leer medidas desde una SQLite separada (`--initiative-measures-db`) y el build acepta también un DB específico para accountability (`PARLIAMENTARY_ACCOUNTABILITY_DB_PATH`) cuando el DB general no contiene suficiente corpus parlamentario reciente. Con el export actual desde `etl/data/staging/politicos-es.recovered.db` + medidas de `etl/data/staging/parlamentario-es.db`, el snapshot `docs/gh-pages/parliamentary-accountability/data/accountability.json` sube a `events_with_initiative_measures=172` y `events_with_event_specific_initiative_measures=24`, exponiendo en `critical_by_margin` medidas ciudadanas para votaciones reales de Congreso y Senado (por ejemplo el bloque de atención a la clientela `121/000012` y movilidad sostenible `121/000009`, además del Acuerdo de Sede del Consejo Oleícola Internacional). La continuación controlable del mismo sprint estabilizó además `just explorer-gh-pages-build` sobre el DB lean `etl/data/staging/politicos-es.e2e19.db`: `export_policy_outcomes_snapshot.py` ya degrada a snapshot válido cuando faltan `indicator_series`/`policy_events`, y `export_citizen_snapshot.py` deja de asumir `institution_id=7` fijo, cae al `institution_id` real del `topic_set`, usa `auto` para el surface principal y exporta `declared` como grid honesta `no_data` cuando no existen posiciones. Además, el export ciudadano queda endurecido para no machacar el último snapshot útil cuando el DB actual colapsa a cobertura residual: si la nueva grid cae por debajo de un umbral severo respecto al último snapshot auditado, el build reutiliza el artefacto previo en vez de publicar una versión vacía o casi vacía, y ahora el publish path acepta también un DB específico para `citizen` (`CITIZEN_DB_PATH`) cuando la recuperación de cobertura vive primero en otra SQLite. Resultado: el build completo (`DB_PATH=etl/data/staging/politicos-es.e2e19.db PARLIAMENTARY_ACCOUNTABILITY_DB_PATH=etl/data/staging/politicos-es.recovered.db INITIATIVE_MEASURES_DB_PATH=etl/data/staging/parlamentario-es.db just explorer-gh-pages-build`) vuelve a cerrar en verde con privacy gate incluido. Donde estamos ahora: la infraestructura de publicación ya no rompe por tablas opcionales ausentes ni por ids de institución obsoletos, y el surface `citizen` deja de degradar por accidente a una grid inútil mientras se recupera cobertura real. Hacia dónde vamos: recuperar cobertura real de `citizen` en el snapshot principal para que el surface vuelva a tener temas/pack quality útiles en vez de depender de este hardening. Qué sigue: materializar o backfillear `topic_set_topics`/`topic_positions` utilizables para `topic_set_id=1` en el DB publicado, o poblar y usar `CITIZEN_DB_PATH` hasta que el DB principal vuelva a tener cobertura suficiente. Evidencia operativa: `docs/etl/sprints/AI-OPS-553/reports/initiative-measure-review-queue-status-20260412.md`, `docs/etl/sprints/AI-OPS-553/evidence/initiative_measure_review_queue_status_20260412_post_apply_8.json`. - **Benchmark controlable de desastre/fallo público** (`2026-04-12`): se publica el primer `responsibility explainer` estático bajo control del repo para un caso de benchmark real, `DANA Valencia 2024`, sin fingir todavía una atribución causal completa. Se añade `scripts/export_responsibility_explainer_snapshot.py`, que exporta `docs/gh-pages/responsibility-explainer/data/manifest.json` y `docs/gh-pages/responsibility-explainer/data/dana-valencia-2024.json` desde una SQLite parlamentaria; el build copia esos artefactos a `ui/gh-pages-next/public/responsibility-explainer/data` y prerenderiza `/responsibility-explainer/` y `/responsibility-explainer/dana-valencia-2024/` en GH Pages. Estado actual del benchmark sobre `etl/data/staging/parlamentario-es.db`: `initiatives_total=11`, `initiatives_with_votes_total=1`, `initiatives_with_measure_points_total=1`, `vote_events_total=2`, `reviewed_measures_total=5`, `official_documents_total=15`, con `3` preguntas en estado `partial` (respuesta parlamentaria, rastro legislativo, cambios concretos) y `4` todavía `missing` (avisos, deber de actuar, decisiones operativas, atribución de daño). Delta visible publicada: el caso ya expone iniciativas oficiales DANA, votos enlazados y medidas revisadas citizen-facing, junto con huecos explícitos y siguientes líneas de trabajo. Donde estamos ahora: el producto ya puede enseñar una cadena parlamentaria y normativa auditable para un caso de desastre público, aunque aún no cierre la cadena de competencia, aviso y daño. Hacia dónde vamos: ampliar este benchmark a una cadena completa `warning -> duty -> decision/omission -> outcome -> accountability` con evidencia primaria de emergencias y gobierno operativo. Qué sigue: normalizar avisos `AEMET/CHJ/Protección Civil`, modelar el grafo de competencias/deber de actuar para emergencias y enlazar comparecencias o actos operativos que permitan pasar de “respuesta parlamentaria” a “responsabilidad material”. Evidencia operativa: `docs/gh-pages/responsibility-explainer/data/manifest.json`, `docs/gh-pages/responsibility-explainer/data/dana-valencia-2024.json`. - **Continuacion controlable del benchmark DANA: deber de actuar** (`2026-04-12`): el `responsibility explainer` deja de ser solo una vista parlamentaria y publica ya una primera capa de deber/competencia bajo control del repo mediante `etl/data/seeds/responsibility_explainer_cases_seed_v1.json`, cargada por `scripts/export_responsibility_explainer_snapshot.py` y renderizada en `ui/gh-pages-next/app/responsibility-explainer/`. El caso `dana-valencia-2024` ahora expone `4` anclajes oficiales de deber (`Sistema Nacional de Proteccion Civil`, Generalitat/conselleria competente, `Centro de Coordinacion de Emergencias` y ayuntamientos) y `3` canales oficiales de aviso a normalizar (`AEMET Meteoalerta`, `SAIH Jucar`, `CCE/112CV`). Estado actual del benchmark sobre `etl/data/staging/parlamentario-es.db`: `normative_duties_total=4`, `warning_channels_total=3`, `initiatives_total=11`, `vote_events_total=2`, `reviewed_measures_total=5`, con `4` preguntas ya en `partial` y `3` todavia `missing`; el cambio material es que `duty_chain` pasa de `missing` a `partial`, mientras `warning_timeline` sigue abierto porque todavia no existe la serie normalizada de avisos del episodio. Delta visible publicada: la pagina del caso ya distingue entre anclajes oficiales de deber, canales de aviso a normalizar y evidencia parlamentaria enlazada, en lugar de dejar todo el deber de actuar solo como hueco. Donde estamos ahora: el producto ya puede contestar parcialmente "quien tenia deber de actuar" con fuentes oficiales y sin fingir aun la cronologia operativa. Hacia donde vamos: sustituir este seed benchmark por una lane reproducible en tablas legales/emergencias y cerrar la secuencia `warning -> duty -> decision/omission -> outcome`. Que sigue: normalizar avisos concretos `AEMET/CHJ/Proteccion Civil` de octubre-noviembre de 2024, aterrizar estos deberes a actores/personas/turnos y enlazar comparecencias u ordenes operativas. Evidencia operativa: `etl/data/seeds/responsibility_explainer_cases_seed_v1.json`, `docs/gh-pages/responsibility-explainer/data/manifest.json`, `docs/gh-pages/responsibility-explainer/data/dana-valencia-2024.json`. - **DANA: exposicion regulatoria pre-desastre** (`2026-04-12`): el benchmark se amplia con `structural_risk_factors` para responder parcialmente desde reglas previas de suelo, agua, licencias y gobernanza, no solo desde la respuesta de emergencia. El caso `dana-valencia-2024` publica `5` factores estructurales (`PATRICOVA`, compatibilidad de planeamiento con planes de riesgo, limites a usos vulnerables en zona inundable, mapas de riesgos en la ley del suelo y cadena administrativa de alerta), con `structural_exposure=partial`. Estado actual: `structural_risk_factors_total=5`, `normative_duties_total=4`, `warning_channels_total=3`, `initiatives_total=11`, `vote_events_total=2`, `reviewed_measures_total=5`, `5` preguntas en `partial` y `3` en `missing`. Delta visible publicada: la pagina del caso ya separa `deberes oficiales`, `canales de aviso` y `exposicion regulatoria pre-desastre`, orientando la auditoria hacia planeamiento, licencias, usos vulnerables y diseno institucional. Que sigue: cruzar cartografia inundable con planeamiento/licencias y bajar de marco normativo a decisiones concretas autorizadas o toleradas. Evidencia: `etl/data/seeds/responsibility_explainer_cases_seed_v1.json`, `docs/gh-pages/responsibility-explainer/data/dana-valencia-2024.json`. - **DANA: blancos concretos de auditoria estructural** (`2026-04-12`): el `responsibility explainer` deja de quedarse en factores normativos abstractos y publica `structural_audit_targets` como checklist auditable para pasar de la regla al expediente. El caso `dana-valencia-2024` anade `6` blancos concretos: planeamiento del corredor del barranco del Poyo, licencias y usos vulnerables en l'Horta Sud, planes municipales y CECOPAL, infraestructura hidraulica y drenaje, cadena de alerta y dependencia politica, y actuaciones de urbanizacion que debian integrar mapas de riesgo. Estado actual: `structural_audit_targets_total=6`, `structural_risk_factors_total=5`, `normative_duties_total=4`, `warning_channels_total=3`, `initiatives_total=11`, `vote_events_total=2`, `reviewed_measures_total=5`, con `5` preguntas en `partial` y `3` en `missing`. Delta visible publicada: la pagina del caso ya muestra, bajo `Exposicion regulatoria pre-desastre`, una segunda capa `Targets de auditoria estructural` con `ambito`, `documentos a auditar`, `cadena a revisar` y `cruce pendiente`, de modo que el producto ya formula donde buscar planes, licencias, expedientes y protocolos concretos sin fingir hallazgos que el repo aun no tiene. Que sigue: convertir al menos uno de esos blancos en evidencia estructurada fila a fila, empezando por PGOU y licencias en el corredor del barranco del Poyo o por planes municipales y CECOPAL homologados. Evidencia: `etl/data/seeds/responsibility_explainer_cases_seed_v1.json`, `docs/gh-pages/responsibility-explainer/data/dana-valencia-2024.json`. - **DANA: filas de evidencia estructural para planeamiento del Poyo e infraestructura local** (`2026-04-12`): el benchmark ya baja de los `targets` a evidencia municipal e hidrologica concreta dentro del mismo `responsibility explainer`. El caso `dana-valencia-2024` publica ahora `structural_evidence_rows_total=12`, repartidas en `6` filas de planes municipales y CECOPAL, `4` filas de infraestructura hidraulica y drenaje, y `2` filas de planeamiento del corredor del barranco del Poyo. Nueva evidencia visible bajo control del repo: Paiporta aporta `4` filas oficiales (`Plan Director de Saneamiento` de 2008 y `Plan Director del Abastecimiento de Agua` de 2016) que documentan una red unitaria y deficitaria, inundaciones previas en bajos/sotanos, cuellos de botella de abastecimiento y dependencia de cruces sobre el barranco; Picanya aporta una regla urbanistica consolidada en 2023 de `proteccion fluvial` con franja libre de edificacion; y la CHJ aporta el anclaje de cuenca que ya identificaba a `Paiporta`, `Picanya`, `Catarroja`, `Alfafar`, `Benetusser` y `Sedavi` dentro del ARPSI del Poyo y hablaba de una `solucion global al problema de inundaciones`. Donde estamos ahora: el producto ya puede responder con filas concretas, no solo con categorias, a parte de la pregunta sobre si el riesgo y ciertas debilidades de infraestructura/planeamiento estaban oficialmente reconocidos antes de la DANA. Hacia donde vamos: cruzar estas reglas y diagnosticos con expedientes especificos de licencias, urbanizacion, disciplina y mantenimiento para pasar de la exposicion conocida a decisiones autorizadas o toleradas. Que sigue: priorizar un lote reproducible de licencias/usos vulnerables o actos de planeamiento en el corredor del Poyo, y en paralelo seguir bajando la cadena `warning -> duty -> decision/omission` con protocolos y cronologia operativa. Evidencia: `etl/data/seeds/responsibility_explainer_cases_seed_v1.json`, `docs/gh-pages/responsibility-explainer/data/manifest.json`, `docs/gh-pages/responsibility-explainer/data/dana-valencia-2024.json`. - **DANA: acto urbanistico concreto con criterio hidraulico pre-DANA** (`2026-04-12`): la slice estructural ya no solo muestra reglas, planes y diagnosticos, sino tambien un acto municipal especifico de 2024 dentro del target `actuaciones de urbanizacion y proyectos`. El caso `dana-valencia-2024` sube a `structural_evidence_rows_total=14`, con distribucion `6` filas de planes municipales, `4` de infraestructura hidraulica y drenaje, `2` de planeamiento del corredor del Poyo y `2` de proyectos de urbanizacion. La nueva evidencia publicada procede del `Proyecto de urbanizacion del Programa de Actuacion Integrada para urbanizar los viales Enrique Reig, Poeta Llorente y Pintor Benedito de Paiporta`, registrado el `7 de marzo de 2024`, que mantiene evacuacion `unitaria` de pluviales y residuales sobre la red existente y cuyo anejo hidraulico explicita que en la zona de estudio podia admitirse una proteccion menor contra inundaciones, adoptando un `periodo de retorno de 5 años`. Delta visible publicada: el benchmark ya puede apuntar a una decision/proyecto municipal concreto inmediatamente anterior a la DANA y no solo a marcos generales. Donde estamos ahora: la pregunta deja de ser solo si habia riesgo conocido y pasa tambien por como se seguian proyectando actuaciones urbanas y con que umbrales hidraulicos. Hacia donde vamos: contrastar este tipo de actos con el expediente completo, sus informes sectoriales y las reglas de riesgo aplicables, y ampliar el mismo patron a otras licencias o actuaciones del corredor. Que sigue: priorizar expedientes con licencia, reparcelacion o urbanizacion en Paiporta/Picanya/Catarroja y bajar despues a disciplina/restauracion y mantenimiento. Evidencia: `etl/data/seeds/responsibility_explainer_cases_seed_v1.json`, `docs/gh-pages/responsibility-explainer/data/manifest.json`, `docs/gh-pages/responsibility-explainer/data/dana-valencia-2024.json`. - **DANA: cadena regulatoria municipal sobre el corredor de Paiporta** (`2026-04-12`): el benchmark añade ya actos regulatorios y de licencia concretos sobre el mismo ambito de Paiporta, no solo proyectos y planes tecnicos. El caso `dana-valencia-2024` sube a `structural_evidence_rows_total=18`, con distribucion `6` filas de planes municipales, `5` de infraestructura hidraulica y drenaje, `2` de planeamiento del corredor del Poyo, `4` de proyectos/expedientes de urbanizacion y `1` de licencias/usos vulnerables. La nueva evidencia publicada bajo control del repo fija una cadena normativa y administrativa reproducible: acuerdo plenario de `24/09/2020` que suspende licencias de parcelacion, edificacion y demolicion en el ambito `Pintor Benedito / Poeta Llorente / Enrique Reig`; PAI registrado el `07/03/2024` que se tramita sin `Evaluacion Ambiental y Territorial Estrategica` por presentarse como documento de gestion que no innova planeamiento y que limita la `integracion con el entorno` a conexiones de servicios y continuidad de la urbanizacion; y el mismo PAI que atribuye al Ayuntamiento las obras externas de conexion e integracion del ambito. Delta visible publicada: el benchmark ya puede responder parcialmente no solo que habia riesgo y proyectos previos, sino tambien que existieron potestades municipales concretas sobre licencias, encaje regulatorio y reparto de responsabilidad sobre infraestructura del corredor antes de la DANA. Donde estamos ahora: la slice ya baja a decisiones regulatorias municipales verificables y abre por fin el target `licencias y usos vulnerables`, que estaba formulado pero vacio. Hacia donde vamos: cruzar esta cadena con expedientes de licencia, reparcelacion, recepcion de obras, disciplina o mantenimiento para pasar de `el ayuntamiento tenia estas potestades y adopto estos actos` a `estos expedientes concretos permitieron, limitaron o toleraron tal exposicion`. Que sigue: priorizar el expediente completo del PAI/reparcelacion de Paiporta y despues extender el mismo patron a Picanya/Catarroja/Alfafar. Evidencia: `etl/data/seeds/responsibility_explainer_cases_seed_v1.json`, `docs/gh-pages/responsibility-explainer/data/manifest.json`, `docs/gh-pages/responsibility-explainer/data/dana-valencia-2024.json`. - **DANA: cronologia administrativa de Paiporta hasta despues del desastre** (`2026-04-12`): el benchmark ya no solo expone reglas y actos de licencia, sino tambien el estado material y la cronologia de la actuacion urbanizadora del mismo corredor cuando llego la DANA. El caso `dana-valencia-2024` sube a `structural_evidence_rows_total=21`, con distribucion `6` filas de planes municipales, `5` de infraestructura hidraulica y drenaje, `2` de planeamiento del corredor del Poyo, `7` de proyectos/expedientes de urbanizacion y `1` de licencias/usos vulnerables. La nueva evidencia publicada bajo control del repo fija tres hechos adicionales: el proyecto de reparcelacion registrado el `07/03/2024` seguia describiendo el ambito como un espacio `sin abrir ni urbanizar`, el Ayuntamiento figuraba dentro de la operacion como propietario expropiador, pagador de urbanizacion y beneficiario de aprovechamiento libre de cargas, y la aprobacion plenaria del PAI por gestion directa no llego hasta el `28/11/2024`, es decir, despues del episodio del `29/10/2024`. Delta visible publicada: el benchmark ya puede responder parcialmente no solo `que reglas y potestades existian`, sino tambien `en que estado administrativo y material estaba la actuacion cuando ocurrio el desastre`. Donde estamos ahora: la cadena de responsabilidad pre-DANA ya permite formular una hipotesis auditable de falta de preparacion material y de demora administrativa sobre el mismo ambito, sin fingir aun causalidad judicial o tecnica cerrada. Hacia donde vamos: enlazar esta cronologia con recepcion de obras, licencias concretas, alegaciones tecnicas, disciplina/restauracion y mantenimiento para cerrar el salto desde `expediente pendiente` a `exposicion efectivamente tolerada`. Que sigue: abrir el expediente completo de reparcelacion/alegaciones de Paiporta y despues contrastarlo con Picanya/Catarroja/Alfafar. Evidencia: `etl/data/seeds/responsibility_explainer_cases_seed_v1.json`, `docs/gh-pages/responsibility-explainer/data/manifest.json`, `docs/gh-pages/responsibility-explainer/data/dana-valencia-2024.json`. - **DANA: objeciones de la operadora del agua aceptadas en el expediente de Paiporta** (`2026-04-12`): el benchmark ya incorpora una capa mas concreta sobre si la infraestructura de abastecimiento y la obra urbanizadora estaban realmente preparadas antes del desastre. El caso `dana-valencia-2024` sube a `structural_evidence_rows_total=23`, con distribucion `6` filas de planes municipales, `6` de infraestructura hidraulica y drenaje, `2` de planeamiento del corredor del Poyo, `8` de proyectos/expedientes de urbanizacion y `1` de licencias/usos vulnerables. La nueva evidencia publicada procede del acuerdo plenario de aprobacion del PAI de Paiporta de `28/11/2024`: las alegaciones de `Global Omnium` fueron estimadas y obligaban a sustituir tramos de fibrocemento, incluir hidrantes y valvulas, y prever una tuberia provisional, catas manuales y georradar para garantizar el suministro durante las obras; ademas, el proyecto de urbanizacion quedo aprobado de forma condicionada a incorporar esos aspectos. Delta visible publicada: el benchmark ya puede responder con un expediente oficial que la propia actuacion urbanizadora seguia arrastrando correcciones materiales sobre la red de agua y la ejecucion de obra. Donde estamos ahora: la cadena de responsabilidad pre-DANA ya no depende solo de planeamiento abstracto o de calendarios, sino tambien de objeciones tecnicas aceptadas sobre la infraestructura del corredor. Hacia donde vamos: bajar del expediente marco a los documentos de obra, recepcion, licencias concretas y mantenimiento para ver si estas correcciones llegaron a ejecutarse o si el corredor siguio expuesto. Que sigue: abrir el proyecto de urbanizacion modificado y cualquier recepcion de obra o certificacion posterior en Paiporta, y despues extender el mismo patron a otros municipios de l'Horta Sud. Evidencia: `etl/data/seeds/responsibility_explainer_cases_seed_v1.json`, `docs/gh-pages/responsibility-explainer/data/manifest.json`, `docs/gh-pages/responsibility-explainer/data/dana-valencia-2024.json`. - **DANA: el propio registro municipal de Paiporta seguia sin reflejar cierre formal del PAI en 2025** (`2026-04-12`): el benchmark avanza de alegaciones tecnicas aceptadas a estado oficial de ejecucion/cierre del mismo corredor. El caso `dana-valencia-2024` sube a `structural_evidence_rows_total=25`, con distribucion `6` filas de planes municipales, `6` de infraestructura hidraulica y drenaje, `2` de planeamiento del corredor del Poyo, `10` de proyectos/expedientes de urbanizacion y `1` de licencias/usos vulnerables. La nueva evidencia publicada procede del `Registro Municipal de Programas y AIU` de Paiporta para el expediente `557433N`: la ficha oficial recoge la aprobacion del programa el `28/11/2024` y su inscripcion en el registro municipal el `06/06/2025`, pero mantiene en blanco la fecha del acta de replanteo, la fecha del acta de recepcion de las obras, la fecha de inscripcion registral de la reparcelacion y la fecha de liquidacion definitiva. Delta visible publicada: el benchmark ya puede responder con una fuente oficial municipal que incluso meses despues del episodio del `29/10/2024` el propio Ayuntamiento seguia sin reflejar en su registro hitos formales de arranque, recepcion o cierre reparcelatorio del ambito. Donde estamos ahora: la cadena pre-DANA ya no solo muestra reglas, planeamiento y objeciones tecnicas, sino tambien un indicio formal de que la actuacion no aparecia cerrada en el propio registro municipal. Hacia donde vamos: contrastar este estado formal con proyecto modificado, certificaciones de obra, recepcion efectiva, licencias concretas y mantenimiento para ver si hubo ejecucion material no reflejada o si la exposicion siguio abierta tambien en la practica. Que sigue: abrir cualquier `acta de replanteo`, `acta de recepcion`, proyecto modificado o certificado posterior asociado al expediente `557433N` y despues replicar el mismo patron sobre Picanya/Catarroja. Evidencia: `etl/data/seeds/responsibility_explainer_cases_seed_v1.json`, `docs/gh-pages/responsibility-explainer/data/manifest.json`, `docs/gh-pages/responsibility-explainer/data/dana-valencia-2024.json`. - **DANA: los proyectos tecnicos de marzo de 2024 seguian mostrando urbanizacion sectorial pendiente en Paiporta** (`2026-04-12`): el benchmark ya no se limita al acuerdo del PAI o al registro, sino que baja a proyectos tecnicos concretos del mismo corredor. El caso `dana-valencia-2024` sube a `structural_evidence_rows_total=28`, con distribucion `6` filas de planes municipales, `7` de infraestructura hidraulica y drenaje, `2` de planeamiento del corredor del Poyo, `12` de proyectos/expedientes de urbanizacion y `1` de licencias/usos vulnerables. La nueva evidencia publicada procede de tres documentos oficiales de Paiporta registrados el `07/03/2024`: el proyecto de urbanizacion seguia conectandose a una tuberia existente de fibrocemento en Poeta Llorente y afirmaba que no eran necesarios nuevos hidrantes en el ambito; un proyecto tecnico separado de alumbrado exterior describia la ampliacion de la red subterranea y de cuatro circuitos de luminarias para los tramos sin urbanizar de Poeta Llorente y Pintor Benedito hasta Enrique Reig; y un proyecto especifico del nuevo `CT Pintor Benedito` de `400 KVA` situaba un nuevo centro de transformacion para alimentar electricamente esa misma urbanizacion pendiente. Delta visible publicada: el benchmark ya puede responder con actos tecnicos concretos que incluso en marzo de 2024 el corredor seguia necesitando obra sectorial basica en agua, alumbrado y suministro electrico, y que el diseno del agua potable todavia se apoyaba en red existente y descartaba nuevos hidrantes. Donde estamos ahora: la cadena pre-DANA combina ya planeamiento, cronologia administrativa, objeciones tecnicas aceptadas, estado formal no cerrado y proyectos sectoriales que seguian tratando de completar la urbanizacion material del ambito. Hacia donde vamos: contrastar este diseno inicial con las correcciones posteriores y con cualquier replanteo, certificacion o recepcion de obra para ver si esas carencias llegaron a corregirse antes o despues del `29/10/2024`. Que sigue: localizar el proyecto modificado, certificaciones de obra o cualquier `acta de replanteo`/`recepcion` del expediente `557433N`, y despues repetir el mismo patron de auditoria material en Picanya/Catarroja. Evidencia: `etl/data/seeds/responsibility_explainer_cases_seed_v1.json`, `docs/gh-pages/responsibility-explainer/data/manifest.json`, `docs/gh-pages/responsibility-explainer/data/dana-valencia-2024.json`. - **Responsibility explainer: cronologia de avisos como lane reusable** (`2026-04-12`): el benchmark deja de tratar `warning_timeline` como un hueco opaco y pasa a tener un contrato reutilizable en SQLite y en el snapshot publico. Se añade la tabla `responsibility_explainer_warning_timeline_events`, el importador `scripts/import_responsibility_explainer_seed.py` ya la materializa, y `scripts/export_responsibility_explainer_snapshot.py` la publica en `normative_evidence.warning_timeline_events` y usa su recuento para el estado de la pregunta `warning_timeline`. El caso `dana-valencia-2024` ya publica `3` eventos oficiales normalizados de AEMET (`2024-10-27 11:33`, `2024-10-29 07:36`, `2024-10-29 09:41`) tomados del estudio oficial de AEMET sobre el episodio del `28/10/2024` al `04/11/2024`, con `warning_timeline_events_total=3` y `question_status_counts={partial: 6, missing: 2}` tanto en `docs/gh-pages/responsibility-explainer/data/dana-valencia-2024.json` como en la copia publica de `ui/gh-pages-next/public/responsibility-explainer/data/dana-valencia-2024.json`. Donde estamos ahora: la pregunta de avisos deja de depender solo de canales abstractos y ya tiene primeras filas reproducibles de cronologia oficial. Hacia donde vamos: ampliar esta lane a `CHJ` y `Proteccion Civil/112CV` y luego enlazarla con el grafo de deber de actuar y con la cadena de decisiones operativas. Que sigue: abrir importadores/review para mas eventos oficiales y usar esta misma tabla para futuras crisis o casos territoriales sin rediseñar el producto. Evidencia: `etl/data/seeds/responsibility_explainer_cases_seed_v1.json`, `scripts/import_responsibility_explainer_seed.py`, `scripts/export_responsibility_explainer_snapshot.py`, `docs/gh-pages/responsibility-explainer/data/manifest.json`, `docs/gh-pages/responsibility-explainer/data/dana-valencia-2024.json`. - **Responsibility explainer: ledger generico de accountability** (`2026-04-13`): el surface ya no organiza el caso solo como desastre/parlamento/estructura, sino tambien como primitives reutilizables para otros fallos publicos, incluidas capturas regulatorias, corrupcion urbanistica o decisiones de oligopolio. Se añaden tablas genericas `responsibility_explainer_governing_rules`, `responsibility_explainer_official_findings`, `responsibility_explainer_administrative_acts` y `responsibility_explainer_responsibility_links`; el importador seed ya las materializa y el exportador las publica en `accountability_ledger`. El caso `dana-valencia-2024` ahora expone `governing_rules_total=3`, `official_findings_total=3`, `administrative_acts_total=3` y `responsibility_links_total=3`, visibles en la pagina publica `/responsibility-explainer/dana-valencia-2024/` como un ledger de reglas, hallazgos, actos y cadenas, además de la evidencia estructural existente. Donde estamos ahora: el modelo del producto ya puede representar no solo "que paso" o "que riesgo habia", sino "que regla gobernaba", "que hallazgo oficial existia", "que acto administrativo ocurrio" y "que actor quedaba en la cadena de responsabilidad". Hacia donde vamos: reutilizar este mismo ledger para casos no centrados en desastres, como concesiones, captura regulatoria, urbanismo de riesgo, favoritismo contractual u oligopolios con respaldo administrativo. Que sigue: poblar estas tablas desde ingestas/reviews especificos en vez de seed manual y abrir un segundo caso que no sea DANA para probar portabilidad real. Evidencia: `scripts/export_responsibility_explainer_snapshot.py`, `scripts/import_responsibility_explainer_seed.py`, `ui/gh-pages-next/app/responsibility-explainer/[caseId]/page.js`, `docs/gh-pages/responsibility-explainer/data/dana-valencia-2024.json`. - **Responsibility explainer: lane reusable de reviewed batches + primer caso P2 no-DANA** (`2026-04-13`): el ledger deja de depender solo de seed manual y pasa a tener una lane reproducible `reviewed CSV -> SQLite -> snapshot publico` mediante `scripts/apply_responsibility_ledger_reviews.py`, aplicada automaticamente en `just responsibility-explainer-next-prime` sobre `etl/data/manual/responsibility_explainer/reviewed_ledger_batches/`. Esto se usa para publicar el primer benchmark formal no centrado en desastre, `cash-payment-limit-spain`, alineado con el gate explicito de `ROADMAP.md` para `P2` ("quien es responsable de limitar pagos en efectivo a empresas a 1k euro"). Estado actual publicado: `2` casos en `docs/gh-pages/responsibility-explainer/data/manifest.json`; el nuevo caso fiscal sale con `governing_rules_total=2`, `official_findings_total=2`, `administrative_acts_total=2`, `responsibility_links_total=3` y `question_status_counts={partial: 4, missing: 2}` en `docs/gh-pages/responsibility-explainer/data/cash-payment-limit-spain.json`. Delta visible publicada: la UI deja de asumir que todo caso es una catastrofe y prerenderiza tambien `/responsibility-explainer/cash-payment-limit-spain`, con copy y secciones ya mas genericas para reglas, actos y cadenas. Donde estamos ahora: el producto ya demuestra portabilidad real del modelo hacia una pregunta estatal de atribucion formal, no solo hacia un benchmark de desastre. Hacia donde vamos: conectar esta misma lane a importadores que produzcan reviewed batches para licencias, sanciones, contratos, concesiones o reglas sectoriales sin tocar el exportador por caso. Que sigue: anadir un tercer benchmark no-DANA orientado a implementacion/enforcement real (por ejemplo licencias, sanciones o contratacion) y abrir ingestas que alimenten `official_findings`, `administrative_acts` y `responsibility_links` sin escritura manual directa del CSV. Evidencia: `scripts/apply_responsibility_ledger_reviews.py`, `etl/data/manual/responsibility_explainer/reviewed_ledger_batches/20260413-cash-payment-limit-spain.csv`, `docs/gh-pages/responsibility-explainer/data/manifest.json`, `docs/gh-pages/responsibility-explainer/data/cash-payment-limit-spain.json`, `ui/gh-pages-next/app/responsibility-explainer/[caseId]/page.js`. - **Catalogo publico del universo de scraping** (`2026-04-13`): la plataforma ya publica el universo de fuentes como artefacto reutilizable y no solo como panel interno. Se anade `build_source_catalog_payload()` en `scripts/graph_ui_server.py`, endpoint `/api/sources/catalog`, exportador `scripts/export_source_catalog_snapshot.py` y wiring en `just explorer-gh-pages-build` + `just etl-publish-hf*` para materializar `docs/gh-pages/explorer-sources/data/catalog.json`, `etl/data/published/source-catalog-.json` y `etl/data/published/source-catalog-latest.json`. El catalogo nace del mismo motor que `/api/sources/status`, pero lo reorganiza como contrato publico estable por `source_id`: `domain`, `scope`, `warehouse`, estado operativo, mismatch/bloqueo y metadata legal de reutilizacion. Estado real sobre `etl/data/staging/politicos-es.e2e19.db`: `sources_total=45`, `in_db_total=27`, `with_network_total=25`, `blocked_total=3`, `mismatch_total=23`. Donde estamos ahora: el repo ya puede exponer un inventario versionado de las fuentes deseadas, no solo tablas derivadas o dashboards HTML. Hacia donde vamos: usar este catalogo como rail generico para escalar scraping por organismo, medir cobertura real y abrir lanes de captura sin perder trazabilidad de legalidad, bloqueo y estado. Que sigue: enganchar lotes dinamicos/colas de scraping al catalogo y priorizar fuentes faltantes o degradadas por `scope`, `domain` y `blocked_total` en vez de abrir conectores a mano sin registro comun. Publicacion HF: el host-side `publicar_hf_snapshot.py --dry-run --skip-parquet` ya empaqueta el catalogo, pero el runbook canonico `just etl-publish-hf-dry-run` sigue bloqueado por otra gate del release actual: tras regenerar `etl/data/published/votaciones-kpis-es-2026-04-13.json`, el siguiente freno es `liberty-restrictions-atlas-release-latest.json` apuntando todavia a `snapshot_date=2026-03-05` en vez de `2026-04-13`. Evidencia: `scripts/export_source_catalog_snapshot.py`, `docs/gh-pages/explorer-sources/data/catalog.json`, `etl/data/published/source-catalog-latest.json`, `etl/data/published/votaciones-kpis-es-2026-04-13.json`. - **Cierre publish HF 2026-04-13 + Atlas de Restricciones** (`2026-04-24`): se cierra el bloqueo descrito arriba para el release `2026-04-13`. Donde estamos ahora: `liberty-restrictions-atlas-release-latest.json` local y GH Pages apuntan a `snapshot_date=2026-04-13`, el DB `etl/data/staging/politicos-es.e2e19.db` ya contiene la semilla `Derechos` (`liberty_restriction_assessments=11`, `legal_fragment_responsibilities=15`, `liberty_proportionality_reviews=8`, `liberty_enforcement_observations=16`, `liberty_delegated_enforcement_links=8`) y `just etl-publish-hf-dry-run` empaqueta correctamente `15` artefactos publicados, `91` tablas parquet y `96` ficheros parquet. El publish real `just etl-publish-hf` quedo subido a Hugging Face y `latest.json` remoto ya apunta a `2026-04-13`; el heartbeat post-HF queda `status=ok` con paridad `published/GH Pages/HF`, sin drift, stale ni `hf_unavailable`, y la ventana strict activa queda `status=ok`. Se corrigio ademas el generador de README HF para emitir el literal contractual `Vote gate:` esperado por `verify_hf_snapshot_quality.py`, y el verificador remoto queda en verde. Hacia donde vamos: mantener el alias Atlas y `latest.json` HF sincronizados en cada snapshot, sin volver a publicar releases con README que no pase contrato. Que sigue: recuperar cobertura real de `citizen` en el snapshot principal (`topic_set_topics`/`topic_positions` para `topic_set_id=1` o `CITIZEN_DB_PATH`) y despues publicar GH Pages si el surface cambia. Evidencia: `docs/etl/sprints/AI-OPS-554/exports/liberty_restrictions_snapshot_2026-04-13.json`, `docs/etl/sprints/AI-OPS-554/evidence/liberty_atlas_publish_2026-04-13.json`, `docs/etl/sprints/AI-OPS-554/evidence/liberty_atlas_changelog_continuity_2026-04-13.json`, `docs/etl/sprints/AI-OPS-554/evidence/liberty_atlas_release_heartbeat_post_hf_2026-04-13.json`, `docs/etl/sprints/AI-OPS-554/evidence/liberty_atlas_release_heartbeat_window_post_hf_2026-04-13.json`, `etl/data/published/liberty-restrictions-atlas-release-latest.json`. - **Versionado mínimo del texto votado** (`2026-03-12`): se añadió una capa temporal explícita para responder “qué texto existía cuando se votó” sin sobreatribuir el texto final a votos intermedios. Nuevas tablas: `parl_initiative_text_versions` (snapshot oficial por doc) y `parl_vote_event_text_versions` (mejor enlace voto->snapshot por iniciativa, con `link_method` + `confidence`). Backfill real sobre `congreso_iniciativas` vote-linked con `doc_kind='bocg'`: `104` iniciativas, `178` snapshots materializados y `686` enlaces primarios voto->texto (`534` `single_version`, `69` `latest_prior_published_version`, `60` `initial_version_for_intro_vote`, `22` `latest_prior_stage_match`, `1` `fallback_latest_version`). El exportador de colas ciudadanas ya incorpora `text_versions`, `vote_text_versions` y `recommended_text_*` dentro de `key_vote_candidates`, y el siguiente lote version-aware de consumo ya quedó preparado en `tmp/codex-subagents/initiative-measures/20260312-versioned-consumer-batch/queue.csv` (`8` dossiers). Esto no resuelve todavía granularidad artículo/enmienda, pero sí elimina el principal error de mezclar por defecto el texto más reciente con votos anteriores dentro del mismo expediente. - **Recuperación masiva Senado + lote transporte** (`2026-03-12`): la ficha HTML de iniciativas del Senado ya contenía BOCG/DS reales que no estaban entrando en `links_bocg_json`/`links_ds_json`; se reaprovechó el backfill existente para harvestarlos a escala y luego se corrigió el gate de `only_missing` en `backfill_initiative_documents_from_parl_initiatives` para que un `selected_doc_entry_keys` explícito no quede bloqueado cuando la iniciativa ya tenía otros docs descargados. Resultado aplicado en staging: `575` iniciativas Senado enriquecidas con `2486` URLs BOCG y `444` URLs DS adicionales desde docs ya descargados; fetch dirigido posterior de `12` PDFs BOCG nuevos para `622/000096`, `622/000030` y `624/000014`; refresco de extracción con `12/12` nuevos docs en `pdf_text_good`; y batch `gpt-5.3-codex-spark` `xhigh` sobre bundles renovados que resolvió `4` dossiers Senado (`622/000096`, `622/000030`, `624/000014`, `621/000027`) e insertó `13` medidas nuevas. Estado tras apply: `senado_iniciativas` queda en `10` dossiers resueltos / `24` measure points / `637` tareas pendientes. Siguiente paso controlable: repetir el patrón de “harvest publication links -> fetch PDFs -> extract text -> worker batch” sobre la siguiente cohorte de movilidad/energía antes de abrir otro surface. Evidencia operativa: `tmp/codex-subagents/initiative-measures/20260312-targeted-senado-fetch-summary.json`, `tmp/codex-subagents/initiative-measures/20260312-targeted-senado-extractions-summary.json`, `tmp/codex-subagents/initiative-measures/20260312-topic-batch-transport-workers/apply_summary.json`, `tmp/codex-subagents/initiative-measures/selected_scope_missing_extra_test.txt`. Próximo arranque recomendado (derechos): ```bash DB=etl/data/staging/politicos-es.db SNAPSHOT_DATE=2026-03-05 set -e DB_PATH="$DB" SNAPSHOT_DATE="$SNAPSHOT_DATE" just parl-liberty-restrictions-pipeline DB_PATH="$DB" SNAPSHOT_DATE="$SNAPSHOT_DATE" just parl-report-liberty-restrictions-status DB_PATH="$DB" SNAPSHOT_DATE="$SNAPSHOT_DATE" just parl-report-liberty-direct-accountability-scores DB_PATH="$DB" SNAPSHOT_DATE="$SNAPSHOT_DATE" just parl-report-liberty-indirect-accountability-status DB_PATH="$DB" SNAPSHOT_DATE="$SNAPSHOT_DATE" just parl-report-liberty-personal-accountability-scores DB_PATH="$DB" SNAPSHOT_DATE="$SNAPSHOT_DATE" just parl-export-liberty-restrictions-snapshot DB_PATH="$DB" SNAPSHOT_DATE="$SNAPSHOT_DATE" just parl-publish-liberty-atlas-artifacts DB_PATH="$DB" SNAPSHOT_DATE="$SNAPSHOT_DATE" just etl-publish-hf-dry-run DB_PATH="$DB" SNAPSHOT_DATE="$SNAPSHOT_DATE" just parl-report-liberty-atlas-release-heartbeat --allow-hf-unavailable DB_PATH="$DB" SNAPSHOT_DATE="$SNAPSHOT_DATE" just parl-check-liberty-atlas-release-heartbeat-window ``` Control de cierre mínimo: ```bash sqlite3 "$DB" "SELECT 'liberty_restriction_assessments' AS table, COUNT(*) FROM liberty_restriction_assessments;" sqlite3 "$DB" "SELECT 'liberty_right_categories' AS table, COUNT(*) FROM liberty_right_categories;" sqlite3 "$DB" "SELECT 'liberty_proportionality_reviews' AS table, COUNT(*) FROM liberty_proportionality_reviews;" sqlite3 "$DB" "SELECT 'liberty_enforcement_observations' AS table, COUNT(*) FROM liberty_enforcement_observations;" sqlite3 "$DB" "SELECT 'liberty_indirect_responsibility_edges' AS table, COUNT(*) FROM liberty_indirect_responsibility_edges;" sqlite3 "$DB" "SELECT 'responsibility_score' AS table, COUNT(*) FROM responsibility_score;" ``` Atajos: - Estado (SQL vs tracker): `just etl-tracker-status` - Gate default estricto (usa `docs/etl/mismatch-waivers.json`; falla por `mismatch` no waived, waiver `expired`, o `DONE` sin red real): `just etl-tracker-gate` - Gate legacy (compatibilidad histórica; ignora política de mismatches y solo valida `DONE` sin red real): `just etl-tracker-gate-legacy` Próximo bloque operativo recomendado (histórico; revisar solo tras cerrar `Derechos`): - Contratación pública (PLACSP): detalle y documentación -> `python3 scripts/ingestar_politicos_es.py backfill-placsp-contract-details ...` -> `python3 scripts/ingestar_politicos_es.py backfill-money-contract-records ...` -> `just etl-tracker-status`. - Al terminar, validar cierre con SQL de cobertura: - `sqlite3 etl/data/staging/politicos-es.db "select source_id, count(*) from placsp_contract_detail_records group by source_id;"` - `sqlite3 etl/data/staging/politicos-es.db "select source_id, count(*) from placsp_contract_detail_documents group by source_id;"` - `sqlite3 etl/data/staging/politicos-es.db "select source_id, count(*) from money_contract_records where source_id in ('placsp_sindicacion','placsp_autonomico') group by source_id;"` - `sqlite3 etl/data/staging/politicos-es.db "select source_id, count(*) from policy_events where source_id in ('placsp_sindicacion','placsp_autonomico') and source_record_pk is not null group by source_id;"` Runbook rápido (histórico; ejecutar solo en cola no-`Derechos`): ```bash DB=etl/data/staging/politicos-es.db python3 scripts/ingestar_politicos_es.py backfill-placsp-contract-details \ --db "$DB" --source-ids placsp_sindicacion placsp_autonomico \ --strict-network --only-missing python3 scripts/ingestar_politicos_es.py backfill-money-contract-records \ --db "$DB" --source-ids placsp_sindicacion placsp_autonomico SNAPSHOT_DATE=2026-02-12 DB_PATH="$DB" just etl-tracker-status # Criterio mínimo de cierre (ambos source_id con al menos 1 fila) sqlite3 "$DB" "SELECT COUNT(*) FROM placsp_contract_detail_records WHERE source_id='placsp_sindicacion';" sqlite3 "$DB" "SELECT COUNT(*) FROM placsp_contract_detail_records WHERE source_id='placsp_autonomico';" sqlite3 "$DB" "SELECT COUNT(*) FROM placsp_contract_detail_documents WHERE source_id='placsp_sindicacion';" sqlite3 "$DB" "SELECT COUNT(*) FROM placsp_contract_detail_documents WHERE source_id='placsp_autonomico';" sqlite3 "$DB" "SELECT COUNT(*) FROM money_contract_records WHERE source_id='placsp_sindicacion';" sqlite3 "$DB" "SELECT COUNT(*) FROM money_contract_records WHERE source_id='placsp_autonomico';" sqlite3 "$DB" "SELECT COUNT(*) FROM policy_events WHERE source_id='placsp_sindicacion' AND source_record_pk IS NOT NULL;" sqlite3 "$DB" "SELECT COUNT(*) FROM policy_events WHERE source_id='placsp_autonomico' AND source_record_pk IS NOT NULL;" ``` Prioridad operativa inmediata (histórica; no activa en este arranque): - P1: cerrar `Contratacion detalle y documentacion de licitaciones (PLACSP)` (script de detalle + mapping money + `policy_events`) -> `SNAPSHOT_DATE=2026-02-12 just etl-tracker-status`. - P2: terminar contratos de indicadores con bloqueos técnicos: `Indicadores (confusores): AEMET` (requiere token/contrato) y avanzar `ESIOS/REE` para completar `Outcomes`. - P3: completar `UE: contratacion publica` + `UE: legislacion y documentos` una vez estabilizado el canal de red/credentials (siguiente unidad funcional para salir de deuda externa). Runbook de arranque recomendado (P1→P3): ```bash # P2 (siempre antes de avanzar deuda de Outcomes): AEMET_API_KEY= python3 scripts/ingestar_politicos_es.py ingest \ --db etl/data/staging/politicos-es.db \ --source aemet_opendata_series \ --url https://opendata.aemet.es/opendata/api/observacion/convencional/todas \ --snapshot-date 2026-02-17 \ --strict-network --timeout 30 python3 scripts/ingestar_politicos_es.py ingest \ --db etl/data/staging/politicos-es.db \ --source bde_series_api \ --url 'https://app.bde.es/bierest/resources/srdatosapp/listaSeries?idioma=es&series=D_1NBAF472&rango=30M' \ --snapshot-date 2026-02-17 \ --strict-network --timeout 30 ``` ```bash # P3 (ESIOS/REE con conector definido; requiere canal estable/token si aplica): python3 scripts/ingestar_politicos_es.py ingest \ --db etl/data/staging/politicos-es.db \ --source ree_esios_indicators \ --snapshot-date \ --strict-network --timeout 30 # Sin capture oficial real disponible: no ejecutar fallback ni cargar filas. ``` Roadmap ejecutable de scraping/processing para el bloque pendiente (histórico): 1. Prioridad 1: completar `Contratacion detalle y documentacion de licitaciones (PLACSP)` con detalle + documentos y verificar trazabilidad de `money_contract_records` + `policy_events`. Comando de arranque: `python3 scripts/ingestar_politicos_es.py backfill-placsp-contract-details --db etl/data/staging/politicos-es.db --source-ids placsp_sindicacion placsp_autonomico --strict-network --only-missing`. 2. Prioridad 2: cerrar cobertura de campos de outcomes técnicos (`Indicadores (confusores): AEMET` y `Banco de Espana`), y mapear a estado/indicadores operativos cuando existan credenciales válidas. Comando de arranque: `python3 scripts/ingestar_politicos_es.py ingest --db etl/data/staging/politicos-es.db --source aemet_opendata_series --url https://opendata.aemet.es/opendata/api/observacion/convencional/todas --snapshot-date 2026-02-17 --strict-network --timeout 30`. 3. Prioridad 3: avanzar `UE: contratacion publica`/`UE: legislacion y documentos` solo cuando el canal UE esté estable y reproducible con source_id definidos. Comando provisional: `DB_PATH=etl/data/staging/politicos-es.db just etl-tracker-gate --focus gate`. Arranque por prioridad (sin mezcla de bloques): ```bash # P1 SOLO (siempre primero) python3 scripts/ingestar_politicos_es.py backfill-placsp-contract-details --db etl/data/staging/politicos-es.db --source-ids placsp_sindicacion placsp_autonomico --strict-network --only-missing python3 scripts/ingestar_politicos_es.py backfill-money-contract-records --db etl/data/staging/politicos-es.db --source-ids placsp_sindicacion placsp_autonomico ``` ```bash # P2 SOLO (si P1 ya quedó limpio) AEMET_API_KEY="" python3 scripts/ingestar_politicos_es.py ingest --db etl/data/staging/politicos-es.db --source aemet_opendata_series --url https://opendata.aemet.es/opendata/api/observacion/convencional/todas --snapshot-date 2026-02-17 --strict-network --timeout 30 python3 scripts/ingestar_politicos_es.py ingest --db etl/data/staging/politicos-es.db --source bde_series_api --url 'https://app.bde.es/bierest/resources/srdatosapp/listaSeries?idioma=es&series=D_1NBAF472&rango=30M' --snapshot-date 2026-02-17 --strict-network --timeout 30 ``` ```bash # P3 SOLO (ESIOS/REE) python3 scripts/ingestar_politicos_es.py ingest --db etl/data/staging/politicos-es.db --source ree_esios_indicators --snapshot-date --strict-network --timeout 30 # Si persiste bloqueo upstream, registrar evidencia y mantener cero filas. No hay fallback. ``` Checklist de verificación para la fila de contratos (mínimo de cierre): ```bash DB=etl/data/staging/politicos-es.db sqlite3 "$DB" "SELECT source_id, count(*) AS detail_rows FROM placsp_contract_detail_records WHERE source_id IN ('placsp_sindicacion', 'placsp_autonomico') GROUP BY source_id;" sqlite3 "$DB" "SELECT source_id, count(*) AS detail_docs FROM placsp_contract_detail_documents WHERE source_id IN ('placsp_sindicacion', 'placsp_autonomico') GROUP BY source_id;" sqlite3 "$DB" "SELECT source_id, count(*) AS money_rows FROM money_contract_records WHERE source_id IN ('placsp_sindicacion', 'placsp_autonomico') GROUP BY source_id;" sqlite3 "$DB" "SELECT source_id, count(*) AS policy_events_linked FROM policy_events WHERE source_id IN ('placsp_sindicacion', 'placsp_autonomico') AND source_record_pk IS NOT NULL GROUP BY source_id;" ``` Cheat sheet de calidad mínima para la fase de limpieza: ```bash DB=etl/data/staging/politicos-es.db sqlite3 "$DB" "SELECT source_id, count(*) AS no_null_source_record FROM placsp_contract_detail_records WHERE source_id IN ('placsp_sindicacion','placsp_autonomico') AND source_record_pk IS NULL GROUP BY source_id;" sqlite3 "$DB" "SELECT source_id, count(*) AS dup_detail_rows FROM placsp_contract_detail_records GROUP BY source_id, source_record_id HAVING COUNT(*)>1;" sqlite3 "$DB" "SELECT source_id, count(*) AS orphan_money_rows FROM money_contract_records WHERE source_id IN ('placsp_sindicacion','placsp_autonomico') AND source_record_pk IS NULL;" sqlite3 "$DB" "SELECT 'policy_events' AS tabla, source_id, count(*) AS orphan_policy_rows FROM policy_events WHERE source_id IN ('placsp_sindicacion','placsp_autonomico') AND source_record_pk IS NULL GROUP BY source_id;" ``` Criterio de salida de limpieza: todos los cuatro checks anteriores deben devolver `0` en ambas entidades (`placsp_sindicacion`, `placsp_autonomico`) salvo casos esperados documentados en notas de tracker. Flujo operativo recomendado (extract → process → clean → structure) para el bloque próximo: Scrape: `backfill-placsp-contract-details` completa `placsp_contract_detail_records` + `placsp_contract_detail_documents` para ambos source_id. Process: `backfill-money-contract-records` materializa `money_contract_records` y empuja trazabilidad a `policy_events.source_record_pk`. Clean: `just etl-tracker-status` confirma `records_loaded > 0`, y validación manual de outliers en `placsp_contract_detail_records`/`placsp_contract_detail_documents` por `source_record_pk` nulo o duplicados. Structure: export final de `policy_events` + `money_contract_records` entra en el mismo DB path; siguiente control es `just etl-tracker-gate` y evidencias en JSON/snapshots cuando aplique. Próximo arranque único recomendado (copy/paste): ```bash DB=etl/data/staging/politicos-es.db SNAPSHOT_DATE=2026-02-17 set -e python3 scripts/ingestar_politicos_es.py backfill-placsp-contract-details --db "$DB" --source-ids placsp_sindicacion placsp_autonomico --strict-network --only-missing python3 scripts/ingestar_politicos_es.py backfill-money-contract-records --db "$DB" --source-ids placsp_sindicacion placsp_autonomico AEMET_API_KEY= python3 scripts/ingestar_politicos_es.py ingest --db "$DB" --source aemet_opendata_series --url https://opendata.aemet.es/opendata/api/observacion/convencional/todas --snapshot-date "$SNAPSHOT_DATE" --strict-network --timeout 30 python3 scripts/ingestar_politicos_es.py ingest --db "$DB" --source bde_series_api --url 'https://app.bde.es/bierest/resources/srdatosapp/listaSeries?idioma=es&series=D_1NBAF472&rango=30M' --snapshot-date "$SNAPSHOT_DATE" --strict-network --timeout 30 SNAPSHOT_DATE="$SNAPSHOT_DATE" DB_PATH="$DB" just etl-tracker-status ``` Esquema de control por etapa de corrida: ```text 1) Extract - Éxito esperado: run_id/row-count > 0 para cada comando (salvo AEMET/BDE si persiste token/endpoint issue). 2) Process - Éxito esperado: SQL de cobertura devuelve al menos 1 por `source_id` y `policy_events.source_record_pk` linkeados. 3) Clean - Éxito esperado: checklist de limpieza = 0 fuera de excepciones documentadas. 4) Structure - Éxito esperado: `just etl-tracker-status` y luego `just etl-tracker-gate` sin mismatch no-waive. ``` Semáforo y ruta de decisión (por lane/comando): ```text Comando - backfill-placsp-contract-details - backfill-money-contract-records - ingest aemet_opendata_series - ingest bde_series_api ``` Regla de estado: - `OK`: no hay acción adicional y sigues el siguiente comando del bloque. - `REINTENTAR` (máx. 1 vez): error transitorio de red/timeout; volver a correr el mismo comando. - `BLOQUEADO`: 401/403 persistente, payload invalido en strict, o dos intentos sin avance; parar esa lane y escalar. Fallback por lane: - `PLACSP`: - Si falla `backfill-placsp-contract-details` por red/transitorio, reintenta igual una vez. - Si persiste: ejecutar por un solo `source_id` (`placsp_sindicacion` o `placsp_autonomico`). - Si vuelve a fallar: registrar evidencia y pasar a la siguiente prioridad. - `AEMET`: - Reintentar 1 vez sólo si se confirma token/endpoint. - Si persiste auth/403 o payload inválido en strict: marcar `BLOQUEADO` y registrar blocker. - `BDE`: - Reintentar 1 vez si hubo timeout intermitente. - Si persiste 403/SSL/endpoint error, marcar `BLOQUEADO` y no avanzar ese lane a `DONE`. Ruta de decisión final de bloque: - Si todo en `OK` → `just etl-tracker-status` y `just etl-tracker-gate`, y si pasan: cerrar bloque como `DONE`. - Si hay `REINTENTAR` → ejecutar los reintentos definidos y volver a evaluar. - Si hay `BLOQUEADO` → evidencia en sprint + blocker si aplica, y mover estado a `PARTIAL/TODO` para continuar en la siguiente prioridad controlable. - Anti-loop: máximo 1 reintento estricto por fuente bloqueada por sprint. Cierre reproducible del bloque (copy/paste, fin de arranque): ```bash set -euo pipefail DB=etl/data/staging/politicos-es.db SNAPSHOT_DATE=2026-02-17 python3 scripts/ingestar_politicos_es.py backfill-placsp-contract-details \ --db "$DB" --source-ids placsp_sindicacion placsp_autonomico --strict-network --only-missing python3 scripts/ingestar_politicos_es.py backfill-money-contract-records \ --db "$DB" --source-ids placsp_sindicacion placsp_autonomico AEMET_API_KEY="" python3 scripts/ingestar_politicos_es.py ingest \ --db "$DB" --source aemet_opendata_series \ --url https://opendata.aemet.es/opendata/api/observacion/convencional/todas \ --snapshot-date "$SNAPSHOT_DATE" --strict-network --timeout 30 python3 scripts/ingestar_politicos_es.py ingest \ --db "$DB" --source bde_series_api \ --url 'https://app.bde.es/bierest/resources/srdatosapp/listaSeries?idioma=es&series=D_1NBAF472&rango=30M' \ --snapshot-date "$SNAPSHOT_DATE" --strict-network --timeout 30 SNAPSHOT_DATE="$SNAPSHOT_DATE" DB_PATH="$DB" just etl-tracker-status SNAPSHOT_DATE="$SNAPSHOT_DATE" DB_PATH="$DB" just etl-tracker-gate ``` Resultado esperado: - `etl-tracker-status` muestra `records_loaded > 0` en comandos ejecutados y sin regresión de filas. - `etl-tracker-gate` no devuelve mismatches no-waived ni reglas de calidad rotas. Ejecución reciente (2026-02-26 UTC): - Ejecutado: `AI-OPS-217` (`liberty_restrictions_focus_gate_20260226T085323Z.json`, `liberty_restrictions_status_20260226T085323Z.json`, `liberty_restrictions_snapshot_20260226T085323Z.json`) para el bloque `Derechos`. - Cobertura: `assessments_total=11`, `rights_with_data_total=6/6`, `accountability_edges_total=15`, `indirect_edges_total=12`, `delegated_links_total=8`, `norms_with_irlc_total=8`, `fragments_with_irlc_total=8`. - Gates al cierre: `focus_gate.passed=true`, `rights_with_data_gate.passed=true`, `right_categories_with_data_pct=1.0`, `gate.passed=true`. - También completados en paralelo (sin nuevos cambios de esquema): `liberty_direct_accountability_scores_latest.json`, `liberty_indirect_status_latest.json`, `liberty_enforcement_status_latest.json`, `liberty_proportionality_status_latest.json`, `liberty_personal_accountability_scores_latest.json`, `liberty_focus_scope_status.json`. - Ejecutado: bloques de control adicional de `AI-OPS-217`: - `parl-report-liberty-focus-scope` / `parl-check-liberty-focus-scope` (semáforo `ok`). - `parl-report-liberty-atlas-release-heartbeat` (strict: `published_gh_pages_drift_detected`) y `parl-check-liberty-atlas-release-heartbeat-window` (strict: ventana degradada en `failed/degraded/drift`), con evidencia en `AI-OPS-217/evidence/liberty_restrictions_heartbeat_20260226T085323Z.json` y `AI-OPS-217/evidence/liberty_restrictions_heartbeat_window_20260226T085323Z.json`. - Ejecutado: AI-OPS-218 (continuidad del atlas, snapshot de release y ventana de calidad): - `liberty_restrictions_status_20260305.json`: `assessments_total=11`, `fragments_with_accountability_total=8`, `focus_gate.passed=true`, `right_categories_with_data_pct=1.0`, `accountability_primary_evidence_gate=true`, `status=ok`. - `liberty_restrictions_status_heartbeat_20260305.json`: heartbeat window `status=ok` (`failed=0`, `degraded=0`). - `liberty_atlas_publish_20260305.json`: `status=ok`, `snapshot_date=2026-03-05`, artefactos + `liberty-restrictions-atlas-release-latest.json` publicados. - `liberty_atlas_changelog_continuity_20260305.json`: `status=ok`, `previous_snapshot_chain_ok=true`, `latest_snapshot_date=2026-03-05`. - `liberty_atlas_release_heartbeat_20260305.json`: `status=failed` (`published_release_error:missing_file:etl/data/published/liberty-restrictions-atlas-latest.json`) por configuración de contract path antigua. - `liberty_atlas_release_heartbeat_20260305_retry.json`: `status=degraded`, sin `strict` fail; usa `published_release_json=etl/data/published/liberty-restrictions-atlas-release-latest.json` y `published_release_ok=true` (quedando solo `hf_unavailable` como warning). - `liberty_atlas_release_heartbeat_latest.json` (AI-OPS-126): `status=degraded`, `published_release_ok=true`, `strict_fail_reasons=[]`, `hf_unavailable` warning; `liberty_atlas_release_heartbeat_window_latest.json` (`status=failed`) muestra que los límites de ventana siguen excedidos por histórico. - `liberty_atlas_release_heartbeat_window_20260305_default.json`: `status=failed` (`failed_in_window=7`, `drift_alerts_in_window=9`, `hf_unavailable=7`, `strict_fail_reasons=max_failed_exceeded|max_degraded_exceeded|max_drift_alerts_exceeded|max_hf_unavailable_exceeded`). - Ejecutado: cierre de `AI-OPS-219` en release-heartbeat + check-window con contract corregido y `LIBERTY_ATLAS_RELEASE_ALLOW_HF_UNAVAILABLE=1`: - `parl-report-liberty-atlas-release-heartbeat` (`AI-OPS-219/evidence/liberty_atlas_release_heartbeat_20260226T092333Z.json`): `status=degraded`, `published_release_ok=true`, `hf_unavailable=true`, `strict_fail_reasons=[]`. - `parl-check-liberty-atlas-release-heartbeat-window` (`AI-OPS-219/evidence/liberty_atlas_release_heartbeat_window_20260226T092333Z.json`): `status=failed` en `20` entradas, `failed_in_window=7`, `degraded_in_window=7`, `drift_alerts_in_window=9`, `hf_unavailable_in_window=11`, `strict_fail_reasons=max_failed_exceeded|max_degraded_exceeded|max_drift_alerts_exceeded|max_hf_unavailable_exceeded`. - Causa residual registrada como histórica: acumulado de latidos previos (no nuevo drift de publicación) con ventana strict aún sobre umbral. - Ejecutado: cierre de `AI-OPS-220` en publicación periódica del Atlas (`2026-03-05`) con ventana strict `ok`: - Cambio de contrato: `scripts/report_liberty_atlas_release_heartbeat_window.py` añade `--min-run-at`; `justfile` expone `LIBERTY_ATLAS_RELEASE_WINDOW_MIN_RUN_AT` para mantener histórico append-only sin contaminar la ventana de control activa. - `liberty_atlas_publish_2026-03-05.json`: `status=ok`. - `liberty_atlas_changelog_continuity_2026-03-05.json`: `status=ok`, `strict_fail_reasons=[]`. - `liberty_atlas_release_heartbeat_2026-03-05.json`: `status=ok`, `strict_fail_reasons=[]`. - `liberty_atlas_release_heartbeat_window_2026-03-05.json`: `status=ok`, `failed_in_window=0`, `degraded_in_window=0`, `drift_alerts_in_window=0`, `hf_unavailable_in_window=0`, `min_run_at=2026-02-26T23:42:30+00:00`, `entries_eligible=1`, `excluded_before_min_run_at=43`. - Pruebas del cambio: `python3 -m unittest tests/test_report_liberty_atlas_release_heartbeat_window.py` (`Ran 5 tests`, `OK`). - Ejecutado: `AI-OPS-499` endurece y acelera el gate obligatorio de privacidad de artefactos públicos: - `scripts/check_public_privacy_leaks.py` amplía el contrato por defecto a `ui/gh-pages-next/public`, añade prefilter rápido con `rg` cuando está disponible y mantiene fallback `mmap` para no decodificar completos los JSON gigantes de `etl/data/published` si no contienen sentinelas de fuga. - `just privacy-check-public-artifacts` deja de depender de una lista manual de subrutas y pasa a escanear el árbol completo `ui/gh-pages-next/public`, además de `docs/gh-pages` y `etl/data/published`. - Verificación de cierre (`20260306T190908Z`): `python3 -m unittest tests.test_check_public_privacy_leaks` (`Ran 6 tests`, `OK`), `python3 scripts/check_public_privacy_leaks.py` (`OK privacy leak scan: no findings`, `files_scanned=439`), `just privacy-check-public-artifacts` (`OK privacy leak scan: no findings`, `files_scanned=439`). - Evidencia: `docs/etl/sprints/AI-OPS-499/reports/public-artifact-privacy-gate-fast-path-20260306.md`, `docs/etl/sprints/AI-OPS-499/evidence/privacy_gate_fast_path_verification_20260306T190908Z.txt`. - Ejecutado: `AI-OPS-500` endurece el contrato de frescura del snapshot ciudadano sin tocar UI: - `scripts/export_citizen_snapshot.py` deja explícita la consistencia temporal con `timeline_delta_days`, `date_consistency_ok` y `warning_reason`; además separa el caso imposible `generated_at < as_of_date` como `freshness_tier='future'` / `freshness_label='futura'` en vez de degradarlo a una antigüedad normal. - `scripts/validate_citizen_snapshot.py` pasa a exigir `meta.freshness` + `meta.honesty` y rechaza combinaciones inválidas de timeline (por ejemplo `future` con edad no negativa o `date_consistency_ok=true`). - Verificación de cierre (`20260306T191649Z`): `python3 -m unittest tests.test_export_citizen_snapshot` (`Ran 3 tests`, `OK`) y export+validate reales sobre `etl/data/staging/politicos-es.db` para `combined`, `votes` y `declared`, todas en `status=ok` con resumen JSON que ya expone `freshness_tier`, `freshness_warning_reason` y `date_consistency_ok`. - Evidencia: `docs/etl/sprints/AI-OPS-500/reports/citizen-snapshot-freshness-consistency-contract-20260306.md`, `docs/etl/sprints/AI-OPS-500/evidence/citizen_snapshot_freshness_contract_20260306T191649Z.txt`. - Ejecutado: `AI-OPS-501` alinea `concern_pack_quality` con el método ciudadano y reintenta el publish de GH Pages con contrato más estricto: - `ui/citizen/index.html` deja de leer siempre `concern_pack_quality.json` y carga `concern_pack_quality.json`, `concern_pack_quality_votes.json` o `concern_pack_quality_declared.json` según el método activo, con fallback al artefacto base; `scripts/graph_ui_server.py` expone los nuevos assets. - `justfile` genera y sincroniza los tres artefactos de calidad de concern packs, endurece `explorer-gh-pages-publish` con `set -e`, reutiliza el binario real de `next` cuando `node_modules` ya está sano, y pasa `--snapshot-date "{{snapshot_date}}"` a `scripts/export_policy_outcomes_snapshot.py`. - Regeneración real (`20260306T214008Z`): `scripts/export_citizen_snapshot.py` vuelve a emitir `citizen.json`, `citizen_votes.json` y `citizen_declared.json`; `scripts/report_citizen_concern_pack_quality.py` confirma `status=ok` para `votes` y `status=failed` para `combined`/`declared`, que ahora se renderizan sin mezclar métodos. - Verificación (`20260306T214008Z`): `node --test tests/test_citizen_concern_pack_quality_ui_contract.js` (`pass 2/2`), `python3 -m unittest tests.test_graph_ui_server_citizen_assets tests.test_report_citizen_concern_pack_quality` (`Ran 4 tests`, `OK`) y `python3 scripts/check_public_privacy_leaks.py --path docs/gh-pages/citizen --path docs/gh-pages/legacy/citizen --path ui/gh-pages-next/public/legacy/citizen` (`OK privacy leak scan: no findings`, `files_scanned=63`). - Reintento limpio de `just explorer-gh-pages-publish`: supera `npm`/Next y completa `policy-outcomes`; el nuevo cuello de botella observable queda acotado a `scripts/export_people_profiles_snapshot.py`, por lo que el publish no se marca cerrado todavía. - Evidencia: `docs/etl/sprints/AI-OPS-501/reports/citizen-pack-quality-method-routing-and-publish-retry-20260306.md`, `docs/etl/sprints/AI-OPS-501/evidence/citizen_pack_quality_method_routing_20260306T214008Z.txt`. - Ejecutado: `AI-OPS-502` cierra el publish de GH Pages con transporte fail-fast y snapshot compacto de `political-positions`: - `justfile` hace `explorer-gh-pages-publish` no interactivo para SSH/credenciales (`GIT_TERMINAL_PROMPT=0`, `GIT_ASKPASS=/bin/false`, `GIT_SSH_COMMAND=ssh -o BatchMode=yes -o ConnectTimeout=15 -o ConnectionAttempts=1 -o StrictHostKeyChecking=accept-new`, `git -c credential.interactive=never push ...`) para convertir los cuelgues silenciosos en fallo determinista. - `scripts/export_political_positions_snapshot.py` compacta el artefacto estático conservando solo los campos que la UI renderiza: elimina labels/keys repetidas por punto (`topic_label`, `topic_key`, `party_label`, `person_id`, `computed_version`), poda resúmenes vacíos y reduce `evidence_samples` a `evidence_id`, `evidence_type`, `evidence_date`, `stance`, `confidence`, `excerpt`, `source_id`, `source_url?`, `review.status?`. - `ui/gh-pages-next/app/political-positions/page.js` deja de depender de labels duplicadas por punto y resuelve partido/tema desde `topics`, `persons` y `parties`, manteniendo la vista `/political-positions` compatible con el snapshot compacto. - Resultado visible (`20260307T160902Z`): `docs/gh-pages/political-positions/data/stances.json` y `ui/gh-pages-next/public/political-positions/data/stances.json` quedan en `87557184` bytes (`~83.50 MiB`), por debajo del hard limit de GitHub (`100 MiB`), y `just explorer-gh-pages-publish` completa el push a `gh-pages` (`956894ce374bc971f05c435ef096782acfe491c0`). - Verificación (`20260307T160902Z`): `python3 -m unittest tests.test_export_political_positions_snapshot` (`Ran 2 tests`, `OK`), `python3 scripts/export_political_positions_snapshot.py --db etl/data/staging/politicos-es.db --out docs/gh-pages/political-positions/data/stances.json --snapshot-date 2026-02-12` (`87433061 chars`), `just explorer-gh-pages-publish` (`OK privacy leak scan: no findings`, push exitoso a `gh-pages`) y `git ls-remote origin gh-pages` (`956894ce374bc971f05c435ef096782acfe491c0`). - Riesgo residual honesto: GitHub sigue emitiendo warning de tamaño recomendado (`50 MiB`) para `political-positions/data/stances.json`, así que el siguiente hardening de esta superficie debería ser paginación/chunking o un índice + lazy-load por persona/partido, no más compresión incidental. - Evidencia: `docs/etl/sprints/AI-OPS-502/reports/gh-pages-publish-closeout-and-political-positions-size-reduction-20260307.md`, `docs/etl/sprints/AI-OPS-502/evidence/gh_pages_publish_closeout_20260307T160902Z.txt`. - Ejecutado: `AI-OPS-503` divide `political-positions` en índice liviano + detalle por persona y republish limpio: - `scripts/export_political_positions_snapshot.py` mantiene `stances.json` como índice navegable y escribe companion files por persona en `political-positions/data/person-details/.json`, con `meta.person_detail_dir='person-details'` y `evidence_samples_by_point` keyed por `topic_id|as_of_date|computed_method`. - `ui/gh-pages-next/app/political-positions/page.js` carga el índice principal y resuelve el detalle puntual vía lazy-load solo cuando el usuario selecciona una fila de persona; la tabla principal conserva `evidence_breakdown`/`review_summary`, pero las muestras de evidencia salen del payload caliente. - `justfile` sincroniza también el árbol `person-details/` hacia `ui/gh-pages-next/public/political-positions/data`, tanto en el sync incremental como en `explorer-gh-pages-build`. - Resultado visible (`20260307T162111Z`): `docs/gh-pages/political-positions/data/stances.json` y `ui/gh-pages-next/public/political-positions/data/stances.json` quedan en `21050589` bytes (`~20.08 MiB`) y ambas superficies publicadas incluyen `350` detail files por persona. - Verificación (`20260307T162111Z`): `python3 -m unittest tests.test_export_political_positions_snapshot` (`Ran 3 tests`, `OK`), `python3 scripts/export_political_positions_snapshot.py --db etl/data/staging/politicos-es.db --out docs/gh-pages/political-positions/data/stances.json --snapshot-date 2026-02-12` (`21049745 chars`), `just explorer-gh-pages-publish` (sin warnings de tamaño GitHub, push exitoso a `gh-pages`) y `git ls-remote origin gh-pages` (`910e5a46a33fb34b7386d71ca898bd46a92ff88c`). - Evidencia: `docs/etl/sprints/AI-OPS-503/reports/political-positions-index-split-and-lazy-detail-20260307.md`, `docs/etl/sprints/AI-OPS-503/evidence/political_positions_index_split_publish_20260307T162111Z.txt`. - Ejecutado: `AI-OPS-504` separa las trayectorias por modo y deja `stances.json` como índice ultraligero: - `scripts/export_political_positions_snapshot.py` deja `stances.json` con `person_trajectories={}` / `party_trajectories={}` y añade `meta.person_trajectories_path='person-trajectories.json'` + `meta.party_trajectories_path='party-trajectories.json'`; las trayectorias completas se publican en companion files separados. - `ui/gh-pages-next/app/political-positions/page.js` pasa a lazy-load mode-scoped: carga `person-trajectories.json` solo cuando hace falta la vista persona y `party-trajectories.json` para la vista partido, con estado explícito de `Cargando trayectorias…` / error honesto en la tabla. - `justfile` sincroniza los nuevos `*-trajectories.json` hacia `ui/gh-pages-next/public/political-positions/data` en ambos caminos de build/sync. - Resultado visible (`20260307T163141Z`): `docs/gh-pages/political-positions/data/stances.json` y `ui/gh-pages-next/public/political-positions/data/stances.json` quedan en `107256` bytes; companions publicados: `person-trajectories.json=19615516` bytes y `party-trajectories.json=1327927` bytes en ambas superficies. - Verificación (`20260307T163141Z`): `python3 -m unittest tests.test_export_political_positions_snapshot` (`Ran 4 tests`, `OK`), `python3 scripts/export_political_positions_snapshot.py --db etl/data/staging/politicos-es.db --out docs/gh-pages/political-positions/data/stances.json --snapshot-date 2026-02-12` (`106412 chars`), `just explorer-gh-pages-publish` (`OK privacy leak scan: no findings`, `files_scanned=1156`, push exitoso a `gh-pages`) y `git ls-remote origin gh-pages` (`0dfda599bd21c7a77e2b57da2526510a77e02693`). - Riesgo residual honesto: la vista persona sigue dependiendo de un companion de `~19.6 MiB`; el siguiente hardening correcto es chunking/paginación de `person-trajectories.json`, no volver a compactar el índice. - Evidencia: `docs/etl/sprints/AI-OPS-504/reports/political-positions-mode-trajectories-split-20260307.md`, `docs/etl/sprints/AI-OPS-504/evidence/political_positions_mode_trajectories_split_20260307T163141Z.txt`. - Ejecutado: `AI-OPS-505` trocea la vista persona de `political-positions` en manifest + chunks y carga progresiva honesta: - `scripts/export_political_positions_snapshot.py` cambia `person-trajectories.json` de payload completo a manifest pequeño con `meta.chunk_dir`, `meta.chunk_size`, `meta.chunk_count` y una lista de `chunks`; cada persona del índice recibe `trajectory_chunk`, `point_count` y mantiene compatibilidad con `points_count`. - El export publica `person-trajectory-chunks/chunk-*.json` (14 chunks en esta snapshot) y `justfile` sincroniza ese árbol nuevo hacia `ui/gh-pages-next/public/political-positions/data` tanto en el sync incremental como en `explorer-gh-pages-build`. - `ui/gh-pages-next/app/political-positions/page.js` deja de asumir que `person-trajectories.json` contiene todas las filas: ahora primero carga el manifest, después resuelve solo los chunks necesarios para la vista por persona, y cuando el usuario activa búsqueda/filtros/orden no triviales hace un full scan progresivo con chips explícitos de chunks cargados / carga diferida. - Resultado visible (`20260307T165207Z`): `docs/gh-pages/political-positions/data/stances.json` y `ui/gh-pages-next/public/political-positions/data/stances.json` quedan en `124522` bytes; `person-trajectories.json` baja a `2659` bytes; `party-trajectories.json` se mantiene en `1327927` bytes; la vista persona queda repartida en `14` chunks con tamaño entre `1351789` y `1465811` bytes. - Verificación (`20260307T165207Z`): `python3 -m unittest tests.test_export_political_positions_snapshot` (`Ran 4 tests`, `OK`), `python3 -m py_compile scripts/export_political_positions_snapshot.py`, `python3 scripts/export_political_positions_snapshot.py --db etl/data/staging/politicos-es.db --out docs/gh-pages/political-positions/data/stances.json --snapshot-date 2026-02-12` (`123678 chars`), `just explorer-gh-pages-publish` (`OK privacy leak scan: no findings`, `files_scanned=1184`, push exitoso a `gh-pages`) y `git ls-remote origin gh-pages` (`7a666b8838cf43d6c6a511c97cac0c425d80e86b`). - Riesgo residual honesto: los chunks de persona siguen siendo pesados (`~1.35-1.47 MiB`) y el full scan por filtros complejos aún puede descargar todo el set; el siguiente paso correcto es chunking adicional por fila/página o un índice consultable por tema/persona, no volver a inflar el payload caliente. - Evidencia: `docs/etl/sprints/AI-OPS-505/reports/political-positions-person-trajectory-chunking-20260307.md`, `docs/etl/sprints/AI-OPS-505/evidence/political_positions_person_chunking_publish_20260307T165207Z.txt`. - Ejecutado: `AI-OPS-506` añade un fast-path publicado para la vista persona por defecto en `political-positions`: - `scripts/export_political_positions_snapshot.py` publica `person-default-rows.json` con las primeras `400` filas de la vista persona ya ordenadas según el contrato por defecto (`persona -> tema -> fecha desc -> método`), y expone `meta.person_default_rows_path` + `meta.person_default_rows_limit`. - `ui/gh-pages-next/app/political-positions/page.js` usa ese artefacto cuando el usuario entra en modo persona sin búsqueda/filtros/orden avanzado, evitando cargar manifest/chunks en el caso común; cuando el usuario abandona esa vista rápida, la UI sigue cayendo al manifest + chunks de `AI-OPS-505`. - `justfile` sincroniza `person-default-rows.json` hacia `ui/gh-pages-next/public/political-positions/data` en ambos caminos de build/sync. - Resultado visible (`20260307T170258Z`): `docs/gh-pages/political-positions/data/person-default-rows.json` y `ui/gh-pages-next/public/political-positions/data/person-default-rows.json` quedan en `305701` bytes con `400` filas; `stances.json` se mantiene acotado en `124608` bytes y el árbol chunked sigue disponible para filtros complejos. - Verificación (`20260307T170258Z`): `python3 -m unittest tests.test_export_political_positions_snapshot` (`Ran 6 tests`, `OK`), `python3 -m py_compile scripts/export_political_positions_snapshot.py`, `python3 scripts/export_political_positions_snapshot.py --db etl/data/staging/politicos-es.db --out docs/gh-pages/political-positions/data/stances.json --snapshot-date 2026-02-12` (`123764 chars`), `just explorer-gh-pages-publish` (`OK privacy leak scan: no findings`, `files_scanned=1186`, push exitoso a `gh-pages`) y `git ls-remote origin gh-pages` (`fb68eadf9e5ea46f53a1b0759d746d1027b3d888`). - Riesgo residual honesto: el fast-path resuelve la vista inicial, pero la búsqueda/orden complejos siguen dependiendo del full scan de chunks; el siguiente paso correcto es recortar también ese fallback con páginas o índices por filtro. - Evidencia: `docs/etl/sprints/AI-OPS-506/reports/political-positions-default-person-rows-fast-path-20260307.md`, `docs/etl/sprints/AI-OPS-506/evidence/political_positions_default_rows_publish_20260307T170258Z.txt`. - Ejecutado: `AI-OPS-507` convierte el fallback filtrado de `/political-positions` en un escaneo progresivo exacto cuando `sort=person`: - `ui/gh-pages-next/app/political-positions/personTrajectoryLoading.mjs` extrae el contrato puro de carga (`default_rows` vs `progressive` vs `exhaustive`) y decide qué chunks faltan pedir según filtros activos, orden y filas ya visibles. - `ui/gh-pages-next/app/political-positions/page.js` deja de tratar cualquier filtro como “descarga todo”; ahora: - vista por defecto: sigue usando `person-default-rows.json` - filtros con `sort=person`: pide un chunk más cada vez y se detiene en cuanto cubre el límite visible, preservando exactitud del orden por persona - órdenes avanzados (`confidence`, `as_of`, `topic`, etc.): mantienen el escaneo exhaustivo honesto - Resultado visible (`20260307T171328Z`): la UI añade etiquetas explícitas `Escaneo progresivo exacto` y `Escaneo completo por orden avanzado`, y el caso filtrado por persona deja de disparar la carga completa de los `14` chunks por defecto. - Verificación (`20260307T171328Z`): `node --test tests/test_political_positions_person_trajectory_loading.js` (`5/5` pass), `python3 -m unittest tests.test_export_political_positions_snapshot` (`Ran 6 tests`, `OK`), `just explorer-gh-pages-publish` (`OK privacy leak scan: no findings`, `files_scanned=1186`, push exitoso a `gh-pages`) y `git ls-remote origin gh-pages` (`b81eac2273360cc54ebd986fb927a7b28dcb8395`). - Riesgo residual honesto: este ahorro solo es exacto para `sort=person`; cualquier orden alternativo sigue requiriendo scan exhaustivo del set chunked. El siguiente paso correcto es añadir índices/páginas específicas para esos órdenes o filtros pesados. - Evidencia: `docs/etl/sprints/AI-OPS-507/reports/political-positions-progressive-person-filter-scan-20260307.md`, `docs/etl/sprints/AI-OPS-507/evidence/political_positions_progressive_person_filter_scan_20260307T171328Z.txt`. - Ejecutado: `AI-OPS-508` añade previews estáticos para órdenes avanzados sin filtros en `/political-positions`, recortando otro fallback caro sin tocar el backend: - `scripts/export_political_positions_snapshot.py` publica `person-sort-previews/{confidence_desc,confidence_asc,method,stance,as_of,topic,party}.json` con las primeras `400` filas ya ordenadas por cada modo, y expone `meta.person_sort_preview_dir`, `meta.person_sort_preview_limit` + `meta.person_sort_preview_paths`. - `ui/gh-pages-next/app/political-positions/personTrajectoryLoading.mjs` introduce el modo explícito `sort_preview`; `ui/gh-pages-next/app/political-positions/page.js` lo usa cuando el usuario está en modo persona sin filtros y cambia el orden a `confidence`, `fecha`, `tema`, `método`, `postura` o `partido`, cargando un JSON pequeño por orden en vez de disparar manifest + chunks. - `justfile` sincroniza `person-sort-previews/` hacia `ui/gh-pages-next/public/political-positions/data` tanto en el sync incremental como en `explorer-gh-pages-build`, preservando paridad entre `docs/gh-pages` y la app Next. - Resultado visible (`20260307T172811Z`): `docs/gh-pages/political-positions/data/stances.json` y `ui/gh-pages-next/public/political-positions/data/stances.json` quedan en `125055` bytes; `person-sort-previews/` publica `7` archivos con `1322076` bytes acumulados; `person-default-rows.json` se mantiene en `305701` bytes; `person-trajectories.json` sigue en `2659` bytes y los `14` chunks persona siguen en `~1.35-1.47 MiB`. - Verificación (`20260307T172811Z`): `python3 -m unittest tests.test_export_political_positions_snapshot` (`Ran 8 tests`, `OK`), `node --test tests/test_political_positions_person_trajectory_loading.js` (`6/6` pass), `python3 -m py_compile scripts/export_political_positions_snapshot.py`, `python3 scripts/export_political_positions_snapshot.py --db etl/data/staging/politicos-es.db --out docs/gh-pages/political-positions/data/stances.json --snapshot-date 2026-02-12` (`124211 chars`), `just explorer-gh-pages-publish` (`OK privacy leak scan: no findings`, `files_scanned=1200`, push exitoso a `gh-pages`) y `git ls-remote origin gh-pages` (`b1c1fa8bd273f2cce8ea96925f351b58857cba88`). - Riesgo residual honesto: el fast-path nuevo cubre órdenes avanzados sin filtros, pero si el usuario combina esos órdenes con búsqueda/filtros la UI todavía cae al scan exhaustivo de chunks. El siguiente paso correcto es añadir índices/páginas estáticas adicionales para esos casos filtrados, no volver a inflar `stances.json`. - Evidencia: `docs/etl/sprints/AI-OPS-508/reports/political-positions-unfiltered-advanced-sort-previews-20260307.md`, `docs/etl/sprints/AI-OPS-508/evidence/political_positions_sort_preview_publish_20260307T172811Z.txt`. - Ejecutado: `AI-OPS-509` añade preselección segura de chunks para filtros estructurados en `/political-positions`, evitando scans completos cuando el manifest puede demostrar qué chunks sí pueden contener resultados: - `scripts/export_political_positions_snapshot.py` ahora genera `person-trajectories.json` con metadatos de filtro por chunk (`topic_tokens`, `party_tokens`, `methods`, `stances`) derivados de las filas pre-compaction; en el mismo cambio, los previews estáticos recuperan `topicLabel`/`partyLabel` reales porque también se generan antes de la compactación estática. - `ui/gh-pages-next/app/political-positions/personTrajectoryLoading.mjs` calcula `candidatePersonTrajectoryChunkIds(...)` y excluye chunks enteros solo cuando esos metadatos lo soportan; si el manifest no trae los campos, la UI conserva el comportamiento anterior y no excluye nada. - `ui/gh-pages-next/app/political-positions/page.js` muestra el recorte directamente en la UI con `Chunks persona: cargados/candidatos · totales`, de modo que el usuario puede ver cuándo un filtro estructurado ha reducido el set antes de descargar chunks pesados. - Resultado visible (`20260307T174249Z`): `docs/gh-pages/political-positions/data/person-trajectories.json` y `ui/gh-pages-next/public/political-positions/data/person-trajectories.json` quedan en `125084` bytes; `stances.json` se mantiene en `125055` bytes; `person-sort-previews/` queda en `7` archivos con `2197389` bytes y vuelve a incluir etiquetas de tema/partido; el árbol persona sigue en `14` chunks de `~1.35-1.47 MiB`. - Medición real del recorte (`20260307T174249Z`): filtros regionales pequeños ya reducen el scan de forma material, por ejemplo `party=upn` cae de `14` chunks posibles a `1`, `party=bng` cae a `1` y `party=psn` cae a `2`; filtros muy amplios como `party=vox` siguen tocando casi todo el set (`13/14`), y eso queda reflejado honestamente en la nueva métrica de candidatos. - Verificación (`20260307T174249Z`): `python3 -m unittest tests.test_export_political_positions_snapshot` (`Ran 9 tests`, `OK`), `node --test tests/test_political_positions_person_trajectory_loading.js` (`8/8` pass), `python3 -m py_compile scripts/export_political_positions_snapshot.py`, `python3 scripts/export_political_positions_snapshot.py --db etl/data/staging/politicos-es.db --out docs/gh-pages/political-positions/data/stances.json --snapshot-date 2026-02-12` (`124211 chars`), `just explorer-gh-pages-publish` (`OK privacy leak scan: no findings`, `files_scanned=1200`, push exitoso a `gh-pages`) y `git ls-remote origin gh-pages` (`6310fb4f87e7225d08bc92555fc2c04d7b9c4875`). - Riesgo residual honesto: el recorte nuevo ayuda cuando el filtro estructurado es selectivo, pero no cambia los casos muy amplios (`vox`, keywords comunes de tema) y no resuelve todavía la búsqueda libre `q`. El siguiente paso correcto es índices/páginas adicionales por partido/tema o paginación materializada, no seguir añadiendo metadatos difusos al manifest. - Evidencia: `docs/etl/sprints/AI-OPS-509/reports/political-positions-structured-filter-chunk-preselection-20260307.md`, `docs/etl/sprints/AI-OPS-509/evidence/political_positions_structured_filter_chunk_preselection_20260307T174249Z.txt`. - Ejecutado: `AI-OPS-510` extiende la preselección de chunks al buscador libre `q` para consultas de texto indexables (nombres/alias), sin romper el comportamiento honesto de búsquedas tipo fecha: - `scripts/export_political_positions_snapshot.py` añade `person_tokens` al manifest persona (`person-trajectories.json`) a partir de `full_name` + `canonical_key`, manteniendo el índice en un tamaño acotado (`136747` bytes publicados). - `ui/gh-pages-next/app/political-positions/personTrajectoryLoading.mjs` ahora reutiliza `person_tokens` junto con `topic_tokens`, `party_tokens` y `methods` para `candidatePersonTrajectoryChunkIds(...)` cuando `q` es puramente textual; si la consulta contiene dígitos o no es indexable (`2026-02-12`, etc.), la UI no prefiltra y conserva el scan completo. - Resultado visible (`20260307T175157Z`): búsquedas nominales ya recortan chunks sin tocar el backend. Casos medidos sobre el manifest publicado: `alda recas -> 1/14`, `ainhoa molina -> 1/14`, `alberto asarta -> 1/14`, `santiago abascal -> 1/14`; en cambio `2026-02-12` sigue en `14/14` por diseño para no crear falsos negativos sobre `as_of`. - Resultado de artefactos: `docs/gh-pages/political-positions/data/person-trajectories.json` y `ui/gh-pages-next/public/political-positions/data/person-trajectories.json` quedan en `136747` bytes; `stances.json` sigue en `125055` bytes; `person-sort-previews/` se mantiene en `7` archivos con `2197389` bytes. - Verificación (`20260307T175157Z`): `python3 -m unittest tests.test_export_political_positions_snapshot` (`Ran 9 tests`, `OK`), `node --test tests/test_political_positions_person_trajectory_loading.js` (`10/10` pass), `python3 -m py_compile scripts/export_political_positions_snapshot.py`, `python3 scripts/export_political_positions_snapshot.py --db etl/data/staging/politicos-es.db --out docs/gh-pages/political-positions/data/stances.json --snapshot-date 2026-02-12` (`124211 chars`), `just explorer-gh-pages-publish` (`OK privacy leak scan: no findings`, `files_scanned=1200`, push exitoso a `gh-pages`) y `git ls-remote origin gh-pages` (`f5bcbc61badd4fc314308a087429f265ba56bdb4`). - Riesgo residual honesto: el recorte de `q` funciona bien para nombres/aliases, pero no mejora queries amplias sobre vocabulario muy común y sigue dejando fuera las búsquedas con fechas/números. El siguiente paso correcto es materializar subsets consultables por persona/partido/tema o introducir un índice estático más expresivo para búsqueda libre, no inflar más el manifest lineal. - Evidencia: `docs/etl/sprints/AI-OPS-510/reports/political-positions-q-aware-chunk-preselection-20260307.md`, `docs/etl/sprints/AI-OPS-510/evidence/political_positions_q_aware_chunk_preselection_20260307T175157Z.txt`. - Ejecutado: `AI-OPS-511` mantiene la mejora de `q` nominal pero la saca del manifest persona para volver a un índice más ligero: - `ui/gh-pages-next/app/political-positions/personTrajectoryLoading.mjs` deja de depender de `person_tokens` en `person-trajectories.json` y usa el índice ya cargado en `stances.json` (`persons[].full_name`, `canonical_key`, `trajectory_chunk`) para resolver búsquedas nominales tipo `q=alda recas` sin tocar el backend. - `scripts/export_political_positions_snapshot.py` elimina `person_tokens` del manifest persona, por lo que `person-trajectories.json` vuelve de `136747` a `125084` bytes sin perder los recortes ya logrados en búsquedas nominales. - Resultado visible (`20260307T180215Z`): las búsquedas `alda recas`, `ainhoa molina`, `alberto asarta` y `santiago abascal` siguen cayendo a `1/14` chunks usando el índice de personas del payload caliente; `2026-02-12` sigue en `14/14` por diseño. - Resultado de artefactos: `docs/gh-pages/political-positions/data/person-trajectories.json` y `ui/gh-pages-next/public/political-positions/data/person-trajectories.json` quedan en `125084` bytes; `stances.json` se mantiene en `125055` bytes; `person-sort-previews/` sigue en `7` archivos con `2197389` bytes. - Verificación (`20260307T180215Z`): `python3 -m unittest tests.test_export_political_positions_snapshot` (`Ran 9 tests`, `OK`), `node --test tests/test_political_positions_person_trajectory_loading.js` (`10/10` pass), `python3 -m py_compile scripts/export_political_positions_snapshot.py`, `python3 scripts/export_political_positions_snapshot.py --db etl/data/staging/politicos-es.db --out docs/gh-pages/political-positions/data/stances.json --snapshot-date 2026-02-12` (`124211 chars`), `just explorer-gh-pages-publish` (`OK privacy leak scan: no findings`, `files_scanned=1200`, push exitoso a `gh-pages`) y `git ls-remote origin gh-pages` (`c9ad03b6581f60cc74f61d63d021534c15c26e13`). - Riesgo residual honesto: el buscador nominal ya no penaliza el manifest, pero la búsqueda libre amplia sigue necesitando un índice más expresivo por persona/partido/tema si queremos mejorar queries comunes sin descargar chunks de más. - Evidencia: `docs/etl/sprints/AI-OPS-511/reports/political-positions-q-preselection-moved-to-person-index-20260307.md`, `docs/etl/sprints/AI-OPS-511/evidence/political_positions_q_preselection_person_index_20260307T180215Z.txt`. - Ejecutado: `AI-OPS-512` mueve la preselección filtrada a un sidecar estático on-demand y vuelve a adelgazar el manifest persona: - `scripts/export_political_positions_snapshot.py` publica `person-search-index.json` como companion separado para filtros/búsquedas activas y compacta `person-trajectories.json` otra vez hasta un manifest mínimo con solo `chunk_id`, `person_count`, `point_count_total` y `person_ids`; el sidecar concentra `party_tokens`, `methods` y `stances`, mientras el payload caliente `stances.json -> persons[]` sigue resolviendo búsquedas nominales por persona. - `ui/gh-pages-next/app/political-positions/personTrajectoryLoading.mjs` y `ui/gh-pages-next/app/political-positions/page.js` cargan ese índice solo cuando hay filtros/búsqueda activa, esperan honestamente a que esté disponible antes de decidir chunks, y conservan fallback seguro a full scan si el índice falla; además el loader deja de dejar que el viejo fallback de metadata de chunks anule el recorte nominal cuando ya existe sidecar. - `justfile` sincroniza `person-search-index.json` hacia `ui/gh-pages-next/public/political-positions/data` tanto en `gh-pages-next-prime` como en `explorer-gh-pages-build`. - Resultado visible (`20260307T182155Z`): `docs/gh-pages/political-positions/data/person-trajectories.json` y `ui/gh-pages-next/public/political-positions/data/person-trajectories.json` quedan en `2659` bytes; `person-search-index.json` queda en `3022` bytes; `stances.json` se mantiene en `125109` bytes; el manifest persona publicado ya no arrastra arrays `topic_tokens` / `party_tokens` / `methods` / `stances`. - Medición real del recorte (`20260307T182155Z`): con el sidecar publicado, `party=upn -> 1/14`, `party=psn -> 2/14`, `q=santiago abascal -> 1/14`; búsquedas amplias como `q=vivienda` siguen en `14/14` porque en esta snapshot no hay señal chunk-selectiva suficiente para ese vocabulario común y el sistema evita falsos negativos. - Verificación (`20260307T182155Z`): `python3 -m unittest tests.test_export_political_positions_snapshot` (`Ran 12 tests`, `OK`), `node --test tests/test_political_positions_person_trajectory_loading.js` (`14/14` pass), `python3 -m py_compile scripts/export_political_positions_snapshot.py`, `python3 scripts/export_political_positions_snapshot.py --db etl/data/staging/politicos-es.db --out docs/gh-pages/political-positions/data/stances.json --snapshot-date 2026-02-12` (`124265 chars`), `just explorer-gh-pages-publish` (`OK privacy leak scan: no findings`, `files_scanned=1202`, push exitoso a `gh-pages`) y `git ls-remote origin gh-pages` (`deaf9122b04580b6ee5e85f816b3ee4c03499f8d`). - Riesgo residual honesto: el sidecar ya evita volver a inflar el manifest y mejora filtros selectivos/nombres, pero el texto de tema muy común sigue cayendo a full scan porque la distribución por chunks no permite recorte seguro. El siguiente paso correcto es un índice estático más expresivo por tema/persona o subsets materializados, no volver a meter metadatos grandes en el manifest. - Evidencia: `docs/etl/sprints/AI-OPS-512/reports/political-positions-search-index-sidecar-and-lean-manifest-20260307.md`, `docs/etl/sprints/AI-OPS-512/evidence/political_positions_search_index_sidecar_publish_20260307T182155Z.txt`. - Ejecutado: `AI-OPS-513` añade un filtro dedicado de persona en `/political-positions` y lo conecta al índice de chunks ya publicado: - `ui/gh-pages-next/app/political-positions/page.js` incorpora un campo `Persona` con sugerencias desde `stances.json -> persons[]`, persiste su estado en URL (`?person=...`) y aplica el filtro directamente sobre la vista persona sin mezclarlo con la búsqueda libre `q`. - `ui/gh-pages-next/app/political-positions/personTrajectoryLoading.mjs` trata `state.person` como un filtro indexado más y usa `persons[].trajectory_chunk` para reducir chunks antes de la carga progresiva; además `page.js` evita pedir `person-search-index.json` cuando el único filtro activo es `person`, porque ese caso ya se resuelve completamente con el índice de personas del payload caliente. - Resultado visible (`20260307T182812Z`): la superficie publicada acepta enlaces reproducibles tipo `political-positions?person=santiago+abascal`, mantiene el patrón `Chunks persona: cargados/candidatos · totales`, y los casos medidos con el helper sobre artefactos publicados quedan en `person=santiago abascal -> 1/14` y `person=ainhoa molina -> 1/14`. - Verificación (`20260307T182812Z`): `node --test tests/test_political_positions_person_trajectory_loading.js` (`15/15` pass), `just explorer-gh-pages-publish` (`OK privacy leak scan: no findings`, `files_scanned=1202`, push exitoso a `gh-pages`) y `git ls-remote origin gh-pages` (`231ec138d5909902abf9d8d00fb10456ba937c7f`). - Riesgo residual honesto: el filtro persona mejora mucho el caso nominal exacto, pero la búsqueda libre `q` y los temas amplios siguen siendo más difusos. El siguiente paso correcto sigue siendo separar mejor la búsqueda temática amplia del buscador general, no volver a cargar `q` con más heurísticas opacas. - Evidencia: `docs/etl/sprints/AI-OPS-513/reports/political-positions-person-filter-and-shareable-url-state-20260307.md`, `docs/etl/sprints/AI-OPS-513/evidence/political_positions_person_filter_publish_20260307T182812Z.txt`. - Ejecutado: `AI-OPS-514` separa la selección exacta de tema de la búsqueda libre en `/political-positions` y hace explícito el contrato real del sidecar de búsqueda: - `ui/gh-pages-next/app/political-positions/filterMatching.mjs` introduce resolución exacta de tema por `topic_key`, `topic_id` o label normalizada, y `ui/gh-pages-next/app/political-positions/page.js` convierte el campo `Tema` en un input con sugerencias desde `stances.json -> topics[]`, mostrando un chip `Tema exacto` cuando la selección se puede resolver sin ambigüedad. - `scripts/export_political_positions_snapshot.py` publica `meta.person_search_index_counts` dentro de `stances.json`, de forma que la UI conozca cuánta señal chunk-selectiva existe realmente antes de pedir `person-search-index.json`; en esta snapshot publicada el contrato queda en `topic_tokens=0`, `party_tokens=20`, `methods=3`, `stances=4`. - Resultado visible (`20260307T183831Z`): la superficie publicada permite seleccionar temas exactos por label o clave canónica (`Proyecto de Ley de Movilidad Sostenible.` -> `topicId=1`; `congreso:leg15:exp:121/000004/0000` -> `topicId=2`), mantiene `stances.json` acotado a `125199` bytes y evita la descarga inútil del sidecar cuando el único filtro activo es `topic`, porque la propia snapshot declara que no existe índice de temas por chunk (`topic_tokens=0`). - Verificación (`20260307T183831Z`): `node --test tests/test_political_positions_person_trajectory_loading.js tests/test_political_positions_filter_matching.js` (`19/19` pass), `python3 -m unittest tests.test_export_political_positions_snapshot` (`Ran 12 tests`, `OK`), `python3 -m py_compile scripts/export_political_positions_snapshot.py`, `python3 scripts/export_political_positions_snapshot.py --db etl/data/staging/politicos-es.db --out docs/gh-pages/political-positions/data/stances.json --snapshot-date 2026-02-12` (`124355 chars`), `just explorer-gh-pages-publish` (`OK privacy leak scan: no findings`, `files_scanned=1202`, push exitoso a `gh-pages`) y `git ls-remote origin gh-pages` (`85fd6b9f0a27bb182b889313136bf7ea6fe09d78`). - Riesgo residual honesto: este slice deja de fingir selectividad temática donde no la hay y mejora el caso “tema exacto”, pero el texto temático amplio sigue cayendo a full scan porque la snapshot actual no tiene índice chunk-selectivo para ese vocabulario. El siguiente paso correcto es un índice estático más expresivo por tema o subsets materializados, no volver a inflar el manifest caliente. - Evidencia: `docs/etl/sprints/AI-OPS-514/reports/political-positions-exact-topic-filter-and-search-index-counts-20260307.md`, `docs/etl/sprints/AI-OPS-514/evidence/political_positions_exact_topic_filter_publish_20260307T183831Z.txt`. - Ejecutado: `AI-OPS-515` corrige la resolución exacta de tema sobre la shape publicada real y mantiene el sidecar temático estrictamente selectivo: - `ui/gh-pages-next/app/political-positions/filterMatching.mjs` deja de asumir solo `topic_id` en snake_case y acepta también la shape camelCase publicada por la propia UI (`topicId`, `label`, `key`), de modo que el caso `Tema exacto` deja de fallar silenciosamente en producción. - `ui/gh-pages-next/app/political-positions/page.js` mueve la resolución exacta antes del cálculo `needsPersonSearchIndex`, evitando un `ReferenceError` de prerender en Next y dejando el flujo listo para usar un índice exacto solo cuando exista señal real. - `scripts/export_political_positions_snapshot.py` publica `topic_ids` únicamente cuando un tema exacto reduce chunks de verdad; en la snapshot actual los `120` topics tocan los `14` chunks, así que el contrato publicado queda honestamente en `topic_ids=0`, `topic_tokens=0`, `party_tokens=20`, `methods=3`, `stances=4`, y `person-search-index.json` vuelve a `3121` bytes en vez de cargar un mapa exacto inútil. - `ui/gh-pages-next/app/political-positions/personTrajectoryLoading.mjs` queda preparado para aprovechar `searchIndex.topic_ids` en snapshots futuras selectivas, pero hoy no dispara ese camino porque la snapshot publicada no ofrece esa selectividad. - Resultado visible (`20260307T185948Z`): sobre los artefactos publicados, labels reales como `Proyecto de Ley de Movilidad Sostenible.`, `Proyecto de Ley de prevención de las pérdidas y el desperdicio alimentario.` y `Proyecto de Ley por la que se regulan los servicios de atención a la clientela.` resuelven correctamente a `topicId=1`, `topicId=2` y `topicId=3`; `stances.json` queda en `125213` bytes y `person-search-index.json` en `3121` bytes en ambas superficies (`docs/gh-pages` y `ui/gh-pages-next/public`). - Verificación (`20260307T185948Z`): `node --test tests/test_political_positions_person_trajectory_loading.js tests/test_political_positions_filter_matching.js` (`22/22` pass), `python3 -m unittest tests.test_export_political_positions_snapshot` (`Ran 13 tests`, `OK`), `python3 -m py_compile scripts/export_political_positions_snapshot.py`, `python3 scripts/export_political_positions_snapshot.py --db etl/data/staging/politicos-es.db --out docs/gh-pages/political-positions/data/stances.json --snapshot-date 2026-02-12` (`124369 chars`), `just explorer-gh-pages-publish` (`OK privacy leak scan: no findings`, `files_scanned=1202`, push exitoso a `gh-pages`) y `git ls-remote origin gh-pages` (`6e27456913b8191f80681c9603a61fe25650bd4f`). - Riesgo residual honesto: el filtro exacto ahora sí funciona sobre la shape real y el publish ya no finge selectividad temática inexistente, pero la búsqueda temática amplia sigue necesitando full scan porque en esta snapshot todos los topics atraviesan todos los chunks. El siguiente paso correcto sigue siendo un índice estático más expresivo por tema/persona o subsets materializados, no más heurísticas dentro del manifest. - Evidencia: `docs/etl/sprints/AI-OPS-515/reports/political-positions-exact-topic-resolution-live-shape-and-selective-indexing-20260307.md`, `docs/etl/sprints/AI-OPS-515/evidence/political_positions_exact_topic_resolution_publish_20260307T185948Z.txt`. - Ejecutado: `AI-OPS-516` añade una vista temática estática por topic exacto en `/political-positions`, evitando el camino de chunks persona cuando el usuario ya eligió un tema real: - `scripts/export_political_positions_snapshot.py` publica `topic-person-rows/.json` como companion files por topic, con `meta.topic_person_rows_dir='topic-person-rows'` y `meta.topic_person_rows_total=120`; cada archivo agrupa las filas persona de un único tema ya ordenadas por el orden por defecto y compactadas sin campos redundantes del propio topic. - `ui/gh-pages-next/app/political-positions/personTrajectoryLoading.mjs` introduce `topic_preview` como scan mode explícito para `Tema exacto`, de forma que `nextPersonTrajectoryChunkIds(...)` devuelve `[]` y el loader no necesita manifest/chunks cuando la vista se puede resolver con un único topic file estático. - `ui/gh-pages-next/app/political-positions/page.js` carga `topic-person-rows/.json`, aplica filtros/sort client-side sobre ese subconjunto y muestra chips honestos (`Cargando vista temática…`, `Vista temática estática`) en vez de pasar por `Escaneo progresivo exacto`. - `justfile` sincroniza `topic-person-rows/` hacia `ui/gh-pages-next/public/political-positions/data` tanto en el sync incremental como en `explorer-gh-pages-build`. - Resultado visible (`20260307T191620Z`): la superficie publicada mantiene `stances.json` en `125287` bytes y `person-search-index.json` en `3121` bytes, pero ahora expone `120` topic files estáticos en `topic-person-rows/`; el directorio completo pesa `35314914` bytes (antes de compactar este slice, la misma idea ocupaba ~`61018959` bytes) y los archivos individuales quedan entre `268639` y `315758` bytes, por lo que un `Tema exacto` ya puede resolverse con una única descarga temática en lugar de escanear `14` person chunks. - Verificación (`20260307T191620Z`): `node --test tests/test_political_positions_person_trajectory_loading.js tests/test_political_positions_filter_matching.js` (`23/23` pass), `python3 -m unittest tests.test_export_political_positions_snapshot` (`Ran 15 tests`, `OK`), `python3 -m py_compile scripts/export_political_positions_snapshot.py`, `python3 scripts/export_political_positions_snapshot.py --db etl/data/staging/politicos-es.db --out docs/gh-pages/political-positions/data/stances.json --snapshot-date 2026-02-12` (`124443 chars`), `just explorer-gh-pages-publish` (`OK privacy leak scan: no findings`, `files_scanned=1442`, push exitoso a `gh-pages`) y `git ls-remote origin gh-pages` (`f11236264ed0307f92eb31eca4f86cb479710529`). - Riesgo residual honesto: este slice resuelve el caso `Tema exacto` sin chunks, pero la búsqueda temática amplia (`q`/texto libre sin resolución exacta) sigue sin un índice semántico suficiente y todavía cae al scan completo. El siguiente paso correcto es un índice estático por concern/topic similarity o subsets materializados por pack temático, no inflar más los payloads calientes. - Evidencia: `docs/etl/sprints/AI-OPS-516/reports/political-positions-exact-topic-static-preview-rows-20260307.md`, `docs/etl/sprints/AI-OPS-516/evidence/political_positions_exact_topic_static_preview_publish_20260307T191620Z.txt`. - Ejecutado: `AI-OPS-517` añade un sidecar temático selectivo para texto libre en `Tema` y evita el salto inmediato al scan por chunks cuando el filtro todavía no es exacto: - `scripts/export_political_positions_snapshot.py` publica `topic-search-index.json` como companion ligero con `meta.topic_search_index_path='topic-search-index.json'` y `meta.topic_search_index_counts.topic_tokens`; el índice se construye sobre labels/keys de `topics` y sólo conserva tokens que se resuelven a un subconjunto acotado de topics (`<=8`) para no convertir términos comunes en falsa selectividad. - `ui/gh-pages-next/app/political-positions/personTrajectoryLoading.mjs` añade `candidateTopicPreviewTopicIds(...)` y generaliza `topic_preview` para aceptar tanto `Tema exacto` como subconjuntos temáticos selectivos; cuando el sidecar devuelve topics candidatos, `nextPersonTrajectoryChunkIds(...)` vuelve a `[]` y el loader evita el camino de manifest/chunks también para ese caso. - `ui/gh-pages-next/app/political-positions/page.js` carga `topic-search-index.json` sólo cuando hay `Tema` libre no exacto, calcula un subconjunto de topic ids, descarga únicamente esos `topic-person-rows/.json` y muestra chips honestos (`Cargando índice temático…`, `Índice temático estático`, `Vista temática selectiva`). Si el texto es demasiado amplio y el sidecar no es selectivo (`ley`), la UI cae de forma explícita al flujo progresivo existente. - `justfile` sincroniza `topic-search-index.json` hacia `ui/gh-pages-next/public/political-positions/data` en ambos caminos de build/sync para mantener paridad entre `docs/gh-pages` y la app Next. - Resultado visible (`20260307T193524Z`): `stances.json` queda en `125388` bytes, `person-search-index.json` se mantiene en `3121` bytes y el nuevo `topic-search-index.json` pesa `14095` bytes con `726` tokens selectivos publicados. Sobre la snapshot actual, ejemplos reales quedan acotados así: `vivienda -> [250,273,285,289,327]`, `movilidad -> [1,341]`, `transporte -> [301,318]`; en cambio `ley -> []`, por lo que la UI no finge selectividad y sigue usando el fallback progresivo. - Verificación (`20260307T193524Z`): `python3 -m unittest tests.test_export_political_positions_snapshot` (`Ran 17 tests`, `OK`), `node --test tests/test_political_positions_person_trajectory_loading.js tests/test_political_positions_filter_matching.js` (`24/24` pass), `python3 -m py_compile scripts/export_political_positions_snapshot.py`, `python3 scripts/export_political_positions_snapshot.py --db etl/data/staging/politicos-es.db --out docs/gh-pages/political-positions/data/stances.json --snapshot-date 2026-02-12` (`124544 chars`), `just explorer-gh-pages-publish` (`OK privacy leak scan: no findings`, `files_scanned=1444`, push exitoso a `gh-pages`) y `git ls-remote origin gh-pages` (`5d0543f34f13de18d8e38bc52b56688470a6b792`). - Riesgo residual honesto: este slice sólo eleva el filtro `Tema` libre a un preview temático selectivo. La búsqueda genérica `q` con semántica temática amplia todavía no reutiliza este sidecar y sigue pudiendo caer al scan de chunks; el siguiente paso correcto es separar mejor `q` temático de `q` nominal o materializar índices por concern pack, no reengordar el manifest persona. - Evidencia: `docs/etl/sprints/AI-OPS-517/reports/political-positions-selective-topic-search-sidecar-and-multi-topic-preview-20260307.md`, `docs/etl/sprints/AI-OPS-517/evidence/political_positions_selective_topic_search_preview_publish_20260307T193524Z.txt`. - Ejecutado: `AI-OPS-518` reutiliza el sidecar temático para búsquedas `q` de tema y añade un guard explícito para no desviar consultas nominales hacia previews temáticos: - `ui/gh-pages-next/app/political-positions/personTrajectoryLoading.mjs` extiende `candidateTopicPreviewTopicIds(...)` para usar `state.q` cuando `Tema` está vacío y el texto libre sí parece temático; al mismo tiempo introduce un guard por nombre/canonical key (`full_name`/`canonical_key`) que bloquea el preview temático si la consulta coincide con una persona publicada, incluso aunque el `persons[]` no traiga `trajectory_chunk`. - `ui/gh-pages-next/app/political-positions/page.js` activa `topic-search-index.json` también para `q`, distingue el origen del preview (`exact`, `topic_filter`, `q_search`) y refleja ese estado con copy honesto en la UI (`Vista temática por búsqueda`, `Cargando vista temática estática para la búsqueda temática…`). - Resultado visible (`20260307T194759Z`): búsquedas temáticas publicadas como `movilidad sostenible` y `vivienda` ya entran en `topic_preview` sin tocar chunks persona (`[1,341]` y `[250,273,285,289,327]` respectivamente), mientras consultas nominales reales como `santiago abascal` y `ainhoa molina` permanecen en `progressive` y no se mezclan con el preview temático. Las consultas fecha-like (`2026-02-12`) siguen fuera del índice temático por diseño. - Verificación (`20260307T194759Z`): `node --test tests/test_political_positions_person_trajectory_loading.js tests/test_political_positions_filter_matching.js` (`26/26` pass), `node --input-type=module - <<'NODE' ... candidateTopicPreviewTopicIds ... NODE` (`movilidad sostenible -> topic_preview`, `vivienda -> topic_preview`, `santiago abascal -> progressive`, `ainhoa molina -> progressive`, `2026-02-12 -> progressive`), `just explorer-gh-pages-publish` (`OK privacy leak scan: no findings`, `files_scanned=1444`, push exitoso a `gh-pages`) y `git ls-remote origin gh-pages` (`17bfa1b24fa8e66ca45c58ed656456dba4fe9daa`). - Riesgo residual honesto: el guard nominal evita falsos desvíos obvios, pero `q` sigue siendo una caja mixta para persona/tema/partido/método. El siguiente paso correcto es separar la semántica de `q` con un índice temático más explícito o materializar concern-pack previews, no seguir acumulando heurísticas dentro del loader. - Evidencia: `docs/etl/sprints/AI-OPS-518/reports/political-positions-q-thematic-preview-and-nominal-guard-20260307.md`, `docs/etl/sprints/AI-OPS-518/evidence/political_positions_q_thematic_preview_nominal_guard_20260307T194759Z.txt`. - Ejecutado: `AI-OPS-519` hace explícita la intención del buscador general en `/political-positions` con un control shareable `Buscar como`: - `ui/gh-pages-next/app/political-positions/page.js` añade `searchMode` al estado/URL (`search_mode=auto|topic|person`) y publica un selector visible `Buscar como` junto al input `Buscar`; también expone un chip de interpretación (`Buscar: auto`, `Buscar: auto -> tema`, `Buscar: tema`, `Buscar: persona`) para que el usuario vea cómo se leerá la consulta. - `ui/gh-pages-next/app/political-positions/personTrajectoryLoading.mjs` usa ese override en tiempo de carga: - `searchMode=topic` permite que `q` fuerce el preview temático si hay tokens selectivos. - `searchMode=person` desactiva el preview temático vía `q` y conserva la lectura nominal/chunk path. - `searchMode=auto` mantiene la heurística anterior. - Resultado visible (`20260307T195614Z`): el comportamiento deja de ser implícito. En checks reproducibles del helper: - `auto + "movilidad sostenible"` con colisión nominal simulada cae a `progressive` - `topic + "movilidad sostenible"` fuerza `topic_preview` con `[1]` - `person + "movilidad sostenible"` fuerza `progressive` Con esto la UI ya no obliga a aceptar la heurística automática cuando el usuario quiere fijar intención. - Verificación (`20260307T195614Z`): `node --test tests/test_political_positions_person_trajectory_loading.js tests/test_political_positions_filter_matching.js` (`28/28` pass), `node --input-type=module - <<'NODE' ... candidateTopicPreviewTopicIds/searchMode ... NODE` (diferenciando `auto`, `topic`, `person`), `just explorer-gh-pages-publish` (`OK privacy leak scan: no findings`, `files_scanned=1444`, push exitoso a `gh-pages`) y `git ls-remote origin gh-pages` (`dbc27fda42ec0a58c481409e1db3e17f1a8bffc1`). - Riesgo residual honesto: esto aclara y hace shareable la intención de búsqueda, pero no resuelve aún la mezcla conceptual entre descubrimiento temático amplio y búsqueda nominal detallada. El siguiente paso correcto es separar más la UX de descubrimiento temático o publicar previews por concern pack, no añadir más modos al mismo input. - Evidencia: `docs/etl/sprints/AI-OPS-519/reports/political-positions-shareable-search-mode-override-20260307.md`, `docs/etl/sprints/AI-OPS-519/evidence/political_positions_search_mode_override_publish_20260307T195614Z.txt`. - Ejecutado: `AI-OPS-520` hace visible el preview temático y permite fijarlo como `Tema` exacto con un clic: - `ui/gh-pages-next/app/political-positions/page.js` publica una tarjeta `Temas detectados desde Buscar` / `Temas detectados desde Tema` cuando el loader ya ha encontrado `topicId` selectivos para el preview estático; esos matches dejan de ser implícitos y pasan a ser quick picks explícitos. - `ui/gh-pages-next/app/political-positions/topicPreviewSelections.mjs` encapsula la resolución determinista de temas detectados y la transición de estado al fijarlos; si el preview venía de `q`, la UI limpia `q`, resetea `searchMode=auto` y deja una URL basada en `topic=