Skip to content

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.py con 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; emite experiments/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.mdel 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; ejecuta just 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:

$ uv run python scripts/demo/recorder.py

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
git clone https://github.com/borjatarraso/lynx-cortex cd lynx-cortex uv sync --frozen just demo
For details, see the [Phase 39 capstone overview](phase-39-capstone/README.md).

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:

  1. Tirar abajo cualquier stack previo: just demo-cold-down.
  2. Limpiar artefactos cacheados que acelerarían falsamente la ejecución grabada: rm -rf ~/.cache/lynx-cortex-warmup/.
  3. Grabar: uv run python scripts/demo/recorder.py.
  4. Verificar la grabación: asciinema play experiments/39-demo-script/cast-YYYY-MM-DD.cast.
  5. 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.py completo; siete bloques presentes y contract-tested.
  • scripts/demo/recorder.py completo; produce un cast bajo demanda.
  • experiments/39-demo-script/transcript.jsonl escrito limpiamente durante ejecuciones de demo.
  • experiments/39-demo-script/cast-YYYY-MM-DD.cast comiteado.
  • docs/demo-recording.md escrito con tanto el embed del cast como la transcripción textual.
  • docs/README.md refrescado con el quickstart de 90 segundos y el enlace a la grabación.
  • tests/integration/test_demo_end_to_end.py pasa en CI.
  • La tabla de aceptación imprime 20/20 en verde en just demo.

Trampas comunes

  1. "Funciona localmente; la grabación quedó limpia." La grabación debe ser desde just demo-cold, no just demo (que deja estado previo). Estado limpio = resultado repetible.
  2. Embeber el cast inline en README.md. El README lo renderizan muchas herramientas; solo mkdocs maneja bien las tags <script>. Pon el embed en docs/demo-recording.md (que es markdown renderizado por mkdocs); enlaza desde el README.
  3. 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.
  4. Dejar que narrate() imprima sin escribir a la transcripción. Un print de 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.
  5. 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ún LYNX_CORTEX.md §7.6).
  • Después /phase-start 40 — el postmortem.