Árbol de procedimiento del pipeline ParlaIbero

Cada skill diaries-* con sus opciones de invocación, prerequisitos, condiciones de decisión y la acción que toma según cada rama. El hilo conductor es la puntuación de confianza, que cada skill convierte en estado del pipeline.

AUTO · complete FLAG · revisar HALT · detener SKIP · no aplica TEST / decisión artefacto E/S ERROR · aborta

Claude es el orquestador: lee cada SKILL.md, ejecuta las utilidades deterministas de lib/utils/, y resuelve con su juicio los casos ambiguos. Todos los skills de sesión aceptan --session · --batch · --all + --force · --dry-run.

El modelo de confianza — hilo conductor de todo el pipeline

Umbrales globales (lib/confidence.py)

ConfianzaNivelEstadoAcción
≥ 0.85AUTOcompletecontinúa sin interrumpir
0.65–0.85FLAGflagrevisión diferida (/diaries-review)
< 0.65HALThaltintervención manual obligatoria
Cada skill escribe su resultado con update_state.py en state/{iso2}/pipeline_state.json.

Overrides por skill (country_config.thresholds)

Skillautoflagpor qué
diaries-tag0.800.60corpus difícil; etiquetado irregular
diaries-match0.900.70vinculación es crítica → más estricto
resto0.850.65globales por defecto
Algunos skills de país (matrix, match, merge, standardize) determinan el estado por tasas (cobertura, % de error), no solo por un umbral — ver cada tarjeta.

Vista del pipeline — flujo de artefactos

0
diaries-bootstrap — configura el país, audita trabajo previo, infiere config.
country_config/{iso2}.yaml · state/…/pipeline_state.json
1
diaries-ocr — OCR Ollama. SKIP si pdf_digital / html
source/{iso2}/ocr/{id}/page_*.txt
2
diaries-extract — consolida el raw en texto FIEL con marcadores ---PAGE N---. NO limpia: toda la limpieza es de correct. XML de CL: parse_cl_xml.py → matriz por turno, entra directo al nivel de matrix
source/{iso2}/extracted/{id}.txt (capa fiel)
3
diaries-correct — el ÚNICO limpiador: 5 pasos en orden fijo + bucle de residuo + deshifenización de 2ª pasada.
source/{iso2}/corrected/{id}.txt
4
diaries-meta — fecha, nº, tipo, legislatura y la PRESIDENCIA por TRAMOS. lee extracted/ — la capa FIEL
source/{iso2}/meta/{id}.json
5
diaries-dedupe — duplicados en dos clases: (A) misma sesión bajo >1 session_id (boletín + fecha real) · (B) filas dobladas por ingesta multi-track. ⚠ ANTES de tag
source/{iso2}/_quarantine_duplicates/
5b
diaries-deputies opcional aquí — si las actas traen pase de lista nominal, el padrón sale de corrected/ y se pasa a tag con --deputies.
source/{iso2}/deputies/deputies.csv
6
diaries-tag ⚠ — etiqueta oradores (2 niveles · 5 clases de marcador) sobre corpus ya deduplicado.
source/{iso2}/tagged/{id}.txt
7
diaries-matrix — parse <int> → filas CSV (acumulativo). gate incrustados ≤0,1% · Prolegomena orden 0 · ANEXOS → dm_speech=0
source/{iso2}/matrix/interventions_raw.csv
8
diaries-deputies — BD de diputados + deduplicación + padrón publicable.
source/{iso2}/deputies/deputies.csv · standardize/{ISO2}_deputies.csv
9
diaries-match — speaker_raw → id_dep (exact→fuzzy→struct→LLM). tabla CONGELADA (106.938 decisiones · 13 países) reimpuesta al final
source/{iso2}/match/matching_table.csv
10
diaries-merge — join period-aware de las 3 tablas. 3-bis: presidencia por TRAMO · 3-ter: gate de integridad referencial matriz ↔ padrón
source/{iso2}/merge/interventions_enriched.csv
11
diaries-standardize — esquema canónico (16 columnas). guardia --force · 4-ter: desdoblar Prolegomena → asignar_ids → verificar VALORES
source/{iso2}/standardize/{ISO2}_interventions.csv ⚠ canónico
12
diaries-document — README, diccionario, JSON-LD.
docs/{iso2}/README.md · data_dictionary.md · dataset.jsonld

Skills del pipeline — detalle de decisiones

0 diaries-bootstrap
Configura el país: audita lo ya hecho, infiere config, entrevista, inicializa estado
nivel paíssolo --country
Paso 0 · Test de material fuente
¿Existe y no está vacío source/{iso2}/raw/?
vacío / no existe→ERRORaborta: «copia los archivos fuente antes de ejecutar»
hay archivos→lista session_ids (stem de cada archivo) y continúa
Paso 0b · Inventario de trabajo previo (por sesión y por país)
Para cada sesión comprueba qué etapas ya tienen output → celda OK / SKIP / . (pendiente). Clasifica:
ninguna etapa→sesión virgen → pending
algunas etapas con output→marca esas complete (detectado), resto pending
ocr/ vacío + formato digital/html→SKIP OCR (normal y correcto)
ya existe pipeline_state.json→NO sobreescribe: fusiona sesiones nuevas y actualiza pendientes detectadas
Paso 1 · Inferencia automática del formato (discriminador FIABLE)
¿Los archivos de análisis son raw o ya procesados?
corrected/ o extracted/→infiere formato del contenido o pregunta (confianza 0.70/1.0)
raw/ → assess_source.py→estructura por año (capa de imagen vía pdfimages), NO char-count
año DIGITAL→pdf_digital → texto embebido, OMITIR OCR
año ESCANEADO/MIXTO→pdf_image → capa OCR de calidad desconocida (ante duda: OCR)
⚠ Paso 1d — chequeo de integridad del prefijo de orador en escaneados: si garble_ratio > 0.40 con corrupción real (p.ej. SEROR≈SEÑOR) → source_format: pdf_poor_ocr, OCR obligatorio. (UY 1987: embebido detectó 14 intervenciones, OCR 164.)
Paso 2 · Entrevista dirigida
campo confianza < 0.80→muestra evidencia y pregunta (máx 8 preguntas)
usuario confirma regex→úsalo directo (confianza 1.0)
usuario dice «no sé»→deja needs_review: true
ya hay sesiones tagged→toma sus patrones como referencia y reduce la entrevista
Salidas
country_config/{iso2}.yaml state/{iso2}/pipeline_state.json docs/{iso2}/onboarding_report.md
1 diaries-ocr
OCR con Ollama (LightOnOCR-2:1b → glm-ocr) sobre escaneados
nivel sesión0.85 / 0.65
Opciones
--session YYYY-MM-DD--batch YYYY-MM --all--force--dry-run
Paso 0 · Test de formato (puerta de entrada)
¿source_format requiere OCR?
pdf_digital / html→SKIP imprime aviso, NO toca estado, deriva a extract
pdf_image / pdf_poor_ocr→arranca Ollama (mata huérfanos, KEEP_ALIVE=-1) y procesa
Paso 1 · Filtro de pendientes
status complete o skipped→excluida (salvo --force)
--dry-run→imprime comando, no ejecuta ni actualiza
Paso 2d · Confianza y estado
confidence = mean_confidence (del OCR) páginas en low_confidence_pages < 0.50 → advertencia en el resumen
AUTOconfidence ≥ 0.85
FLAG0.65 ≤ confidence < 0.85
HALTconfidence < 0.65
Rendimiento: el GPU satura con 1 inferencia → workers = ollama_num_parallel = 6 es el óptimo; 8 perjudica. Causa #1 de lentitud = procesos huérfanos (matar antes del batch). Paralelizar a nivel de página.
Salida
source/{iso2}/ocr/{id}/page_*.txt
2 diaries-extract
Consolida el raw en texto FIEL con marcadores de página — NO limpia (la limpieza entera es de diaries-correct)
nivel sesión0.85 / 0.65
Paso 3b/3c · Mapeo source_format → modo de extracción
pdf_digital→pdf_digital — páginas vía fitz (paginado)
pdf_image / pdf_poor_ocr→ocr_pages — lee los page_*.txt ya producidos · NO reprocesa OCR
html / docx→html / docx — con sus trampas de encoding (charset declarado, mojibake por archivo)
xml (CL)→NO pasa por extract_text.py: parse_cl_xml.py — el XML ya viene TAGGED y MATCHED (id oficial de la Cámara como id_dep) → matriz por TURNO directa; esas sesiones entran al pipeline en el nivel de matrix (PT exigirá parser propio, esquema distinto)
doc (CL)→textutil -convert txt → tratar como text_flat
text_flat→text_flat — texto ya extraído; con page_break_marker segmenta por esa marca
La capa FIEL — conserva la frontera de página y NO se limpia nunca
---PAGE N--- en línea propia→se CONSERVA (extract.emit_page_marker, default true): es el ANCLA con la que correct quita el mobiliario
cabeceros / mobiliario / hifenación→NO se tocan aquí — --strip-headers queda como legado opt-in, fuera del flujo canónico
extracted/ como punto de retorno→cambiar una regla de limpieza NO obliga a volver a los binarios: se re-corre correct sobre extracted/; y meta lo consulta ÍNTEGRO (el cabecero por página trae los metadatos)
Paso 3e–3h · GATES de calidad (miden, NO limpian) → confianza y estado
confidence = valid_ratio si chars(salida) < 500 → confidence = min(confidence, 0.60) GATE 3f fragmentación: línea mediana < 25 car o cortes con guion > 2% → probar otro modo de extracción; si persiste, bajar confianza GATE 3g (escaneadas): garble_ratio > 0.50 en el prefijo de orador → confidence = min(confidence, 0.55) → HALT (re-OCR obligatorio) (sin lógica de cabeceros: en esta etapa no se limpia nada)
AUTO≥ 0.85 y sin alertas
FLAG0.65–0.85 · o fragmentación persistente
HALT< 0.65 · prefijo OCR corrupto
Gate de salida del batch: > 2% de archivos con < 200 bytes → PARA e investiga el formato. Trampas por origen: charset DECLARADO del HTML, elementos void, el <p> VACÍO como frontera de párrafo, mojibake 2× (PT) y DOCX heterogéneo por archivo.
Salida
source/{iso2}/extracted/{id}.txt — capa FIEL con ---PAGE N--- (no se limpia NUNCA)
3 diaries-correct
El ÚNICO limpiador: secuencia de 5 pasos + bucle de residuo + deshifenización de 2ª pasada
nivel sesión0.85 / 0.65
Rediseño 2026-08: correct es el ÚNICO limpiador. extract entrega extracted/ fiel y SIN limpiar (con los ---PAGE N--- conservados); TODA la eliminación de cabeceros, folios, mobiliario e hifenización ocurre aquí, una sola vez. Tres partes: Paso 0b DESCUBRIR el mobiliario del país (por frecuencia, por década → country_config) → Paso 2 APLICAR → Paso 2-R VERIFICAR EL RESIDUO en bucle.
Secuencia canónica de limpieza — 5 pasos, orden obligatorio
1 · identificar el separador de página→---PAGE N--- (o el del país): el ANCLA de todo lo demás
2 · eliminar el mobiliario→anclado al separador — CINCO clases: furniture_block/line/masthead/prefix/inline; masthead con keep-first; NUNCA borrar Prolegomena
3 · eliminar el separador→solo AHORA, ya vacío de mobiliario (quitarlo antes deja el mobiliario huérfano)
4 · aislar los marcadores de orador→línea en blanco delante de cada speaker_tag_patterns: frontera dura que el paso 5 no cruza
5 · restaurar la fluidez→deshifenizar y unir las líneas de caja DENTRO de cada turno, sin cruzar los marcadores del paso 4
El separador, el mobiliario y el marcador son DE CADA PAÍS: se DESCUBREN leyendo su texto (y por década), se GUARDAN en su country_config y se VERIFICAN antes de promover. Nunca en bloque, nunca adivinando ni reusando los de otro país.
Paso 2c/2d · Pase determinista + resolución LLM de saltos ambiguos
correct_text.py --config: corre los 5 pasos con el vocabulario del país; protege los marcadores en el merge y los reensambla (marker_reassembly · marker_join_next con condición de CIERRE — patrones SIMÉTRICOS en género). Devuelve furniture_removed, marker_joins y ambiguous_breaks.
salto = artefacto (corte de columna/justificación)→une las dos partes con Edit
salto = párrafo real (fin de oración + mayúscula)→no toca nada
batch --all masivo→NO se resuelven uno a uno (son mayormente legítimos): pase determinista + confianza por integridad
Gates 2d-bis / 2d-ter · el marcador ni PARTIDO ni ENTERRADO
residuo de marcadores partidos ≠ ~0→no avanzar a tag: añadir el patrón a marker_join_next y re-correr (PY: saltárselo costó ~100.000 turnos, el 25% del corpus)
caída de marcadores > 2% por sesión (extracted vs corrected)→HALT la sesión: el acumulador ENTERRÓ marcadores (BR −28%); el −N ES la señal — verificar con patrones vacíos es ciego
Paso 2-R · Bucle de residuo de mobiliario (supervisado)
descubrir → aplicar → verificar residuo → refinar … hasta ~0 DOS detectores: (1) limpiar_mobiliario --medir (patrones conocidos) (2) INDEPENDIENTE por frecuencia: líneas idénticas que recurren en >20% de sesiones y no cierran frase — no comparte léxico con el config ⚠ un 0 limpio es SOSPECHOSO hasta explicarlo (corpus aplanado no se ve)
Paso 2-quater · Deshifenización de 2ª pasada sobre corrected/ completo
deshifenizar.py --textdir — léxico del PROPIO corpus, no un diccionario externo: une el guion+salto residual y el guion+ESPACIO (~209 k solo en ES). La variante espacio exige evidencia positiva del léxico y CONSERVA lo ambiguo. Primero --medir, luego aplicar.
Paso 2e · Confianza — por INTEGRIDAD, no por ratio de ambiguos
⚠ NO usar ratio_ambiguous como proxy: es alto POR GÉNERO del texto (orden del día, listas, marcadores) → FLAG masivo falso si out_chars ≥ 0.80·in_chars y total_paragraphs > 0 y out_chars > 300 → confidence = 0.95 → complete si no (pérdida de contenido / salida vacía o minúscula) → confidence = 0.60 → flag
Salida
source/{iso2}/corrected/{id}.txt
4 diaries-meta
Metadatos: fecha, nº de sesión, tipo, legislatura y la PRESIDENCIA por TRAMOS
0.85 / 0.65
Posición: justo después de diaries-correct, antes de diaries-tag. Su insumo es extracted/{id}.txt — la capa FIEL: los metadatos de sesión viven a menudo en el cabecero de cada página, que diaries-correct elimina; corrected/ queda solo como respaldo por sesión concreta. Ejecutarlo aquí es lo que permite que diaries-dedupe elimine los escaneos duplicados antes de la fase de etiquetado. Además, la presidencia por TRAMOS que extrae (presidency.segments) es el insumo del Paso 3-bis de diaries-merge, que atribuye los roles de mesa sin nombre.
⚠ Paso 0 obligatorio: usar la heurística registrada del país (meta_patterns) o DESCUBRIRLA — muestra de 40 documentos repartidos por TODO el período, validada con cuatro señales (resuelve contra el padrón · contigüidad temporal · cardinalidad · reparto por género). Los regex se anclan a la fórmula de cabecera de la sesión (no a la 1ª fecha del texto, que suele ser un acta/decreto citado). El parser de meses debe cubrir variantes locales (setiembre) y el ordinal de día (1º).
Paso 0c-bis · Presidencia por TRAMOS — no un único presidente
⚠ «Un presidente por sesión» falla el 35,0% de los casos (medido): la presidencia cambia de manos DENTRO de la sesión. Meta registra la SECUENCIA de quién preside y en qué tramo (presidency.segments), no un solo nombre; president (singular) se mantiene por compatibilidad y se DERIVA — es la apertura, segments[0].name.
mesa de la carátula/Prolegomena→presidente de APERTURA (via: prolegomena) + candidatos a relevo (vicepresidentes, secretarios)
relevos anunciados en el CUERPO→patrón DESCUBIERTO por país («Ocupa la Presidencia el señor X»…) → via: relevo
«reasume la Presidencia» sin nombre→via: back_to_base — el centinela que devuelve la mesa al titular; sin registrarlo, el tramo del vice no se cierra nunca
scope del país→presidency.scope: session | segments en el country_config — lo consume attribute_presidency.py aguas abajo
Prioridad de fuentes de date (mayor → menor autoridad)
1 · cabecero corrido (voto mayoritario)→MÁXIMA autoridad: repetido en cada página, robusto
2 · masthead de portada→válido solo si el cabecero lo corrobora (único punto de fallo)
3 · nombre de archivo (session_id)→ÚLTIMO RECURSO, puede tener errores; nunca gana al cabecero
Paso 2d · Resolución de date por voto del cabecero corrido
cabecero = nombre (≥2 votos)→el masthead era el error → AUTO
cabecero = nombre − 1 día→nombre corrido +1 → adopta fecha del cabecero (date≠session_id) → AUTO
mismatch/fallback NO resuelto por el cabecero→nunca AUTO → FLAG (revisión)
sin fecha en cabecero, masthead ni nombre→HALT
Paso 2e · Confianza ponderada (5 campos × 0.20)
cada campo: regex → 0.20 | LLM → 0.17 | no encontrado → 0.0 confidence = Σ contribuciones de {date, session_number, session_type, legislature, president} (el override de date manda sobre el resultado del peso)
Salida
source/{iso2}/meta/{id}.json
5 diaries-dedupe
Duplicados en DOS clases: (A) misma sesión bajo >1 session_id · (B) filas dobladas por ingesta multi-track — antes de tag
boletín + fecha
Va ANTES de diaries-tag, no antes de matrix (corregido 2026-07-24). Su único insumo es meta.date, y diaries-meta lee extracted/ (la capa fiel): nada obliga a esperar al etiquetado. Hacerlo después tiene un coste de corrección, no solo de tiempo — el Paso 0c de diaries-tag rankea las formas de marcador por frecuencia sobre TODO el corpus para decidir cuáles entran al léxico; con sesiones duplicadas dentro, sus formas se cuentan dos veces y esa calibración sale sesgada. Deduplicar después ya no lo arregla.
⚠ Imprescindible antes de matrix: sin esto, los escaneos duplicados doblan el conteo de intervenciones. Señal fiable = nº de boletín + fecha real verificada por el día de la semana; el contenido (Jaccard) da falsos negativos por distinta calidad OCR.
Clase A · Decisión por grupo de sesiones (boletín + fecha real)
mismo boletín ∧ misma fecha real→DUPLICADO → cuarentena (conserva el keeper)
mismo día, boletín distinto→sesiones legítimas (mañana/tarde) → intactas
mismo boletín, fecha real distinta, contenido no similar→boletín OCR mal leído → revisar, no se toca
Clase B · Paso 1-bis — filas DOBLADAS dentro de una sesión (solo ingesta multi-track)
desplazamiento CONSTANTE de intervention_order→detector preferente: la firma inequívoca de la ingesta doble, sin umbrales (CO 2007-05-02: pares todos a 526 de distancia)
solapamiento ≥ 70% · 25–70%→duplicada / parcial → deduplicar fila a fila (conserva: con id_dep → texto más largo → track de calidad MEDIDA en ese país)
solapamiento < 25%→complementaria, NO es duplicado — no tocar. NUNCA descartar una copia entera (CL: ninguna contenía a la otra; la «peor» era la mejor)
siempre→informa y pide confirmación explícita, incluso con --write/--yes; tras deduplicar, renumerar intervention_order (1-based y continuo)
Cuarentena reversible + estado POR SESIÓN
Clase A: redundantes → source/{iso2}/_quarantine_duplicates/ (corrected,tagged,extracted,meta,ocr,raw) manifest.json: redundant → keeper · restaurar = mover de vuelta · idempotente Clase B: filas retiradas → standardize/{ISO2}_quarantine_intrasesion_dups.csv (nunca se borran) Estado POR SESIÓN (no _country): cada redundante → skipped · cada keeper → complete
Salida
source/{iso2}/_quarantine_duplicates/ + manifest.json standardize/{ISO2}_quarantine_intrasesion_dups.csv (clase B)
6 diaries-tag ⚠
Etiqueta oradores — skill crítico, dos niveles de detección
0.80 / 0.60
Prerequisito: el corpus debe estar YA deduplicado (meta → dedupe → tag). Si el país trae pase de lista nominal, construye además el padrón antes de tagear (diaries-deputies, ruta del pase de lista) y pásalo con --deputies: mejora el scoring del nivel 2 probabilístico. Antes esa mejora era inalcanzable porque el padrón se construía cuatro pasos después.
El honorífico y el terminador son específicos de cada país y cambian por OCR/era/taquígrafo. Paso 0 obligatorio: discover_speaker_patterns.py descubre las formas por su FORMA — el vocabulario nunca se adivina (falló en SV y dos veces en GT). Punto de partida: docs/_metodologia/FASE_C_MARCADORES_POR_PAIS.md — terminadores, clases de marcador y variantes OCR ya pagados; el descubrimiento los CONFIRMA y amplía, no los reinventa. Semilla de terminadores por país: : (ES·GT·EC) · .— (UY — ignorarlo enterró 7.851 marcadores) · .- (grafía OCR del anterior, EC) · — standalone (PT); tolerancia OCR: se\S{1,2}or (seflor, sefior…), EL→FL (EC), H.D.→HE. (PA). Paso 0b obligatorio la 1ª vez: analyze_speaker_markers.py descubre variantes/errores antes de tagear.
Las CINCO clases de marcador — cada una con tratamiento propio
FORMAL→núcleo VERSAL + terminador del país (:, .—…) — la única clase que abre turno (produce <int>)
APARTE→interjección entre paréntesis DENTRO del turno de otro — NO abre turno; se separa como turno propio solo si hay TEXTO delante, no por distancia (BR: 18.718)
MENCIÓN→Titlecase + verbo en el discurso («el señor García afirma…») — ignorar; el discriminador frente al turno real es el núcleo VERSAL
VOTO nominal→«Por el señor Diputado X», listas de votación — NO abre turno: tag.vote_patterns lo EXCLUYE del tagging y diaries-matrix declara esas filas dm_speech = 0
NARRATIVA→el orador va en una CLÁUSULA en prosa, no en un núcleo versal (DO, CO) — modo propio tagging_style: narrative; el speaker_raw se NORMALIZA a la persona, no a la frase
Paso 2c/2d · Detección de dos niveles + LLM
NIVEL 1 estricto (speaker_tag_patterns)→marcador canónico; guion [-–—] (OCR usa em-dash) → etiqueta directa
NIVEL 2 score ≥ 0.60→etiqueta (recupera ~5% de terminadores irregulares)
NIVEL 2 score ∈ [0.40, 0.60)→caso límite → FLAG / resolución LLM
span sospechoso (untagged)→LLM decide orador (inserta <int speaker>) o lo descarta
Paso 2e · Confianza + guardia estructural
confidence = n_auto / (n_auto + n_untagged + n_llm·(1 − avg_llm_conf)) si n_auto + n_untagged + n_llm == 0 → confidence = 0.50 (sesión sin oradores: sospechosa) GUARDIA (fuente escaneada): si source_format ∈ {pdf_poor_ocr, pdf_image} y chars > 5000 y n_auto < 3 → confidence = min(confidence, 0.50) «prefijo de orador corrompido por OCR embebido → re-OCR obligatorio»
AUTOconfidence ≥ 0.80
FLAG0.60 ≤ confidence < 0.80
HALTconfidence < 0.60
Salida
source/{iso2}/tagged/{id}.txt
7 diaries-matrix
Parse de texto etiquetado → filas CSV (determinista)
nivel sesión
Gate de entrada + Paso 3a/3b · Prerequisitos y auditoría previa
diaries-tag con sesiones en HALT (falsos negativos)→ABSOLUTE FAIL — NO construir la matriz: un marcador sin etiquetar atribuye su intervención entera al orador anterior; volver a /diaries-tag
falta tagged/ o meta/→HALT, informa qué falta
n_tags <int> == 0 en fuente→HALT: archivo sin etiquetas válidas
intervention_format→auto · inline (<int speaker>) · standalone (PT)
Paso 3e/3f · Cobertura de tags (salvaguarda anti-bug del parser)
tasa_cobertura = (interventions_added + filtered_short) / n_tags_fuente confianza = 1 − untagged_blocks / (interventions_added + untagged_blocks) si tasa_cobertura < 0.80 → confianza = min(confianza, tasa_cobertura)
completeconfianza ≥ 0.80
FLAGconfianza < 0.80 · o cobertura < 0.50
HALTcobertura < 0.20 (fallo sistemático)
Paso 5 · Auditoría de marcadores INCRUSTADOS en el text (gate de salida)
ratio ≤ 0,1%→aceptable (residuo de OCR irreducible) → split_embedded_markers.py divide las filas residuales y recupera el turno (parte del flujo, decidido 2026-08-23)
ratio > 0,1%→NO parchear el CSV ni avanzar a deputies/match/merge: corregir la CAPA de origen y re-ejecutar correct → tag → matrix (PA: 4 iteraciones recuperaron +42.132 intervenciones)
0,00% con la config por defecto→sospechoso hasta explicarlo, no una buena noticia (AR: 7 filas «detectadas» frente al 1,00% real medido a mano — el guion se corrompe en —/_/~/·)
Paso 5-bis · PROLEGOMENA — la carátula y el sumario del acta
recover_frontmatter.py: todo lo anterior al primer marcador entra como Prolegomena — una fila por acta con intervention_order = 0, dm_speech = 0 y sin orador, SIN desplazar la numeración del resto. Sin tope de longitud (los documentos de la cuenta de CL, 1,3 M car., son legítimos). Prueba de aceptación: ningún marcador de intervención dentro del Prolegomena.
Paso 5-ter / 6 · ANEXOS del diario + inventario no-discurso (declarar, NO borrar)
separador EXPLÍCITO de anexos en el acta→todo lo posterior → dm_speech = 0 DECLARADO (matrix.annex_separator del config); la fila se queda con su orden y su texto íntegro
sin separador marcado en el acta→NO se especula: discriminancia ≠ frontera — las filas quedan como están
acotaciones / votaciones / cabeceras→default dm_speech = 1; el 0 solo donde está DEMOSTRADO. Se DECLARAN, no se apartan — el sidecar quedó ANULADO como política
Salida
source/{iso2}/matrix/interventions_raw.csv (acumulativo)
8 diaries-deputies
Construye la BD de diputados + deduplicación obligatoria + padrón publicable
nivel país--force · --dry-run
Paso 2 · Guardias
falta interventions_raw.csv→ERROR ejecuta diaries-matrix
deputies.csv existe y no --force→ERROR usa --force
Paso 3/4 · Dos rutas de construcción
deputies.external_file definido→Ruta A: valida columnas, genera id_dep si falta, mapea a esquema canónico
external_file vacío→Ruta B: extrae speakers únicos, agrupa por similaridad (clúster 4+ → juicio LLM)
Paso 5 · Deduplicación (SIEMPRE, 3 pasadas)
Pasada 1: fuzzy token_sort_ratio ≥ 85→typos, acentos, OCR, orden de tokens
Pasada 2: subconjunto de apellido + mismo nombre→apellido completo vs primero (inter-legislatura)
Pasada 3: vecinos ±5 ordenados por apellido→iniciales expandidas (Apellido1 A. ↔ Apellido1 Apellido2)
par ambiguo→presenta al usuario (partido/distrito/fechas) antes de fusionar
Mismas filas con mismo id_dep en distintas legislaturas son correctas. Re-escanea tras cada ronda de fusión.
Paso 6 · Confianza
confianza = 1 − clustered_ambiguous / total_unique ( < 0.85 → FLAG )
Prerequisito · depende de la RUTA (matrix NO siempre hace falta)
A · archivo externo→el propio archivo · en cualquier momento
Pase de lista nominal (preferente)→corrected/ · ANTES de diaries-tag, alimenta su scoring con --deputies
B · desde speakers→matrix/interventions_raw.csv · después de diaries-matrix
El roster desde el pase de lista es estrictamente superior: incluye a quien no habla, el período es explícito y es dato primario del acta. Además el índice fecha→presentes que produce es el insumo de la desambiguación por asistencia de diaries-match: un paso temprano mejora dos posteriores.
Paso 6 · Derivar sex + padrón PUBLICABLE
derive_sex.py: cascada de 6 niveles con procedencia declarada en sex_source (exactitud medida 98,98%; por sexo: M 99,54% · F 97,43%, y eso se declara) — se corre en su momento, tras diaries-standardize. El padrón publicable standardize/{ISO2}_deputies.csv se publica junto a la matriz, unible por id_dep (+ legislature): forma LARGA, una fila por (id_dep, legislature) — id_dep NO es único. Los pares huérfanos matriz→padrón no se corrigen ni se apartan: el acta manda, se declaran en process_report.md.
Salida
source/{iso2}/deputies/deputies.csv source/{iso2}/standardize/{ISO2}_deputies.csv (publicable)
9 diaries-match
Vincula speaker_raw → id_dep (umbrales altos)
nivel país0.90 / 0.70
Paso 0 · La vinculación CONGELADA manda
106.938 decisiones speaker_raw → id_dep congeladas en 13 países, con manifiesto sha256 (solo BR, CL y PE no tienen tabla: se construyeron fuera del pipeline). frozen_matching.py --check dice ANTES de empezar cuánto trabajo manual hay en juego; en un arranque desde cero el --apply de este paso es un no-op silencioso (la tabla aún no existe) — el que protege de verdad es el Paso 6-final. La clave se casa EXACTAMENTE, sin normalizar tildes ni caja.
Cascada de métodos de vinculación
Paso 4 · fuzzy_match.py: limpia títulos, matchea contra nombre completo Y apellidos (token_set_ratio), filtra por vigencia temporal (mandato vs fecha de sesión).
match_method = exact→vinculado
= fuzzy (≥ threshold)→vinculado
= ambiguous (apellido compartido)→trae candidates → LLM elige por contexto
= unmatched→Paso 4b heurísticas estructurales (prefijo, iniciales, apellido+nombre) → si no, LLM
Paso 5 · LLM resuelve ambiguous + unmatched con contexto (±3 intervenciones, partido, fecha) → llm o unmatched final.
Paso 4c-quater · Género del CARGO — solo la forma FEMENINA vincula por sexo
LA PRESIDENTA / LA SECRETARIA→solo la forma femenina del cargo autoriza a vincular a una mujer
forma masculina del cargo→es GENÉRICA — cubre a cualquiera que ocupe el cargo → NO se vincula en bloque por sexo
forma de cargo → persona→NUNCA en bloque: el cargo lo ocupan personas distintas según el tramo; la atribución va por TRAMO (merge, Paso 3-bis). Coste medido: 6.514 filas reatribuidas; UY, 16.812 filas de «A PRESIDENTA» en un hombre
Paso 6-final · RE-IMPONER la vinculación congelada (OBLIGATORIO)
Tras reconstruir y enriquecer la tabla: frozen_matching.py --check + --apply. Es el paso que protege de verdad en el reproceso completo. Lo que ya no casa se INFORMA, no se borra: cada huérfana va a docs/{iso2}/vinculacion_huerfana.csv — una decisión humana a rehacer sobre la forma nueva. Reportar los recuentos del --check (coinciden · contradicen · nuevas · huérfanas).
Primacía del diario: una intervención NUNCA se elimina porque su orador no case con el padrón — la fila sin id_dep es un hueco visible y auditable; la borrada, una pérdida silenciosa.
Paso 7 · Estado por TASA de cobertura
confianza_global = n_matched_auto(exact|fuzzy, conf≥0.90) / n_unique_speakers sin_resolver = (n_unique − n_matched_auto) / n_unique
completesin_resolver ≤ 10%
FLAGsin_resolver > 10%
HALTsin_resolver > 30%
Salida
source/{iso2}/match/matching_table.csv
10 diaries-merge
Join pandas de raw + matching + deputies
nivel país
Paso 3 · Verificación de integridad (crítica)
filas_salida ≠ filas_entrada→HALT: el join perdió/duplicó filas
falta cualquiera de los 3 CSV→ERROR ejecuta el skill que falta
Paso 4 · Estado por match_rate
completematch_rate ≥ 0.90 y filas coinciden
FLAGmatch_rate ≥ 0.70 y filas coinciden
HALTmatch_rate < 0.70 o filas no coinciden
Paso 3-bis · Atribución de la PRESIDENCIA por TRAMO (attribute_presidency.py)
El paso de mayor impacto del pipeline y el más fácil de olvidar: hasta el 42,1 % del corpus (PE) y el 36,5 % (DO) son intervenciones etiquetadas solo con el cargo, sin nombre. El acta declara quién presidió y diaries-meta ya lo extrae como presidencia por TRAMOS (presidency.segments: apertura + relevos + back_to_base). Se ejecuta con la utilidad genérica attribute_presidency.py, parametrizada en el country_config (presidency:) — nunca un script por país. En un reproceso se RE-EJECUTA desde cero: el enriched regenerado pierde estas atribuciones. Se acepta por el ACIERTO medido con el juez de género del vocativo (audit_presidency_gender.py), nunca por la reproducción.
1 sola presidencia en la sesión→atribuye · match_method = president_meta
scope: segments (la mesa rota dentro)→atribuye por TRAMO siguiendo las marcas de relevo del propio acta; el relevo sin nombre (back_to_base) cierra el tramo del vice
varias, de distinto sexo→desempata PRESIDENTE/PRESIDENTA contra el sexo del padrón
titular + «en funciones» juntos, o 2+ del mismo sexo→NO atribuir — el acta nombra a quienes presidieron, no cuándo se relevaron. Ante la duda, no atribuir: la fila sin id_dep es un hueco visible; la mal atribuida, un error silencioso
Paso 3d · Consistencia temporal (obligatoria)
fecha_inicio ≤ date ≤ fecha_fin para toda fila con id_dep referencias medidas: DO 99,96% · PE 97,40% < 99% → FLAG desalineadas · dos clases con tratamiento distinto: hueco intermedio del roster (hay filas antes Y después) → añadir fila, heredar partido/distrito, notas: periodo:inferido-servicio-continuo extremo (habla fuera de todo mandato conocido) → posible error de match, NO arreglar en automático: sacarlo al informe
Paso 3-ter · Gate de INTEGRIDAD REFERENCIAL matriz ↔ padrón (obligatorio antes de standardize)
id_dep inexistente en el padrón→error duro: errata de id o fila que falta en deputies.csv — se corrige o se completa el padrón, no pasa al canónico
par (id_dep, legislatura) huérfano→el acta manda: la fila NUNCA se elimina — se completa el padrón (suplencias, tomas de posesión a media legislatura) o se declara en process_report.md
El padrón está en forma LARGA (una fila por id_dep + legislatura): el join es por id_dep + legislatura — solo por id_dep multiplica las filas de la matriz. Y la confianza del Paso 4 se RECALCULA sobre el enriched FINAL, tras el 3-bis (el match_rate del join solo puede dejar en flag filas que 3-bis ya resolvió).
Salida
source/{iso2}/merge/interventions_enriched.csv
11 diaries-standardize
Esquema canónico final (16 columnas) + padrón auxiliar — salida del corpus
nivel país
Paso 1 · GUARDIA de sobreescritura (antes que nada)
{ISO2}_interventions.csv existe y NO --force→ERROR aborta. «Sobreescribir elimina el archivo canónico». Nunca continúa sin --force
Paso 5/6 · Validación de esquema → estado por % de error
confianza = 1 − validation_errors / total_rows
completeerrores ≤ 5% de filas
FLAGerrores > 5% de filas
HALTerrores > 20% de filas
Esquema canónico = 16 columnas, en este orden: id_session · id_int · legislature · legislative_session · session_number · date · session_type · intervention_order · speaker_raw · id_dep · speaker_name · sex · party · district · dm_speech · text. session_number acepta entero o alfanumérico (12, 12A, 001O); date debe ser ISO YYYY-MM-DD; separador , salvo ES y GT, que usan ;. standardize_csv.py declara las 16 (crea id_session/id_int vacías y las preserva); el Paso 4-ter las RELLENA.
Paso 4-ter · TRES sub-pasos en orden fijo: desdoblar → asignar → verificar VALORES
a · desdoblar Prolegomena duplicados→desdoblar_prolegomena_duplicado.py (primero --medir): dos carátulas con orden 0 en la misma clave colisionan en id_int …00000 — elimina las copias EXACTAS y sufija las distintas (104 → 104B, sesión propia con carátula y sin turnos). 7 países las tenían (CR 7.678 · ES 1.650 …)
b · asignar los ids→asignar_ids.py: id_session = {ISO2}{leg:03d}{ses:04d} (9 car.) · id_int = +{orden:05d} (14 car.; orden 0 → …00000). Se NIEGA a escribir si detecta id_int repetidos
c · verificación de VALORES→obligatoria — la cabecera no basta: asignar_ids termina con rc=0 incluso al negarse (buscar «✗» en su salida) y los ids quedan VACÍOS. Comprobar id_int vacíos = 0 y duplicados = 0; con vacíos, resolver las claves repetidas; con duplicados, PARA y restaura el backup
⚠ Seguridad de reejecución — qué destruye un --force
El guardia protege el archivo, pero NO avisa de lo que se pierde. Sobre un corpus ya cerrado suele haber operaciones POSTERIORES al pipeline estándar: 1 · sidecar — LEGADO de la línea base, YA NO es política vigente: nada se aparta, se DECLARA con dm_speech = 0. Si existe un *_sidecar.csv, al regenerar sus filas se REINTEGRAN al canónico con dm_speech = 0; no se re-extraen a un sidecar nuevo 2 · atribuciones post-merge → viven solo en el enriched, (match_method = president_meta) no en la matching_table 3 · correcciones humanas → manual / NOT_SPEAKER / role Compara SIEMPRE el recuento antes y después. Si al regenerar bajan las filas vinculadas, has perdido trabajo: recupéralo del backup antes de continuar.
Salida ⚠ canónica
source/{iso2}/standardize/{ISO2}_interventions.csv
12 diaries-document
Documentación del corpus (entrevista + README canónico)
nivel país
Paso 1 · Prerequisito + reutilización de entrevista
falta {ISO2}_interventions.csv→ERROR ejecuta standardize
corpus_info.json existe + --force→carga como base y ofrece modificar
existe sin --force→salta entrevista, regenera docs
nuevo→entrevista de 5 preguntas (fuentes, transformación, autores, notas)
Paso 5/6 · Generación + verificación
generate_docs.py OK→complete (confianza 1.0)
falta algún archivo de salida→HALT
Salidas
docs/{iso2}/README.mddata_dictionary.mddataset.jsonld

Skills de control — disponibles en cualquier momento

Cdiaries-status
Tabla de estado (solo lectura)
Lee pipeline_state.json y agrega por skill: total · OK · FLAG · HALT · Pend · confianza media. Skills país: entrada _country. ⚠ diaries-dedupe se cuenta POR SESIÓN aunque sea de nivel país: marca complete/skipped en cada sesión afectada, no en _country.
hay HALT→«revisa errores y re-ejecuta con --force»
hay FLAG→deriva a /diaries-review
todo complete→«corpus listo» → /diaries-report
solo pending→sugiere el siguiente skill pendiente
Cdiaries-review
Bucle interactivo de FLAGs + listado y resolución de HALTs
--skill (filtro opcional)
Paso 2-bis · Casos HALT — listar con su error y marcar resueltos
lista cada HALT con su error_message→la intervención manual se hace FUERA del skill; los HALT no entran al bucle de FLAGs
intervención ya realizada→registra CorrectionRecord punctual + marca complete (o flag si debe pasar por el bucle)
no realizada→se mantiene en halt; seguirá apareciendo aquí y en /diaries-status
Por cada caso FLAG (en orden de pipeline) — menú de opciones
[A] Aceptar→marca complete con la confianza actual
[C] Corregir→registra CorrectionRecord puntual + marca complete
[R] Reintentar MiniMax→solo diaries-ocr: re-OCR de páginas <0.70, recalcula estado
[E] Escalar→marca HALT (intervención manual)
[S] Saltar→mantiene FLAG
[Q] Salir→rompe el bucle
Cdiaries-feedback
Inyecta correcciones — 3 tipos con distinta propagación
Propagación según --type
punctual (--from sesión)→1 sesión · registra CorrectionRecord · no propaga
rule (--from sesión + --pattern/--action)→re-marca como pending todas las sesiones posteriores a --from → re-ejecutar el skill
strategy→añade a global_rules[] · requiere re-ejecutar con --force
Tras editar valida el JSON (json.load); si falla, no confirma el guardado.
Cdiaries-report
Informe del proceso + diagnóstico de salud
Paso 4 · Salud global del pipeline
VERDE→automatización ≥ 80% · sin HALTs · confianza media ≥ 0.85
AMARILLO→automatización ≥ 60% · HALTs ≤ 5% · confianza ≥ 0.75
ROJO→cualquier otra combinación
Incluye histogramas ASCII de confianza por skill e historial de correcciones (punctual/rule/strategy). Salida: docs/{iso2}/pipeline_report.md — interno; nunca process_report.md, que es el informe público de la cadena del depósito (decisión ar-0029)
Lee antes lo que registró /diaries-decide (decisions.jsonl) en vez de reconstruir el razonamiento: cada entrada trae qué se decidió, qué se descartó, con qué evidencia y qué cambió.
Cdiaries-decide
Registra una decisión metodológica con su evidencia
--pais {iso2} | transversal--supersede
Cuándo — decisiones que alguien podría cuestionar después
umbral, criterio o definición→registrar por qué ESE valor
se aparta / divide / transforma→registrar qué se descartó y con qué prueba
fuentes que discrepan→registrar cuál mandó y por qué
se CORRIGE una decisión previa→--supersede · el caso más valioso
trabajo rutinario→NO registrar
La evidencia es obligatoria y se valida (mínimo 40 caracteres). Existe para impedir que se registre una preferencia como si fuera un hallazgo: «se usó el umbral 0,5» no sirve; «se usó 0,5 porque con controles independientes daba ratios de 400× a 4.500× sobre 40.000 filas» sí. Si no se midió nada, hay que decirlo así.
Las superadas no se borran: se marcan y se enlazan desde la que las anula. Ejemplo real: cr-0001 (dividir las filas con marcador incrustado) → cr-0002 (era el índice del acta; apartarlo). Esa cadena es el historial de correcciones del corpus.
Salida: state/{iso2}/decisions.jsonl · state/_transversal/decisions.jsonl → docs/_metodologia/generados/METODOLOGIA.md (generado con scripts/generar_metodologia.py, no se edita a mano)
Cdiaries-validate
Integridad estructural por muestreo + calibración de la tasa de fuga
--country {iso2}--n 300
No dice si el pipeline terminó (eso es /diaries-status): dice si lo que produjo es correcto. Principio de independencia: sus detectores no comparten código ni léxico con correct/tag; un control que comparte léxico con su productor hereda sus puntos ciegos.
Paso 1 · Muestra ponderada por intervención + auto-filtro (validate_structural.py)
trivialmente correcta según reglas deterministas→PASS
cualquier sombra de duda→FLAG a revisión humana — el filtro separa la OBVIEDAD, no juzga la calidad
D1 pureza ningún marcador de otro orador en text (buscado INLINE, nunca anclado a línea) D2 atribución mandato en la LEGISLATURA (no fecha exacta) · orador nombrado sin id_dep D3 carácter sin glifos corrompidos (uso privado, carácter de reemplazo) la homonimia NO se marca: ya la adjudicó /diaries-match con revisión humana
Paso 2 · Calibración de la tasa de fuga — obligatoria (prepare_leak_calibration.py)
200 filas PASS al azar, por igual entre países→las revisa una persona; una fuga es un PASS con problema: un error silencioso
qué países→los que tienen {ISO2}_muestra.csv del paso 1, o los de --country
cuándo parar→se declara la tasa medida con su intervalo; perseguir el «0 en 200» es una regresión infinita
Paso 3 · Triaje de los FLAG (export_flags_review.py)
hueco de padrón: sin id_dep, sin mandato→se resuelve solo (casi siempre, personas que no figuran en el padrón)
marcador ajeno · cadena de firmas · glifos corruptos→revisión humana, con el contexto recortado alrededor del hallazgo
qué países→los mismos que el paso 2: los que tienen muestra, o los de --country
Paso 4 · Fechas contra la cabecera del acta (audit_session_dates.py)
OKFECHA_DISTINTAOCR_CABECERACONFLADASIN_CABECERA
discrepancia→NO manda automáticamente el acta: decide la cronología contra las sesiones vecinas
desfase CONSTANTE→defecto del patrón, nunca de los datos
más de la mitad sin cabecera→no es una cifra de calidad: el patrón no la encuentra
Paso 5 · Integridad de intervention_order (check_order_integrity.py)
hueco EXPLICADO→Prolegomena en orden 0 · filas declaradas dm_speech = 0 presentes en la matriz
hueco HUÉRFANO→la posición no está en ninguna parte → fila perdida de verdad
No usa AUTO/FLAG/HALT por confianza: el veredicto es determinista. No corrige nada: «sin mandato» y «sin id_dep» no son instrucciones de borrado (la intervención se mantiene siempre), y una fecha mal derivada se arregla reprocesando, no con un UPDATE sobre la matriz.
Salidas
docs/validacion/{ISO2}_muestra.csv docs/validacion/calibracion_fuga[_rondaN].csv docs/validacion/revision_flags.csv docs/{iso2}/auditoria_fechas.csv
Cdiaries-teach
Guía interactiva — 8 caminos
Caminos
1 · qué es el sistema2 · país nuevo desde cero 3 · retomar pipeline4 · problema con un skill 5 · confianza AUTO/FLAG/HALT6 · correcciones y propagación 7 · mapa completo de skills8 · dejar constancia de una decisión
Cdiaries-refine
Mejora un SKILL.md a partir de testing real
Observación de testing → localiza la sección afectada → propone cambio quirúrgico → [A]plicar / [E]ditar / [C]ancelar → edita el SKILL.md y registra en REFINEMENTS.md.
No modifica datos de sesiones ni el estado. Cambios estructurales grandes → mostrar borrador completo antes de aplicar.