diff --git a/docs/build-content.py b/docs/build-content.py
index c106a6cc9..4f7a252a6 100644
--- a/docs/build-content.py
+++ b/docs/build-content.py
@@ -43,7 +43,7 @@
"features/": ("2026-10-21", "21f5c8dddb47df96"),
"manual/": ("2026-10-21", "0f7e605dc087544d"),
"pitch/": ("2026-10-21", "7cfe9803d7d11df5"),
- "demo/": ("2026-10-21", "317b8850ecc3c5de"),
+ "demo/": ("2026-10-22", "9dae5fe7a3dbf01b"),
"guides/": ("2026-10-21", "888b965163695896"),
"guides/build-and-verify-a-web-app/": ("2026-10-21", "0b0d7ae775899b60"),
"guides/red-team-a-web-app/": ("2026-10-21", "3af532e9ff5592f3"),
diff --git a/docs/demo/README.md b/docs/demo/README.md
index b8378fe9a..fc36c793e 100644
--- a/docs/demo/README.md
+++ b/docs/demo/README.md
@@ -1,22 +1,28 @@
-# h5i demo: Build → Verify → Red-team → Regression
+# h5i demo: Build → Verify → Test the running app → Regression
-The 60-second film is a deterministic HTML timeline, rendered to MP4. It keeps
+The 80-second film is a deterministic HTML timeline, rendered to MP4. It keeps
the opening question and the shell, console, editor, and browser windows. The
agent follows one board application through four stages.
| Time | Beat |
| --- | --- |
| 0:00–0:07 | Opening question and the Build and Verify Your Web Apps title card. |
-| 0:07–0:13 | Build: the agent writes the board's Rust kernel. |
-| 0:13–0:29 | Verify: extract to Lean 4, show `authorized`, run proof and the configured mutation, then reveal the kernel proof boundary. |
-| 0:29–0:49 | Red-team: open the same board in a browser, replay Alice's request with Bob's post ID, and record the adapter finding. |
-| 0:49–0:56 | Regression: fix the adapter's visibility check and replay a YAML test; Alice gets 200 and Bob's private post returns 403. |
-| 0:56–1:00 | Close: Build and Verify Your Web Apps. with h5i. The Verifiable Web Framework for Rust. |
+| 0:07–0:19 | Build: a large editor shows the board's Rust delete permission check. |
+| 0:19–0:28 | Extract: Charon and Aeneas visibly generate the Lean 4 author and moderator branches from Rust, then the full generated definition appears. |
+| 0:28–0:42 | Verify: read the `authorized` theorem, its `writes_of` case split, and the scripted Lean check result. |
+| 0:42–0:52 | Explore: the theorem dependency graph follows the real Lean lemmas, then states the kernel write proof boundary. |
+| 0:52–1:09 | Test the running board end to end with Development Infrastructure. Its red-teaming tool reveals the HTTP read flaw. |
+| 1:09–1:16 | Regression: fix the adapter's visibility check and replay a YAML test; Alice gets 200 and Bob's private post returns 403. |
+| 1:16–1:20 | Close: Build and Verify Your Web Apps. with h5i. The Verifiable Web Framework for Rust. |
The Rust kernel and Lean theorem names shown are drawn from
`examples/app/board`. Its `authorized` theorem concerns successful **kernel
-writes**. The private-post `GET /api/posts/{id}` route, its IDOR, and the
-adapter fix are an **illustrative scenario** for this film; that route is not
+writes**. The dependency graph shows only dependencies present in the board's
+Lean proof. `h5i app check examples/app/board` records `delete_spec → writes_of`,
+`writes_of → authorized`, and `other_mod → authorized` in its declaration
+catalog; the film renders that small, fixed subset. The private-post
+`GET /api/posts/{id}` route, its IDOR, and the adapter fix are an
+**illustrative scenario** for this film; that route is not
present in the checked-in board server, and no live finding or regression run
is claimed. The UI labels the scenario. This distinction matters: a proof of
kernel writes cannot establish the behavior of a separate HTTP read route.
@@ -39,7 +45,7 @@ Rendering needs ffmpeg, Playwright, and Chromium:
```bash
node render.mjs --scale 1 --out-height 1080 --fps 24 --crf 20 --preset veryfast
-node render.mjs --stills 10,26,37,43,52 --scale 1
+node render.mjs --stills 14,20,22,25,34,45,50,64,73 --scale 1
```
The page is fingerprinted by `docs/build-content.py`; changes require an
diff --git a/docs/demo/index.html b/docs/demo/index.html
index 40f6ae940..ab4054dab 100644
--- a/docs/demo/index.html
+++ b/docs/demo/index.html
@@ -3,32 +3,32 @@
if p.author == user ||
+ is_moderator(&s.moderators, user) {
+ Ok((one(Write::DelPost(id)),
+ Reply::Done))
+} else {
+ Err(Error::Forbidden)
+}
+
Charon→Aeneas
+
Generated · BoardKernel.lean
if p.author = userthen let v ← one (Write.DelPost id) …else let b ← is_moderator s.moderators user if b … else ok (core.result.Result.Err Error.Forbidden)
/* the camera: a framing per beat, hard cuts between. The desk is drawn at stage size, so 1 is "see everything";
a zoom pushes toward the console and leaves most of the shell in frame. */
const shots = [
- {a:T.ws, b:T.tEvidence, from:{x:0,y:0,s:1}, to:{x:0,y:0,s:1}},
+ {a:T.ws, b:T.graph, from:{x:0,y:0,s:1}, to:{x:0,y:0,s:1}},
+ {a:T.graph, b:T.tEvidence, from:{x:0,y:0,s:1}, to:{x:0,y:0,s:1}},
{a:T.tEvidence, b:T.hack, from:{x:-70,y:40,s:1.1}, to:{x:-70,y:40,s:1.1}},
{a:T.hack, b:T.tFindings, from:{x:0,y:0,s:1}, to:{x:0,y:0,s:1}},
{a:T.tFindings, b:T.tBoxes, from:{x:-70,y:40,s:1.1}, to:{x:-70,y:40,s:1.1}},
@@ -545,15 +613,14 @@
Private postssigned in as Alice
/* the agent's shell. Outputs are scripted, not a live scan or build. */
const term = [
{at:T.write+.2, out:[`✎ writing examples/app/board/kernel/src/lib.rs`]},
- {at:T.write+2.6, out:[`✎ delete: only author or moderator may remove a post`]},
+ {at:T.write+4.0, out:[`✎ delete: only author or moderator may remove a post`]},
{at:T.extract+.2, cmd:'h5i app extract examples/app/board', cps:70},
{at:T.extract+1.3, out:[`Charon + Aeneas · Rust kernel → generated Lean 4`]},
{at:T.prove+.2, out:[`✎ theorem authorized: successful writes follow the policy`]},
- {at:T.prove+2.0, out:[`✎ proofs/Theorems.lean checks the extracted kernel`]},
+ {at:T.prove+2.0, out:[`✎ writes_of splits six command cases; authorized checks each write`]},
{at:T.verify+.2, cmd:'h5i app prove examples/app/board', cps:70},
{at:T.verify+1.4, out:[`Lean 4 passed · authorized · inv_preserved · transition_total`]},
- {at:T.verify+2.1, cmd:'h5i app mutate examples/app/board', cps:70},
- {at:T.verify+3.5, out:[`1/1 configured kernel mutation caught by proofs`]},
+ {at:T.graph+.4, out:[`authorized uses writes_of and other_mod`]},
{at:T.hack+.3, clear:true},
{at:F+.5, cmd:'h5i browser open http://board.local/posts', cps:74},
{at:F+1.7, out:[`✔ session audit-42 · signed in as Alice`]},
@@ -589,6 +656,7 @@
Private postssigned in as Alice
/* the pointer: moves to a tab, clicks it */
const clicks = [
{at:T.tApps, target:'ri-apps'},
+ {at:T.graph, target:'g-authorized'},
{at:T.tEvidence, target:'tab-evidence'},
{at:T.hack, target:'ri-sessions'},
{at:T.tFindings, target:'tab-findings'},
@@ -601,8 +669,8 @@
Private postssigned in as Alice
}
function drawPointer(t) {
const p = $('ptr');
- if (t < T.tEvidence - 1.2 || t >= T.close) { p.style.opacity = 0; return; }
- let from = {x: 1500, y: 470}, to = from, moveAt = T.tEvidence - 1.2, clickAt = Infinity;
+ if (t < T.graph - 1.2 || t >= T.close) { p.style.opacity = 0; return; }
+ let from = {x: 1500, y: 470}, to = from, moveAt = T.graph - 1.2, clickAt = Infinity;
for (let i = 0; i < clicks.length; i++) {
const c = clicks[i]; const start = c.at - .9;
if (t >= start) { from = i ? centerOf(clicks[i-1].target) : from; to = centerOf(c.target); moveAt = start; clickAt = c.at; }
@@ -653,16 +721,32 @@
Private postssigned in as Alice
$('tab-theorems').classList.toggle('cur', atab === 'theorems'); $('tab-evidence').classList.toggle('cur', atab === 'evidence');
on('p-theorems', atab === 'theorems'); on('p-evidence', atab === 'evidence');
[T.verify+1.4, T.verify+1.8, T.verify+2.2].forEach((at, i) => { const r = $('th'+(i+1)); r.classList.toggle('proven', t >= at); });
- const rcpt = t >= T.verify + 3.5;
+ const rcpt = t >= T.verify + 1.4;
vis('pl-norcpt', !rcpt); vis('pl-rcpt', rcpt); $('n-rcpt').textContent = rcpt ? 1 : 0;
- [1,2,3,4,5].forEach(i => on('ev'+i, t >= T.tEvidence + .3 + i*.22));
+ [1,2,3,4].forEach(i => on('ev'+i, t >= T.tEvidence + .3 + i*.22));
// the editor: the agent writes Rust, Aeneas writes Lean, the agent writes the proof
- const etab = t >= T.prove ? 'thm' : t >= T.extract + 1.2 ? 'lean' : 'rs';
+ const etab = t >= T.prove ? 'thm' : t >= T.extract + 4.2 ? 'lean' : 'rs';
$('et-rs').classList.toggle('cur', etab === 'rs'); $('et-lean').classList.toggle('cur', etab === 'lean'); $('et-thm').classList.toggle('cur', etab === 'thm');
on('ed-rs', etab === 'rs'); on('ed-lean', etab === 'lean'); on('ed-thm', etab === 'thm');
- for (let i = 0; i < 14; i++) on('rs'+i, t >= T.write + .4 + i*.22);
- for (let i = 0; i < 5; i++) on('le'+i, t >= T.extract + 1.5 + i*.25);
- for (let i = 0; i < 8; i++) on('tl'+i, t >= T.prove + .4 + i*.6);
+ for (let i = 0; i < 14; i++) on('rs'+i, t >= T.write + .4 + i*.35);
+ for (let i = 0; i < 17; i++) on('le'+i, t >= T.extract + 1.5 + i*.26);
+ for (let i = 0; i < 8; i++) on('tl'+i, t >= T.prove + .4 + i*.7);
+ // Charon and Aeneas turn the visible Rust branch into Lean, frame by frame.
+ const ex = $('extract-view'), progress = ramp(t, T.extract+.3, 3.3);
+ ex.style.opacity = t < T.extract || t >= T.extract+4.7 ? 0 :
+ eo(ramp(t,T.extract,.4)) * (1-ramp(t,T.extract+4.1,.6));
+ $('extract-fill').style.width = `${progress*100}%`;
+ $('extract-done').textContent = progress >= 1 ? 'BoardKernel.lean generated' : 'generating…';
+ $('extract-arrow').style.transform = `translateX(${Math.sin(t*9)*5}px)`;
+ ex.querySelectorAll('.extract-line').forEach((line,i) => {
+ const q = eo(ramp(t,T.extract+.75+i*.27,.45));
+ line.style.opacity = q;
+ line.style.transform = `translateX(${(1-q)*-12}px)`;
+ });
+ $('proof-status').style.opacity = t >= T.verify+1.4 && t < T.graph ? eo(ramp(t,T.verify+1.4,.5)) : 0;
+ for (const id of ['rs5','rs6']) $(id).classList.toggle('focus', t >= T.write+5 && t < T.extract);
+ for (const id of ['le8','le13','le14']) $(id).classList.toggle('focus', t >= T.extract+5 && t < T.prove);
+ $('tl2').classList.toggle('focus', t >= T.prove+5 && t < T.graph);
}
/* dust: a seeded field of motes drifting upward, parallaxed against the camera */
@@ -698,16 +782,17 @@
Private postssigned in as Alice
/* the one line of narration, as a lower third */
const caps = [
- {at:T.write+.6, html:`Build the board's Rust kernel and HTTP app.`},
- {at:T.extract+.6, html:`Verify the extracted kernel with Lean 4.`},
- {at:T.prove+.6, html:`Theorem authorized covers successful kernel writes.`},
- {at:T.verify+.6, html:`Lean checks the proof; mutations test whether it detects changed logic.`},
- {at:T.tEvidence+.6, html:`Verified kernel: Lean 4 proves authorization of kernel writes.`},
- {at:T.hack+.6, html:`Unverified HTTP adapter: red-team the same app's read route.`},
- {at:F+5.8, html:`Replay Alice's request with Bob's post ID.`},
- {at:T.tFindings+.6, html:`Finding: red-teaming exposes a read route that bypasses kernel policy.`},
- {at:T.deny+.3, html:`Fix the adapter's visibility check, then replay the request.`},
- {at:T.tBoxes+.6, html:`Regression: Alice gets 200; Bob's private post returns 403.`},
+ {at:T.write+.6, html:`Only authors or moderators can delete posts.`},
+ {at:T.extract+.6, html:`Rust is extracted into Lean 4.`},
+ {at:T.prove+.6, html:`authorized covers successful kernel writes.`},
+ {at:T.verify+.6, html:`Lean checks the proof.`},
+ {at:T.graph+.6, html:`Follow the lemmas behind authorized.`},
+ {at:T.tEvidence+.6, html:`The proof covers kernel writes.`},
+ {at:T.hack+.6, html:`Development Infrastructure tests the app end to end.`},
+ {at:F+5.8, html:`Its red-teaming tool replays Alice's read for Bob's post.`},
+ {at:T.tFindings+.6, html:`The HTTP read route exposes Bob's private post.`},
+ {at:T.deny+.3, html:`Fix the route and save the replay as a test.`},
+ {at:T.tBoxes+.6, html:`The regression passes. Alice gets 200; Bob gets 403.`},
];
function renderCap(t) {
let i = -1; for (let k = 0; k < caps.length; k++) if (t >= caps[k].at) i = k;
@@ -756,12 +841,15 @@