Metodología de parchado a producción
Estándar para escribir scripts que corrigen datos ya publicados (GSheets / SQLite) de forma consistente, segura y auditable. Destilado del precedente real
scripts/patches/patch_missing_zonales.py. Todo parche nuevo debe seguir este patrón.
Por qué existe este estándar
Sección titulada «Por qué existe este estándar»Cuando un campo llega mal o vacío a producción (síntoma del meta-patrón Deuda-Datos-Correccion-Manual-Implicita), hay dos arreglos:
- Corregir el pipeline — la cura: el próximo lote sale bien.
- Parchar lo ya publicado — el alivio: arregla el histórico que el pipeline corregido no va a re-procesar.
Casi siempre se necesitan ambos. Este documento estandariza el (2) para que cada parche se vea igual, sea seguro de correr y no reinvente lógica que ya vive en el pipeline.
:::caution Principio rector — síntoma vs causa raíz El parche alivia el síntoma; la cura vive en el pipeline (principio 12 de Deuda-Datos-Correccion-Manual-Implicita). Todo parche debe nacer con un follow-up de origen: la corrección equivalente en el pipeline para que el dato deje de salir mal. Un parche sin follow-up es deuda que vuelve cada mes. :::
Cuándo se parcha (criterio de decisión)
Sección titulada «Cuándo se parcha (criterio de decisión)»| Situación | Acción |
|---|---|
| El dato sale mal y seguirá saliendo mal | Corregir el pipeline (causa). Obligatorio. |
| Hay histórico ya publicado que el pipeline corregido no re-procesa | Parchar (alivio). |
| El dato se puede regenerar corriendo el pipeline de nuevo | Re-correr el pipeline, no parchar. |
| Corrección puntual de < ~5 celdas, una sola vez | Editar a mano + anotar; no amerita script. |
Regla práctica: si vas a tocar las mismas celdas más de una vez, o más de ~10 celdas, es un script de parche, no una edición manual.
Dos clases de parche
Sección titulada «Dos clases de parche»Según cómo afectan el dato existente, hay dos clases. Comparten todos los rasgos de abajo, pero difieren en el rasgo de sobrescritura (#4):
- Relleno aditivo (ej.
patch_missing_zonales.py): solo rellena celdas vacías. Nunca sobrescribe lo que ya tiene valor → cero regresión por construcción. Es el caso por defecto y el más seguro. - Normalización de grafía (ej.
patch_proceso_typos.py): sobrescribe valores existentes con su forma canónica ("obras mediana"→"OBRAS MEDIANAS"). Es seguro solo si la transformación es idempotente, preserva la semántica, y proviene de una fuente única auditada (el mismonormalize_*del pipeline). Exige cero-falsos-positivos estricto: tocar únicamente celdas cuyo valor cambia a un canónico conocido — nunca por reformateo casual (p. ej. saneo de espacios).
Anatomía estándar de un script de parche
Sección titulada «Anatomía estándar de un script de parche»Los 11 rasgos que todo script en scripts/patches/ debe cumplir, destilados de patch_missing_zonales.py y patch_proceso_typos.py:
- Ubicación y nombre.
scripts/patches/patch_<que>.py. Un parche = un campo/problema (SRP). - Docstring con la estrategia explícita. Encabeza el archivo con: qué corrige, la cascada de resolución numerada (“primera regla que matchea gana”) y la sección
Uso:con los dos modos. - Dry-run por defecto,
--applypara escribir.argparsecon--apply(action="store_true"). Sin la bandera, el script solo reporta, no toca producción. - Respeta la clase de parche (ver arriba). Relleno aditivo: salta filas con valor (
if val and val != "NAN": continue) — nunca sobrescribe (principio 7 del doc de deuda). Normalización: solo toca celdas dondenormalize_*(val) != valy el resultado es un canónico conocido; el passthrough (p. ej. saneo de espacios) no cuenta como cambio. Cualquiera de las dos clases: cero regresión por diseño. - Cascada de resolución con método etiquetado. Cada resolución devuelve
(valor, método)— elmétodoes la procedencia ("sf_reprocess","ot_maps","prefix_205","classify_zone_desc"…). Esto hace auditable de dónde salió cada valor (principio 9 del doc de deuda). - Reutiliza las herramientas del pipeline — no dupliques lógica (DRY). El parche importa y usa los mismos normalizadores/clasificadores que la ingesta (
classify_zone,normalize_text,get_ot_maps,PAT_OT,std_ids). Si el pipeline clasifica una zona de una forma, el parche debe clasificarla igual. Esta es la regla más importante: una sola fuente de verdad de normalización, usada en ingesta + parche. - Reporte completo antes de aplicar. Imprime: total resolubles vs. sin resolver, conteo por método, conteo por valor, y detalle fila por fila (
fila N: VALOR (via método) | claves). El dry-run debe dejar ver exactamente qué se va a escribir. - Lo no resuelto NO se toca, se reporta. Cero falsos positivos: lo ambiguo se lista para revisión manual, nunca se rellena con un valor adivinado (principio 9 del doc de deuda).
- Idempotente. Correrlo dos veces no cambia nada la segunda vez (aditivo: las filas ya no están vacías; normalización: los valores ya son canónicos). Verifícalo: un segundo dry-run debe reportar 0 cambios.
- Escritura resiliente. Escribe en lote con reintento (
call_with_retry(ws.update_cells, cells)), no celda-por-celda en bucle sin retry. Respeta los rate limits de GSheets (ver Debugging-GSheets-Rate-Limits). - Salida UTF-8 (Windows). El reporte usa caracteres no-ASCII (
→, tildes,ñ). En consolas Windows (cp1252, p. ej. anaconda) eso aborta conUnicodeEncodeErroral imprimir. Reconfigura la salida al inicio del script:for _stream in (sys.stdout, sys.stderr):if hasattr(_stream, "reconfigure"):_stream.reconfigure(encoding="utf-8")
Esqueleto mínimo
Sección titulada «Esqueleto mínimo»# scripts/patches/patch_<que>.py"""Parche one-shot: <qué corrige y por qué quedó mal>.
Estrategia (cascada, primera que matchea gana): 0. <fuente más confiable> 1. <regla determinística> ... N. Fallback: no se toca (se reporta)
Uso: python scripts/patches/patch_<que>.py # dry-run python scripts/patches/patch_<que>.py --apply # escribe a producción
Follow-up de origen: <link al fix del pipeline / issue>"""import argparse# Reutilizar SIEMPRE las herramientas del pipeline (DRY):from utils.utils_text import normalize_text # normalización compartidafrom static_data.classifiers import classify_zone # clasificador compartido
def _resolver(row) -> tuple[str | None, str]: """Devuelve (valor, metodo) o (None, 'unresolved'). Procedencia auditable.""" ...
def main(): ap = argparse.ArgumentParser() ap.add_argument("--apply", action="store_true", help="Escribir a producción") args = ap.parse_args()
patches, unresolved = [], [] for row in filas: if ya_tiene_valor(row): # aditivo: no sobrescribir continue val, metodo = _resolver(row) (patches if val else unresolved).append(...)
reporte(patches, unresolved) # conteos + detalle fila a fila
if not args.apply: print(f"[DRY-RUN] {len(patches)} cambios. Usa --apply para escribir.") return escribir_con_retry(patches)Checklist
Sección titulada «Checklist»Antes de correr --apply:
- Corrí el dry-run y revisé el detalle fila por fila.
- Las reglas reutilizan los normalizadores/clasificadores del pipeline (no hay lógica duplicada).
- Identifiqué la clase (relleno aditivo o normalización) y confirmé su invariante: aditivo no sobrescribe; normalización solo toca cambios a un canónico conocido.
- Lo no resuelto/ambiguo queda listado, no rellenado a la fuerza.
- La salida no rompe en consola Windows (UTF-8 reconfigurado — rasgo #11).
Después de aplicar:
- Anoté cuántas celdas se tocaron y por qué método (procedencia).
- Abrí/anoté el follow-up de origen (fix del pipeline) para que el dato deje de salir mal.
- Registré el parche (commit + nota en sesión/engram).
Caso de referencia
Sección titulada «Caso de referencia»Clase relleno aditivo — scripts/patches/patch_missing_zonales.py (commit 3635aae, PR #40): rellenó 105 filas de FACTURACIÓN sin Zonal_Ejecutora. Cascada de 8 niveles (SF reprocesado → OT maps → prefijo OT → misma factura → SAP → pagos pendientes → classify_zone sobre la descripción → no-tocar). Reutiliza classify_zone y normalize_text del pipeline — la regla DRY de este estándar.
Clase normalización de grafía — scripts/patches/patch_proceso_typos.py (PR #58): normaliza la columna Proceso de FACTURACIÓN reutilizando normalize_proceso (PR #57). Corrige "obras mediana" → "OBRAS MEDIANAS", "eerr" → "EE.RR.". Aplicó 7 celdas de 6567, cero falsos positivos, idempotencia verificada (segundo dry-run = 0 cambios). Lógica de decisión pura (_resolver_cambios) separada del I/O para tests sin red. Destapó el rasgo #11 (UTF-8 en Windows).
Principios Aplicados
Sección titulada «Principios Aplicados»- Single-Source-of-Truth — la normalización vive en un solo lugar (los helpers/clasificadores del pipeline) y tanto la ingesta como el parche la consumen; el parche nunca define su propia versión.
- Criterio Modularizacion - DRY — rasgo #6: prohibido duplicar lógica de resolución entre pipeline y parche.
- Criterio Modularizacion - Adapter Port — la misma función de normalización se aplica en dos puntos de entrada distintos (ingesta y corrección post-hoc) sin reimplementarse.
- Separation-of-Concerns — un parche = un campo/problema; el reporte (qué se haría) está separado de la escritura (
--apply).
Relacionados
Sección titulada «Relacionados»- Deuda-Datos-Correccion-Manual-Implicita — el meta-patrón que motiva los parches; el parche ataca el síntoma, el pipeline la causa.
- Como-Crear-Pipeline-Nuevo — dónde vive la corrección de origen (el follow-up).
- Debugging-GSheets-Rate-Limits — escritura resiliente a GSheets.
- Pedidos-HES · Facturacion — dominios donde más se parcha.