The problem
One at a time
MockupLabGridView is a flat LazyVGrid of cached PNG thumbnails, and
the only live render in the whole subsystem is the single MockupWebView in the detail
pane. Cross-game drift — two games claiming the same interaction verb, three different stamp
treatments — stays invisible until the app is wired and rebuilt.
A frame is not a file
The mockups that work best — prime-suspect-case-view.html,
lineup-round-view-v6.html — are single-file prototypes with a harness bar of
named states: First entry · Played before · Working screen · Mid-case
· Case closed. The comparison worth making is first entry across every game,
which no file-level grid can express.
The rules exist and nothing reads them
docs/mockups/_principles.md, _kit.html, and the spec’s binding
sections (§5.2 visual grammar, §7.6 interaction-verb ownership) govern every mockup.
No generation prompt and no review consults any of them.
Half the work happens in a browser
Mockups get generated in claude.ai on the desktop and in the Claude app on the phone. Those
artifacts land in ad-hoc folders such as Game-Content-and-Ideas/…, never reach
docs/mockups/, and carry no canon out or back.
Approach
A frame is (file, state), not a file
The board’s unit is a frame: a prototype file plus the name of a state
inside it. Files declare their states through a small contract — a cr-frames
JSON block listing {id, label, beat}, and a message listener that sets
state on request.
beat is a shared vocabulary (entry · brief · play ·
commit · reveal · closed). It is what makes the comparison matrix work: rows
are games, columns are beats, and a column shows the same moment across every game. A file
without the contract contributes one frame at its default state, labelled single state,
so nothing needs retrofitting before the board is useful.
The board is one HTML document in one web view
The board renders as a generated page holding one <iframe> per frame, lazily
mounted with IntersectionObserver and driven into its state by
postMessage after load. A single WKWebView hosts it.
Rejected: a SwiftUI canvas with a web view per card. That is 49 live
WKWebViews and two rendering paths — screen and export — that drift apart.
Frames are inlined, not linked — measured, not assumed
The first design pointed each frame at the real file with
src="../mockups/x.html", which keeps the board tiny and always current.
WebKit will not do it at scale. Measured on Peregrine’s own files, loaded through
loadFileURL(allowingReadAccessTo:):
8 frames → 8 rendered. 16 → 16. 30 → 27. 45 → 12.
The ceiling is cumulative rather than concurrent. Feeding the 45-frame board through a queue
capped at four concurrent loads still stalled at 27, and only reached 43 after about a minute of
timeouts and retries. Frames that lose never fire load at all.
The same 45 frames inlined as srcdoc all rendered in under three seconds, because
a srcdoc frame shares the parent document’s origin and escapes that ceiling. So
each frame’s HTML is read off disk and embedded.
postMessage is kept for state even though an inlined frame is same-origin and could
be called directly, so one frame contract covers both cases. Verified: three cells of one file,
driven into three different states, rendering side by side.
Export writes that same document to docs/storyboards/{slug}.html. Inlining makes it
self-contained, which is what lets it work on the phone where relative paths to sibling files may
not resolve. The copy is a snapshot, and it stays current because an edited mockup changes the
rendered bytes and the exporter’s content comparison rewrites the file.
Frames are files; sessions are the editing layer
Slots reference a path under the project, not a MockupSession.id. Artifacts arrive
from three places — the Lab, claude.ai on the desktop, the Claude app on the phone —
and the file is the only thing all three produce. MockupDocScanner and
MockupFolderImporter already join files to sessions, so “open this frame in the
Lab” still resolves when a session exists, and offers Iterate on this when
the file is an external reference.
The canon is a repo file both sides can edit
docs/mockups/_principles.md is the default canon, resolved per board and
overridable. It feeds the generation prompt, the cohesion review, and the clipboard packet that
goes to the browser. The review proposes canon amendments as well as fixes,
because the rules are expected to move as the experience gets evaluated. The canon stays a
markdown file in git; CodeRaven reads and writes it and never owns it.
The browser round-trip is clipboard-shaped, and that is a real limit
Copy Canon Packet puts canon, kit, frame contract, and the selected
frames’ HTML on the clipboard, ending with a return-fence contract. Claude’s reply can
carry a coderaven_storyboard JSON fence with frame, canon,
and board kinds, which the Lab’s import sheet applies after an explicit commit
— the same shape as Press Raven’s working pressraven_return intake.
Nothing reaches the repo from claude.ai automatically. Pasting happens in both directions.
A watched docs/mockups/_inbox/ plus the existing mobile iCloud drop covers files that
arrive as downloads rather than as text.
Phase 10 — the registry is a database with a form over it
Testing produced one verdict that outweighs its pass rate: “the setup is robust but the UI feels more like a database that requires a lot of manual steps / manual entry.” Every noun in Phases 2 and 3 is a table, and almost every field is typed by hand. The seeder proposes once, at import; purposes, states, beats, flows and loops are manual from then on.
The three AI paths do not fix this, because they are pointed one layer too low. They operate on mockups. None of them operates on the registry, so the structure stays hand-maintained however good they turn out to be.
Read the files before opening a conversation. Four
*-flow-spec.md files already sit in docs/mockups/, the spec
covers 21 games, and 8 of 56 mockups carry a visible state harness. A screen’s
purpose is legible from its own markup. That evidence is unread today, and reading it
is cheaper than asking.
A contextual pane, backed by a persistent thread
Rejected: a Mira-style session over the whole registry. The thing worth changing is usually the thing on screen, and a separate session re-establishes that context every time. A pane that carries the current selection and keeps one thread per project gets the immediacy of context and the memory of a session.
Cost: a pane is a poor place for long multi-step work, and cross-cutting questions — “which games have no commit beat” — will feel cramped in it.
Per-unit, on demand — not project-wide, not ambient
A project-wide fill would propose roughly 150 fields across 45 screens at once, replacing manual entry with manual review of a wall of guesses: the same chore wearing a different hat. A unit is one to five screens and reviews in a minute. Ambient nudges were rejected because they turn a tool into a nag, and because the honest trigger for a fill is “I am working on this unit now”, which only Sarah knows.
Cost: the registry fills unit by unit rather than arriving complete.
What Phase 10 re-points
/cr-frames becomes largely redundant. Its job — read a
prototype’s harness, work out its states, write them down — is what the
first item does automatically, without a CLI ritual to remember. The testing note
“not entirely sure what we’re doing here / when to use this”
is the symptom of asking a person to run something the app should offer.
What the file-reading pass actually yielded
Measured on Peregrine rather than estimated: 37 of 56 mockups carry a screen-reader description that reads as the screen’s purpose, 5 declare a usable set of states through their harness, and 3 are named by a spec that says which file it accompanies. All deterministic — no model call, so a proposal can be wrong about wording but cannot be invented.
Three defects surfaced on the first real run and were fixed before any UI existed.
Pressed-state buttons inside a mockup — Prime Suspect’s POSSIBLE / UNLIKELY /
PRIME / CLEARED suspect pins — were read as screen states, yielding five real states
and four impostors. Buttons written inside JavaScript template strings produced states
named esc-a-t. And a spec stem like peregrine-spec-v2 mangled
into a claim on peregrine-v2.html, a file that does not exist.
The real lesson from Design Studio is not navigation
Design Studio never shows an empty field. Every card arrives full — a screen registry entry carries a name, a description, its flows and its emotional beat, all generated — and the only thing asked of a person is what should change about this. The registry built here shows a name taken from a filename and leaves the rest blank. That difference, not the layout, is why one reads as a conversation and the other as data entry.
So the order matters more than the pane does. Generate first, correct second. The file-reading pass is the cheap half; a model pass over the same ground fills what no file answers. Both should land before a conversation surface is built, or the pane becomes a slower way to type into blanks.
One pane whose scope follows the selection, and can be widened
The pane is primary and contextual — on a screen it revises that screen, on a unit that unit — and carries a visible scope chip that can be changed to the whole registry when the question is cross-cutting. That answers “contextual primary, full-registry also” without two surfaces, and it removes the cost previously accepted: a cramped place for “which games have no commit beat” becomes a scope change rather than a different tool.
Say what can be asked, rather than showing a blank box. Design Studio’s revision bar is a question, not a field: “Revise the registry: Missing a screen, or should two be merged?” The prompt teaches the move.
What to borrow, and what not to
Borrow: the detail card with labelled sections, the inline revise bar phrased as a question, the prev/next stepper that moves through items without navigating, select-on-left with detail-on-right, and Regenerate beside Confirm.
Do not borrow the linear wizard spine. Design Studio walks one product through one pass, so First Contact → Core Loop → Task Flows → Screen Registry can be a progress bar. This registry holds 21 units, is re-entrant, and is never finished. The card grammar scales per unit; the wizard does not. The stakes differ too — Design Studio is conceptual, while this describes a built app, so its cards need provenance the originals do not carry: which file said this, whether the screen is built, which take is current.
Decisions made for you
Storyboard is a mode inside the Mockup Lab tab, not a new project tab
It reads the same files and the same sessions; a seventh ProjectTab would split one
subsystem across two surfaces. Reversible by adding a case to ProjectTab.
Board state lives in a per-project sidecar, not on MockupSession
storyboards.json sits beside target-zone-presets.json. A slot can be
empty, can point at a file with no session, and can appear on several boards — none of which
fits a field on a session.
Phase 1 ships a board with no state-switching and no review
Forty-nine files on one wall is most of the value and depends on none of the rest. The cost is that the first board shows each prototype only in its default state.
The board document carries a copy of every frame rather than a pointer
Peregrine’s full wall comes to about 1.5 MB. Linking is prettier and does not work; the measurements are in Approach. Reversible only by accepting that large boards render partially.
The work
Phase 1 — A wall you can look at
11 tasks Built. Document layer verified against Peregrine’s real files.- Add
Storyboard,StoryboardGroup,StoryboardSlot,StoryboardSlotSourceto a newModels/Storyboard.swiftwith defensive Codable - Add
StoryboardStorepersistingstoryboards.jsonper project, mirroring the target-zone-presets sidecar pattern - Add
StoryboardDocumentBuilderthat renders a board to a self-contained HTML page with lazily mounted<iframe>frames - Measure how many frames a
WKWebViewwill actually render, and pick the frame-embedding strategy from the result - Add
StoryboardBoardViewhosting the generated document in oneWKWebViewwith zoom and frame-width controls - Add a
.storyboardcase toMockupStudioContainerView's navigation enum and a Storyboards entry point inMockupLabHeader - Add board CRUD UI — create, rename, delete, and a board picker
- Seed a new board from
MockupDocScannerentries so an existing project starts populated - Add slot arrangement — drag to reorder within a group, move between groups, add and remove groups
- Add
StoryboardExporterwriting the board todocs/storyboards/{slug}.html - Run the board in the app and confirm the SwiftUI chrome behaves — Sarah
Phase 2 — The registry spine
9 tasks Built. Seeder verified on all 54 mockups.- Add
Models/DesignRegistry.swiftwithDesignUnit,Screen,ScreenVariant,ScreenVersion,ScreenStatusand defensive Codable - Give
ScreenVersionasourceoflab,browser,externalorbuildso a build-derived version has somewhere to live later - Add
DesignRegistryStorepersistingdesign-registry.jsonper project besidestoryboards.json - Add
ScreenRegistrySeederthat proposes units, screens, variants and versions from filenames, carrying its reasoning per grouping - Treat a trailing
-v{n}as a version of one screen, and a same-stem suffix such as-leanor-liveas a variant - Treat a
v2_prefix as an earlier version rather than a later one, and say so in the proposal - Add a seeding review sheet where a proposed tree can be merged, split, renamed or rejected before it is written
- Add a registry browser — units in a rail, screens in the middle, the selected screen's variants and version stack on the right
- Add screen editing — name, purpose, status, states, and promoting an older version back to current
Phase 3 — Flows, and boards rendered from them
8 tasks Built. 52 of 52 frames through the refactored renderer.- Add
FlowandFlowStepreferencing a screen by id, with an optional pinned state per step - Add
CoreLoopper unit, reusing Design Studio's trigger/action/reward/investment shape - Extend
StoryboardSlotwith a source of screen reference, file path, or empty placeholder - Resolve a screen-backed slot to its current version at render time in
StoryboardExporter - Add flow boards — a board that renders a flow's steps in order and follows the flow as it changes
- Add registry boards — a board that renders a unit's screens, or the whole registry, crossed with beats
- Add a flow editor that orders a handful of steps, replacing drag-ordering of a 54-item list
- Keep ad-hoc file boards working, and add multi-select plus filtering to the arrange sheet for them
Phase 4 — Intake: new mockups reach the registry
5 tasks Built.- Surface unfiled mockups — files on disk that no screen claims — as a count and a list
- Add an intake step that asks whether a file is a new screen, a new variant, or a new version of an existing screen
- Suggest the likely screen for an unfiled file by filename proximity, ranked, never auto-applied
- Fold a re-seed into existing units and screens by name so a later mockup joins rather than twins
- Add "add missing screens" to a registry board so a board picks up screens added after it was made
Phase 5 — States and the comparison matrix
5 tasks Built. Matrix verified as aligned beat columns.- Write the frame contract as
docs/mockups/_frame-contract.mdand add it to the canon set - Parse the
cr-framesJSON block inMockupDocScannerand expose declared states per file - Drive each frame into its slot's state by
postMessageafter iframe load, with asingle statelabel when the file declares none - Add the matrix layout — rows are groups, columns are beats — and the flow and room layouts
- Add a
/cr-framescommand that retrofits the contract onto an existing prototype file
Phase 6 — Canon and cohesion review
7 tasks Built. No live model call yet.- Add
StoryboardCanonresolving the canon markdown, the kit file, and the frame contract for a board - Add a canon pane with plain-text editing and a Refine with Claude turn that presents a diff before writing
- Add
StoryboardCohesionService.runReview(board:scope:)carrying the canon and the scoped frames' HTML - Add a per-frame HTML budget with comment stripping and repeated-data collapsing so a wide board still fits one turn
- Model the report as findings that cite a canon rule plus proposed canon amendments
- Render the report in a tray modelled on
DesignCohesionReportTrayView, with Send to chat and Apply amendment rows - Inject the canon into
MockupAIService.buildPromptas a section between Additional Context and Conversation History
Phase 7 — Generating from a slot
3 tasks Built. No live model call yet.- Add empty slots carrying an intent string and a beat
- Generate from a slot with the group's sibling frames and the canon attached as context automatically
- Write the generated file to
docs/mockups/and bind it to the slot on completion
Phase 8 — The browser round-trip
5 tasks Built, less the phone leg.- Add Copy Canon Packet for a board, a group, or a selection, ending with the return-fence contract
- Parse the
coderaven_storyboardreturn fence in the Lab import sheet withframe,canon, andboardkinds - Make the scanner see
docs/mockups/_inbox/one level deep and surface inbox items as unfiled - Add an Adopt action that files an inbox item into
docs/mockups/with a slug and optionally onto a slot - Route the mobile iCloud file drop into
docs/mockups/_inbox/
Phase 9 — Built status from the codebase
3 tasks Built. 21 of 45 screens matched on the first run.- Scan the project's source for view types and propose matches against registry screens
- Mark a screen built and link the source file, leaving the match editable
- Show a built screen's current implementation beside the mockup proposing to replace it
Phase 10 — From data entry to conversation
16 tasks File-reading half built. Shape revised after looking at Design Studio.- Add
RegistryFillServiceproposing a screen's purpose, states and beats by reading its own mockup - Link a screen to the spec written about it, from the spec's own "Accompanies" line
- Propose flows from
*-flow-spec.mdfiles already in the project, matching steps to registry screens by name - Propose a unit's loop from the project spec, leaving it blank rather than inventing one when the spec is silent
- Review fills in one sheet per unit, with confidence per field and only high-confidence pre-ticked
- Add
RegistryEdit— a typed vocabulary of create, rename, move, split, merge, setPurpose, setStates, setFlow, setLoop - Apply a
RegistryEditbatch through a diff review, reusing the accept-or-reject shape the seeder and the cohesion tray already use - Add a revise bar to the screen, unit and registry views, phrased as a question rather than a blank field
- Give the pane a visible scope chip — this screen, this unit, the whole registry — defaulting to the selection and changeable
- Add a prev/next stepper so screens in a unit can be worked through without returning to the list
- Persist one conversation thread per project so the pane accumulates memory instead of re-establishing context each time
- Offer openers derived from what is blank — screens with no purpose, units with no loop, games with no commit beat
- Add a model pass filling the fields no file answers, so a card arrives full and the only question left is what to change
- Extend the cohesion review to propose registry edits alongside canon amendments, so "these are the same screen" becomes a merge
- Make screen generation callable from the conversation, so a missing screen can be asked for rather than navigated to
- Write a glossary of board, registry, unit, screen, variant, version, state, beat, flow and step into the canon, and draw both the UI tooltips and the prompts from it
Risks & edge cases
A fill pass can be confidently wrong at scale
Proposing a purpose for 45 screens produces 45 plausible sentences, and plausible-but-wrong is harder to catch than blank. Per-unit scope and per-field confidence are the mitigations; neither is a guarantee, and a fill that is usually right is exactly the kind of thing that stops being read.
A conversation can become the same manual work with extra steps
If every request needs careful phrasing, the pane is a worse form than the fields it replaces. Openers derived from blank fields are the hedge, and they only help if they name the work actually wanted.
Conversation will not fix the vocabulary, and may worsen it
A model using board, unit, screen and view loosely teaches the loose usage. The glossary has to land before the pane, and both the UI and the prompts have to draw from it.
Board documents are large and get rewritten wholesale
Peregrine’s full wall is about 1.5 MB of generated HTML under
docs/storyboards/, and any change to any frame rewrites it. Boards of four to twelve
frames — the intended size — come to a few hundred KB. A 24 MB inline budget caps
the worst case; frames past it render as “too large to include”. Whether these belong
in git is a call worth making before the first one is committed.
The in-app surface is compile-verified only
The generated document is verified against Peregrine’s real files through a headless
WKWebView harness — 45 of 45 frames, the state contract, the missing-file cell.
The SwiftUI chrome around it (navigation, the arrange sheet, the zoom slider) has never been run,
because this environment cannot drive a native macOS GUI.
Review context volume
Forty-nine frames of full HTML will not fit one turn. Boards are expected to hold four to twelve frames and the review is scope-limited, but a wide matrix over long prototypes can still overflow. The per-frame budget task is a mitigation, not a guarantee.
A board is a snapshot taken when it was last rendered
Opening the board re-reads its frames and rewrites the document, so it is current whenever it is
looked at in the app. The copy sitting in docs/storyboards/ between openings can lag
the mockups it shows.
Fonts
All 47 Peregrine frames @import Google Fonts, so a full board issues that many font
requests. It rendered in under three seconds on a warm connection; offline it degrades to a
fallback face.
Thumbnails stay cold
MockupWebView captures a snapshot only once the detail pane has been opened, so grid
cards for the 49 reference files remain placeholders. The storyboard renders live rather than
fixing that.
Out of scope
Four things deliberately left out
Annotation on the board. Annotating keeps going through the existing per-session preview pane.
Writing back into externally authored files. Fork remains the sanctioned iteration path.
Any change to Design Studio’s Mockups phase. The cohesion review here is a second implementation against a different model, and the two are not merged yet.
Board version snapshots.
Final review
The plan’s strength is that it reuses what already works and inverts one assumption.
The 49 Peregrine mockups are already imported, MockupDocScanner already joins files to
sessions, MockupProjectExporter already owns docs/mockups/, and Design
Studio’s cohesion review is a working template. The inversion — treating a frame as
(file, state) rather than as a file — is what makes the comparison matrix possible, and it comes
straight from the harness bar already present in the prototypes that work best.
It is weakest at the boundary between CodeRaven and claude.ai. Phase 5 is a clipboard protocol, and a clipboard protocol is only as good as the discipline applied to it. It will work; it will not feel like a door opening by itself.
The second weakness is that the cohesion review is a second implementation of something Design Studio already has. That defers a merge, and it means a bug fixed in one will not be fixed in the other.
The rendering question turned out to be the load-bearing one, and settling it early was worth more than any amount of further design. The linked-frame approach read better on paper and fails at 45 frames; only a measurement would have caught that.
One adjustment worth considering: Phases 1 and 2 could merge. A board without state-switching undersells the idea for exactly the files that matter most, the contract parsing is small, and the state mechanism is already verified working.
Phase 10 exists because testing said something the pass rate could not. Fifty-three of fifty-four implementation tasks were complete and the thing still felt like data entry — a design failure no amount of green ticks would have surfaced. The lesson repeats Phase 1’s: the load-bearing questions here get answered by using the thing, not by planning it.
Phase 10 only, up from 63. The scope question resolved cleanly — one pane whose scope follows the selection answers both needs without two surfaces — and Design Studio supplies a working precedent for the card grammar rather than a guess. The file-reading half is built and measured. What still holds it down is the generate-first half: cards must arrive full for the pane to feel like conversation, and a model writing 45 purposes is the part most likely to produce plausible wrongness. The plan as a whole stands at 72.