Estilo de comprobación — guía para replicar en otras sesiones

Empaquetado como skill (2026-08-10): trivasa-comprobacion, en el repo claude-skills — mismo contenido que este documento, pero es la fuente viva a partir de ahora (el ejemplo de código de este documento quedó desactualizado: usa Console()/Table() sueltos en vez de helpers_output.py, ver rich-output-checks). Esta página queda como referencia histórica de origen — para escribir o revisar un script nuevo, usar el skill, no este documento.

Este documento describe el patrón usado en layout_gastos/ para validar queries contra datos reales. Está pensado para pegarse al inicio de una sesión nueva de Claude Code, así no hay que re-explicar el estilo cada vez.

Filosofía

  1. Nunca confiar en documentación vieja sin validar con datos reales. Confirmar contra la base de datos, aunque exista un .md previo que diga lo contrario.
  2. Incremental, una pieza a la vez. No construir la query completa de una — validar un bloque chico (un folio conocido) antes de escalar al universo completo.
  3. Aislar antes de generalizar. Cuando algo no cuadra, separar los casos que fallan en su propio CSV/query antes de intentar arreglar la query principal a ciegas.
  4. Explicar el “no cuadra” con causa raíz, no forzar la regla. Un folio que no cuadra por una razón de negocio conocida (reversión, reclasificación, redondeo en cientos de líneas) se documenta como excepción — no se ajusta la fórmula para que “pase” artificialmente.

Conexión por default

connection_200_trivasadb3.py (.200/TRIVASADB3) — nunca cambiar a connection_207.py (.207, producción) salvo que el usuario lo pida explícito. .200 puede estar rezagada respecto a producción para el mes más reciente — si algo no cuadra y podría ser por eso, decirlo, no asumir.

Convención de las queries SQL (docs/queries/gastos/*.sql)

  • Un archivo por pieza de negocio (ej. v_impuestos_por_ceco.sql, no una query gigante que lo haga todo).
  • Comentario al inicio explicando el porqué de cada decisión no obvia (por qué ese join, por qué ese filtro, qué bug evita) — no solo qué hace la query. Ejemplo real de esta sesión:
  -- Comparacion en MONEDA ORIGINAL, sin convertir con tipo de cambio: tanto
  -- Grd_Precio_Neto_Importe como Cxp_Precio_Neto_Importe estan en la moneda
  -- del documento -- confirmado exacto con folio USD real (05-0175047,
  -- ambos lados = 3,201.60 USD, sin necesidad de multiplicar por
  -- Grd_Tipo_Cambio ni Cxp_Tipo_Cambio).
  • Parámetros con :fecha_ini/:fecha_fin (no hardcodeados) — el script Python los reemplaza con .replace() antes de correr.
  • Gr_Folio ya es el string completo ('SS-FFFFFFF'), nunca reconstruir con CONVERT/RIGHT a menos que se necesite un formato de display distinto al nativo.
  • Ojo con columnas money/numeric de pocos decimales en divisiones para prorrateo (Grc_Importe, Grc_Factor) — CAST(... AS FLOAT) antes de dividir, o la suma de pesos no da 1.0 exacto (bug real encontrado esta sesión, ver docs/queries/gastos/v_impuestos_por_ceco.sql).

La baseline: contra qué se compara

Toda comprobación necesita un ESPERADO que sea una fuente de verdad YA CONFIABLE — no un valor derivado de la misma query que se está validando (eso sería circular, siempre “cuadraría”). En este proyecto, las fuentes de verdad usadas hasta ahora:

Qué se validaContra qué (ESPERADO)Por qué es confiable
Universo de folios (v0.1)Export nativo de MPRO (Excel)Es lo que ve Contabilidad hoy
Cargo/Abono por folio (v0.2/v1.0)Gasto_Registro_Documento.Grd_Precio_Descontado_ImporteColumna fuente, no derivada de póliza
Impuesto por CECO (v2.3)Gasto_Registro_Documento.Grd_Impuesto_ImporteMismo principio — columna fuente
Monto CXP (v2.2)Cuenta_X_Pagar.Cxp_Precio_Neto_Importe (moneda original, sin convertir)Registro independiente de la CXP, no un cálculo nuestro
Reconciliación completa de un mesv01_baseline_[mes].csv (CSV ya validado en una pieza anterior)Una vez que una pieza cierra al 100%, su CSV se vuelve baseline para las siguientes

Regla: si no hay una columna/tabla independiente contra la cual comparar, no se puede “comprobar” — solo se puede “calcular”. Dejarlo explícito en el mensaje al usuario en vez de presentar un cálculo como si fuera una validación.

Colores y umbrales (regla fija en este proyecto)

color = "green" if pct >= 99 else "yellow" if pct >= 95 else "red"
  • Verde (≥99%): aceptable para cerrar la pieza como validada, aunque no sea exactamente 100% — el resto normalmente son excepciones de negocio ya conocidas (ver protocolo de diagnóstico más abajo).
  • Amarillo (95-99%): revisar antes de cerrar — puede que falte identificar un patrón de excepción real.
  • Rojo (<95%): no cerrar la pieza — señal de que falta algo estructural en la query, no solo ruido de redondeo.

Estos umbrales aplican a la columna % de las tablas rich, tanto en el resumen por origen como en el resumen por mes (nunca solo texto plano — siempre tabla, siempre con color en el %, para que salte a la vista dónde está el problema sin tener que leer cada número).

Estructura estándar del script de comprobación (Python + rich)

"""
[Nombre de la pieza] - Comprobacion contra [fuente de verdad].
Lee [archivo].sql (la query guardada, no una copia duplicada en el script).
"""
import sys
sys.path.append("..")
from connection_200_trivasadb3 import engine
import pandas as pd
from rich.console import Console
from rich.table import Table
 
console = Console()
FECHA_INI, FECHA_FIN = "2026-01-01", "2026-02-01"
 
with open("docs/queries/gastos/[archivo].sql") as f:
    QUERY = f.read().replace(":fecha_ini", f"'{FECHA_INI}'").replace(":fecha_fin", f"'{FECHA_FIN}'")
 
df = pd.read_sql(QUERY, engine)
df["FOLIO"] = df["FOLIO"].str.strip()   # por si acaso, aunque Gr_Folio nativo no debería traer espacios
 
# --- Comparar contra la fuente de verdad conocida ---
df["DIFERENCIA"] = df.CALCULADO - df.ESPERADO
df["CUADRA"] = df.DIFERENCIA.abs() < 1
 
n, c = len(df), df.CUADRA.sum()
console.print(f"\n[bold]Folios:[/bold] {n}")
console.print(f"[bold]Cuadran:[/bold] {c} ({100*c/n:.2f}%)\n")
 
# --- Tabla resumen por ORIGEN, con importes (no solo %) ---
resumen = df.groupby("ORIGEN").agg(
    n=("FOLIO", "count"), cuadran=("CUADRA", "sum"),
    esperado=("ESPERADO", "sum"), calculado=("CALCULADO", "sum"),
).reset_index()
t1 = Table(title="Resumen por ORIGEN")
t1.add_column("Origen"); t1.add_column("n", justify="right")
t1.add_column("Cuadran", justify="right"); t1.add_column("%", justify="right")
t1.add_column("Esperado ($)", justify="right"); t1.add_column("Calculado ($)", justify="right")
for _, r in resumen.iterrows():
    pct = 100 * r.cuadran / r.n
    color = "green" if pct >= 99 else "yellow" if pct >= 95 else "red"
    t1.add_row(str(r.ORIGEN), str(r.n), str(r.cuadran), f"[{color}]{pct:.1f}%[/{color}]",
               f"{r.esperado:,.2f}", f"{r.calculado:,.2f}")
console.print(t1)
 
# --- Folios que NO cuadran, con importes, ordenados por magnitud de diferencia ---
mal = df[~df.CUADRA]
console.print(f"\n[bold red]No cuadran:[/bold red] {len(mal)}")
if len(mal):
    t2 = Table()
    for c_ in ["FOLIO", "ORIGEN", "ESPERADO", "CALCULADO", "DIFERENCIA"]:
        t2.add_column(c_, overflow="fold")
    for _, r in mal.sort_values("DIFERENCIA", key=abs, ascending=False).head(15).iterrows():
        t2.add_row(str(r.FOLIO), str(r.ORIGEN), f"{r.ESPERADO:,.2f}", f"{r.CALCULADO:,.2f}", f"{r.DIFERENCIA:,.2f}")
    console.print(t2)
 
df.to_csv("[nombre]_validacion.csv", index=False)
console.print(f"\n[green]Guardado: [nombre]_validacion.csv[/green]")

Para semestre/rango largo, agregar una segunda tabla “por mes” con la misma estructura de columnas (Folios, Cuadran, % con color, Esperado ($), Calculado ($)), derivando MES de la fecha con pd.to_datetime(...).dt.strftime("%Y-%m").

Cuando algo no cuadra: protocolo de diagnóstico

  1. No arreglar a ciegas. Aislar los folios que fallan en su propio CSV (df[~df.CUADRA].to_csv(...)).
  2. Buscar patrones en la muestra antes de investigar uno por uno: ¿la diferencia es siempre el doble exacto? ¿siempre negativa? ¿se repiten los mismos montos en folios distintos? Esas firmas casi siempre delatan la causa (fan-out de join, filtro de moneda faltante, bug de tipo de dato) antes de tocar la base de datos de nuevo.
  3. Drill-down de UN folio real con todo el detalle crudo (sin filtros, sin agregar) antes de generalizar el fix — nunca arreglar la query completa basándose en una sospecha sin confirmar con un caso concreto.
  4. Explicaciones de negocio válidas para “no cuadra” ya confirmadas en este proyecto (reusar antes de investigar de cero):
    • GASTO_RECLASIFICACION: Cargo=Abono simétrico, Importe neto $0.
    • Folios de reversión (Importe <= -$1): usar solo Abono, Cargo cae en cuenta de Provisión/Pasivo.
    • Gr_Genera_Cxp = 'NO': folio no genera CXP individual por diseño.
    • CONTROL_COMBUSTIBLE: CXP consolidada semanal, link por Cxp_Referencia = Grd_Referencia + mismo proveedor, no por folio.
    • Ruido de redondeo en folios con cientos de líneas de póliza (diferencia de $1-5, acumulada en centavos).

Nomenclatura de archivos

  • Query: docs/queries/gastos/v_[nombre_descriptivo].sql (no v1, v2 genéricos salvo que sea literalmente una nueva versión de algo que ya existía).
  • Script de comprobación: validar_[nombre]_[periodo].py.
  • CSV de salida: [nombre]_validacion.csv o [nombre]_[periodo].csv.
  • .md de cierre por pieza: docs/v[X]_cierre.md cuando una pieza queda 100% validada y lista para no tocarse más.