Skip to content

Clickable options are judged on a real press and keep their source (0.8.0) - #110

Merged
umeranjum17 merged 2 commits into
mainfrom
fm/interactive-prototype-export-options-kee-23
Oct 30, 2026
Merged

umeranjum17 merged 2 commits into
mainfrom
fm/interactive-prototype-export-options-kee-23

Conversation

@umeranjum17

@umeranjum17 umeranjum17 commented Oct 30, 2026 •

Copy link
Copy Markdown
Owner

Sign-off found first-draft's export script-free, so a design option could not be clicked, and the captain wants design picks as live interactive demos. I measured before building, so this PR only closes the gaps measurement proved.

What already worked on 0.7.0 (measured, not assumed)

I wrote a clickable inbox option (a row opens its message, Unread narrows the list), ran it through the shipped export path (staticExport(html, { script: true })), and drove a real CDP mouse press on the result in headless Chrome:

  • the exported page keeps the option's inline <script> verbatim;
  • its CSP is default-src 'none'; form-action 'none'; script-src 'unsafe-inline'; ... — script runs, nothing loads or is sent;
  • the view already serves each option from a separate frame origin under sandbox allow-scripts, so a click works inside the sandboxed frame;
  • a <script src=...> is still refused (unchanged).

So the export, network and sandbox parts of the ask were already true. The two real gaps were judging and source.

What this adds

1. The judge scores behaviour by a real click (src/render-check.mjs clicks(), src/judge.mjs, src/critic.mjs). Before the independent judge sees an option, the host presses the first control a person can press — button, [role=button], [role=tab], [role=menuitem], input[type=submit|button]; never a link, because a link is navigation — at 390 and at 1280 with Input.dispatchMouseEvent (a real trusted mouse event, not element.click()), and keeps the page before and after that press. The judge is asked for behaviour { score, reason, defects } and is sent click-390-before, click-390-after, click-1280-before, click-1280-after. The log it also gets says which control was pressed (label, tag), how many controls exist, after (the text a person reads once the page redrew), and idle (the page changes on its own, so a change is not credited to the press).

  • A press that changes nothing is named as a defect and the option does not pass: the bar for behaviour is the same 9 the other axes carry. critique.behaviour = { present, score, reason, defects, clicked, widths }.
  • A page drawn from App.jsx has no script left, so it is never asked about behaviour (present: false with the reason). Only a live index.html option is judged on its press — the drawn path's dead buttons are a known limitation of the snapshot, not a new failure.
  • Prompt contradiction fixed: the judge was told this is "not an interaction … audit"; for an interactive option it now says "not an accessibility or factual-proof audit" so the behaviour question is coherent.

2. The exported option keeps its source sidecar (src/source.mjs, src/make.mjs, src/cli.mjs, src/polish.mjs).

  • Called <option>.source/ and it sits beside the option file (inbox.html → inbox.source/), the same convention as <option>.motion/. It holds the files the engine wrote: App.jsx plus the files it imports, as plain readable text — not the base64 blob that was the only copy of a drawn option's source before. make JSON names it in files[].source_dir, and each polish round writes its own <round-N>.source/.
  • A live index.html option is its own readable source (its script and CSS sit in the page), so it is not duplicated. The in-band first-draft-source comment stays: it is how refine seeds the next round.
  • A re-run replaces the folder instead of mixing two runs, and a source path that escapes the folder (../, absolute) is refused.

For sign-off (separate task in that repo)

  • What "Open live" must read: the option's Play route, <view-url>#/o/<n>/play — exactly what post already passes as each option's --link. It must not read the reference/source link. That route serves the option from the frame origin (<view-port+1>/o/<n>) with sandbox allow-scripts, script-src 'unsafe-inline' and connect-src 'none', i.e. the clickable page.
  • What first-draft now offers: sidecar <option>.source/ beside the option (drawn options only); sandbox sandbox allow-scripts in a separate origin with default-src 'none' and no network; and critique.behaviour in the make/judge JSON with the frames under the same evidence conventions as <option>.motion/.

Proof

proof: the environment ships the done-done-verify-first-draft verify skill; this change adds the feature it now drives, features/clickable-option-export.md and the click journey (done-done run click). Evidence folder: /home/umer/.treehouse/firstmate-cff959/3/firstmate/data/interactive-prototype-export-options-kee-23/run4/ (click-log.json, make.json, refused-dead-control.json, drawn-source-sidecar.json, steps.json, and frames/click-390-before|after.jpg, frames/click-1280-before|after.jpg).

node journeys/click.mjs (nonpaid, real CLI, local model and judge stand-ins) at this head: a clickable inbox option exports with its inline script under script-src 'unsafe-inline' and default-src 'none'; the host's real press changes the page at 390 and 1280 and the click log shows the narrowed list (only the unread row left); the judge is sent the before/after frames and answers with a behaviour verdict; a control wired to nothing is judged clicked: false and refused; a drawn App.jsx option writes App.jsx readable in <option>.source/ and is not asked about behaviour; and a real click on the page the view serves (sandbox allow-scripts, connect-src 'none') still changes it. Scores are synthetic stand-in scores: they prove the protocol, not design quality.

npm test: 30/30 pass, including the new test/click.test.mjs (verdict schema + sidecar paths, no browser) and the existing motion, critic, make, polish and view integrations.

Scope

No board restyle, no engine or draft prompt rewrite, no new design option, nothing in the sign-off repo. Version bumped to 0.8.0 so the existing release workflow publishes on merge; the workflow file is untouched. No done-done run was executed at this head (its judge spends model calls); the journey above is the same drive done-done run click performs.

Checks

outcome: A real mouse press on the Unread button in the exported page is recorded as clicked: true at 390 wide and at 1280 wide, and the clip of each press shows the inbox list narrowing to the unread row once the page redraws.
repro: n/a
proof: done-done-verify-first-draft (features/clickable-option-export.md); done-done run 20261030T112801Z-5fc65295-2ca128 (journey click) at head 5fc6529
evidence: 20261030T112801Z-5fc65295-2ca128/frames/click-390.mp4, 20261030T112801Z-5fc65295-2ca128/frames/click-1280.mp4, 20261030T112801Z-5fc65295-2ca128/click-log.json

…keep their source (0.8.0)

Sign-off found the export script-free, so a design option could not be
clicked, and 0.7.0 already kept a live option's inline script: measured
with a clickable inbox option, the export keeps the script under
`script-src 'unsafe-inline'` and `default-src 'none'`, and a real mouse
press in Chrome changes the page. The gaps were judging and source.

- render-check gains clicks(): at 390 and 1280 it presses the first
  control a person can press (button, role=button/tab/menuitem, submit)
  with Input.dispatchMouseEvent, and keeps the page before and after the
  press, the control's label, what a person reads after it, and whether
  the page also changed on its own (idle).
- The judge is asked for `behaviour { score, reason, defects }` with
  those frames, only for a live page that carries a control; a press that
  changes nothing is a defect and the option does not pass (bar stays 9).
  A page drawn from App.jsx has no script left, so it is never asked.
- A drawn option writes its engine source readable beside it in
  `<option>.source/`, and make/judge JSON name it in `files[].source_dir`;
  the in-band comment still travels with the page for refine.
- journeys/click.mjs drives the whole path through the real CLI with
  local stand-ins: export, press, judge verdict, a dead control refused,
  the drawn source sidecar, and a real click on the framed page the view
  serves.

Version 0.8.0 so the release workflow publishes on merge; the workflow
itself is unchanged.
The board refuses a click proved by stills of mostly text, so the press
is now kept as a short sequence and encoded into a video.

- render-check's clicks() keeps 11 CDP frames across the press (PRESS_AT,
  about 1.2 s) with each frame's own real timestamp, and takes the
  before/after stills from that sequence; a control that changes nothing
  keeps no frames.
- The judge still reads only the before/after frames: the behaviour log
  says how many frames span the press and over how long, never the bytes.
- journeys/click.mjs encodes each width into frames/click-<width>.mp4
  with ffmpeg (concat with the real frame durations, at most 1280 wide,
  about a second and a half), refuses before any work with a clear
  message when ffmpeg is absent, and records the frame timestamps and the
  clips in click-log.json.
@umeranjum17
umeranjum17 merged commit 9881cf1 into main Oct 30, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant