Á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)
| Confianza | Nivel | Estado | Acción |
|---|---|---|---|
| ≥ 0.85 | AUTO | complete | continúa sin interrumpir |
| 0.65–0.85 | FLAG | flag | revisión diferida (/diaries-review) |
| < 0.65 | HALT | halt | intervención manual obligatoria |
Cada skill escribe su resultado con
update_state.py en
state/{iso2}/pipeline_state.json.Overrides por skill (country_config.thresholds)
| Skill | auto | flag | por qué |
|---|---|---|---|
diaries-tag | 0.80 | 0.60 | corpus difícil; etiquetado irregular |
diaries-match | 0.90 | 0.70 | vinculación es crítica → más estricto |
| resto | 0.85 | 0.65 | globales 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-countaño DIGITAL→
pdf_digital → texto embebido, OMITIR OCRañ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: trueya 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 procesaPaso 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_*.txt2
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 OCRhtml / 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_flattext_flat→
text_flat — texto ya extraído; con page_break_marker segmenta por esa marcaLa 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 mobiliariocabeceros / mobiliario / hifenación→NO se tocan aquí —
--strip-headers queda como legado opt-in, fuera del flujo canónicoextracted/ 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ás2 · eliminar el mobiliario→anclado al separador — CINCO clases:
furniture_block/line/masthead/prefix/inline; masthead con keep-first; NUNCA borrar Prolegomena3 · 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 cruza5 · 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}.txt4
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 nuncascope del país→
presidency.scope: session | segments en el country_config — lo consume attribute_presidency.py aguas abajoPrioridad 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 corridocabecero = 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}.json5
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 = 0NARRATIVA→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 frasePaso 2c/2d · Detección de dos niveles + LLM
NIVEL 1 estricto (speaker_tag_patterns)→marcador canónico; guion
[-–—] (OCR usa em-dash) → etiqueta directaNIVEL 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 descartaPaso 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}.txt7
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 íntegrosin 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íticaSalida
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 --deputiesB · desde speakers→
matrix/interventions_raw.csv · después de diaries-matrixEl 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 PUBLICABLEderive_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.csv10
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_metascope: 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 vicevarias, 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ónicopar (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.mdEl 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.csv11
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 repetidosc · 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
--forceEl 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.csv12
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.jsonldSkills 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-reviewtodo complete→«corpus listo» →
/diaries-reportsolo 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
--typepunctual (--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 skillstrategy→añade a
global_rules[] · requiere re-ejecutar con --forceTras 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 valiosotrabajo 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
--countrycuá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
--countryPaso 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 matrizhueco 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.csvCdiaries-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.