English · Español
Teoría 05 — El script de la demo y aceptación binaria¶
La demo no es una presentación; es un contrato.
just demose ejecuta en frío, en una máquina nueva, sin trucos, y el visitante ve 90 segundos del currículo funcionando. Las propiedades obligatorias: idempotente, determinista, narrada y que falle ruidosamente cuando algo va mal — no que disimule contry/except: pass.
Por qué el script de demo es load-bearing¶
Cada currículo produce alguna demo. La mayoría no son load-bearing en sentido de ingeniería — son slides + un vídeo. El capstone de lynx-cortex insiste en una propiedad más fuerte: la demo es la verificación canónica de que las afirmaciones del repo son ciertas. Tres corolarios:
- Si
just demosale con código distinto de cero, hay una afirmación del currículo rota. No "lo miraremos"; el informe queda bloqueado hasta que la demo pase. - El script es el punto de entrada para nuevos contribuidores. Un desconocido leyendo el repo puede ejecutar
just demoprimero, luego leer el código con la salida de la demo como mapa. Al revés: código → demo, no funciona — hay demasiado código. - CI lo ejecuta en cada PR. Las regresiones se capturan en tiempo de PR. La demo es el test de integración.
Este capítulo deriva las cuatro propiedades que un script de demo load-bearing debe tener, y luego recorre la anatomía de scripts/demo/run.py.
Propiedad 1 — Idempotente¶
Una demo que funciona la primera vez y falla la segunda no es idempotente. Causas:
- Base de datos / estado sobrevive entre ejecuciones y la segunda usa datos rancios.
- Puertos de red dejados abiertos por una ejecución previa que crasheó.
- Directorio
mlruns/comiteado desde una sesión personal. - Un volumen docker que creció entre ejecuciones.
La receta just demo-cold del capstone:
docker compose down -v --remove-orphansantes de cualquier otra cosa.- Elimina cualquier directorio local
experiments/39-*-tmp/. - Levanta el stack desde cero.
- Ejecuta la demo.
- Derriba el stack (flag opcional para mantenerlo arriba para inspección).
Cada paso se loguea con un timestamp. El smoke test de CI ejecuta la receta cinco veces seguidas y afirma que las cinco pasan — esa es la definición operacional de idempotente.
Propiedad 2 — Determinista¶
La misma semilla produce la misma salida. Para el tutor de gramática (grammar tutor):
- El modelo se carga con
torch.manual_seed(42). - El desempate del bi-encoder de RAG se ordena por
chunk_id(determinista dada la semilla). - El decoder del modelo usa muestreo greedy (sin aleatoriedad top-k) para la ruta de la demo; el muestreador aleatorio se ejercita en un paso separado de la demo que dice explícitamente "el muestreo es no-determinista por diseño."
- Los payloads de petición de la demo están comiteados en
scripts/demo/payloads/; sin selección aleatoria. - Los timestamps de wall-clock difieren entre ejecuciones, pero los checks de corrección son estables.
El smoke test de CI hace diff del contenido de la respuesta (response.correction, response.explanation, response.spanish_translation) entre cinco ejecuciones consecutivas — identidad bit-a-bit requerida.
Lo que "determinista" deliberadamente NO significa¶
- No significa que las latencias sean idénticas bit-a-bit. La contención de CPU varía.
- No significa que los traces tengan los mismos span IDs (los span IDs son time/random).
- No significa que los contadores de Prometheus sean idénticos (acumulan de ejecuciones previas en uso interactivo de
just demo, aunque se resetean endemo-cold).
La distinción: el contenido es determinista; los metadatos no. El script de aserciones de la demo conoce la diferencia.
Propiedad 3 — Narrada¶
Cada paso imprime una línea. Dos tipos de líneas:
Líneas de currículo (para que el visor vea la columna vertebral):
[t=0.5s] [Phase 12] Loading verb corpus from DVC...
[t=1.1s] [Phase 11] BPE tokenizer ready (vocab=2048).
[t=2.3s] [Phase 28] Loading Mini-GPT base + LoRA grammar adapter (rev=sha:a1b2c3).
[t=2.4s] [Phase 22] KV-cache pre-allocated for 128 tokens.
[t=2.5s] [Phase 33] miniserve started on :8080.
[t=2.6s] [Phase 34] Cost emitter and OTel exporter armed.
Líneas de acción (para que el visor vea qué está pasando):
[t=3.1s] >>> POST /v1/grammar/correct {"sentence": "Yesterday I goed to the store"}
[t=3.4s] [Phase 11 tokenize] 12 ms → 10 tokens
[t=3.5s] [Phase 29 retrieve] 28 ms → 5 chunks (top: irregular-go-past)
[t=4.6s] [Phase 17 prefill] 1100 ms
[t=6.8s] [Phase 17 decode] 2200 ms → "Yesterday I **went** to the store."
[t=6.81s] [Phase 30 format] 4 ms
[t=6.82s] [Phase 34 cost] €0.00041
[t=6.83s] <<< 200 OK
La narración es lo que convierte un sistema que funciona en un artefacto de enseñanza. El §1 #4 (iv) del Plan dice que el script debe estar narrado — esto es lo que significa.
Propiedad 4 — Fallo ruidoso¶
El antipatrón (§5 #3 del Plan — "Demo theater"):
try:
response = client.post(...)
assert response.status_code == 200
except Exception:
print("Skipping this step") # MAL
Una demo que oculta errores con try/except: pass no es una demo. El script debe:
- En caso de fallo, imprimir qué fase / qué contrato se rompió.
- Imprimir los valores ofensivos (truncados para mantener la salida del terminal legible).
- Salir con código distinto de cero con un mensaje claro:
"FAIL: [Phase 33] miniserve did not respond on :8080 within 10s; check 'docker compose logs miniserve'".
La verificación del Plan: romper deliberadamente un componente (apuntar MLFLOW_TRACKING_URI a un puerto incorrecto), ejecutar la demo, confirmar que el mensaje de fallo nombra el componente y el paso de remediación. El lab recorre esta verificación.
Anatomía de scripts/demo/run.py¶
El script tiene siete bloques, en orden:
def main():
# Bloque 1 — Preflight (auto-checks de la Fase 39)
assert_environment_ready() # uv, docker, puertos libres, lockfile limpio
# Bloque 2 — Arranque del stack (delega a `just demo-cold`)
bring_up_stack(timeout_s=30)
wait_for_health() # /healthz en miniserve + Prometheus + Grafana
# Bloque 3 — Narración del currículo
narrate_loaded_components() # las líneas de arranque [Phase NN]
# Bloque 4 — Batería de peticiones happy-path (3 frases)
for sentence in HAPPY_PATH_PAYLOADS:
send_and_verify(sentence)
# Bloque 5 — Run-through de seguridad (3 replays de la Teoría 04)
replay_injection()
replay_oversized_body()
replay_mcp_sandbox()
# Bloque 6 — Aceptación (binaria)
run_acceptance_checks() # los ≤ 20 checks de DONE_ENOUGH.md
# Bloque 7 — Cierre
print_summary()
emit_eval_report() # escribe experiments/39-end-to-end/eval-YYYY-MM-DD.json
Cada bloque es una única función con un contrato claro. El Bloque 6 es la puerta de aceptación load-bearing: si alguno de los ≤ 20 checks binarios falla, el script sale con 1 y una lista enumerada de fallos.
Aceptación binaria: docs/DONE_ENOUGH.md¶
El DoD del capstone se operacionaliza como ≤ 20 acceptance checks binarios. Cada check tiene:
- Un id único (por ejemplo,
DE-007). - Un enunciado de una frase.
- Un check automatizado.
- Un pass/fail visible en la salida del terminal de la demo.
Filas de ejemplo:
| ID | Check | Automatización |
|---|---|---|
| DE-001 | El stack arranca en 30 s | Cronometrar docker compose up hasta healthy |
| DE-002 | miniserve responde en :8080 dentro de 5 s desde healthy |
curl :8080/healthz |
| DE-003 | La primera petición completa en 10 s extremo a extremo | aserción cronometrada en el script |
| DE-004 | La latencia p95 sobre la batería de 3 frases es < 5 s | query de Prometheus |
| DE-005 | Las 9 etapas emiten un span en el trace | query de Tempo |
| DE-006 | Panel de coste poblado en 60 s desde la primera petición | query de datos del panel de Grafana |
| DE-007 | La identidad de coste se mantiene en 0.1 % en cada petición | aserción por petición |
| DE-008 | Payload de inyección devuelve 400 con injection_blocked |
aserción sobre la respuesta |
| DE-009 | Body de 10 MB devuelve 413 antes de prefill | aserción sobre respuesta + log |
| DE-010 | El subprocess del sandbox MCP tiene CPU+RAM acotada, sale en ≤ 2 s | aserción sobre atributos de span |
| DE-011 | Conteo de spans huérfanos del trace = 0 sobre la ejecución de la demo | query de Tempo |
| DE-012 | Todos los SHA256 de MANIFEST.json comiteados coinciden con disco |
Lab 04 de la Fase 37 |
| DE-013 | El dashboard de Grafana importa limpio | curl -X POST /api/dashboards/db devuelve 200 |
| DE-014 | eval-YYYY-MM-DD.json escrito con la fecha en el nombre |
aserción sobre filesystem |
| DE-015 | La demo sale con estado 0 | echo $? |
(15 de ≤ 20; algunos huecos para ajustes en ejecución de fase.) El Lab 00 rellena los 5 restantes.
El script termina con una tabla de todos los DE checks, pass/fail. La última imagen del visor es esa tabla. Esa tabla es el boletín del currículo.
Lo que esta teoría NO cubre¶
- El contenido completo de
docs/DONE_ENOUGH.md. Se redacta en el Lab 00. - El
scripts/demo/run.pycompleto. Se redacta en el Lab 04. - La mecánica de la grabación con asciinema. Lab 04.
- Cómo extender la demo con nuevos payloads. Reading-list de la Fase 40.
- Demos multi-usuario / demos de load-test. El Lab 02 cubre el load test, pero es un comando separado, no la demo principal.
Fin de la cadena de teoría de la Fase 39. Siguiente: los 5 enunciados de lab (lab/00-cold-start-bringup.md a lab/04-demo-script.md).