+ + +
Design review Thursday
Can we walk the board flows at 10:00?
diff --git a/.agents/skills/done-done-verify-first-draft/SKILL.md b/.agents/skills/done-done-verify-first-draft/SKILL.md
index 6037787..0700787 100644
--- a/.agents/skills/done-done-verify-first-draft/SKILL.md
+++ b/.agents/skills/done-done-verify-first-draft/SKILL.md
@@ -51,6 +51,7 @@ Harness: A shell runs the CLI and curl for the url. A browser tool opens the url
- [Logo mode: concepts from the brief (wordmark-led, abstract, symbol; distinct base shapes; letter + object dropped), self-critique at 16 and 32 px, brand sheets with kind, idea and palette, checked SVG files and the app icon set](features/logo-mode.md): Run `first-draft-axi logo "
+ + +
Can we walk the board flows at 10:00?
diff --git a/journeys/click.mjs b/journeys/click.mjs new file mode 100644 index 0000000..6d86db9 --- /dev/null +++ b/journeys/click.mjs @@ -0,0 +1,137 @@ +// Nonpaid real-CLI proof that a clickable option stays clickable: an inbox brief whose rows and filter are wired by an inline script -> +// make exports the option with its script, loading nothing -> the host presses one of its controls with a real mouse event in a headless +// browser and keeps a frame sequence across it, encoded into click-390.mp4 and click-1280.mp4 -> the independent judge is asked about +// behaviour and returns a verdict -> a control that +// changes nothing is judged and refused -> a drawn App.jsx option keeps its source readable beside it -> the view serves the clickable page +// in its sandy frame and a real click on what "Open live" opens still works. Local stand-ins answer the model and the judge, so scores are +// synthetic, never design quality. +import '../test/fixtures/provider-hooks.mjs'; +import assert from 'node:assert/strict'; +import { execFile, spawn } from 'node:child_process'; +import { mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { promisify } from 'node:util'; +import { renderCheck } from '../src/render-check.mjs'; +import { standIn } from '../test/fixtures/critic-model.mjs'; +import { installJudge } from '../test/fixtures/judge-cli.mjs'; +const root = new URL('..', import.meta.url).pathname, out = process.env.DONE_DONE_EVIDENCE_ROOT ?? process.argv[2]; +if (!out) throw Error('Give an evidence folder'); +mkdirSync(out, { recursive: true }); +const BRIEF = 'An inbox app: pick a message from the list to read it, and filter the list to unread'; +const ffmpeg = (...args) => promisify(execFile)('ffmpeg', args, { maxBuffer: 32_000_000 }); +// The press is a clip, not only two stills: the frame sequence across it (render-check's PRESS_AT) is encoded into a short mp4 per width. +// Refuse before any work when ffmpeg is not on PATH, rather than after the CLI has made the option. +try { await ffmpeg('-version'); } catch { throw Error('the click clips need ffmpeg: click-390.mp4 and click-1280.mp4 cannot be encoded without it. Install ffmpeg on PATH and run the journey again'); } +const encode = async (width, sequence) => { + const dir = mkdtempSync(join(tmpdir(), `fd-click-seq-${width}-`)), list = join(dir, 'frames.txt'); + sequence.forEach(({ dataUrl }, i) => writeFileSync(join(dir, `f-${String(i).padStart(3, '0')}.jpg`), Buffer.from(dataUrl.slice(dataUrl.indexOf(',') + 1), 'base64'))); + // Each frame keeps the time it was really taken, so the clip plays the change at the pace a person saw it. + writeFileSync(list, `${sequence.map(({ at_ms }, i) => `file 'f-${String(i).padStart(3, '0')}.jpg'\nduration ${((sequence[i + 1]?.at_ms ?? at_ms + 200) - at_ms) / 1000}`).join('\n')}\n`); + const file = join(out, 'frames', `click-${width}.mp4`); + try { + await ffmpeg('-y', '-loglevel', 'error', '-f', 'concat', '-safe', '0', '-i', list, '-an', '-vf', "fps=15,scale='min(1280,iw)':-2", '-c:v', 'libx264', '-preset', 'slow', '-crf', '30', '-pix_fmt', 'yuv420p', '-movflags', '+faststart', file); + return { file, bytes: statSync(file).size }; + } finally { rmSync(dir, { recursive: true, force: true }); } +}; +// What the stand-in model writes: an inbox whose rows open their message and whose filter narrows the list, both wired by an inline script. +const inbox = (title, wired = true) => `
+