proof — an uncut clip of the changed journey
A reviewer should be able to watch what a user would see, at normal speed, start to finish. The clip is the evidence; a screenshot, a contact sheet or a sped-up recording is not.
Source of truth for what a tier needs: docs/process/PROOF-POLICY.yml (T0 nothing; T1 focused tests + typecheck;
T2 adds one clip of the changed journey, desktop, plus phone if layout changed, on a fake-mind world; T3/T4 add more).
Get the tier of the current diff with node tools/proof/tier.mjs --base origin/main.
Rules
- Uncut and real speed. One browser context, one take. Never trim, splice, speed up or slow down. If the journey
is longer than the policy's
max_seconds, record a shorter journey that still shows the change, not a faster one. - The take starts at arrival (the default): the recording begins once the world is on screen and the arrival card
is gone, so the clip opens on the world, not on loading. From there it is one uncut, real-speed take of the journey,
which is what the policy asks. When loading or arrival is itself what the change is about (the arrival card, a
loading screen, first paint), or the journey is not on a world page (the world list, sign-in), pass
--from-load: the take then starts at page creation. The sidecar'sstart(arrivalorload) says which was used, andupload.mjs --proofcopies it into proof.jsonmedia[]. - By day. On a local world URL the recorder sets the world to midday with its clock paused before the page opens
(
--time-of-day 12, the default; the god env route as the dev owner). Dev worlds boot at 06:00 and run a world day per real hour, so without it a world up for 40 minutes is at night. Setting it is oneworldentry in the world's god history, and the clock stays paused afterwards. Pass another hour, or--time-of-day keepwhen the clock, the light or the god history is what the change is about. The sidecar'sworldTimesays what was set. - Real input. Drive with
page.mouse,page.keyboard,page.touchscreenor locator clicks, the way a user would. Do not call app internals frompage.evaluateto make things happen on camera. - Fake minds, isolated data. Local worlds only,
BRAIN_MODE=fake, a fresh data dir. Never record against a production world unless the maintainer asks, and never with a signed-in personal browser profile. - Never commit video (or frames) into the repo. Work under
.e2e/(gitignored). - The clip must show the change. Watch it (or at least extract frames, below) before you attach it.
1. Boot a fake-mind world
Needs a built web client (pnpm install --frozen-lockfile && pnpm build; skip if
apps/web/dist/index.html exists and is current for your change). In a fresh clone the build's prebuild check fails
until the web media are in place: node tools/assets/pull.mjs --only 'apps/web/**' (a few files) is enough for it.
node tools/god-skill/dev-world.mjs proof 4710 --genesis worlds/templates/hello-hamlet.json --fresh --data .local/proof
# prints http://127.0.0.1:4710/w/proof?dev=1 (pid N, log .lane/proof.log); the server keeps running detached
The homepage (world list, New world) is served by the world launcher, not by a single world. For a journey that starts there, run a launcher (its worlds always get fake minds) and make a world for the list through its API:
node tools/world-launcher/launcher.mjs --port 4720 --range 4721-4723 --data "$PWD/.local/proof-launcher" --eager 0 &
curl -s -X POST -H 'content-type: application/json' -H 'authorization: Bearer dev:proof' \
-d '{"name":"Proof Garden","template":"hello-hamlet"}' http://127.0.0.1:4720/__sim/worlds
# record against http://127.0.0.1:4720/ ; stop the launcher by its pid (it stops its worlds)
Pick the template that contains the changed surface (worlds/templates/: hello-hamlet is small and fast; eden-2026,
mars-two and others are larger). Use a free port; e2e/config.mjs lists ports reserved for other harnesses. Stop the server
with kill <pid> when done.
Quick proof the app works: the local smoke
Before recording, or as the whole proof of a T1 change that touches the server or web app, run the local smoke. It boots its own isolated launcher (fake minds, dev sign-in, data under the run dir) on a free port, opens the world list and one world, checks that a person is drawn with no console errors and no failed requests, and stops what it started:
node e2e/local-smoke.mjs --port 9950 --clip # --build first on a fresh clone; omit --port for any free port
# → .local/e2e/<timestamp>/summary.json, shots/1-world-list.png 2-world.png 3-person.png, clip.mp4
node tools/proof/upload.mjs --pr $N .local/e2e/<timestamp>/summary.json .local/e2e/<timestamp>/shots/*.png
status: "pass" in summary.json is the evidence; its clip is one uncut, real-speed take of the smoke journey. It is
not a clip of your change: a T2 change still records its own journey below. Details: docs/testing/e2e.md#local-smoke.
2. Write the steps
Copy .claude/skills/proof/steps.example.mjs to .e2e/proof/<slug>/steps.mjs and script the changed journey:
do the thing, show the result. With the default arrival start the steps begin on the arrived world (the arrived mark
is 0:00); with --from-load they begin right after navigation. mark('label') stamps a chapter time into the sidecar
so the PR can say "the panel opens at 0:07". Keep it to the journey the change affects.
3. Record
node .claude/skills/proof/record.mjs --url 'http://127.0.0.1:4710/w/proof?dev=1' \
--steps .e2e/proof/<slug>/steps.mjs --out .e2e/proof/<slug>/desktop.mp4 --viewport desktop
# layout or touch changed? also:
node .claude/skills/proof/record.mjs --url '...' --steps .e2e/proof/<slug>/steps.mjs \
--out .e2e/proof/<slug>/phone.mp4 --viewport phone
- Browsers: uses
PLAYWRIGHT_BROWSERS_PATH, else/opt/pw-browserswhen present. If Chromium is missing:PLAYWRIGHT_BROWSERS_PATH="$PWD/.e2e/browsers" pnpm --filter mars-campus-viewer exec playwright install chromium. - Playwright is resolved the same way the playtest harness does (
tools/playtest/lib/platform.mjs: a globalplaywright, else@playwright/testfrommars/viewer). No GPU is needed; WebGL runs on SwiftShader. --max-secondsdefaults to the policy'smax_seconds. Exit 3 means the journey ran past it: shorten it.- With ffmpeg: an mp4 with a progress bar along the bottom and a
hh:mm:ss / totaltimecode (the timecode needs an ffmpeg built with drawtext; the sidecar'stimecodesays whether it is there). Without ffmpeg: the raw.webm; say so in the PR (players still show their own scrub bar). - Each clip gets a
<clip>.jsonsidecar: url, viewport, duration,start,startedAt, marks (arrivedamong them),worldTime, console errors, commit and dirty flag. A Playwright withoutpage.screencastrecords from page creation even without--from-load; the sidecar says so instartFallback. - The page has
--arrival-seconds(default 120) to arrive before the take starts; a page that never shows a world fails (exit 1) with a hint to use--from-load, a still of where it stuck (<clip>.open-failed.png) and a sidecar. Console errors in the sidecar are findings: explain or fix them.
The run takes well under the 10-minute foreground limit. For several clips in one go, run them in the background
(nohup setsid node ... > .e2e/proof/<slug>/rec.log 2>&1 &) and poll the log.
4. Check it yourself
for t in 1 4 8; do ffmpeg -loglevel error -y -ss $t -i .e2e/proof/<slug>/desktop.mp4 -frames:v 1 .e2e/proof/<slug>/t$t.png; done
Look at the frames (Read the PNGs). The changed state must be visible, the world must have loaded (not a blank or loading canvas), and nothing in frame should be private. If it does not show the change, fix the steps and record a new take; do not attach a clip that does not prove the claim.
5. Attach it to the PR (never commit it)
Check every outgoing text/JSON payload with node tools/proof/publication.mjs <payload files> before publication.
Inspect media pixels, audio, filenames and binary metadata separately. Device paths, hostnames, device/private IPs,
local URLs and operational identifiers belong only in private records. Use relative paths and neutral capture
labels. The uploader blocks detected text leaks before uploading the batch; this cannot certify media privacy.
node tools/proof/upload.mjs --pr $N --proof .e2e/journal/$ISSUE/proof.json .e2e/proof/$SLUG/desktop.mp4 .e2e/proof/$SLUG/phone.mp4
- The default target is the PR's own prerelease
proofs-<n>(tag on the default branch, created on first use); one release per PR keeps each under GitHub's 1000-asset limit, which the old sharedproofsrelease reached.--release <tag>(orSIM_PROOF_RELEASE) names another release. Assets are namedpr-<n>.<file>, and a re-take gets-2,-3, so earlier takes, including failed ones, stay linkable.SIM_PROOF_UPLOAD_URL(+SIM_PROOF_UPLOAD_TOKEN) switches to another endpoint, such as a CDN. - With
--proof, clip and image links are written into proof.jsonmedia[], other files (logs, JSON) intofiles[], and proof.json is uploaded last. Paste its link and the clip links into the PR's Proof section. Thejournalskill covers proof.json. - Inline preview. GitHub serves release assets as downloads, so an mp4 link never plays in the PR. For each
video,
upload.mjsmakes a small animated GIF with ffmpeg (<clip>.preview.gif: 6 s at real speed from the clip'sarrivedmark, or from 0 for a--from-loadtake; at most 5 MB), uploads it beside the clip, records it asmedia[].previewand prints a line like[](…pr-412.desktop.mp4). Paste that line: the preview plays in the PR and clicking it opens the full clip. When the change shows later in the clip, pick the window with--preview-start <s>(and--preview-seconds <s>). The preview is a pointer, not the proof: the uncut mp4 is. Always put an explicit Full-resolution uncut video link before each preview, stating the actual encoded dimensions and duration. Preserve capture resolution and frame the action so contact can be reviewed. Do not upscale a capture or use the reduced GIF to claim motion proof. - Exit 3: the upload is blocked (no
gh, not signed in, no permission, or no network, as in many cloud sessions). Tell the user which clips are waiting and where. Post a PR comment with each clip's duration, viewport, sanitized sidecar summary (marks, console errors, commit), and a repository-relativerecord.mjscommand and steps file, so anyone can re-record it. Mark the proof "pending upload" in the PR body. Never paste video as base64, never commit it, and never push it to another branch.
gh pr may not work in cloud sessions; comment with
gh api repos/$REPO/issues/$N/comments -F [email protected]/proof/$SLUG/comment.md.
What to write next to the clip
One line per clip: what journey, which world template, viewport, duration, commit, and the moment the change is visible ("0:07 the seat highlights and the Soul sits"). Plus any console errors and why they are acceptable.
