English · Español
Lab 04 — El script de la demo: finalizar y grabar¶
El último lab del currículo. Aquí se cierra
scripts/demo/run.pycon sus siete bloques, se graba el cast de asciinema y se publica el "report card" final: la tabla DE-001…DE-020 con todos los checks en verde. Si esa tabla termina en verde, el currículo ha terminado.
Objetivo¶
Finalizar scripts/demo/run.py (Teoría 05 §anatomía). Grabar un cast de asciinema de una ejecución limpia. Comitear docs/demo-recording.md apuntando al cast. Verificar la tabla final de aceptación — los ≤ 20 DE checks en verde.
Por qué este lab es el último¶
Los Labs 00–03 construyeron y verificaron cada pieza. El Lab 04 cose la narración, graba el artefacto que un extraño ve, y firma el cierre de la fase.
Este es el lab donde el currículo se vuelve show-able. Tras este lab, Borja puede dar a alguien la URL del repo, decir "ejecuta just demo," y marcharse.
Entregables¶
scripts/demo/run.py— el script de siete bloques de la Teoría 05, completo y testeado.scripts/demo/recorder.py— envuelve la ejecución con asciinema; emiteexperiments/39-demo-script/cast-YYYY-MM-DD.cast.experiments/39-demo-script/transcript.jsonl— transcripción estructurada de la ejecución grabada (cada línea [Phase NN] como un evento JSON).experiments/39-demo-script/cast-YYYY-MM-DD.cast— la grabación de asciinema.docs/demo-recording.md— página markdown con el player de asciinema embebido + un resumen textual para accesibilidad.docs/README.md— el quickstart, refrescado:git clone && uv sync --frozen && just demo, una screenshot de la tabla final de aceptación.tests/integration/test_demo_end_to_end.py— test de integración final; ejecutajust demo-cold, afirma exit 0, todos los DE-checks pasan, transcripción bien formada.
Paso 1 — Finalizar scripts/demo/run.py¶
El esqueleto de siete bloques de la Teoría 05 §anatomía se completa. Cada bloque tiene un contrato:
| Bloque | Función | Contrato |
|---|---|---|
| 1 — Preflight | assert_environment_ready() |
Aborta con mensaje claro si uv, docker, puertos o lockfile no están listos |
| 2 — Arranque del stack | bring_up_stack() |
Delega a just demo-cold-up; hace polling de salud durante 30 s |
| 3 — Narración | narrate_loaded_components() |
Imprime las líneas de arranque [Phase NN] desde una tabla de config |
| 4 — Happy path | send_and_verify() × 3 |
Tres peticiones canónicas; cada una imprime tiempos etapa por etapa |
| 5 — Seguridad | replay_injection(), replay_oversized_body(), replay_mcp_sandbox() |
Tres replays del Lab 03 |
| 6 — Aceptación | run_acceptance_checks() |
Lee docs/DONE_ENOUGH.md, ejecuta cada check, imprime la tabla |
| 7 — Cierre | print_summary(), emit_eval_report() |
Coste total, p95, resumen de accuracy; escribe eval-YYYY-MM-DD.json |
El script completo tiene < 400 líneas. La mayor parte del trabajo se delega a helpers de labs previos; este script es el director de orquesta.
Narración como flujo de eventos estructurado¶
Cada línea impresa es también un evento JSON escrito a experiments/39-demo-script/transcript.jsonl. Formato:
{"t": 0.5, "phase": 12, "event": "load_corpus", "msg": "Loading verb corpus from DVC..."}
{"t": 1.1, "phase": 11, "event": "tokenizer_ready", "msg": "BPE tokenizer ready (vocab=2048).", "vocab_size": 2048}
{"t": 2.3, "phase": 28, "event": "model_loaded", "msg": "Loading Mini-GPT base + LoRA grammar adapter (rev=sha:a1b2c3).", "lora_rev": "sha:a1b2c3"}
...
Dos salidas de una sola llamada a print: el texto legible por humanos en el terminal y el evento legible por máquina en el JSONL. Esto permite al test de CI parsear la transcripción sin scrapear el terminal.
Un pequeño helper narrate() hace ambos:
def narrate(phase: int, event: str, msg: str, **fields):
t = time.time() - DEMO_START_T
print(f"[t={t:.1f}s] [Phase {phase:02d}] {msg}")
transcript.write_event({"t": t, "phase": phase, "event": event, "msg": msg, **fields})
Paso 2 — Renderer de tabla de aceptación¶
El Bloque 6 lee las 20 filas de docs/DONE_ENOUGH.md, ejecuta cada automatización, imprime la tabla:
=================================================================
Phase 39 — Acceptance Checks
=================================================================
| ID | Check | Pass |
|--------|-------------------------------------------------|------|
| DE-001 | Stack starts within 30 s | ✓ |
| DE-002 | miniserve responds on :8080 within 5 s | ✓ |
| DE-003 | First request completes within 10 s | ✓ |
| DE-004 | p95 latency over 3-sentence battery < 5 s | ✓ |
| ... | ... | ... |
| DE-020 | Demo exits with status 0 | ✓ |
=================================================================
Result: 20/20 passed.
=================================================================
Si algún check falla, el marcador de fila es ✗, la tabla va seguida de una sección # Failures: enumerando cada uno, y el script sale con 1.
Paso 3 — Grabación con asciinema¶
scripts/demo/recorder.py:
import subprocess, datetime
from pathlib import Path
def record_demo():
today = datetime.date.today().isoformat()
out_dir = Path("experiments/39-demo-script")
out_dir.mkdir(parents=True, exist_ok=True)
cast = out_dir / f"cast-{today}.cast"
cmd = [
"asciinema", "rec",
"--command", "just demo-cold",
"--idle-time-limit", "2",
"--title", f"Lynx Cortex — capstone demo {today}",
str(cast),
]
subprocess.run(cmd, check=True)
print(f"Recorded: {cast}")
Idle-time-limit de 2 s previene que el cast incluya pausas largas (carga de modelo, etc.) en tiempo real — la reproducción se siente con ritmo.
Ejecutar:
El archivo de cast es < 100 KB para una grabación de 90 segundos; suficientemente pequeño para comitear al repo.
Fallback: transcripción plana¶
Si asciinema no está disponible, el transcript.jsonl textual es el fallback. La página docs/demo-recording.md enlaza ambos: el player de asciinema (preferido) y un renderizado markdown del JSONL (accesible).
Paso 4 — docs/demo-recording.md¶
# Demo recording
This is the canonical recording of the Phase 39 capstone demo.
## Watch (asciinema)
<script id="asciicast-2026-06-XX" src="../experiments/39-demo-script/cast-2026-06-XX.cast" async></script>
## Read (transcript)
(Below is the human-readable rendering of `transcript.jsonl`. Times are wall-clock from the start of the recording.)
| t (s) | Phase | Event |
|---|---|---|
| 0.0 | 39 | demo_start |
| 0.5 | 12 | load_corpus |
| 1.1 | 11 | tokenizer_ready |
| ... | ... | ... |
| 87.3 | 39 | acceptance_table_printed (20/20 passed) |
| 88.1 | 39 | eval_report_emitted (experiments/39-end-to-end/eval-2026-06-XX.json) |
| 89.4 | 39 | demo_complete (exit=0) |
## Reproduce
La tabla de transcripción se auto-genera por scripts/demo/render_transcript.py desde el JSONL; comiteado en docs junto al cast.
Paso 5 — Refrescar docs/README.md¶
El quickstart de cara al visitante. Tres secciones:
# Lynx Cortex
A first-principles AI systems curriculum: 40 phases from a transistor to a deployed
grammar tutor that corrects English verb conjugations and provides Spanish
translations for 20 verbs × 5 tenses × 3 persons.
## 90-second demo
```
git clone https://github.com/borjatarraso/lynx-cortex
cd lynx-cortex
uv sync --frozen
just demo
```
You will see: the stack come up, three grammar corrections, three security defenses
fire, and a 20-row acceptance table all green. Total: ~90 seconds.
Recording: [docs/demo-recording.md](demo-recording.md)
## Curriculum
[40-phase roadmap](../ROADMAP.md). Each phase has a `theory/` and `lab/` directory
under `docs/phase-NN-*/`.
## Architecture
[docs/ARCHITECTURE.md](ARCHITECTURE.md) — C4 context + container + sequence diagrams.
## Definition of "done enough"
[docs/DONE_ENOUGH.md](DONE_ENOUGH.md) — the 20 binary checks the demo verifies on
every run.
La primera acción del README es just demo — la demo es el punto de entrada.
Paso 6 — Test de integración final¶
tests/integration/test_demo_end_to_end.py:
import json, subprocess
from pathlib import Path
def test_demo_runs_clean():
"""`just demo-cold` exits 0; all DE checks pass; transcript well-formed."""
result = subprocess.run(["just", "demo-cold"], capture_output=True, text=True, timeout=180)
assert result.returncode == 0, f"demo failed: {result.stdout[-2000:]}"
transcript = Path("experiments/39-demo-script/transcript.jsonl")
assert transcript.exists(), "transcript not written"
events = [json.loads(line) for line in transcript.read_text().splitlines() if line.strip()]
assert events[-1]["event"] == "demo_complete", "demo didn't reach completion event"
assert events[-1]["exit"] == 0, "demo exited non-zero"
# Verify acceptance table.
acceptance = next(e for e in events if e["event"] == "acceptance_table_printed")
assert acceptance["passed"] == acceptance["total"], \
f"acceptance failed: {acceptance['passed']}/{acceptance['total']}"
Este es el garante final. CI lo ejecuta; el PR debe pasar.
Paso 7 — Una grabación limpia¶
La grabación para docs/demo-recording.md debe ser de una ejecución limpia:
- Tirar abajo cualquier stack previo:
just demo-cold-down. - Limpiar artefactos cacheados que acelerarían falsamente la ejecución grabada:
rm -rf ~/.cache/lynx-cortex-warmup/. - Grabar:
uv run python scripts/demo/recorder.py. - Verificar la grabación:
asciinema play experiments/39-demo-script/cast-YYYY-MM-DD.cast. - Si hay un glitch visible (resize de terminal, pulsación accidental), re-grabar. El cast es un producto; entrega uno limpio.
Qué pinta tiene "hecho"¶
-
scripts/demo/run.pycompleto; siete bloques presentes y contract-tested. -
scripts/demo/recorder.pycompleto; produce un cast bajo demanda. -
experiments/39-demo-script/transcript.jsonlescrito limpiamente durante ejecuciones de demo. -
experiments/39-demo-script/cast-YYYY-MM-DD.castcomiteado. -
docs/demo-recording.mdescrito con tanto el embed del cast como la transcripción textual. -
docs/README.mdrefrescado con el quickstart de 90 segundos y el enlace a la grabación. -
tests/integration/test_demo_end_to_end.pypasa en CI. - La tabla de aceptación imprime 20/20 en verde en
just demo.
Trampas comunes¶
- "Funciona localmente; la grabación quedó limpia." La grabación debe ser desde
just demo-cold, nojust demo(que deja estado previo). Estado limpio = resultado repetible. - Embeber el cast inline en README.md. El README lo renderizan muchas herramientas; solo mkdocs maneja bien las tags
<script>. Pon el embed endocs/demo-recording.md(que es markdown renderizado por mkdocs); enlaza desde el README. - Olvidar la transcripción textual. El cast es genial para aprendices visuales; algunos espectadores (lectores de pantalla, indexadores de búsqueda, futuro-tú grepeando git log) necesitan texto. Ambos.
- Dejar que
narrate()imprima sin escribir a la transcripción. Unprintde debug en medio de la demo no aparece en el JSONL; el CI parsea solo el JSONL y puede no capturar una regresión. Toda la narración pasa por el helper. - Saltarse la verificación de "loud failure" del Lab 03 en la grabación. Opcional — la demo limpia es para el espectáculo; la verificación de loud-failure se captura en el log de experimento del Lab 03. Mantenerlos separados.
Fin de la secuencia de labs de la Fase 39. Siguiente:
- Abrir la Fase 39 vía
/phase-start 39. - Recorrer Labs 00 → 04 en orden.
- Escribir
PHASE_39_REPORT.md(la reflexión del capstone; estructurada segúnLYNX_CORTEX.md§7.6). - Después
/phase-start 40— el postmortem.