Skip to content

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 demo se 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 con try/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:

  1. Si just demo sale 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.
  2. El script es el punto de entrada para nuevos contribuidores. Un desconocido leyendo el repo puede ejecutar just demo primero, 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.
  3. 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:

  1. docker compose down -v --remove-orphans antes de cualquier otra cosa.
  2. Elimina cualquier directorio local experiments/39-*-tmp/.
  3. Levanta el stack desde cero.
  4. Ejecuta la demo.
  5. 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 en demo-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:

  1. En caso de fallo, imprimir qué fase / qué contrato se rompió.
  2. Imprimir los valores ofensivos (truncados para mantener la salida del terminal legible).
  3. 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.py completo. 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).