Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .tours/01-primary-user-flow.tour
Original file line number Diff line number Diff line change
Expand Up @@ -82,21 +82,21 @@
},
{
"file": "src/Walkthrough.jsx",
"line": 94,
"line": 101,
"pattern": "export const Walkthrough = ",
"title": "12 — One frame at a time, as a pure function",
"description": "Remotion calls this once per frame number. Which step is active, how far the camera has eased, where the pointer has glided, how full the progress bar is — all computed from the frame number alone. No state, no effects, no fetching. That purity is what makes a render reproducible and lets the opening-frame probe compare frame 0 with frame 4."
},
{
"file": "src/Walkthrough.jsx",
"line": 172,
"line": 181,
"pattern": "opacity: prevImg \\? fadeIn : 1",
"title": "13 — The one guard not to simplify away",
"description": "`opacity: prevImg ? fadeIn : 1`. The fade is a CROSS-fade and only means anything with the previous step's still underneath. Step 0 has no previous step, so an unguarded ramp faded the first frame up from this container's white — every clip opened on a 0.37 second flash and a looping README GIF re-flashed every loop. That is defect D2. `npm run probe:opening` exists to keep it fixed."
},
{
"file": "src/Walkthrough.jsx",
"line": 188,
"line": 197,
"pattern": "width: WT_W \\* progress",
"title": "14 — And out the other side",
"description": "Caption lower-third above, progress bar here, and the frame is done. `remotion render` collects these into an MP4; the README's ffmpeg palette command turns that into the GIF you paste into a README. You have now followed the whole path."
Expand Down
42 changes: 42 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1381,6 +1381,48 @@ Re-capturing requires an explicitly selected current spec ID in `COLLAB_ONLY` an
its intended application running; a bare capture command is refused. Render: `node run-remotion.mjs render src/index.js WTC-NRsolo`
/ `WTC-NRsync` / `WTC-NRfresh` / `WTC-NRdeepDive`.

## Real-world example: Node Foyer (fail-closed asserts, FOYER-V3 R1)

[Node Foyer](https://github.com/HomenShum/node-foyer) is a portfolio wall that probes each
hosted product's own public files and shows one honest state per repo. Its three walkthroughs
(`walkthrough.foyer.specs.mjs`, captured by `walkthrough.foyer.mjs`) go one step past every
other spec in this repo: **every `cap` op carries an `assert`** — checked against the live DOM
immediately before the screenshot, not after — so a capture that would have shown a wrong or
stale state aborts instead of shipping (fail-closed, same `zz-fail.png` contract as
`walkthrough.mjs`, applied to a *claim* rather than only to a crash). Each capture also reads the
wall's own `foyer-build-sha` at the first and last frame and discards the run if the served build
moved mid-capture — see `walkthrough.foyer.mjs`'s `assertHolds` and `freshBuildSha`.

<details><summary><b>Node Foyer · the wall</b> (production, 1440x900)</summary>

<img src="assets/feature-foyer-FYwall.gif" alt="Node Foyer's wall: the header's provenance line, all 22 cards, a verified card (NodeVoice) beside a reachable-only card (NodeProof), a stable-sweeps count, the NodeRoom apparatus on hover (probe URL, HTTP status, sha256), the dead fixture staying UNKNOWN with its tried URL, and the Foyer's own card verified on both its frontend and backend layers" width="720">

Judge: `fix-then-publish`, 20/22 (`gemini-3.6-flash`).

</details>

<details><summary><b>Node Foyer · the phone sheet</b> (production, 390x844)</summary>

<img src="assets/feature-foyer-FYphone.gif" alt="Node Foyer at phone width: a soft-hyphenated long product name, the Details button opening a real dialog with the wall inert behind it, a 44px Open target, and Close returning focus to Details" width="390">

Judge: `fix-then-publish`, 22/22 (`gemini-3.6-flash`).

</details>

<details><summary><b>Node Foyer · the honest fallback</b> (built preview, no ledger URL)</summary>

<img src="assets/feature-foyer-FYagent.gif" alt="Node Foyer built with no VITE_CONVEX_URL: the wall root's own data-foyer-snapshot-source attribute says file, then the same build's /.well-known/agent-ui.json and /api/apps.json contract files rendered raw" width="720">

Judge: `rework` (2 of 3 sampled runs; scores 13/8/6 out of 22 — this repo's own documented judge
variance, see "The instrument is noisy" above). The honest defect underneath the noise is real
and repeats across runs: a `goto` between the wall and a raw JSON response has no in-app element
to click, so `cursor_truth`/`state_coverage` score low every time. Fabricating a click here would
violate STORYBOARD.md's own rule against claiming an interaction the frame does not show; the
judge's own suggestion — a terminal/curl panel showing the actual fetch — is a real fix for a
follow-up round, not this one.

</details>

## Designing for specific stacks

What's worth *showing* in a walkthrough differs by architecture — a single-cursor
Expand Down
Binary file added assets/feature-foyer-FYagent.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/feature-foyer-FYphone.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/feature-foyer-FYwall.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 6 additions & 6 deletions docs/START_HERE.md
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ emits something that passes for evidence.** The same reasoning added the

**File:** `iterate.mjs`
**Symbol:** the top-level round loop — `iterate.mjs:62` (`for (let r = 1; r <= rounds`)
**Called by:** `npm run iterate` — `package.json:43` (`"iterate": "node iterate.mjs"`) — and
**Called by:** `npm run iterate` — `package.json:45` (`"iterate": "node iterate.mjs"`) — and
nothing else. No script and no other file in this repository spawns it.
**Calls next:** `iterate.mjs:72` (`judge-rubric.mjs`) → Google Gemini

Expand Down Expand Up @@ -285,7 +285,7 @@ contain `_`.
**Output** — a composition registry the Remotion CLI and studio read.
**Failure behavior** — a composition whose steps array is empty still registers, with
`durationInFrames` clamped to 1 by the `Math.max(1, …)`; the renderer paints a blank
frame rather than crashing — `src/Walkthrough.jsx:97` (`if (!steps.length) return`).
frame rather than crashing — `src/Walkthrough.jsx:104` (`if (!steps.length) return`).
**Next** — Step 6, the file this registry reads.

---
Expand Down Expand Up @@ -331,9 +331,9 @@ its spoken narration. That is the only other thing that edits generated data.
## Step 7 — Rendering: one function turns a step list into every frame

**File:** `src/Walkthrough.jsx`
**Symbol:** `Walkthrough` — `src/Walkthrough.jsx:94` (`export const Walkthrough`)
**Symbol:** `Walkthrough` — `src/Walkthrough.jsx:101` (`export const Walkthrough`)
**Called by:** Remotion, once per frame, via the `Composition` in Step 5
**Calls next:** `src/Walkthrough.jsx:49` (`const burstFrame`), `src/Walkthrough.jsx:34` (`const camTarget`), plus `Pointer` and `Ripple`
**Calls next:** `src/Walkthrough.jsx:56` (`const burstFrame`), `src/Walkthrough.jsx:41` (`const camTarget`), plus `Pointer` and `Ripple`

**Why this exists**
This is the whole visual language of the product in one component: which captured
Expand All @@ -356,9 +356,9 @@ export const Walkthrough = ({ wt }) => {

**Input** — one walkthrough object as the `wt` prop.
**Output** — the JSX for exactly one frame.
**Failure behavior** — an empty step list returns a plain dark frame — `src/Walkthrough.jsx:97` (`if (!steps.length) return`) — instead
**Failure behavior** — an empty step list returns a plain dark frame — `src/Walkthrough.jsx:104` (`if (!steps.length) return`) — instead
of throwing. A missing PNG surfaces as a Remotion asset error naming the file.
**The one bug fixed here that you must not undo** — `src/Walkthrough.jsx:172` (`opacity: prevImg ? fadeIn : 1`): the still is drawn with
**The one bug fixed here that you must not undo** — `src/Walkthrough.jsx:181` (`opacity: prevImg ? fadeIn : 1`): the still is drawn with
`opacity: prevImg ? fadeIn : 1`. The fade is a *cross*-fade and only means anything
with the previous step underneath. On step 0 there is no previous step, so an
unguarded ramp faded the first frame up from the container's white — every clip opened
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@
"capture:roomos": "node walkthrough.roomos.mjs",
"capture:visual": "node walkthrough.visual.mjs",
"capture:solo": "node walkthrough.solo-founder.mjs",
"capture:foyer": "node walkthrough.foyer.mjs",
"studio": "node run-remotion.mjs studio src/index.js",
"studio:foyer": "node run-remotion.mjs studio src/foyer-index.js",
"studio:roomos": "node run-remotion.mjs studio src/roomos-index.js",
"render": "node run-remotion.mjs render src/index.js",
"render:example": "node run-remotion.mjs render src/index.js WT-NodeRoom out/example.mp4 --concurrency=2",
Expand Down
Binary file added public/wt/FYagent/00.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/wt/FYagent/01.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/wt/FYagent/02.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
44 changes: 44 additions & 0 deletions public/wt/FYagent/capture.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
{
"id": "FYagent",
"repo": "node-foyer",
"title": "Node Foyer — the honest fallback",
"demoUrl": "http://127.0.0.1:5270/",
"captureKind": "preview",
"capturedAt": "2026-09-13T01:39:33.590Z",
"captureBuildSha": "8a0fedd48d04e582a094f10f53f4fd49c48a7c31",
"capturePromoted": false,
"snapshot": {
"source": "file",
"sha256": "a122eb091348d68bb014c84056778c466f57877e67bba95fd519f58cf4ac3df0",
"generatedAt": "2026-09-12T22:29:43.131Z"
},
"storyboard": {
"premise": "The ledger URL an agent would read the wall from is unset in this build — no dev, no prod Convex.",
"question": "Does the wall degrade to the committed snapshot file honestly, or does it hang, blank, or lie about where its data came from?",
"axis": "ledger-backed snapshot (production) vs file-fallback snapshot (this preview, no ledger URL at all)",
"conflict": "no VITE_CONVEX_URL — the exact condition an agent hits before any ledger is configured",
"evidence": "the wall root's own data-foyer-snapshot-source=\"file\" attribute, and the same two machine-readable contract files production serves",
"verdict": "the wall renders the committed snapshot and says so on its own root node; /.well-known/agent-ui.json and /api/apps.json are still served from this build, unchanged",
"exit": "an agent with no ledger configured gets a working wall and an honest source attribute — never a blank screen or a silent lie"
},
"frames": [
{
"path": "wt/FYagent/00.png",
"sha256": "c86b8a0bcfeb059c3c4516c2ba89b041df63948f4be09adf185af8a831168622",
"caption": "Picture an agent-workspace harness opening this build with no ledger URL configured: the wall falls back to the committed snapshot file, and says so on its own root node — not a blank screen, not a silent lie.",
"asserted": "testid:foyer-wall[data-foyer-snapshot-source]=\"file\""
},
{
"path": "wt/FYagent/01.png",
"sha256": "09b801426a393b96bce83bd9125dd43b0fec9d0aff2354287505572e15146ec6",
"caption": "That agent fetches this fixed URL directly — no browser click, no ledger: the same build still serves its machine-readable contract file.",
"asserted": "css:pre text matches"
},
{
"path": "wt/FYagent/02.png",
"sha256": "1b662b828e986e6d02f3d2709c24dfe8cac3876af702c796b6072eca9ab2a842",
"caption": "And the plain apps.json an agent-workspace harness already knows how to read, from the exact same build.",
"asserted": "css:pre text matches"
}
]
}
Binary file added public/wt/FYphone/00.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/wt/FYphone/01.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/wt/FYphone/02.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/wt/FYphone/03.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/wt/FYphone/04.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
56 changes: 56 additions & 0 deletions public/wt/FYphone/capture.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
{
"id": "FYphone",
"repo": "node-foyer",
"title": "Node Foyer — the phone sheet",
"demoUrl": "https://node-foyer.vercel.app/",
"captureKind": "production",
"capturedAt": "2026-09-13T01:37:32.139Z",
"captureBuildSha": "ec2a44e19b304ab4ca2aa181f8722908a0329b5a",
"capturePromoted": false,
"snapshot": {
"source": "ledger",
"sha256": "274ab988a64bfede8dc6e430018b3d5db82312927541f12d5e5327ea17a2c0bf",
"generatedAt": "2026-09-13T01:12:41.554Z"
},
"storyboard": {
"premise": "At phone width there is no hover, so the same apparatus a desktop reviewer sees on mouseover has to become a real, tappable dialog.",
"question": "Is the bottom sheet an actual modal — focus trapped, background inert, closes cleanly — or a menu that only looks like one?",
"axis": "desktop hover apparatus vs mobile tap-opened sheet: same ApparatusBody, two entry points",
"conflict": "a long product name (NodeBenchBoilerplate) has to wrap without breaking mid-word inside a 390px card",
"evidence": "the sheet's role=\"dialog\", its backdrop over the now-inert wall, the same [data-probe-row] apparatus, and a 44px Open target",
"verdict": "the name soft-hyphenates cleanly, Details opens a real dialog with the wall inert behind it, Open stays a full 44px target, and Close returns focus to Details",
"exit": "the mobile sheet is not a stripped-down view — it is the same evidence, reachable by tap instead of hover"
},
"frames": [
{
"path": "wt/FYphone/00.png",
"sha256": "42dab25cb328790de3b7a5bbca1ab5ea6a74c253ef63793507f1e903cc8f63c8",
"caption": "Long product names soft-hyphenate instead of breaking mid-word, even in a 390px card.",
"asserted": "css:[data-testid=\"foyer-card-NodeBenchBoilerplate\"] .foyer-card__name[aria-label]=\"NodeBenchBoilerplate\""
},
{
"path": "wt/FYphone/01.png",
"sha256": "8dc1c06dd1bc03a3ca03af626febc62efe8595b4fc9ef9289ca5bab76cea7217",
"caption": "No hover on a phone — Details is the real, tappable path to the same apparatus.",
"asserted": "css:[data-testid=\"foyer-card-NodeRoom\"] button.foyer-details-btn visible"
},
{
"path": "wt/FYphone/02.png",
"sha256": "57e780a2702c65b25ecf2e8080e6d92b05a74b5093af44bee5c11e4496d2639c",
"caption": "The sheet opens as a real dialog over a backdrop — the wall behind it is now inert, not just visually dimmed.",
"asserted": "css:.foyer-sheet-backdrop visible"
},
{
"path": "wt/FYphone/03.png",
"sha256": "e520c45e0cde011aafd5704ab1d56da0cfcbf8bb4fb7272fba7ce1cfca5fded0",
"caption": "Open stays a full 44px target on the card face, never a text sliver.",
"asserted": "css:[data-testid=\"foyer-card-NodeRoom\"] a[href] visible"
},
{
"path": "wt/FYphone/04.png",
"sha256": "e520c45e0cde011aafd5704ab1d56da0cfcbf8bb4fb7272fba7ce1cfca5fded0",
"caption": "Close returned focus to Details — nothing is left stranded on body.",
"asserted": "css:[data-testid=\"foyer-card-NodeRoom\"] button.foyer-details-btn focused"
}
]
}
Binary file added public/wt/FYwall/00.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/wt/FYwall/01.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/wt/FYwall/02.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/wt/FYwall/03.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/wt/FYwall/04.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/wt/FYwall/05.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/wt/FYwall/06.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
68 changes: 68 additions & 0 deletions public/wt/FYwall/capture.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
{
"id": "FYwall",
"repo": "node-foyer",
"title": "Node Foyer — the wall",
"demoUrl": "https://node-foyer.vercel.app/",
"captureKind": "production",
"capturedAt": "2026-09-13T01:37:26.530Z",
"captureBuildSha": "ec2a44e19b304ab4ca2aa181f8722908a0329b5a",
"capturePromoted": false,
"snapshot": {
"source": "ledger",
"sha256": "274ab988a64bfede8dc6e430018b3d5db82312927541f12d5e5327ea17a2c0bf",
"generatedAt": "2026-09-13T01:12:41.554Z"
},
"storyboard": {
"premise": "Node Foyer claims to read every product's own public files, live, on every sweep, and never invent a state.",
"question": "Does the grid actually reflect what each product currently serves, and does the apparatus behind every colour hold a real probe?",
"axis": "verified (two layers agree) vs reachable-only (answered, nothing to compare) vs unknown (never answered)",
"conflict": "a fixture adapter (__fixture_dead) wired to a URL that always fails, run on every sweep so the honest-failure path is exercised, not assumed",
"evidence": "hover apparatus: probe URL, HTTP status, fetch time, remote Date header, sha256, the per-layer match line, and the stated reason",
"verdict": "22 cards, one honest state each; the Foyer's own card is verified on both its frontend and backend layers; the dead fixture stays UNKNOWN with its tried URL on screen",
"exit": "trust the pill colour because the apparatus behind it is inspectable on this same screen, not because the pill says so"
},
"frames": [
{
"path": "wt/FYwall/00.png",
"sha256": "708bfd30e1d6100339c189d6350d386cda564e3f67cd2b0c4455e6bc790b7727",
"caption": "Every push to main deploys both layers; this wall reads back what each product actually serves, right now.",
"asserted": "css:.foyer-header__line visible"
},
{
"path": "wt/FYwall/01.png",
"sha256": "708bfd30e1d6100339c189d6350d386cda564e3f67cd2b0c4455e6bc790b7727",
"caption": "22 repos, one honest card each — hosted products probed live, everything else marked registry-only.",
"asserted": "css:[data-testid^=\"foyer-card-\"] count=22"
},
{
"path": "wt/FYwall/02.png",
"sha256": "708bfd30e1d6100339c189d6350d386cda564e3f67cd2b0c4455e6bc790b7727",
"caption": "Same colour rule for both: NodeProof only answered (amber, reachable) — NodeVoice's two layers agreed (green, verified).",
"asserted": "css:[data-testid=\"foyer-card-NodeVoice\"] .foyer-pill[data-state]=\"verified\""
},
{
"path": "wt/FYwall/03.png",
"sha256": "708bfd30e1d6100339c189d6350d386cda564e3f67cd2b0c4455e6bc790b7727",
"caption": "\"same state for 11 sweeps\" — the ledger's own stability count, not a claim about one lucky probe.",
"asserted": "testid:foyer-card-NodeRoom[data-foyer-stable-sweeps]=\"12\""
},
{
"path": "wt/FYwall/04.png",
"sha256": "28a3a3e7d40949872ae29d754e8bb3355e93f4e9c79a2ccedf6eb867aa0d6312",
"caption": "Hover reveals the apparatus: the exact URL probed, its HTTP status, when it answered, and the sha256 of what it returned.",
"asserted": "testid:foyer-apparatus-NodeRoom visible"
},
{
"path": "wt/FYwall/05.png",
"sha256": "2ecd6456275032d5daa53424e2bd3a9ad95b202658f328d4c6a7d1e5b777c199",
"caption": "The dead fixture stays UNKNOWN, forever — its apparatus shows the exact URL it tried and failed, never a guess.",
"asserted": "testid:foyer-apparatus-__fixture_dead text matches"
},
{
"path": "wt/FYwall/06.png",
"sha256": "16faeaeea0203dee218abafff8f068e5b95d2b4ec4906889a8fc9592a45a4433",
"caption": "The Foyer probes itself, too: frontend and backend agree on the same build sha, so its own card is verified on both layers.",
"asserted": "testid:foyer-apparatus-node-foyer text matches"
}
]
}
28 changes: 28 additions & 0 deletions src/FoyerRoot.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import React from "react";
import { Composition } from "remotion";
import { Walkthrough, WT_FPS, WT_W, WT_H, wtDuration } from "./Walkthrough.jsx";
import { FOYER_WALKTHROUGHS } from "./walkthrough.foyer.data.js";

// FOYER-V3 R1: the three Node Foyer walkthroughs (FYwall, FYphone, FYagent), each its own
// composition ("WT-<id>", same convention as src/Root.jsx) so run-remotion.mjs can render
// one at a time — `remotion render src/foyer-index.js WT-FYwall out/foyer-FYwall.mp4`.
// Reuses Walkthrough.jsx unchanged: each entry's `captureViewport` (written by
// walkthrough.foyer.mjs) drives Walkthrough.jsx's geometryFor so the phone (390x844) and
// wall/agent (1440x900) captures each get their own aspect ratio instead of the historical
// 1280x800 default.
export const FoyerRoot = () => (
<>
{FOYER_WALKTHROUGHS.map((w) => (
<Composition
key={"WT-" + w.id}
id={"WT-" + w.id}
component={Walkthrough}
durationInFrames={Math.max(1, wtDuration(w))}
fps={WT_FPS}
width={WT_W}
height={WT_H}
defaultProps={{ wt: w }}
/>
))}
</>
);
Loading
Loading