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 /runsquedaba enQUEUEDpara 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.mdmarcaba 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 batchresearchSUCCEEDED en 4,8 s (≈100 corridas/min extrapolado:throughput_corridas_min99,9); latencia delPOST32–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 enqa_reports/). - Caos:
kill -9deworker-batchcon 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 -9deworker-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 → :s2conup -d --no-deps(sindown), healthy en 7,61 s (qa_reports/v2/runs/2026-09-22/g9/rollback-drill.json),e2e_batch10/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:
- Imagen
:s4en el servicioworker-stream. FINAZ_STREAM_RUN_IDen el compose (activa el fencing).- Un grupo de consumo nuevo (no heredar offsets del defectuoso).
verify_releasesobre 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:
- S5/G3: daemon en
:s3sin fencing; deudas del fencing (diario en fichero bajo bloqueo de fila PG,event_iddrenados no persistidos,leases.adquirirsin CAS sobre el fence,--once-ncon fencing supone offsets densos). - G7: datos sintéticos; cobertura nominal; spec no mapeado a
TrainingSpec; códigos de error locales pendientes de ADR. - Release: las imágenes
:s4(api, core, neural) no están enrelease-manifest.s2.jsonni las cubreverify_release.sh; los servicios vivos siguen en:s2/:s3. - G9: el rollback vivo cubre el servicio
apiy el ruteo canary; el rollback deworker-batchno se ha ensayado; la API no consume el ruteo en caliente. - 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.
- Datos reales (2026-09-22): 141 551 barras de 44 series ingeridas en
finaz_tecon snapshot SEALED, peroknown_at= cierre de barra sin evidencia de disponibilidad del proveedor: el gap «PIT real con evidencia de proveedor» de G4 sigue abierto. - 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-streamsigue en:s3sin fencing y con emit no golden; la corrección está probada en:s4y 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¶
- Con tus palabras: ¿por qué G3 puede estar en PASS mientras S5 está INCOMPLETO? ¿Qué pregunta distinta responde cada uno?
- Enumera los cuatro pasos para cerrar S5 y explica por qué hace falta un grupo de consumo nuevo.
- Imagina que un test de G4 se salta porque
catboostno está instalado en tu portátil. ¿Qué estado tendría G4 si ésa fuera toda la evidencia? ¿Dónde habría que ejecutarlo? - Abre
qa_reports/v2/runs/2026-09-22/g9/rollback-drill.jsonen el servidor: ¿cuál es suveredicto, cuántos segundos duró el pasoe.rollback_releasey cuánto el drill completo (segundos_total)? - 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?