# Lab Report — §VM-01-G: The Migration Front ## Retiring per-node special cases from `storyRender` into three small registries **Authored:** 2026-07-28 (design lock) · **Ship addenda:** 2026-07-28 → 2026-08-05 · **Re-verified against `play.html` @ `a66119f` on 2026-08-23 (§DOC-02db)** **System:** `` `function storyRender(node, prefix) {@34567` `` and the registries `` `const NODE_PANELS = [@31318` ``, `` `const NODE_HOOKS = [@34190` ``, `` `const NODE_VERBS = [@34291` `` **Status:** ✅ Design held · ✅ 11 slices shipped · ⚠️ 2 censused blocks unclaimed · ⚠️ 2 design asks still open > **This is a HISTORY document.** It records what was *designed* on 2026-07-28, what each slice > *shipped*, and what is *live* now. The maintained homes are `mechanics.md` (systems), > `world.md` / `story.md` (content), and BACKLOG.md (open work). **Where those and this report > disagree, they win.** Claims that did not survive are marked and kept, never deleted — a > silently removed claim reads as a claim that held. --- ## 1. Abstract `storyRender` had become two programs stacked in one function: a 2,445-line wall of hardcoded `if (node.code === 'XX')` blocks, then a generic data-driven section engine. The boundary between them — the *migration front* — was moving the wrong way: new content shipped as new bespoke blocks because the render language had no vocabulary for the shapes content authors kept reaching for. This report locks a design (three small registries, each matched to a class of block), slices the migration into eleven independently shippable steps, and records what each step actually found. **Re-verified at 26 days: 79 of 79 named symbols resolve (100%).** `storyRender` is **4,412 → 1,489 lines (−66%)** while the file *grew* 37,953 → 38,712 lines; `node.code ===` comparisons are **125 → 96**, and below the front **47 → 3**. The design decision (Option C, layered) held; the *class-to-tool mapping* inside it did not — `NODE_HOOKS` absorbed 61 blocks against a predicted ~9, for a reason no slice could design around. Ten deltas and five new findings are recorded in §10–§11, including one that takes the whole Act VIII homecoming off the board. ## 2. Method Every claim was re-measured against the live file by `grep`, `sed` and `git show` — not carried forward from the July text and not read off a doc table (§AUDIT-03m). Historical claims were re-derived at the hashes the report itself cites, so a drifted anchor is distinguishable from a wrong one. Behaviour was tested, not inferred: **147 assertions across 14 Playwright suites were run at HEAD** (§9). The WBAPI server was **not** started; the working tree carries an in-flight CSS recolor and a write session would have engaged Hazard #1. **Counting convention.** A `node.code === 'XX'` occurrence inside a comment is prose, not a comparison — the §AUDIT-03f lesson, now enforced by gate #13 phase 6. Raw textual counts are given first, code counts in parentheses, so the two never quietly swap places. --- ## 3. Intention, inspiration, and what it buys the player The original report justified this work as line reduction. That is the least interesting thing it did, and the eleven ship records now show why. **The thesis is a language thesis.** When a data language lacks a shape, the file does not stall — it grows imperative code instead of vocabulary. Measured at lock time: **76 → 124 comparisons in twelve days**, roughly four new hardcoded per-node branches per day, every one of them a small private program with its own guards, its own DOM ids and its own idea of what "already happened" means. None of that is visible to a player. All of it is visible in the bug rate. **What the player gets is not architecture — it is the audit the architecture forced.** A block cannot move until someone proves the move is a no-op, and proving that means reading the block line by line against a golden capture. Eleven slices of that reading found things no bug report ever surfaced: - **Glut's Gift was never granted.** Its once-guard was `!visited['HG1']`, and `storyCollectLoot` flips `visited[code]` earlier in the same render — so the jar the node's own text promises (*"She gives you the jar as you arrive. … It is warm in your hand."*) never entered the inventory. The prose and the mechanics disagreed, and the prose was right. - **`quest_glut_06` was dead a second way** — circular completion, its only flag writer being its own `onComplete`. Invisible, because the quest never listed in the first place. - **The LCY counterfeit-writ beat was circular-dead since it shipped**, so the Scene-5 confrontation listed "the three claims" when the first had never been witnessed. - **The entire Visby and Tilbury arcs had never run**, because `NODE_MAP.VS` and `NODE_MAP.TL` carried no `code` field. Verified historically: `VS` gains `code:"VS"` in `c9f3946` and had it in no prior revision. - **Six double-pays** — the Aldous confrontation paying +800gp and two Letters of True Passage, DA2 granting a *second permanent INT point*, DA3 a duplicate knowledge entry, DSF a second Sea Element, plus the Ori and seal-delivery pairs G3 found. - **Six authored narratives that no player had ever read**, destroyed by their own handler's bare `storyRender` before the eye could reach them. That is the return on this refactor: **ten beats that promised something and delivered nothing, found by moving furniture.** The vocabulary is the mechanism; the reading pass is the payoff. And the vocabulary keeps paying — a `NODE_PANELS` entry cannot have a dead once-guard, because it has no guard of its own to get wrong. **What it adds to the game.** 416 nodes that can each carry state-aware flavour, a one-time arrival scene, a priced verb or a bespoke interface — authored as three lines of data instead of forty lines of DOM code, and fenced by a gate that fails the build when a node code goes stale. Density the player can walk into, with fewer places for it to quietly stop existing. --- ## 4. The finding, as locked (2026-07-28 @ `8973fd7`) Re-derived at the cited hash; all three anchors verify exactly. | Measure | Locked value | Re-derived 2026-08-23 | |---|---|---| | `storyRender` span | 30887–35298 = **4,412 lines** | ✅ exact | | Migration front (`_mkSection` definition) | **33332** | ✅ exact | | `node.code ===` in `storyRender` | **124** | 125 raw / **124 code** ✅ | | …above the front (30887–33332) | 77 across ~58 named blocks | 78 raw / **77 code** ✅ | | …inside the engine region (33332–35298) | **47** | ✅ exact | | Drift since the row was written (2026-07-16) | 76 → 124, **~4/day** | consistent | The front had moved **backward** by 48 comparisons in twelve days. §D01 dungeon gates, §SIREN-01, §PAUL-01, La Riva and the §BOARD signposts all shipped as new bespoke blocks. The thesis was not theoretical; it was a rate. ## 5. Block taxonomy Five shapes cover the ~58 named blocks above the front. | Class | Shape | Count | Data-expressible? | |---|---|---|---| | **A** State-gated flavour panel | remove-by-id → `if (code && flags)` → styled div → `insertAdjacentElement('afterend')` | ~22 | **Yes** — `{id, node, when, style, text}` | | **B** One-time arrival scene | Class A plus a once-guard flag | ~8 | **Yes** — A + `{once}` | | **C** Legacy quest auto-activation | `if (code) { if (!quests[id] && cond) quests[id]='active' + storyMsg }` | ~7 | **Yes** — this *is* `gate` + `activateNode` | | **D** Effect-button (consent verb) | conditional button → gold cost, flag writes, item grant, favor, unlock | ~12 | **Mostly** — needs a `cost` leaf | | **E** Bespoke UI | real interfaces with their own state machines | ~9 | **No** — the fix is *registration*, not data | The 47 engine-region exceptions were logged as Class F, deferred, so the count stayed honest. ## 6. Two hazards that constrained every slice 1. **Insertion order is LIFO.** Every A/B/D block inserts `afterend` of `#story-text-box`, so *later blocks render above earlier ones*. Any dispatch must be order-preserving by construction, or every multi-panel node reshuffles visually. 2. **Remove-then-recreate.** `storyRender` is re-entrant; each block removes its old div by id before conditionally recreating it. The dispatcher must replicate: sweep, then create. Both proved load-bearing. Hazard 1 alone kept four blocks out of `NODE_PANELS` across three slices, and it is why every hook is dispatched **in place** rather than from one loop. ## 7. The design decision — Option C, layered (locked by the user, 2026-07-28) Neither *all-bits* nor *all-render-fns* fits, because the classes want different tools; forcing one tool onto all five is how `itemsMinAny` happened. - **C1 `NODE_PANELS`** — ordered data table for A+B. Pure render data, no VM, `textContent` only (the §DATA-01 lesson). - **C2 `NODE_HOOKS`** — registered render fns for E. Bodies move verbatim; behaviour unchanged. - **C3 into the VM** — C becomes `gate` + `activateNode` + `onActivate`; D becomes `choice` and verbs, needing exactly one new opcode: a **`cost`** leaf, used by Yva, Corelli and ferry-class buttons alike. **Rejected:** all-bits (a DOM-building opcode is the Host/Script Separation violation this track exists to end) and all-render-fns (registering 58 functions relocates imperative code without retiring it, and the front keeps moving). --- ## 8. Slice plan → shipped: the delta table | Slice | Spec (2026-07-28) | Shipped | Hash | Verdict | |---|---|---|---|---| | **G1** | `NODE_PANELS` for A+B, ~700–900 lines → table + ~40-line renderer | 12 panels; two dead panels found and fixed (§LXII AO once-guard, §SIREN LJ3→LSO dead code) | `25c3710` `7a130cd` `bf6f486` | ✅ **as specified** | | **G2** | `NODE_HOOKS` for E, one dispatch loop | 7 hooks, ~440 lines — but **in-place dispatch, not one loop**: Corelli's computed 5-node schedule has no single safe position (Hazard 1) | `f1ff031` | ⚠️ **shipped, design deviated** | | **G3** | C → `gate` + `activateNode` per arc | 5 blocks, 15 quests + 3 adjacent. Premise was half-stale: most already carried an audit-appended `activateNode` and had been mass-activating with vacuous gates; `actNumber` proved to be node act, not progress, so **no `actMin` leaf was needed**; `VS`/`TL` had no `code` field and had never run | `c9f3946` | ⚠️ **shipped, premise rewritten** | | **G2b** | blocked on "a ctx-argument design" | The ctx was **one field** — of 89 locals, the 29 Birka blocks read only `npcRowDiv`. 895 lines moved byte-for-byte | `bedf2c1` | ✅ **blocker dissolved by measurement** | | **G4a–d** | D per-verb; own child report | `choice` gains a host end; `NODE_VERBS` invented (not in this design); `cost` leaf ships; CDG's concurrent menu | `b905733` `f7a60a5` `4c2a831` `b0c2478` | ✅ **child report, as required** | | **G-FU** | "triage the 47 after G1–G4" | Count **held at 47**. Found a *second* special-case stack **below** the front (~1,200 lines, ~38 blocks) and shipped the fence first: gate #13 phase 6, 20 classified synthetic battle codes | `02ff4aa` | ✅ **+ one thesis correction** | | **G-FU-a** §CROWN-01 | 8 blocks; combat verbs, iodine 2-verb sequence, marks once-panel | 7 verbs, 2 verbatim hooks, 2 panels. Two of three shapes corrected: WG0's button is panel-embedded; the iodine burn writes a **numeric** field `flag_write` cannot express | `56c08f1` | ⚠️ **2 of 3 shapes corrected** | | **G-FU-b** §HUNT-01/02 | 6 blocks, "2 combat verbs" | **Zero verbs** — both battle buttons are panel-embedded (the WG0 rule, 2nd application); 5 panels; WRO's done panel stayed in its hook for a measured stacking reason | `cf2fec8` | ⚠️ **corrected; 22/22 zero-delta** | | **G-FU-c** Roen arc | 8 blocks; "the second `choice` consumer" | **Zero verbs, no `choice`** — the VS shadow pair has real D2 semantics on ask-2-blocked chrome; 6 hooks, 4 panels | `7d8cb39` | ⚠️ **corrected; 37/37 zero-delta** | | **G-FU-d** harbor chains | "GCI intercept = D3 menu" | Corrected **by a string**: `sbChosenRole` is an enum `flag_write` cannot express. 6 hooks, 3 panels, zero verbs. MME hull repair pays through the **`cost` leaf** — the 7th gold site, the plan shape that held | `8cdda7b` | ⚠️ **corrected; 46/49** | | **G-FU-e** §LXX family | "small verbs + done-panels" | **One** verb (DSF smelt, the family's only bare button); 4 hooks, 3 panels | `3dfdc26` | ⚠️ **corrected; 20/25** | | **G-FU-f** SSJ + specials | "SSJ → hooks verbatim, no design call" | Exactly that. 124 lines, line-multiset 124/124 | `6a571de` | ✅ **first slice with zero corrections** | **One pattern dominates the "corrected" column**, and it was not in the taxonomy: the **panel-embedded button** — a `