Módulo 8: Project Kioskos Reliability And Governance System
Corriendo la compuerta completa contra un día limpio
Descripción
La lección 4 confirmó la mitad de la promesa de este módulo: run_full_gate() atrapa, con precisión, los seis problemas reales del incidente de S04. Esta lección confirma la otra mitad, la que ningún módulo anterior de esta guía pudo probar todavía —cada uno trabajó exclusivamente sobre el archivo roto—: ¿qué pasa cuando el sistema completo corre sobre un archivo que no tiene ningún problema? Si la respuesta fuera "también encuentra algo", el sistema completo no serviría de nada — sería indistinguible de una alarma que suena siempre, sin importar qué pasa del otro lado de la puerta.
Conexión con el módulo. La lección 4 corrió el gate sobre lo roto. Esta lección lo corre, sin ningún cambio, sobre lo limpio. La comparación entre ambas corridas —6 fallos contra 0— es la evidencia central de todo este módulo, y cierra el hilo que abrió la lección 1: un sistema de confianza no es el que dice que no siempre, es el que distingue.
Una analogía: el mismo guardia, ahora con una llave legítima
Un guardia de seguridad que hace sonar la alarma cada vez que alguien se acerca a la puerta —sin importar si la persona tiene una llave legítima o no— no está protegiendo nada; está entrenando a todo el edificio a ignorarlo. La prueba real de que un sistema de seguridad funciona no es solo que reacciona ante un intruso —eso ya lo confirmó la lección 4—; es que deja pasar, sin ninguna alarma, a la persona que sí tiene derecho a entrar. Esta lección le entrega al mismo guardia —run_full_gate(), sin cambiar una sola línea— una llave legítima: un día de ventas real de Kiosko, de las tiendas de siempre, a los precios de siempre. Si el guardia hace sonar la alarma de todas formas, el problema no está en la puerta — está en el guardia.
El material: reconstruyendo un día limpio de S01-S03
Por qué "reconstruido", no "inventado"
Ningún valor de este archivo nuevo se elige al azar. Cada uno viene, de forma directa, de datos que ocho guías anteriores de este ecosistema ya confirmaron confiables:
| Elemento del día limpio | De dónde sale |
|---|---|
Tiendas: S01, S02, S03 | Las tres tiendas originales de Kiosko — deliberadamente sin S04, la tienda bajo sospecha |
Productos: P001-P004 | El catálogo completo de dim_product, sin ningún producto inventado |
Precios: 0.55/1.20/0.75/4.50 | Exactamente REFERENCE_PRICES, la línea base calculada en el módulo 5 sobre las 40 filas de la semana canónica |
Cantidad de filas: 8 | Del mismo orden de magnitud que un día típico de la semana canónica (el lunes 2026-08-03 tuvo 8 órdenes reales) |
order_ts: 2026-08-15, entre 08:05 y 09:48 | Dentro de la ventana de 24 horas del SLA, contado hacia atrás desde PIPELINE_RUN_AT |
Qué esperar (verificado sin ejecutar código, solo aritmética): el archivo tiene que respetar dos restricciones simultáneas para que check_freshness() reporte PASS con sla_hours=24 y run_at="2026-08-16T09:00:00" — el order_ts más reciente no puede ser anterior a 2026-08-15T09:00:00, y tiene que ser anterior o igual a PIPELINE_RUN_AT (el archivo no puede tener una fecha futura). Guarda este archivo exactamente como está, cuatro líneas menos que S04 a propósito —un día normal, no un incidente—:
order_id,store_id,product_id,quantity,unit_price,order_ts
ORD-9601,S01,P001,3,0.55,2026-08-15T08:05:00
ORD-9602,S02,P002,2,1.20,2026-08-15T08:20:00
ORD-9603,S03,P003,1,0.75,2026-08-15T08:35:00
ORD-9604,S01,P004,1,4.50,2026-08-15T08:50:00
ORD-9605,S02,P001,4,0.55,2026-08-15T09:05:00
ORD-9606,S03,P002,2,1.20,2026-08-15T09:20:00
ORD-9607,S01,P003,2,0.75,2026-08-15T09:35:00
ORD-9608,S02,P004,1,4.50,2026-08-15T09:48:00
Ocho líneas, order_id nuevos (ORD-9601 a ORD-9608, continuando la numeración después de S04, sin colisionar con ningún order_id anterior de esta guía), las tres tiendas originales repartidas sin ningún patrón especial, cada unit_price copiado exactamente de REFERENCE_PRICES (P001=0.55, P002=1.20, P003=0.75, P004=4.50, sin ninguna variación), cada product_id dentro de dim_product, ningún order_id repetido, ninguna cantidad negativa o nula, ningún unit_price vacío. El order_ts más tardío es 2026-08-15T09:48:00.
Cargarlo en kiosko.duckdb
# load_clean_day.py
import duckdb
con = duckdb.connect("kiosko.duckdb")
con.execute("""
CREATE OR REPLACE TABLE orders_clean_day AS
SELECT * FROM read_csv('orders_2026-08-15.csv', header=True,
columns={
'order_id': 'VARCHAR', 'store_id': 'VARCHAR', 'product_id': 'VARCHAR',
'quantity': 'BIGINT', 'unit_price': 'DOUBLE', 'order_ts': 'TIMESTAMP'
})
""")
row_count = con.sql("SELECT COUNT(*) FROM orders_clean_day").fetchone()[0]
print(f"Filas cargadas en orders_clean_day: {row_count}")
Qué esperar.
Filas cargadas en orders_clean_day: 8
Ejemplo trabajado: el mismo run_full_gate(), sin ningún cambio
# run_clean_day.py
import duckdb
import pandera
import polars as pl
from kiosko_trust import (
PIPELINE_RUN_AT, REFERENCE_PRICES, contract_to_pandera_schema, gate_failure_count,
load_contract, run_full_gate,
)
con = duckdb.connect("kiosko.duckdb")
dim_product_df = con.sql("SELECT * FROM dim_product").pl()
clean_day_df = con.sql("SELECT * FROM orders_clean_day").pl()
contract = load_contract("orders_contract.yaml")
schema = contract_to_pandera_schema(contract)
print(f"Filas leidas de orders_clean_day: {clean_day_df.height}\n")
gate_results = run_full_gate(
clean_day_df, dim_product_df, REFERENCE_PRICES, schema,
run_at=PIPELINE_RUN_AT, sla_hours=contract.sla.freshness_hours,
min_rows=contract.sla.row_count.min, max_rows=contract.sla.row_count.max,
)
print("=== run_full_gate() sobre orders_2026-08-15.csv (dia limpio) ===")
for r in gate_results:
print(f" [{r['status']}] {r['check']:<14} {r['detail']}")
failures = gate_failure_count(gate_results)
print(f"\nFallos: {failures} de {len(gate_results)} checks")
assert failures == 0, f"se esperaban 0 fallos, se obtuvieron {failures}"
print("assert failures == 0 -> OK")
Qué esperar (verificado corriendo python3 run_clean_day.py real, con kiosko.duckdb conteniendo orders_clean_day y dim_product, pandera==0.32.1):
Filas leidas de orders_clean_day: 8
=== run_full_gate() sobre orders_2026-08-15.csv (dia limpio) ===
[PASS] completeness ninguna fila
[PASS] uniqueness ninguna fila
[PASS] validity ninguna fila
[PASS] consistency todos los product_id existen en dim_product
[PASS] accuracy todos los precios dentro de la linea base
[PASS] freshness 23.2h de 24h de SLA
[PASS] volume 8 filas, rango [5, 20]
Fallos: 0 de 7 checks
assert failures == 0 -> OK
Siete PASS, ninguna excepción. freshness pasa por un margen ajustado a propósito —23.2 de 24 horas de SLA, no un número redondo cómodo como 1 hora—, para que este resultado sea una prueba real de la aritmética de check_freshness(), no una coincidencia donde cualquier archivo del último mes hubiera pasado igual. volume pasa con 8 filas, cómodamente dentro de [5, 20], el mismo rango que el contrato ya declaró para el primer archivo de una tienda nueva. Y las cinco verificaciones de fila —completeness, uniqueness, validity, consistency, accuracy— no encuentran ni una sola fila con problemas, porque, a diferencia de S04, este archivo se construyó exactamente para no tenerlos.
Compara este resultado, línea por línea, contra la lección 4: los mismos siete nombres de check, el mismo orden, la misma función exacta corriendo por debajo — y sin embargo, un resultado completamente opuesto. Esa es, precisamente, la evidencia que este módulo necesitaba: la misma compuerta, corrida sin ningún cambio, produce un veredicto distinto porque los datos son distintos, no porque el sistema tenga un sesgo hacia decir que sí o hacia decir que no.
Tabla: la comparación completa, S04 contra el día limpio
| Check | S04 (orders_2026-08-14.csv) | Día limpio (orders_2026-08-15.csv) |
|---|---|---|
| completeness | FAIL (ORD-9503) | PASS |
| uniqueness | FAIL (ORD-9502 x2) | PASS |
| validity | FAIL (ORD-9507) | PASS |
| consistency | FAIL (ORD-9508) | PASS |
| accuracy | FAIL (ORD-9509) | PASS |
| freshness | FAIL (47.58h de 24h) | PASS (23.2h de 24h) |
| volume | PASS (12 filas) | PASS (8 filas) |
| Total de fallos | 6 de 7 | 0 de 7 |
| Filas en cuarentena | 6 de 12 | 0 de 8 (no hace falta correr quarantine()) |
Diagrama: el mismo sistema, dos entradas, dos veredictos
flowchart TD
G["run_full_gate()\nla MISMA funcion, sin ningun cambio"]
A["orders_2026-08-14.csv\nS04, el incidente"] --> G
B["orders_2026-08-15.csv\nS01-S03, dia limpio"] --> G
G --> R1["6 de 7 FAIL\n-> quarantine() + raise_alert()"]
G --> R2["0 de 7 FAIL\n-> el archivo sigue el pipeline\nsin ninguna friccion"]
Profundización: ¿y si el gate fuera demasiado permisivo?
Vale la pena hacerse la pregunta incómoda antes de dar este resultado por bueno: ¿0 fallos prueba que el sistema es preciso, o podría estar probando que el sistema simplemente no detecta casi nada? La respuesta no está en esta lección sola — está en la combinación de las lecciones 4 y 5. Si run_full_gate() fuera demasiado permisivo (por ejemplo, si tolerance en check_price_baseline() estuviera mal calibrado, como advirtió el módulo 5, lección 7), el resultado sobre S04 también habría sido optimista, y la lección 4 lo habría revelado con un número de fallos menor a 6. El hecho de que la lección 4 sí atrapó los seis problemas reales, con la calibración exacta que ya validó el módulo 5, es lo que le da credibilidad al 0 de esta lección — un sistema demasiado permisivo habría fallado en ambas direcciones, no solo en una. El Ejercicio 1 de esta lección te deja comprobar esto tú mismo, rompiendo el día limpio a propósito.
Errores comunes
Pensar que 0 de 7 FAIL significa que el sistema "no encontró nada porque no sabe buscar". Qué pasa: alguien, acostumbrado a que las cuatro lecciones anteriores de resultado siempre mostraran algún fallo, interpreta un resultado limpio como sospechoso — "¿de verdad no hay ningún problema, o el sistema se lo está perdiendo?". Por qué pasa: después de siete módulos enfocados casi exclusivamente en encontrar problemas, un resultado sin ninguno se siente, por contraste, poco confiable. Cómo detectarlo: revisa la sección "Profundización" de esta lección — la credibilidad de este 0 no viene de esta lección sola, viene de que la misma función, sin ningún cambio, sí encontró los seis problemas reales de S04 en la lección anterior. Cómo corregirlo: evalúa siempre un sistema de detección con ambos tipos de evidencia —¿atrapa lo que está mal? ¿deja pasar lo que está bien?— nunca con uno solo. Esta lección existe, específicamente, para completar la segunda mitad de esa evaluación.
Modificar el día limpio "para que se vea más parecido a S04" agregando alguna fila con un problema menor. Qué pasa: alguien, queriendo que este ejemplo sea "más realista", agrega una fila con algún detalle imperfecto —una cantidad ligeramente alta, un timestamp fuera de orden— pensando que ningún día real de ventas es perfecto. Por qué pasa: la intuición de que "los datos reales siempre tienen algo mal" es, en general, razonable — pero confunde el propósito específico de este archivo. Cómo detectarlo: revisa la sección "Por qué reconstruido, no inventado" de esta lección — cada valor de este archivo tiene una razón trazable a datos ya confirmados de Kiosko; agregar una imperfección arbitraria rompería esa trazabilidad. Cómo corregirlo: el día limpio de esta lección tiene un propósito pedagógico específico y deliberado —demostrar que el sistema no genera falsas alarmas sobre datos genuinamente correctos—, no simular todo el espectro de variabilidad que un día real de Kiosko podría tener. Si quieres explorar qué pasa cuando el día limpio sí tiene un problema, el Ejercicio 1 de esta lección lo hace de forma controlada, midiendo exactamente el efecto de un solo cambio a la vez.
Ejercicios
Ejercicio 1 — Rompe el día limpio a propósito, con un solo cambio, y confirma que el gate reacciona. Cambia unit_price de ORD-9601 de 0.55 a 5.50 (un error de un solo dígito, diez veces el precio real), vuelve a cargar orders_clean_day en kiosko.duckdb, y corre run_clean_day.py de nuevo. Confirma cuántos fallos ves ahora, y en qué check.
Ver solución
Con ORD-9601 a 5.50 en vez de 0.55, el resultado pasa de 0 a 1 fallo, exclusivamente en accuracy: deviation = |5.50 - 0.55| / 0.55 = 9.0, muy por encima de tolerance=0.5. Las otras seis verificaciones siguen en PASS sin ningún cambio. Este ejercicio confirma, con evidencia directa, la pregunta que planteó la "Profundización" de esta lección: el gate sí reacciona a un problema real cuando aparece, incluso en un archivo que hasta ese momento estaba perfectamente limpio — el 0 original no era el resultado de un sistema que no sabe detectar nada, era el resultado correcto para datos genuinamente correctos.
Ejercicio 2 — Calcula, sin correr ningún código, cuál sería el resultado de check_freshness() si el order_ts más tardío del día limpio fuera 2026-08-15T08:00:00 en vez de 2026-08-15T09:48:00. Usando la fórmula de check_freshness() (hours_since_latest = (run_at - latest_ts) / 3600), calcula el número exacto y determina si seguiría en PASS o pasaría a FAIL.
Ver solución
run_at = 2026-08-16T09:00:00, latest_ts = 2026-08-15T08:00:00. La diferencia es exactamente 25 horas (1 día más 1 hora). Con sla_hours=24, 25 > 24, así que el resultado sería FAIL, no PASS — un margen de solo una hora determina el veredicto completo. Este cálculo confirma por qué esta lección eligió 09:48:00 como el order_ts más tardío, en vez de un número más cómodo: demuestra que check_freshness() no tiene ningún margen de tolerancia oculto — la frontera entre PASS y FAIL es exactamente sla_hours, ni una fracción de hora más.
Ejercicio 3 — Argumenta si sería razonable construir un tercer archivo, "un día promedio con un solo problema menor", para probar el gate en una zona intermedia entre S04 (6 fallos) y el día limpio (0 fallos). En 2-3 frases, considerando lo que ya demostró el Ejercicio 1 de esta lección, argumenta si un tercer escenario así agregaría evidencia nueva.
Ver solución
No agregaría evidencia cualitativamente nueva, aunque sí podría ser útil como ejercicio adicional de práctica: el Ejercicio 1 de esta lección ya demostró, con un solo cambio controlado, que el gate reacciona de forma proporcional —un problema produce un fallo, no seis—, así que un archivo "intermedio" simplemente confirmaría el mismo comportamiento con una combinación distinta de filas rotas y limpias, sin revelar ningún principio que las dos corridas de las lecciones 4 y 5 (más el Ejercicio 1) no hayan cubierto ya. El valor pedagógico de mantener exactamente dos corridas —los dos extremos, 6 y 0— es que aísla la pregunta central con la máxima claridad posible: ¿el sistema distingue entre "todo mal" y "todo bien"? Un tercer punto intermedio sería una buena práctica personal, pero no un requisito para responder esa pregunta con evidencia suficiente.
Resumen y siguiente paso
En esta lección corriste run_full_gate(), sin ningún cambio, sobre un archivo completamente distinto al de la lección 4: un día limpio de 8 filas, reconstruido con datos ya confirmados confiables de la semana canónica de Kiosko. El resultado —0 fallos de 7 checks— completa la evidencia que este módulo necesitaba: el mismo sistema que atrapó, con precisión, los seis problemas reales de S04, no genera ninguna falsa alarma sobre datos que de verdad están bien.
Antes de avanzar deberías poder: explicar por qué cada valor del día limpio viene de datos ya confirmados de Kiosko, no de números inventados; y describir qué pasaría si run_full_gate() reportara algún fallo sobre este archivo (el Ejercicio 1 ya te mostró la respuesta con un caso concreto).
La lección 6 cambia de tema: publica la capa de gobierno del sistema completo —LINEAGE_MAP, generate_catalog(), ACCESS_POLICY, mask_pii()— que aplica por igual sobre S04, sobre el día limpio, y sobre cualquier archivo futuro de Kiosko, sin importar el resultado de las siete verificaciones.
Recursos
- Módulo 5, lección 4, de esta misma guía — fuente exacta de
REFERENCE_PRICES, la línea base que este día limpio reproduce sin ninguna variación.src/guides/data-reliability-and-governance-guide/workbook/module-05-accuracy-and-deterministic-anomaly-detection/es/04-building-a-price-baseline-from-kioskos-clean-week.md. En español. - Módulo 6, lección 3, de esta misma guía — fuente exacta de por qué la referencia de "ahora" tiene que ser una constante fija, la base de por qué esta lección eligió
2026-08-15con cuidado.src/guides/data-reliability-and-governance-guide/workbook/module-06-freshness-volume-and-lineage/es/03-a-fixed-reference-clock-never-datetime-now.md. En español. - DISEÑO de esta guía.
src/guides/data-reliability-and-governance-guide/DISENO.md. En español.