# Lab Report — §VM-01-G4: Class D per-verb (the migration front's last slice) > **Status: CLOSED — design locked `f340143` 2026-08-04 · ask answered (refuse-at-click) · > G4a `b905733` · G4b `f7a60a5` · G4c `4c2a831` · G4d `b0c2478`, all shipped 2026-08-04/05.** > **Verified against live `play.html` on 2026-08-23 (§DOC-02dc)** — 618 → this. The design > pass and its four ship addenda are compressed into §3–§7; every surviving number is re-measured, > every dead one is marked. Parent: [`lab-report-vm01g-migration-front.md`](lab-report-vm01g-migration-front.md) > (verified 2026-08-23, §DOC-02db). Policy: **Host/Script Separation** + **Lab Report Policy** > (CONTRIBUTING.md). --- ## Abstract The §VM-01 track replaces per-node `if (node.code === 'XYZ')` branches inside `storyRender` with small declarative vocabularies. G4 was its last block class: **Class D — the conditional button that costs something and changes the world.** This report's design pass made three calls that the next twelve slices had to live with, and it made them from measurement rather than from the parent report's taxonomy. Two of them were corrections *to* that taxonomy: the `choice` opcode shipped six weeks earlier **had no host end and could only throw**, and "Class D" was not one shape but three, only a third of which are choices at all. The third call was a single missing opcode — `cost`. **Scored 19 days and eleven further slices later: the engineering held completely.** All 21 named symbols resolve, four of five re-measurement figures reproduce byte-exact at their own cited hashes, every test count in the addenda is exact (57/57 across six files), and `cost` was in fact the last opcode the VM ever needed — `src/js/quest.js` has had **one commit since**, and the parity gate still prints the same **25,030 bytes**. What did not hold is smaller and more interesting: one inherited number is scope-mislabelled, one correction is right for the wrong reason, and **three surfaces this report itself classified as choices were never given a slice** — while the plan declares itself COMPLETE. --- ## 1. Intent, inspiration, and what it does for play **The inspiration is a complaint about adventure games**: the moment a game stops telling you what it wants and starts quietly withholding the option, you cannot play it — you can only guess at it. The track's working phrase is **telling-vs-asking**. The whole §12 ask below is that phrase turned into a design knob, and the user answered it the way the phrase implies. **What the feature adds to the game, concretely:** 1. **The world can ask you a question and take your answer.** Before G4a, a branching beat had to be hand-built as raw DOM by whoever authored it. After it, `{kind:'choice', prompt, options}` renders real buttons, waits across a render, and applies **only the branch you picked**. Kern & Sable at `DUS` is the first conversation in this game's history that is *data*. 2. **The game says its price out loud.** `cost` refuses **at click**, never at render — an unaffordable verb still lists, still reads, and answers *"💰 You don't have 50gp."* An option you can see and cannot afford is information; an option that silently vanished is a bug the player cannot report. 3. **Narrative that used to be destroyed now survives.** `storyMsg` *replaces* `#story-move-msg`, so an inline handler that spoke and then re-rendered erased its own line. The verb driver buffers a chain's narrative and hands it to `storyRender` as the **prefix**. Six authored beats have become readable for the first time because of this — Yva's speech, Brynn's firewood, Kenickie's *"Yeah. Okay. I'll hold onto this,"* and three more in later slices. 4. **A beat that is data can be tested, and one that is DOM cannot.** Every surface moved here is now covered by a golden-DOM-plus-bounding-box diff and a pinned registry, so the class of rot the whole track exists to stop — *a beat that quietly stops firing and nobody notices* — is now a failing gate rather than a player's puzzled silence. **A design constraint worth keeping in view:** the reason this report refuses to force `choice` onto all of Class D is the same reason it refuses to invent a `prompt_text` ask for two sites. The parent's warning — *forcing one tool onto five classes "is how `itemsMinAny` happened"* — is a standing hazard in this repo, and §4 below is that warning being obeyed inside the slice written to honour it. --- ## 2. Method Read all 22 blocks end to end at `ca0113c`; no sampling. Verification (2026-08-23) added: every named symbol through one batched `grep -c`; each re-measurement figure re-derived **at its own cited hash** via `git show :play.html`; the six test files run at HEAD rather than trusted from the ship record; and `git log -S` on the one symbol whose stated rationale looked wrong. **Citation convention.** Section numbers of the form §4, §9.1, §9.3, §10, §12⅞ refer to the **original 618-line report's** numbering and are kept so its four ship addenda and the §VM-01-G4c-FU backlog row stay traceable. §-numbers without a decimal (§3-F1, §6-N2) are this document's own. --- ## 3. The design pass — six findings, as locked **F1 — `choice` had no host end.** Inc A built the suspending half correctly (`*execBits(bits, ctx)`, `*choice(bit, ctx)`, the `_uqfPump` slot) and the half that *renders* an ask and resumes with an answer was never written. `renderChoiceBlock` — the name the old comments promised — occurred **0 times** in the file and 0 times in `src/js/quest.js`. All four live `execBits` entry points wrapped in `_uqfRunToCompletion`, which **throws** on an ask. So *"migrate the buttons to `choice`"* was two steps, and the second was the prerequisite. **G4a therefore ships the host renderer with zero content migrated.** **F2 — "Class D" is three shapes.** The parent's description fit the *click handler* of all 22 blocks and the *surface* of very few: | | Shape | Count | |---|---|---| | **D1** | one button, no alternative, panel retired on use | 13 surfaces | | **D2** | N mutually exclusive options; picking any retires the panel | 7 surfaces / 6 blocks | | **D3** | concurrent verb menu — all visible at once, none exclusive | 1 surface, 5 verbs (`CDG`) | `choice` is **exclusive by construction**. A D1 through it is a one-option choice — a labelled effect chain with ceremony. A D3 through it is an outright behaviour change: three concurrent boss buttons would become *pick one, the other two vanish*. So the vocabulary is a **verb** — `{id, node, when, label, bits}` — of which `choice` is one possible bit. **F3 — two blocks capture free text, and must not move.** Entry 42 (`LHR`) and the Secret Gate void-toll rune (`ZRH`) render a `