Saltar a contenido

Gates: cómo se certifica la plataforma

Qué vas a aprender

  • Qué es un gate y por qué el runtime causal se gobierna por gates y no por fechas.
  • La diferencia entre los gates de plataforma (G0–G9) y los operativos de servidor (S0–S6).
  • El criterio honesto: los cuatro estados posibles y por qué un skip no es un PASS.
  • El estado tras el hito 4 (2026-09-22): todo PASS salvo S5, y por qué.
  • Qué es una exclusión declarada (D-42) y en qué se diferencia de «esconder un fallo».
  • Dónde está la evidencia (qa_reports/v2/) y cómo leerla.

Fuentes

docs/v2/GATES.md en la versión del servidor (la que refleja el hito 4), el capítulo 16 del pack (16_implementation_roadmap.md), docs/v2/GAP_REPORT.md y la evidencia en qa_reports/v2/runs/ del servidor (2026-09-19, 2026-09-20, 2026-09-22). Commits del hito 4 consultados en el servidor: 2f4b2d9, 77107b5 (G0-B), e17b44e, edf6c6c, 4d867d5 (G7), a5db1ac (S6), 3dac695 (G9), 78197cb (G3), 349b182 (datos reales).


1. Qué es un gate

El pack lo define así (§16):

Los gates son condiciones de integración/promoción, no fechas ni estimaciones de esfuerzo. […] Una capacidad no se anuncia AVAILABLE hasta superar su gate. No se desbloquea una fase sólo por tener archivos de código.

Un gate responde a una pregunta concreta con evidencia: «¿funciona el streaming con fencing y caos?», «¿se puede restaurar desde un backup externo?». Para salir de un gate hay que publicar:

  • informe JUnit y quality report,
  • hashes de inputs, schemas, plan e imagen,
  • capacidades soportadas y no soportadas,
  • límites conocidos.

Y una regla dura: un fallo temporal (causalidad), de seguridad o de restricción dura es bloqueante.

Analogía: la ITV

Un coche no pasa la ITV por tener todas las piezas, ni porque «el mes que viene tocaba». Pasa porque un inspector mide frenos, luces y emisiones y firma el resultado. Y si no pudo medir las emisiones porque el aparato estaba roto, no te dan la pegatina «por si acaso»: vuelves otro día.


2. El criterio honesto

Cuatro estados, ni uno más

Estado Significado
PASS Criterio cumplido con evidencia real y reproducible
INCOMPLETO Depende de algo pendiente, simulado, omitido o no reproducible, aunque los tests unitarios pasen
UNSUPPORTED La capacidad no existe (y se dice)
PENDIENTE Aún no se ha abordado

De dónde viene esta disciplina

El 2026-09-19 una auditoría externa (informe Astras) revisó el estado anunciado. La verificación independiente (docs/v2/GAP_REPORT.md) confirmó 11 de 11 hallazgos. Algunos:

  • La API no ejecutaba corridas: POST /runs quedaba en QUEUED para siempre.
  • La auth era falsificable con cabeceras x-finaz-*.
  • Los servicios de stream no eran servicios operativos.
  • Los healthchecks tenían un heartbeat falso (un bucle aparte que tocaba un fichero).
  • GATES.md marcaba PASS gates que el propio documento llamaba «parciales».

Desde entonces, GATES.md lleva esta regla en su cabecera:

Un gate que depende de una condición pendiente, simulada, omitida o no reproducible es INCOMPLETO, aunque sus tests unitarios pasen. No se usan símbolos verdes acompañados de «parcial».

Skip ≠ PASS

En la suite del runtime (tests_v2/) hay tests marcados como skip porque su dependencia no está en ese entorno (un driver de PostgreSQL, un broker, una librería de modelos). La regla:

Un skip por dependencia ausente NO cuenta como PASS del gate que depende de ella

Por eso las suites de modelos se ejecutan dentro de sus imágenes en el servidor, donde la dependencia sí existe. Un pytest verde en el portátil con 60 skips no certifica nada de lo que se ha saltado.

Cifras de la suite global. Hito 4 (2026-09-22), relanzada en el servidor tras las fusiones de G3/G7/G9/S6/datos: 1641 passed, 69 skipped, 0 failed (JUnit en qa_reports/v2/runs/2026-09-22/qa-final/). Por imagen, en el hito 4 sólo se relanzó neural en :s4: 55/55 sin skips. La última evidencia por imagen del resto es del QA final del hito 3 (2026-09-20): 0 fallos en 8/8 (api 105, core 261 con 12 skip, io 77/5, quant 54/3, opt 105/5, neural 43, chronos 35/2, timesfm25 68/3); aquel día la suite global dio 1613 passed / 66 skipped en el servidor.


3. Gates de plataforma (G0–G9)

flowchart LR
    G0A[G0-A<br/>contratos] --> G1
    G0B[G0-B<br/>baseline del banco] --> G1
    G1[G1<br/>vertical batch] --> G2[G2<br/>replay +<br/>recuperación]
    G2 --> G3[G3<br/>stream durable]
    G1 --> G4[G4<br/>training tabular<br/>y estadística]
    G4 --> G5[G5<br/>riesgo y cartera]
    G5 --> G6[G6<br/>discreto]
    G4 --> G7[G7<br/>NHITS]
    G4 --> G8[G8<br/>foundation]
    G3 --> G9[G9<br/>hardening]
    G6 --> G9
    G7 --> G9
    G8 --> G9
Gate Pregunta Evidencia clave Estado (2026-09-22)
G0-A ¿Contratos, fixtures y ADR congelados, sin refs rotas? Validador del pack PASS_STRUCTURAL_ONLY (1014/1014); tests_v2/contracts 38; tests_v2/legacy 20 PASS
G0-B ¿El baseline del banco de backtesting es reproducible en su entorno? Hashes del canónico OK (154+480 particiones SHA-256); suite del banco 2752 passed / 0 failed + 16/16 slow; qa_reports/v2/runs/2026-09-22/g0b/ PASS (exclusión declarada D-42)
G1 ¿Vertical batch con publicación, PIT, guards, auth y una API que lo ejecuta? e2e_batch.py 10/10 con tokens vivos; persistencia PG real PASS
G2 ¿Replay y recuperación local deterministas? Kill en 5 fronteras + checkpoint commit/restore vivo; replay 23 eventos con log_hash = S5 PASS
G3 ¿Stream durable con paridad 3 modos, fencing, handoff y caos? Paridad al golden; fencing por lease PG; drill 2 owners 36/36 con :s4 PASS (daemon pendiente → S5)
G4 ¿Training tabular/estadístico con evidencia? Job g4-quant-001 PASS PASS
G5 ¿Riesgo/cartera con veto independiente? Job g5-opt-001 PASS (ERC + min-var OPTIMAL) PASS
G6 ¿Discreto con postvalidación? CP-SAT OPTIMAL con prueba de optimalidad (mismo artefacto) PASS
G7 ¿NHITS con artefactos y calibración? Job g7-neural-002 PASS en finaz-te-neural:s4; suite 55/55 PASS (datos sintéticos; cobertura nominal)
G8 ¿Foundation con provenance? Chronos-2 y TimesFM 2.5 con SHA verificado PASS
G9 ¿Hardening: carga, caos, restore, supply-chain, rollback? Carga y caos en vivo; SBOM SPDX; drill de rollback release-to-release; shadow MATCH PASS

Tres gates que merecen detalle

G3 — streaming. El drill vivo del 2026-09-22 (deployment/v2/smoke/caos_2owners_g3.py, capítulo 3) probó que un intruso con el lease ajeno vigente falla con STREAM_LEASE_OCUPADO, que un owner pausado y adelantado por otro falla con STREAM_FENCE_OBSOLETO sin escribir nada, y que tras un kill -9 el siguiente owner espera y retoma exactamente: 299/299 sin pérdida ni duplicados, values_hash = golden. G3 es PASS porque el mecanismo está demostrado; lo que falta es desplegarlo en el daemon (eso es S5).

G9 — hardening. Evidencia en vivo:

  • Carga (qa_reports/v2/runs/2026-09-20/load-g9-research.json): 8/8 corridas batch research SUCCEEDED en 4,8 s (≈100 corridas/min extrapolado: throughput_corridas_min 99,9); latencia del POST 32–139 ms (GATES dice 26–139; el fichero registra 32,49 ms como mínimo); cada corrida ~0,53 s de principio a fin. Replay 23 eventos en 3,1 s y stream 299 registros con golden, según GATES (sin fichero propio en qa_reports/).
  • Caos: kill -9 de worker-batch con 20 corridas en vuelo → 20/20 SUCCEEDED sin pérdida ni duplicados (caos-worker-batch-kill9.json; GATES dice 60/60, el fichero registra 20); kill -9 de worker-stream → reanudación con paridad; reinicio de la API → persistencia.
  • Rollback release-to-release (deployment/v2/ops/rollback_drill.py, 2026-09-22): api :s2 → :s4 → :s2 con up -d --no-deps (sin down), healthy en 7,61 s (qa_reports/v2/runs/2026-09-22/g9/rollback-drill.json), e2e_batch 10/10 en ambas releases, el run creado en la candidata sigue consultable tras volver, volúmenes intactos.
  • Shadow: hash del oráculo = hash del runtime causal (MATCH).

G0-B — baseline del banco de backtesting. Aquí aparece la exclusión D-42 (sección 5).


4. Gates operativos de servidor (S0–S6)

Los gates S vienen del runbook de servidor del pack: no preguntan «¿funciona el código?», sino «¿está bien operado en este servidor?».

Gate Criterio Evidencia Estado
S0 Inventario sin secretos 24 CPU, 251 GiB, Docker 29.1.3 / Compose 2.40.3, sin GPU, finaz-net + 4 servicios healthy PASS
S1 Aislamiento y mínimo privilegio Un rol PG/CH/MinIO por servicio; claves por fichero montado, nunca en docker inspect PASS
S2 Release: locks, entrypoints, migraciones, digests IDs de imagen = release-manifest.s2.json; SBOM SPDX; base pineada por digest; verify_release.sh PASS
S3 Transferencia de datos con SHA-256 rsync --checksum + docker save/load PASS
S4 Vertical API completo con reinicio e2e_batch.py 10/10 + restart_cancel.py CANCEL_OK PASS
S5 Stream operativo como servicio Daemon healthy, pero en :s3 sin fencing y con emit no golden INCOMPLETO
S6 Canary/shadow, rollback y restore desde backup externo Backup cada 6 h, retención 20, RPO medido 3,4 s, RTO total ~6,4 s en drill, copia off-host verificada PASS

Por qué S5 es INCOMPLETO

Es el único gate que no está en PASS, y la razón es instructiva: el mecanismo correcto existe y está probado (G3), pero el servicio que corre no lo usa.

flowchart LR
    subgraph Hoy["Servicio vivo (2026-09-22)"]
        D3["worker-stream<br/>finaz-te-core:s3<br/>sin --run-id (sin fencing)<br/>emit values_hash fe5812e2… ≠ golden"]
    end
    subgraph Probado["Probado en el drill G3"]
        D4["finaz-te-core:s4<br/>GuardiaFence + lease PG<br/>emit = golden 9e906a09…"]
    end
    D3 -. "promoción pendiente" .-> D4

El defecto del :s3: al reanudar, tomaba el high-water sobre registros aceptados pero aún sin drenar, y no persistía los event_id ya vistos; así, copias DUPLICATE se aceptaban como nuevas y la EMA se desviaba. Está corregido en :s4. Para cerrar S5 hace falta:

  1. Imagen :s4 en el servicio worker-stream.
  2. FINAZ_STREAM_RUN_ID en el compose (activa el fencing).
  3. Un grupo de consumo nuevo (no heredar offsets del defectuoso).
  4. verify_release sobre la nueva release.

Por qué no se «arregló en caliente»

La evidencia de G3 (daemon-final.json) registra que un up -d habría recreado el contenedor por deriva de configuración y reescrito stream-emit.json. No se ejecutó para no destruir la evidencia del defecto antes de documentarlo. Primero se documenta, luego se promueve.

Resumen de estado tras el hito 4

Gate Estado Gate Estado
G0-A PASS S0 PASS
G0-B PASS (D-42) S1 PASS
G1 PASS S2 PASS
G2 PASS S3 PASS
G3 PASS S4 PASS
G4 PASS S5 INCOMPLETO
G5 PASS S6 PASS
G6 PASS
G7 PASS (sintético)
G8 PASS
G9 PASS

Veredicto global: INCOMPLETO, sólo por S5. Ninguna condición simulada ni skip se presenta como PASS.


5. Exclusiones declaradas: D-42

G0-B exige que la suite del banco (tests/) pase en el servidor. Pasa: 2752 passed, 0 failed. Pero hay un test deseleccionado: test_loader::TestRendimiento, un presupuesto absoluto de rendimiento (cargar NVDA 1 min en menos de 250 ms) calibrado en el portátil (i5-1335U). En el servidor (EPYC 7272) tarda 236–272 ms: a veces pasa, a veces no, según la carga.

La decisión D-42 declara esa exclusión explícitamente:

Esconder un fallo Exclusión declarada
Se quita el test y no se dice Se deselecciona un test identificado, con su motivo
Se sube el umbral hasta que pase No se toca el banco de backtesting (está congelado)
El estado dice «PASS» a secas El estado dice «PASS (exclusión declarada D-42)»
No hay evidencia Log de rendimiento y resumen en qa_reports/v2/runs/2026-09-22/g0b/

Analogía: la letra pequeña a la vista

Una exclusión declarada es letra pequeña en negrita y en la portada. Cualquiera que lea el estado ve la condición sin tener que buscarla.


6. Limitaciones declaradas (no se ocultan)

GATES.md cierra con una lista de límites que acompañan a los PASS. Las principales:

  1. S5/G3: daemon en :s3 sin fencing; deudas del fencing (diario en fichero bajo bloqueo de fila PG, event_id drenados no persistidos, leases.adquirir sin CAS sobre el fence, --once-n con fencing supone offsets densos).
  2. G7: datos sintéticos; cobertura nominal; spec no mapeado a TrainingSpec; códigos de error locales pendientes de ADR.
  3. Release: las imágenes :s4 (api, core, neural) no están en release-manifest.s2.json ni las cubre verify_release.sh; los servicios vivos siguen en :s2/:s3.
  4. G9: el rollback vivo cubre el servicio api y el ruteo canary; el rollback de worker-batch no se ha ensayado; la API no consume el ruteo en caliente.
  5. S6: RPO observado 3,4 s pero máximo teórico 6 h (intervalo del cron); un solo RAID1 en el servidor y la copia off-host depende de que el portátil del operador esté encendido.
  6. Datos reales (2026-09-22): 141 551 barras de 44 series ingeridas en finaz_te con snapshot SEALED, pero known_at = cierre de barra sin evidencia de disponibilidad del proveedor: el gap «PIT real con evidencia de proveedor» de G4 sigue abierto.
  7. Sin live trading, sin GPU, sin K8s, sin duplicar PG/CH/Redpanda/MinIO.

Cómo leer un PASS

Lee siempre el PASS junto con su paréntesis y con esta lista. «G7 PASS (datos sintéticos)» no significa «NHITS predice bien el mercado»; significa «el adaptador NHITS real cumple sus garantías de causalidad, deadline, baseline y artefacto».


7. La evidencia: qa_reports/v2/

Cada gate apunta a ficheros concretos. Estructura en el servidor:

qa_reports/v2/
├── INDICE.md, consistencia.json, quality_report.json, junit.xml
├── sbom/                              SBOM SPDX de las imágenes
└── runs/
    ├── 2026-09-19/                    JUnit por imagen + smokes e2e_batch y restart_cancel (G1, S4)
    ├── 2026-09-20/                    jobs S7 (G4–G8), caos kill -9, carga G9, backup-restore
    ├── 2026-09-20-qa-final/           JUnit por imagen (QA final hito 3)
    └── 2026-09-22/                    hito 4
        ├── g0b/                       baseline legacy (junit, hashes, perf NVDA 1 min)
        ├── g3/                        drill 2 owners (caos-2owners.json, logs por owner, daemon-final.json)
        ├── g7/                        NHITS (result, manifest, artefacto, SBOM, junit, recursos)
        ├── g9/                        drill de rollback (rollback-drill.json, logs)
        ├── s6/                        RPO, crontab, pull off-host
        ├── datos/                     ingesta real y snapshot sellado
        └── qa-final/
cd ~/FinazTradingEngine
python3 -c '
import json
d = json.load(open("qa_reports/v2/runs/2026-09-22/g3/caos-2owners.json"))
print(d["gate"], "total:", d["total"], "fallos:", d["fallos"])
print(sum(c["resultado"] == "PASS" for c in d["checks"]), "/", len(d["checks"]), "PASS")
'
# G3 total: 299 fallos: []
# 36 / 36 PASS
import json
from pathlib import Path

for f in Path("qa_reports/v2/runs/2026-09-22").rglob("*.json"):
    try:
        d = json.loads(f.read_text())
    except ValueError:
        continue
    checks = d.get("checks") if isinstance(d, dict) else None
    if isinstance(checks, list):
        malos = [c for c in checks if c.get("resultado") != "PASS"]
        print(f, len(checks), "checks,", len(malos), "no PASS")

Resumen

  • Un gate es una condición de promoción con evidencia, no una fecha. Sin gate en verde, una capacidad no se anuncia.
  • G0–G9 certifican la plataforma; S0–S6, la operación en el servidor.
  • Cuatro estados: PASS, INCOMPLETO, UNSUPPORTED, PENDIENTE. Lo simulado, omitido o no reproducible es INCOMPLETO. Skip ≠ PASS.
  • Tras el hito 4 (2026-09-22): todo PASS salvo S5 (INCOMPLETO): el daemon worker-stream sigue en :s3 sin fencing y con emit no golden; la corrección está probada en :s4 y falta promoverla. Veredicto global: INCOMPLETO.
  • D-42 es una exclusión declarada (un test de rendimiento absoluto calibrado en otra máquina), no un fallo escondido.
  • La evidencia vive en qa_reports/v2/runs/<fecha>/<gate>/.

Para practicar

  1. Con tus palabras: ¿por qué G3 puede estar en PASS mientras S5 está INCOMPLETO? ¿Qué pregunta distinta responde cada uno?
  2. Enumera los cuatro pasos para cerrar S5 y explica por qué hace falta un grupo de consumo nuevo.
  3. Imagina que un test de G4 se salta porque catboost no está instalado en tu portátil. ¿Qué estado tendría G4 si ésa fuera toda la evidencia? ¿Dónde habría que ejecutarlo?
  4. Abre qa_reports/v2/runs/2026-09-22/g9/rollback-drill.json en el servidor: ¿cuál es su veredicto, cuántos segundos duró el paso e.rollback_release y cuánto el drill completo (segundos_total)?
  5. Redacta una «exclusión declarada» hipotética al estilo de D-42 para un test que dependa de la hora del sistema. ¿Qué cuatro elementos debe incluir?