Módulo 7: The Incident And Data Governance
Proyecto: la respuesta al incidente y la política de acceso de S04
Descripción
Este proyecto cierra el módulo. Tienes build_failure_report() (lección 2), quarantine() (lección 3), raise_alert() y el runbook.md escrito (lección 4), ACCESS_POLICY con apply_access_policy() (lección 5), mask_pii() (lección 6), y generate_catalog() (lección 7). Falta un solo paso: un script de cierre que junte las seis piezas en un solo lugar, corrido de punta a punta sobre el incidente real de S04, con un reporte final que confirme, con evidencia ejecutada, que Kiosko por fin tiene lo que le faltaba después de seis módulos de puro diagnóstico — un sistema que actúa cuando algo falla, y que protege lo que no todo el mundo debería ver.
Conexión con el módulo. Este proyecto no introduce ningún concepto nuevo — es la síntesis de las lecciones 2 a 7, aplicada de punta a punta al mismo incidente que diagnosticaron los seis módulos anteriores de esta guía. Cierra el hilo completo de esta guía hasta aquí, y deja a S04 lista para el proyecto final de todo el ecosistema: el módulo 8, donde el sistema completo —contrato, tests, consistencia, anomalías, freshness, linaje, incidente y gobierno— corre dos veces, sobre lo roto y sobre lo limpio.
Una analogía: el hospital completo, en un solo turno
Las lecciones de este módulo construyeron, una por una, las piezas de un hospital: la sala de urgencias que separa casos por gravedad (quarantine()), la alarma que avisa sin depender de que alguien esté mirando (raise_alert()), el protocolo escrito en la pared (runbook.md), el registro de quién puede ver qué expediente (ACCESS_POLICY), la forma de proteger un historial sensible incluso para quien tiene autorización de consultarlo (mask_pii()), y el directorio del edificio completo (generate_catalog()). Este proyecto es el turno completo de ese hospital: un paciente llega (orders_2026-08-14.csv), se le diagnostica, se le trata según protocolo, y su información se maneja con el cuidado correcto — todo en una sola pasada, con las seis piezas trabajando juntas en vez de aisladas en lecciones separadas.
El material que necesitas
Necesitas, en la misma carpeta de trabajo de este módulo:
modulo_7_incidente_gobierno/
├── kiosko.duckdb (orders_s04 del modulo 2,
│ dim_product de la leccion 2,
│ customers de la leccion 5)
└── s04_incident_and_governance.py (este proyecto)
Si tu kiosko.duckdb no tiene alguna de estas tres tablas todavía: orders_s04 se construye en el módulo 2, lección 4; dim_product se reconstruye dentro de este mismo script con los mismos cuatro productos de los módulos 3 y 5, para que este proyecto sea autosuficiente; customers se construye en la lección 5 de este módulo. Este proyecto no vuelve a explicar ninguno de esos tres pasos, los da por hechos.
La solución de referencia, verificada
# s04_incident_and_governance.py -- proyecto de cierre del modulo 7
import hashlib
from datetime import datetime
import duckdb
import pandera.polars as pa
import polars as pl
import yaml
PIPELINE_RUN_AT = "2026-08-16T09:00:00"
REFERENCE_PRICES = {"P001": 0.55, "P002": 1.2, "P003": 0.75, "P004": 4.5}
MASK_SALT = "kiosko-mask-salt-2026"
# --- Modulos 2, 3, 5 y 6: las herramientas reusables, sin cambios ---
class OrdersSchema(pa.DataFrameModel):
order_id: str = pa.Field(unique=True)
unit_price: float = pa.Field(nullable=False, ge=0)
quantity: int = pa.Field(gt=0)
CHECK_TO_DIMENSION = {
"not_nullable": "completeness",
"field_uniqueness": "uniqueness",
"greater_than(0)": "validity",
}
def validate_referential_integrity(orders_df: pl.DataFrame, dim_product_df: pl.DataFrame) -> pl.DataFrame:
return orders_df.join(dim_product_df, on="product_id", how="anti")
def check_price_baseline(df: pl.DataFrame, reference_prices: dict[str, float], tolerance: float = 0.5) -> pl.DataFrame:
return (
df.with_columns(pl.col("product_id").replace_strict(reference_prices, default=None).alias("reference_price"))
.filter(pl.col("unit_price").is_not_null() & pl.col("reference_price").is_not_null())
.with_columns(((pl.col("unit_price") - pl.col("reference_price")).abs() / pl.col("reference_price")).alias("deviation"))
.filter(pl.col("deviation") > tolerance)
)
def check_freshness(df: pl.DataFrame, run_at: str, sla_hours: int, timestamp_col: str = "order_ts") -> dict:
latest_ts = df.select(pl.col(timestamp_col).max()).item()
run_at_dt = datetime.fromisoformat(run_at)
hours_since_latest = (run_at_dt - latest_ts).total_seconds() / 3600
return {
"check": "freshness", "latest_row_ts": str(latest_ts), "run_at": run_at,
"sla_hours": sla_hours, "hours_since_latest": round(hours_since_latest, 2),
"status": "PASS" if hours_since_latest <= sla_hours else "FAIL",
}
# --- Modulo 7, lecciones 2-4: el incidente ---
def build_failure_report(df: pl.DataFrame, dim_product_df: pl.DataFrame, reference_prices: dict[str, float]) -> pl.DataFrame:
rows: list[dict] = []
try:
OrdersSchema.validate(df, lazy=True)
except pa.errors.SchemaErrors as exc:
for r in exc.failure_cases.iter_rows(named=True):
rows.append({"row_idx": r["index"], "order_id": df["order_id"][r["index"]],
"dimension": CHECK_TO_DIMENSION[r["check"]], "detail": f"{r['column']}={r['failure_case']}"})
indexed = df.with_row_index("row_idx")
for r in validate_referential_integrity(indexed, dim_product_df).iter_rows(named=True):
rows.append({"row_idx": r["row_idx"], "order_id": r["order_id"], "dimension": "consistency",
"detail": f"product_id={r['product_id']} no existe en dim_product"})
for r in check_price_baseline(indexed, reference_prices, tolerance=0.5).iter_rows(named=True):
rows.append({"row_idx": r["row_idx"], "order_id": r["order_id"], "dimension": "accuracy",
"detail": f"unit_price={r['unit_price']} se aleja {round(r['deviation'], 1)}x del precio de referencia ({r['reference_price']})"})
return pl.DataFrame(rows).sort(["row_idx", "dimension"])
def quarantine(df: pl.DataFrame, failures: pl.DataFrame) -> tuple[pl.DataFrame, pl.DataFrame]:
bad_idx = failures["row_idx"].unique().to_list()
indexed = df.with_row_index("row_idx")
quarantined_df = indexed.filter(pl.col("row_idx").is_in(bad_idx)).drop("row_idx")
clean_df = indexed.filter(~pl.col("row_idx").is_in(bad_idx)).drop("row_idx")
return clean_df, quarantined_df
def raise_alert(check_name: str, failure_count: int, sample: list[dict]) -> dict:
return {
"alert": "data_quality_incident", "pipeline": "kiosko_orders_s04", "check_name": check_name,
"run_at": PIPELINE_RUN_AT, "severity": "high" if failure_count >= 5 else "medium",
"failure_count": failure_count, "sample": sample[:3],
}
# --- Modulo 7, lecciones 5-7: el gobierno ---
ACCESS_POLICY: dict[str, list[str]] = {
"analyst": ["order_id", "store_id", "product_id", "quantity", "unit_price", "order_ts", "customer_id", "customer_email"],
"finance": ["order_id", "store_id", "product_id", "quantity", "unit_price", "order_ts", "customer_id", "customer_email", "customer_phone"],
"support": ["order_id", "customer_id", "customer_email", "customer_phone"],
}
PII_COLUMNS = {"customer_email", "customer_phone"}
ROLES_WITH_RAW_PII = {"finance", "support"}
def apply_access_policy(df: pl.DataFrame, role: str, policy: dict = ACCESS_POLICY) -> pl.DataFrame:
allowed = [c for c in policy[role] if c in df.columns]
return df.select(allowed)
def mask_pii(df: pl.DataFrame, columns: list[str], salt: str = MASK_SALT) -> pl.DataFrame:
def _hash(value: str) -> str:
return hashlib.sha256((salt + value).encode()).hexdigest()[:12]
exprs = [pl.col(c).map_elements(_hash, return_dtype=pl.String).alias(c) for c in columns]
return df.with_columns(exprs)
def build_role_view(df: pl.DataFrame, role: str) -> pl.DataFrame:
view = apply_access_policy(df, role)
if role not in ROLES_WITH_RAW_PII:
pii_present = [c for c in view.columns if c in PII_COLUMNS]
if pii_present:
view = mask_pii(view, pii_present)
return view
def generate_catalog(tables: list[dict]) -> list[dict]:
catalog = []
for t in tables:
n_pii = sum(1 for c in t["columns"] if c["sensitivity"] == "pii")
entry = dict(t)
entry["n_columns"] = len(t["columns"])
entry["n_pii_columns"] = n_pii
catalog.append(entry)
return catalog
TABLES_SOURCE = [
{
"table": "orders_s04", "owner": "data-engineering@kiosko", "contract": "contracts/orders_contract.yaml",
"columns": [
{"name": "order_id", "sensitivity": "none"}, {"name": "store_id", "sensitivity": "none"},
{"name": "product_id", "sensitivity": "none"}, {"name": "quantity", "sensitivity": "none"},
{"name": "unit_price", "sensitivity": "none"}, {"name": "order_ts", "sensitivity": "none"},
],
},
{
"table": "dim_product", "owner": "data-engineering@kiosko", "contract": None,
"columns": [
{"name": "product_id", "sensitivity": "none"}, {"name": "product_name", "sensitivity": "none"},
{"name": "category", "sensitivity": "none"}, {"name": "unit_cost", "sensitivity": "none"},
],
},
{
"table": "dim_store", "owner": "data-engineering@kiosko", "contract": None,
"columns": [
{"name": "store_id", "sensitivity": "none"}, {"name": "store_name", "sensitivity": "none"},
{"name": "city", "sensitivity": "none"}, {"name": "country", "sensitivity": "none"},
],
},
{
"table": "customers", "owner": "delivery-app@kiosko", "contract": None,
"columns": [
{"name": "customer_id", "sensitivity": "none"}, {"name": "customer_phone", "sensitivity": "pii"},
{"name": "customer_email", "sensitivity": "pii"},
],
},
]
def main() -> None:
pl.Config.set_fmt_str_lengths(60)
con = duckdb.connect("kiosko.duckdb")
df = con.sql("SELECT * FROM orders_s04").pl()
dim_product_df = con.sql("SELECT * FROM dim_product").pl()
customers_df = con.sql("SELECT * FROM customers").pl()
print("=== Kiosko: incidente y gobierno de S04 (modulo 7, cierre) ===\n")
print("--- 1. El incidente: deteccion, contencion, alerta ---")
failures = build_failure_report(df, dim_product_df, REFERENCE_PRICES)
clean_df, quarantined_df = quarantine(df, failures)
print(f"failures: {failures.height} filas fisicas | clean_df: {clean_df.height} | quarantined_df: {quarantined_df.height}")
sample = failures.select(["order_id", "dimension", "detail"]).to_dicts()
row_alert = raise_alert("s04_full_gate", quarantined_df.height, sample)
freshness_result = check_freshness(df, run_at=PIPELINE_RUN_AT, sla_hours=24)
file_alert = raise_alert("freshness", 1, [freshness_result])
print(f"Alerta de fila: check_name={row_alert['check_name']} severity={row_alert['severity']} failure_count={row_alert['failure_count']}")
print(f"Alerta de archivo: check_name={file_alert['check_name']} severity={file_alert['severity']} status={freshness_result['status']} hours_since_latest={freshness_result['hours_since_latest']}")
print("runbook.md: 6 pasos (Detectar, Triage, Contener, Causa raiz, Arreglar, Postmortem) -- escrito en la leccion 4, no se re-imprime aqui")
print("\n--- 2. El gobierno: acceso por rol + masking (sobre customers) ---")
for role in ["analyst", "finance", "support"]:
view = build_role_view(customers_df, role)
print(f" role={role:<8} columnas={view.columns}")
print(f"\n Ejemplo, role=analyst:")
print(build_role_view(customers_df, "analyst"))
print("\n--- 3. El catalogo ---")
catalog = generate_catalog(TABLES_SOURCE)
with open("catalog.yaml", "w") as f:
yaml.safe_dump(catalog, f, sort_keys=False, allow_unicode=True)
total_pii = sum(e["n_pii_columns"] for e in catalog)
print(f"catalog.yaml escrito: {len(catalog)} tablas, {sum(e['n_columns'] for e in catalog)} columnas, {total_pii} columnas PII documentadas")
print("\n=== Cierre del modulo 7 ===")
print(f"Incidente: {quarantined_df.height} de {df.height} filas en cuarentena, {clean_df.height} filas limpias siguen el pipeline normal")
print(f"Alertas estructuradas: 2 (fila + archivo), runbook.md escrito con los 6 pasos")
print(f"Gobierno: {len(ACCESS_POLICY)} roles con politica de acceso, PII enmascarada de forma deterministica, {len(catalog)} tablas catalogadas ({total_pii} columnas PII)")
if __name__ == "__main__":
main()
Qué esperar (verificado corriendo python3 s04_incident_and_governance.py real, con kiosko.duckdb conteniendo orders_s04, dim_product y customers, pandera==0.32.1, polars==1.43.2, duckdb==1.5.5, pyyaml instalado):
=== Kiosko: incidente y gobierno de S04 (modulo 7, cierre) ===
--- 1. El incidente: deteccion, contencion, alerta ---
failures: 6 filas fisicas | clean_df: 6 | quarantined_df: 6
Alerta de fila: check_name=s04_full_gate severity=high failure_count=6
Alerta de archivo: check_name=freshness severity=medium status=FAIL hours_since_latest=47.58
runbook.md: 6 pasos (Detectar, Triage, Contener, Causa raiz, Arreglar, Postmortem) -- escrito en la leccion 4, no se re-imprime aqui
--- 2. El gobierno: acceso por rol + masking (sobre customers) ---
role=analyst columnas=['customer_id', 'customer_email']
role=finance columnas=['customer_id', 'customer_email', 'customer_phone']
role=support columnas=['customer_id', 'customer_email', 'customer_phone']
Ejemplo, role=analyst:
shape: (6, 2)
┌─────────────┬────────────────┐
│ customer_id ┆ customer_email │
│ --- ┆ --- │
│ str ┆ str │
╞═════════════╪════════════════╡
│ C001 ┆ eb25122b17a2 │
│ C002 ┆ aa87243c2e43 │
│ C003 ┆ 71b31bf92a73 │
│ C004 ┆ 93f1b0b8ab41 │
│ C005 ┆ 3ae5fff0dc4f │
│ C006 ┆ b07fd99bf511 │
└─────────────┴────────────────┘
--- 3. El catalogo ---
catalog.yaml escrito: 4 tablas, 17 columnas, 2 columnas PII documentadas
=== Cierre del modulo 7 ===
Incidente: 6 de 12 filas en cuarentena, 6 filas limpias siguen el pipeline normal
Alertas estructuradas: 2 (fila + archivo), runbook.md escrito con los 6 pasos
Gobierno: 3 roles con politica de acceso, PII enmascarada de forma deterministica, 4 tablas catalogadas (2 columnas PII)
Lee este resultado completo con el mismo cuidado que ya entrenaste en los proyectos de los seis módulos anteriores. La sección 1 confirma, en un solo lugar, todo el trabajo de las lecciones 2 a 4: seis filas físicas rotas, separadas en cuarentena sin perder las seis limpias, con dos alertas estructuradas —una de severidad alta para el incidente de fila, una de severidad media para el archivo tardío— y el runbook ya escrito referenciado, no reinventado en cada corrida. La sección 2 confirma el trabajo de las lecciones 5 y 6: tres roles, tres niveles de acceso distintos a la misma tabla customers, con analyst viendo su columna de correo enmascarada mientras finance y support la ven en texto plano, exactamente la diferencia de necesidad legítima que motivó la Profundización de la lección 5. La sección 3 confirma el trabajo de la lección 7: un catálogo completo, con las dos únicas columnas PII de todo Kiosko correctamente identificadas. Y el cierre final resume, en tres líneas, el sistema completo que este módulo construyó — detección que ya tenían los módulos 1 a 6, más contención, comunicación y gobierno, que no tenía ninguno.
Diagrama: de dónde venías, a dónde llegaste
flowchart TD
A["Modulo 6: DIMENSIONS_STILL_OPEN = []\n-- deteccion completa,\nninguna accion todavia"] --> B["Leccion 2:\nbuild_failure_report()\n-- une M2+M3+M5"]
B --> C["Leccion 3:\nquarantine() --\n6 limpias, 6 en cuarentena"]
C --> D["Leccion 4:\nraise_alert() x2\n+ runbook.md"]
D --> E["Leccion 5:\ncustomers + ACCESS_POLICY\n-- quien ve que columna"]
E --> F["Leccion 6:\nmask_pii() --\ndeterminista, verificado"]
F --> G["Leccion 7:\ngenerate_catalog() --\ncatalog.yaml"]
G --> H["Este proyecto:\nlas 6 piezas juntas,\nun solo script"]
H --> I["Modulo 8:\nel sistema completo,\ncontrato -> tests -> ... ->\nincidente -> gobierno,\ncorrido 2 veces"]
Cerrando la promesa del módulo, punto por punto
| Lo que la lección 1 del módulo prometió | Evidencia de que este módulo lo entregó |
|---|---|
| Nombrar las tres respuestas posibles frente a un check que falla | Lección 2: rechazar todo (foundations M7), pasar en silencio (módulo 1), cuarentena+alerta (este módulo) |
| Unir las herramientas de calidad existentes en un solo reporte de fallas | Lección 2: build_failure_report(), 6 filas físicas, cinco dimensiones |
| Construir un mecanismo que separe sin perder ni ocultar nada | Lección 3: quarantine(), 6 limpias + 6 en cuarentena = 12, verificado |
| Estructurar una alerta, sin integración real a ningún sistema externo | Lección 4: raise_alert(), corrida sobre un incidente de fila y uno de archivo |
| Escribir un runbook con los seis pasos aplicados al incidente real | Lección 4: runbook.md, con cifras exactas de S04 en cada paso |
| Construir control de acceso a nivel de columna, por rol | Lección 5: customers, ACCESS_POLICY, tres roles, dos tablas distintas |
| Enmascarar PII de forma determinista, verificado dos veces | Lección 6: mask_pii(), mismo hash en dos corridas independientes |
| Documentar todo en un catálogo mínimo | Lección 7: generate_catalog(), 4 tablas, 2 columnas PII |
Juntar las seis piezas en un solo sistema, corrido sobre S04 real | Este proyecto: el script completo, verificado de punta a punta |
Errores comunes
Pensar que este proyecto "cierra" completamente la confiabilidad y el gobierno de Kiosko. Qué pasa: alguien, satisfecho con el reporte completo de este proyecto —seis piezas trabajando juntas, sin ningún error—, concluye que Kiosko ya tiene un sistema de datos listo para producción sin ningún trabajo pendiente. Por qué pasa: después de siete módulos de construcción, un reporte final limpio se siente como el punto de llegada. Cómo detectarlo: revisa qué corre exactamente el main() de este proyecto — a mano, desde la terminal, cada vez que alguien lo ejecuta. Nada de esto corre automáticamente cuando llega un archivo nuevo; nada envía una alerta real a ningún canal; nada aplica ACCESS_POLICY a una consulta SQL real en producción. Cómo corregirlo: este proyecto demuestra que el mecanismo funciona, con evidencia ejecutada — pero convertir ese mecanismo en un sistema de producción real (orquestado, con integraciones reales, con control de acceso aplicado a nivel de base de datos) es, explícitamente, el trabajo de las guías hermanas que el módulo 8 de esta guía nombra en su lección 7 (airflow-and-declarative-orchestration-guide, aws-core-services-guide / cloud-security-and-guardrails-guide).
Modificar ROLES_WITH_RAW_PII para agregar "analyst" "para simplificar las pruebas". Qué pasa: alguien, al extender este proyecto para su propio uso, agrega "analyst" al conjunto de roles con PII en texto plano, para no tener que lidiar con hashes al depurar. Por qué pasa: los valores enmascarados son más difíciles de leer a simple vista que el dato real, y durante el desarrollo eso se siente como una fricción innecesaria. Cómo detectarlo: revisa si tu razón para modificar ROLES_WITH_RAW_PII tiene que ver con la conveniencia de quien escribe el código, en vez de con un motivo de negocio legítimo del rol analyst. Cómo corregirlo: la lección 6 ya estableció, con un argumento concreto, por qué analyst no tiene un motivo legítimo de ver PII en texto plano — la conveniencia durante el desarrollo nunca es, por sí sola, una razón válida para debilitar una política de acceso. Si necesitas depurar con datos reales, usa un entorno separado con datos de prueba explícitamente marcados como tales, nunca relajando la política que protege datos reales (o, como en este caso, datos de ejemplo que simulan datos reales).
Ejercicios
Ejercicio 1 — Corre el proyecto completo tú mismo, desde cero. En una carpeta nueva, con kiosko.duckdb conteniendo orders_s04 (módulo 2) y customers (lección 5 de este módulo), corre python3 s04_incident_and_governance.py. Confirma que ves exactamente 6 filas en cuarentena, dos alertas con severidades high y medium, y 4 tablas en el catálogo.
Ver solución
Si orders_s04 tiene las doce filas exactas de orders_2026-08-14.csv y customers tiene las seis filas exactas de la lección 5, la salida debería reproducir exactamente la de este proyecto: failures: 6, clean_df: 6, quarantined_df: 6; la alerta de fila con severity=high, failure_count=6; la alerta de archivo con severity=medium, status=FAIL, hours_since_latest=47.58; el rol analyst con dos columnas, ambas otras con tres; y el catálogo con 4 tablas, 17 columnas, 2 PII. Si tu resultado difiere, revisa primero que MASK_SALT siga siendo exactamente "kiosko-mask-salt-2026" — cualquier cambio ahí altera todos los hashes de la sección 2.
Ejercicio 2 — Extiende el reporte final con un conteo de cuántos roles pueden ver cada columna de customers en texto plano, sin enmascarar. Usando ACCESS_POLICY, PII_COLUMNS y ROLES_WITH_RAW_PII ya definidos, calcula cuántos de los tres roles tienen acceso PII en texto plano a customer_email, y cuántos a customer_phone.
Ver solución
def count_raw_pii_access(policy: dict, pii_columns: set, raw_roles: set) -> dict:
counts = {}
for column in pii_columns:
roles_with_column = {role for role, cols in policy.items() if column in cols}
roles_with_raw_access = roles_with_column & raw_roles
counts[column] = len(roles_with_raw_access)
return counts
result = count_raw_pii_access(ACCESS_POLICY, PII_COLUMNS, ROLES_WITH_RAW_PII)
print(result)
Salida esperada:
{'customer_email': 2, 'customer_phone': 2}
Dos de los tres roles (finance y support) tienen acceso en texto plano a ambas columnas; analyst, el único que aparece en ACCESS_POLICY["analyst"] con customer_email pero no en ROLES_WITH_RAW_PII, la ve siempre enmascarada, y nunca tuvo acceso a customer_phone en absoluto. Este ejercicio es un buen ejemplo del tipo de auditoría que valdría la pena correr periódicamente en un sistema real: contar, de forma automática, cuántos roles tienen acceso sin enmascarar a cada columna sensible, para detectar si la política se volvió más permisiva de lo esperado con el tiempo.
Ejercicio 3 — Argumenta si quarantine(), raise_alert(), apply_access_policy() y mask_pii() deberían ser cuatro funciones separadas (como están en esta guía) o una sola función handle_incident() que las combine todas. En 2-3 frases, considerando cómo se usan en main() de este proyecto, argumenta a favor de mantenerlas separadas.
Ver solución
Mantenerlas separadas es la elección correcta, por la misma razón que ya justificó cada lección individual de este módulo: cada función responde una sola pregunta —¿qué filas separar?, ¿cómo estructurar una notificación?, ¿qué columnas mostrar?, ¿cómo ocultar un valor sensible?— y esa separación es precisamente lo que le permitió a este proyecto combinarlas de formas distintas: quarantine() se usa una sola vez sobre orders_s04, mientras que apply_access_policy() y mask_pii() (combinadas en build_role_view()) se llaman tres veces, una por cada rol, sobre customers. Si las cuatro estuvieran fusionadas en una sola función handle_incident(), cualquier caso de uso que necesitara solo una de las cuatro piezas —por ejemplo, un futuro módulo de otra guía que solo necesite mask_pii() para anonimizar un dataset de ejemplo, sin ningún incidente de calidad involucrado— tendría que arrastrar las otras tres innecesariamente. La composición de funciones pequeñas, cada una con una responsabilidad clara, es el mismo principio de diseño que ya sostuvo build_failure_report() en la lección 2, y que sostiene, en el fondo, toda la arquitectura de esta guía desde el módulo 1.
Resumen y siguiente paso: el cierre de este módulo
Con este proyecto cierras el módulo 7, y con él, la brecha final entre "Kiosko detecta sus propios problemas" (el logro completo de los módulos 1 a 6) y "Kiosko actúa sobre ellos, y protege lo que no todo el mundo debería ver" (el logro de este módulo). Construiste quarantine(), la tercera respuesta frente a un check que falla —ni rechazar todo, ni pasar en silencio—; raise_alert() y un runbook.md completo, que convierten un incidente detectado en una acción humana concreta; y las tres piezas de gobierno de datos —ACCESS_POLICY, mask_pii(), generate_catalog()— que responden, con evidencia ejecutada, la pregunta que ningún módulo anterior de esta guía necesitó hacerse: quién puede ver qué.
Y, en el camino, seguiste una disciplina que atravesó los ocho módulos de esta guía: cada pieza nueva se construyó reusando, nunca reescribiendo, el trabajo de los módulos anteriores — build_failure_report() combina, sin duplicar, las herramientas exactas de los módulos 2, 3 y 5; quarantine() y raise_alert() operan sobre ese mismo reporte; ACCESS_POLICY y mask_pii() protegen una tabla completamente nueva sin tocar ni una línea del esquema compartido de orders. Ese hábito de composición, más que cualquier función individual, es la lección más grande que deja este módulo.
Hacia dónde sigues. El módulo 8 —el capstone de esta guía completa— ensambla todo, desde el contrato del módulo 4 hasta el gobierno de este módulo, en un solo sistema, corrido dos veces: una sobre orders_2026-08-14.csv, reportando exactamente los mismos seis fallos que ya conoces; otra sobre un día limpio reconstruido de la semana canónica de Kiosko, reportando cero fallos — la prueba final de que el sistema completo no solo detecta lo roto, deja pasar lo correcto sin fricción. Y su última lección cierra el ecosistema completo, nombrando, una por una, las guías hermanas que resuelven lo que este sistema de confianza dejó pendiente a propósito.
Recursos
- Pandera — documentación oficial (
SchemaErrors.failure_cases, base técnica debuild_failure_report(), reusada sin cambios desde el módulo 2). pandera.readthedocs.io. En inglés. - Polars — documentación oficial completa (
with_row_index,join,select,map_elements— el conjunto completo de expresiones usadas en este módulo). docs.pola.rs. En inglés. - PyYAML — documentación oficial (
yaml.safe_dump, usada paracatalog.yaml). pyyaml.org/wiki/PyYAMLDocumentation. En inglés. - Módulo 6, proyecto (lección 8), de esta misma guía — fuente del estado exacto de
S04en el que arrancó este módulo (DIMENSIONS_STILL_OPEN: []).src/guides/data-reliability-and-governance-guide/workbook/module-06-freshness-volume-and-lineage/es/08-project-s04s-freshness-volume-and-lineage-report.md. En español. - DISEÑO de esta guía — el mapa completo de los ocho módulos, incluido el módulo 8 que sigue.
src/guides/data-reliability-and-governance-guide/DISENO.md. En español.