glytch/fktry/lanes/LANE-UI.md
type-two 83e3ad2107 [orchestrator] round 3 review + contracts v4 + composite seam codex + round 4 orders
Review: 5/5 lanes pass, 238/238, tsc clean. Verified live: scram duty-cycling with
tanks covering (zero brownout), shipment stamps, strain fever 0.824. Found 1 real bug
in review: UI BROWNOUT banner fires on covered deficit (topstrip.ts:66) - ordered.

Contracts v4: research (lab kind, setResearch, ResearchState, researched event),
SelectionState protocol (kills __remove sentinel, main.ts owns remove dispatch),
accent?, coolRadius? (spatial cooling), bandwidth.capacity?, save/load hardened,
heat ruled GROSS (net = heat - cool). tsconfig now typechecks data/ (DATA's proof).
Orchestrator patched lab entries into exhaustive kind maps (my breakage, my fix).

Rulings: pathspec-commit rule after 3 shared-index collisions (worktrees = named
escalation); bloom escalator reading accepted; UI ?uidemo + SCREEN boredom approved.

Codex: docs/FKTRY_LORE.md grew §10 THE COMPOSITE SEAM - keying/transparency artifact
family from the Corridor Crew compositing history + our corridorkey-mrp-mlx (which
doubles as a PYXLFK ground-truth artifact factory). DATA transcribes it round 4.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 19:43:48 +10:00

370 lines
24 KiB
Markdown

# LANE-UI — HUD, build tools, and the voice of the game
You are an Opus 4.8 executor on the UI lane of FKTRY. Read `../MASTERPLAN.md` and
`../CONTRACTS.md` first; the world bible is `../../docs/FKTRY_LORE.md`. The game's
voice is §1's "OSHA-poster deadpan" — you write most of the words the player reads.
Every label, tooltip, and error is an opportunity; "Inventory full" is a crime when
"OUTPUT BUFFER AT CAPACITY. THIS IS A YOU PROBLEM." is available.
**Mission:** everything DOM. Build bar, inspector, resource readouts, commission fax,
pause/speed, hotkeys. Plain TypeScript + DOM — no framework, no CSS libs.
**Owned paths:** `src/ui/**` (styles included — inject a `<style>` tag or import a css
file from your dir).
**Never touch:** contracts, main.ts, other lanes. You never mutate snapshots; you
dispatch Commands via the bus and read state from `update(snap, events)`.
**Standing rules (all rounds):**
- `#ui` root has `pointer-events:none`; every interactive child you create re-enables
it. Clicks must fall through to the game world everywhere else — test this.
- Keyboard map lives in one file (`hotkeys.ts`) — it will grow.
- Terminal-industrial aesthetic: monospace, `#d8ffd8` on dark, hot accents `#ff3fd4` /
`#3fffe0` (match GLYTCH's palette; see `../../index.html` for reference). No rounded
corners. This is machinery, not a startup.
---
## CURRENT ORDERS — Round 4 (goal: research UI, one real bug, protocol migration)
Round 3 review: `live.test.ts` — booting the real sim against the real DOM — is the
best verification asset any lane has produced; the SANITISING machine-swap showed
exactly the right instinct when an order's premise fails. Verdicts: CONTRACT REQUEST 1
GRANTED (v4 `SelectionState` + `setSelection`/`selection` on the bus — main.ts now owns
the remove dispatch; see order 2). Request 2 GRANTED (`MachineDef.accent?` — your
derivation becomes the documented fallback, DATA fills the field where it's wrong).
`?uidemo` PROPOSAL APPROVED. Tech-panel skip was right at the time — but it was
another round-start grep going stale: `tech.json` has 24 nodes at HEAD (DATA landed
mid-round again). It's a real order now.
1. **BUG, found live in review: the BROWNOUT banner lies.** `topstrip.ts:66` keys on
`out || ratio > 1` — during a covered deficit (tanks draining, sim `brownout:
false`, THE SCREEN correctly calm) the panel screams BROWNOUT. That state is
*survival*, not failure. Fix: BROWNOUT = the flag only; deficit-with-reserve gets
its own amber state (suggest "ON RESERVE · 34s" — `stored/(draw-gen)` is seconds
left, same math SCREEN uses for dread). Add a test pinning covered-deficit ≠
brownout.
2. **Selection protocol migration**: write via `bus.setSelection(...)`, drop the
`REMOVE_DEF` sentinel export and — important — **delete your own `remove`
dispatch**: main.ts dispatches it on click now (double-fire today is harmless
only because the second one no-ops).
3. **Research UI, the centerpiece**: `T` opens the tech tree (24 nodes, 5 eras),
pick → `setResearch`; progress from `snapshot.research` (packs delivered vs
cost); locked machines in the build bar get greyed + padlock + "REQUIRES:
<tech>" tooltip (lock rule in CONTRACTS v4 notes); `researched` event → toast in
house voice ("STANDARD RATIFIED. NEW CRIMES AVAILABLE.") and the padlocks fall
off live.
4. **`?uidemo`** — as approved: boots `__fktryDemo()` after init.
5. **Accent adoption**: read `accent?` first, your derivation second, kind table
third (same precedence as RENDER — one sentence in NOTES confirming you both
read it identically).
**Definition of done:** banner distinguishes RESERVE from BROWNOUT under test; a
full research run is verifiable in `live.test.ts` (lock visible → research → unlock
→ place succeeds); remove mode works through the typed protocol with RENDER's tint.
---
## NOTES (append-only log, newest at bottom)
```
### Round N — YYYY-MM-DD — [your model]
SHIPPED: / DECISIONS: / BLOCKED/BROKEN: / CONTRACT REQUEST: / PROPOSAL: / NEXT:
```
### Round 1 — 2026-07-17 — Opus 4.8
**SHIPPED:** All five ordered surfaces, in `src/ui/**`, verified in a browser (screenshots
taken of each). Modules: `index.ts` (root/wiring), `style.ts` (injected CSS), `voice.ts`
(all player-facing copy), `hotkeys.ts` (the keymap, one file as ordered), `buildbar.ts`,
`topstrip.ts`, `inspector.ts`, `fax.ts`, `toasts.ts`, `chips.ts` (shared item chip),
`selection.ts` (writes `bus._sel`), `palette.ts`, `pick.ts`, `dom.ts`, `fixture.ts` (dev).
1. Build bar — one button per machine, hotkeys 1-9, R rotates, Esc/right-click clears,
selected button goes hot magenta, hint line shows facing + verbs.
2. Top strip — bandwidth meter (cyan → amber ≥85% → red + flashing BROWNOUT over 100%),
buffer, shipped ticker + per-item chips, TICK / HALTED.
3. Inspector — name, codex flavor, INTAKE/OUTPUT chips w/ live buffers, process bar,
heat readout w/ THROTTLING, jam reason in amber. Esc or X closes.
4. Commission fax — fax-paper card under THE SCREEN, wants as chips w/ `1/1` progress,
reward line, and a FAX SENT rubber stamp on `commissionDone`.
5. Toasts — max 4, 6s TTL, colored per event kind, house voice.
**VERIFIED:** `npm run check` clean; `npm test` 70/70 (other lanes' suites). Dev server on
:8151 (8150 was another lane's session). Confirmed with own eyes: hotkey 1 → `bus._sel`
`{seam-extractor,0}`; R → dir 1 → 2; Esc → null; TAB → inspector paints SEAM EXTRACTOR
with flavor + MDAT ORE chip + process bar; brownout → red border/BROWNOUT/buffer 0 + both
toasts; FAX SENT stamp + its toast; pointer fall-through (`#ui` computes `pointer-events:
none`, empty world hits CANVAS, panels are solid). No console errors.
**DECISIONS:**
- All player-facing strings live in `voice.ts` so the OSHA-deadpan register stays in one
place and isn't re-invented per panel.
- The UI keeps its own flattened `roster` (pick.ts) instead of holding the snapshot:
CONTRACTS says buffers may be reused between snapshots, and pointer/key handlers fire
*between* frames. The backing array is reused so 500 entities don't allocate per frame.
- Esc is overloaded deliberately: drops the pending build first, closes the inspector
second — dropping the tool is what a player expects from the first press.
**BLOCKED/BROKEN (read this part):**
- **Click-to-inspect does not work.** `ui.init(root, data, bus)` never sees the Renderer,
and `UIBus` has only `dispatch`/`selectedBuild` — so the UI cannot reach
`renderer.pickTile`, which is the only thing that knows the camera. LANE-RENDER has a
working `pickTile` this round; I just can't call it. `pick.ts` opportunistically probes
`window.__fktryRenderer.pickTile` (nobody sets it) and otherwise falls back to **TAB
cycles the unit manifest**, which is how the inspector is reachable with no console
today. Tile→entity hit-testing (incl. footprint rotation) is written and ready; it is
one contract line away from working. See CONTRACT REQUEST 1.
- **LANE-DATA shipped 21 machines; hotkeys only cover 9.** Slots 10-21 are click-only,
and that includes MOSH REACTOR, DECODE ASIC and SCREEN UPLINK — three of the seven M1
chain machines. The M1 chain is fully placeable by *mouse*, and 1-9 works, but "the M1
chain on hotkeys" is not literally true. The bar now wraps to 3 rows so nothing is off
the viewport (it ran off both edges at 21). See PROPOSAL.
- **No DOM tests for `src/ui`.** No `jsdom`/`happy-dom` installed and I may not touch root
config, so this lane has zero unit tests — everything above was verified by hand in the
browser. See CONTRACT REQUEST 4.
- `?uifixture` is dev scaffolding I wrote (`fixture.ts`) because the sim stub reported an
empty world at the time — a fake snapshot on a wall clock so every surface could be seen
working. It is inert without the query param and dispatches nothing. LANE-SIM is real
now; this can be deleted once the real sim can be driven to a brownout by hand.
- Harness note, not a bug: the browser-automation `key: space` delivers an empty
`event.key`, so pause could only be exercised via a dispatched `' '` KeyboardEvent
(works: HALTED renders) and via `dispatch({setPaused})` (works). Real spacebar sends
`' '` per spec; the handler matches that.
**CONTRACT REQUEST:**
1. **`UIBus.pickTile(clientX, clientY): Vec2 | null`** (or pass the `Renderer` to
`ui.init`) — *why:* without it the inspector cannot be opened by clicking a machine,
which is the core interaction of order #3. Renderer already implements it; only the
plumbing is missing. This is my only real blocker.
2. **`MachineDef.color: string`** — *why:* `ItemDef` has `color`, `MachineDef` doesn't, so
the build bar has nothing data-driven to tint icons with. LANE-RENDER's orders say to
tint placeholder boxes "per codex color" — same gap. Right now `palette.ts` and their
renderer each hand-maintain a color table, which is how the build bar and the world end
up disagreeing about what a quantizer looks like. Data should own it once.
3. **`MachineDef.flavor: string`** (codex one-liner) — *why:* orders told me to hard-code
a lookup; I did (`voice.ts`), but only 7 of 21 machines have one and the other 14 fall
back to generic text. This is LANE-DATA's material, not mine.
4. **`jsdom` (or `happy-dom`) devDependency + `environment: 'jsdom'`** — *why:* the UI lane
currently cannot write a single test. Chips/roster/footprint/selection logic are all
pure and testable the moment there's a DOM env.
**PROPOSAL:** build bar paging — group the 21 machines into kind tabs (EXTRACT /
LOGISTICS / REFINE / POWER / SHIP), `[`/`]` to page, 1-9 within the active page. Makes
every machine hotkey-reachable and scales as LANE-DATA keeps adding. Not done this round:
the orders said "one button per machine, hotkeys 1-9" and redesigning the bar unasked is
exactly what MASTERPLAN says not to do. Say the word and it's a small round-2 change.
**NEXT (suggested):** wire click-to-inspect the moment request 1 lands and delete the TAB
fallback; recipe picker in the inspector (`setRecipe` exists in Command and has no UI);
build bar paging; tech/research panel once `tech.json` is populated (it's `[]`).
### Round 2 — 2026-07-17 — Opus 4.8
**SHIPPED:** All five orders. New: `pages.ts` (pagination), `chipspec.ts` (pure chip
arithmetic), six `*.test.ts`. Deleted: `fixture.ts` and the TAB-cycle fallback, as ordered.
1. **Click-to-inspect** via `bus.pickTile`. The `window.__fktryRenderer` probe and the
TAB fallback are gone.
2. **Build bar paging** — kind tabs (EXTRACT / LOGISTICS / REFINE / POWER / SHIP), `[`/`]`,
1-9 within the page. Selecting from anywhere pulls the bar to the owning page.
3. **Inspector** — recipe picker (`setRecipe`), heat bar w/ THROTTLING/SCRAM states,
jam wording via `jamText()`.
4. **Fax** — NEXT IN TRAY peek, STANDING ORDER stamp on `repeat`, SANITISING chip on
shipped anchor-slabs.
5. **57 tests** (min was 10), all green.
**VERIFIED** (against the real sim, in a browser, on a static `vite build`+`preview` —
see BLOCKED for why not the dev server): built a SEAM EXTRACTOR by mouse → DRAW 0→4 with
no generator → **real BROWNOUT**, red panel, buffer 0, underrun toast, THE SCREEN
visibly corrupting. Clicked the unit → inspector opened on entity 1 (`pickTile` →
{18,-7} → `entityAt`). Placed a SOFTWARE DECODER → GEN 0→55, brownout lifted, "BUFFER
RESTORED. RESUME SINNING." Recipe picker: options rendered as readable processes ("1
LUMA BARS → 1 COEFFICIENT PACK + 1 HF DUST"), selecting IDLE dispatched
`setRecipe{entity:1,recipe:null}` and the sim round-tripped it back as "UNIT IS BEING
PAID TO EXIST"; selecting back dispatched `recipe:'quantize'`. Starved wording live in
both toast and inspector: "QUANTIZER: INTAKE STARVED. NOTHING IS ARRIVING." Paging walk
confirmed all 21 machines across 6 pages (1+3+9+5+2+1) with **no page over 9 slots**.
`#ui` still computes `pointer-events:none`. `npm run check` clean; 160/160 tests.
**DECISIONS:**
- **Kind tabs alone could not satisfy "every machine hotkey-reachable at any catalog
size"** — REFINE holds 14 crafters and 1-9 reaches nine. So a page is capped at 9 and an
oversized kind spills into numbered pages (REFINE 1/2, REFINE 2/2). The invariant the
order asked for now holds for any catalog; `pages.test.ts` pins it at 1/9/14/40 crafters.
- Logic was pulled out of the DOM modules into `pages.ts` / `chipspec.ts` / a pure
`createKeyRouter`, which is *why* there are 57 tests without happy-dom (below).
- Data-first everywhere: `MachineDef.flavor` and `.color` win, round-1 hand-transcribed
tables stay as fallback. LANE-DATA's flavor is what's on screen now.
- Unknown jam reasons pass through uppercased rather than being swallowed, so a new sim
stall reason shows up honestly instead of silently reading as generic.
**BLOCKED/BROKEN (read this part):**
- **`happy-dom` is NOT installed** — order 5 says "GRANTED — happy-dom is installed"; it
is not in `package.json` and not in `node_modules` (`npm ls happy-dom` → empty). I did
not add it: root config is orchestrator-owned and editing it is a hard rule. Instead I
factored the testable logic into pure modules and wrote **57 node-env tests** covering
everything order 5 named except the two that genuinely need a DOM: chip *rendering*
(the arithmetic is covered in `chipspec.test.ts`) and `attach()`'s window binding (the
routing is covered in `hotkeys.test.ts`). Install it and I'll add the render tests.
- **STANDING ORDER stamp is built but unobservable.** Data has 3 `repeat` commissions
(dust-allowance, correction-buyback, melt-retainer) but the sim pins the active
commission to `commissions[0]` (`first-taste`, not repeat) and never advances —
`commissionDoneEmitted` latches forever. So nothing can ever reach the stamp. Code is
data-driven and will light up the moment the sim rotates commissions.
- **NEXT IN TRAY is an inference, not truth.** The snapshot has `activeCommission` but no
queue, so "next" = the following entry in data order. It renders (NEXT IN TRAY → DUST
ALLOWANCE) but if the sim ever picks non-sequentially the peek will lie. See CONTRACT
REQUEST 1.
- **Heat bar unverified live.** Only `software-decoder` has `heatPerTick` (0.0025), and
it heats only while crafting; a decoder with no load sits at heat 0, so the bar
correctly stays hidden and I never saw THROTTLING/SCRAM. Thresholds are built to
CONTRACTS (>0.7 throttle, 1.0 scram). Needs a fed chain to confirm.
- **SANITISING chip unverified live** — needs anchor-slabs actually shipped, which needs
the full M1 chain fed. Renders off `shippedTotal`, so it is one shipment away.
- **The dev server is unusable for verification.** Other lanes are editing `render/` and
`screen/` continuously; vite logged **16 page reloads** in ~10 minutes, wiping the world
mid-test every time. I verified on `vite build` + `vite preview` instead. Not a
complaint — just: if a round asks for hands-on verification, expect this.
- During the round I hit LANE-SCREEN's `glitchStack.ts` and LANE-RENDER's `index.ts`
mid-edit and broken (tsc errors, dead rAF loop). Both self-resolved. `src/ui` was clean
throughout; the one failing test at the time was LANE-SIM's own `sim.test.ts`.
- Harness note (unchanged): browser-automation `key: space` sends an empty `event.key`;
space verified via a dispatched `' '` event. Real spacebar sends `' '` per spec.
**CONTRACT REQUEST:**
1. **`SimSnapshot.commissionQueue: string[]`** (or `nextCommission: string | null`) —
*why:* NEXT IN TRAY currently guesses from data order. Low cost, removes a UI lie.
2. **Sim: advance/rotate commissions and honour `repeat`** — not strictly a contract
change, a SIM ask: `commissions[0]` forever means 13 of 14 commissions and all 3
standing orders are unreachable, and the fax's whole queue UI is decorative.
3. **`happy-dom` actually installed** (see BLOCKED) — the grant didn't land in
package.json.
**PROPOSAL:** the inspector should get a REMOVE button (`remove` exists in Command with
no UI, so a misplaced machine currently can't be undone by mouse at all). Small, and it
closes the last build-verb gap.
**NEXT (suggested):** remove-by-mouse; heat/scram + SANITISING confirmation once a chain
can be fed; commission queue UI becomes real once requests 1-2 land; tech panel when
`tech.json` fills (still `[]`).
### Round 3 — 2026-07-17 — Opus 4.8
**SHIPPED:** Orders 1-4. Order 5 (tech panel) skipped — see BLOCKED. New: `live.test.ts`
(real sim + real HUD), `chips.test.ts`, `hotkeys.dom.test.ts`, `fax.test.ts`. **105 UI
tests** (was 57); full suite 238/238; `npm run check` clean.
1. **REMOVE mode**`X` or the REMOVE button beside the tabs arms it (button goes hazard
red, hint turns red, `[ ] PAGE · X REMOVE` in the idle hint). Click demolishes, Esc
stands down, and it *stays armed* between clicks because demolition is usually plural.
Toast: "UNIT RECLAIMED. THE FLOOR REMEMBERS." Clicking open ground does nothing, so a
stray click never reads as a broken tool.
2. **NEXT IN TRAY reads truth**`snap.commissionQueue[1]`. The data-order inference is
**deleted**, not kept: SIM *is* publishing the field (see BLOCKED for the race that
nearly made me miss it). No queue → no peek. A fax that lies quietly is worse than one
that says nothing.
3. **DOM tests** — chip rendering, `attach()`/`detach()` window binding + typing guards,
fax stamp lifecycle (incl. back-to-back stamps not clearing early), all under
happy-dom.
4. **Live pressure verification** — see VERIFIED.
**VERIFIED — live, in the browser** (screenshots taken; `__fktryDemo()` = 118 commands):
- The reference factory builds and runs: DRAW 94 / GEN 180, buffer charging, belts moving,
starved units toasting in house voice.
- **REMOVE end-to-end:** armed the mode (button red, `bus._sel` = `{def:'__remove'}`),
clicked the mosh reactor → **gone from the world**, "UNIT RECLAIMED. THE FLOOR
REMEMBERS." toast, **DRAW dropped 94 → 86** (its draw left with it), inspector
auto-closed because its entity vanished, mode still armed. That's the round's DoD.
- Inspector on a real MOSH REACTOR: DATA's flavor, PROCESS SELECT reading
"1 GOP CRATE → 3 MELT + 1 I-FRAME (ANCHOR SLAB)", INTAKE `GOP CRATE 0/1` amber, OUTPUT
chips, "INTAKE STARVED. NOTHING IS ARRIVING."
- Paging tabs + NEXT IN TRAY (DUST ALLOWANCE) + REMOVE all legible in one bar.
**VERIFIED — headless, against the REAL sim** (`live.test.ts`; boots `createSim()` +
`createUI()` on the real `data/*.json`, builds `referenceFactory()`, pumps ticks through
the same `update(snap, events)` path main.ts uses, then reads the actual DOM):
- **Heat → THROTTLING → SCRAM** on a software decoder, inspected via the real
pickTile→entityAt→open click path. Also pinned: the unit **still reads SCRAM while
cooling back below 1.0** — the exact case `heat >= 1` got wrong, now read off
`EntityState.scrammed` per contracts v3.
- **SANITISING on real anchor-slab shipments** (21 of them) — see BLOCKED for why this
needed a machine swap.
- **STANDING ORDER on a real repeat completion**: the factory runs, SIM's queue advances,
`dust-allowance` (repeat:true) reaches the head, the fax stamps it, and NEXT IN TRAY
matches `commissionQueue[1]` exactly.
- **REMOVE against the real sim**: entity count drops by one, the id is gone, toast fires,
stays armed, Esc disarms, open ground is a no-op.
**DECISIONS:**
- **The `__remove` ghost sentinel (LANE-RENDER: this is the hook — please read).** The UI
has no Renderer reference; the only channel to the ghost is `bus.selectedBuild()`, which
main.ts feeds to `setGhost`. So while remove is armed, `_sel` reads
**`{ def: '__remove', dir: 0 }`** — key your demolition tint off that def id. It is safe
because sim's place handler is `const def = defs.get(cmd.def); if (def) ...`, so main.ts's
click-to-place fires a `place` for `__remove`, matches no machine, and does nothing; the
real removal is dispatched by the UI. Exported as `REMOVE_DEF` from `src/ui/selection.ts`
and pinned by a test. A magic string is not how I'd design this — CONTRACT REQUEST 1.
- **Build-bar icons now follow the two-material rule** (codex §8), prompted by the v3
ruling that `color` is chassis, not accent. DATA's chassis values are all desaturated
neutrals — correct for the world, but 21 icons in `#4a4e52`/`#3f4348` is an unreadable
grey wall. So an icon is body (data `color`) + one emissive element **derived from the
machine's first output item colour** — the same derivation LANE-RENDER uses, so bar and
world agree. My round-1 id/kind tables were never wrong, only mislabeled (the same
mistake the v2 contract made); they now serve as the accent fallback for machines that
output nothing (belts, buffers, uplinks).
- Inspector reads `EntityState.scrammed`, not `heat >= 1`. Re-deriving SIM's hysteresis
here would silently desync the moment they retune `HEAT_RESTART` — exactly what
LANE-RENDER flagged in their round-2 notes.
**BLOCKED/BROKEN (read this part):**
- **The reference factory can never ship anchor-slabs, so SANITISING can't fire from
`__fktryDemo()` as the order assumed.** It recycles them on purpose — reference.ts:
*"Recovered anchor slabs come back out of the reactor. Rather than sell them, feed the
i-only assembler."* Confirmed by probe: 12k ticks ships `hf-dust`, `chroma-slurry`,
`macroblock-bricks`, `melt` — zero slabs, with `dct-press` sitting on `output full`. So
I verified it the way a player would: demolish the i-only assembler at (-6,5), drop an
uplink on it → **21 slabs ship, SANITISING lights up**. That's what `live.test.ts` does.
Nobody's bug; the order's premise just didn't hold.
- **Live verification of heat/scram/SANITISING in the browser is not practically
possible, and it's the harness, not the code.** The browser pane keeps the tab
backgrounded (`document.hidden === true`), which pauses `requestAnimationFrame`, so the
page only advances **~10 ticks per screenshot**. SCRAM needs ~435 ticks of continuous
generation and slabs need thousands — that's hundreds of captures, while parallel lanes'
HMR reloaded the page **~16 times an hour**, wiping the world mid-test (it ate four of my
attempts). Chrome-MCP was no better: it couldn't reach the dev server
(`chrome-error://`) and reported `hidden: true` too. Hence `live.test.ts` — same sim,
same DOM, same code path, deterministic, milliseconds, and it stays as a regression test.
I'd rather hand you that than a screenshot I had to fight the harness for.
- **I nearly reported `commissionQueue` as unpublished.** My first grep for
`snap.commissionQueue` found nothing and I wrote the fallback on that basis; SIM landed
it minutes later (parallel-round race — same thing that happened with `flavor`/`color`
last round). A probe against the real sim caught it. Worth noting as a pattern: in this
operation, "not landed yet" has a short shelf life, and greps at round-start lie by
round-end.
- **Order 5 (tech panel) skipped.** `tech.json` is **still `[]`** — an era tree rendering
an empty array is a panel that says nothing, and the order marked it skippable. It's
~30 lines whenever DATA fills the file.
- The `?showroom` / demolition-tint coordination with LANE-RENDER is one-way so far: I've
defined the sentinel above and pinned it, but they hadn't landed remove-tint when I
finished, so **the hover tint is unverified from my side**. If they key off a different
string, remove mode still works — it just won't glow.
**CONTRACT REQUEST:**
1. **`UIBus.setGhostMode(mode: 'build' | 'remove' | null)`** (or let `setGhost` take a
mode) — *why:* remove-mode hover currently rides a magic def id through
`selectedBuild()`, and main.ts fires a junk `place` command on every demolition click
that only works because the sim happens to ignore unknown defs. It's two lanes agreeing
on a string in a NOTES file. One typed field kills it.
2. **`MachineDef.accent?: string`** — *why:* deriving the icon accent from the first
output item is a decent heuristic, but it's a heuristic, and LANE-RENDER is running the
same one independently. If art direction ever wants a machine's glow to differ from its
product, there's nowhere to say so. (LANE-RENDER asked for this in round 2 as well.)
**PROPOSAL:** `?uidemo` — a URL param that runs `__fktryDemo()` on boot. Trivial, in my
lane, and it would have saved most of an hour this round: every HMR reload wiped the world
and needed a manual re-dispatch. Say the word.
**NEXT (suggested):** tech panel the moment `tech.json` has entries; verify RENDER's
demolition tint once it lands; SANITISING's real fiction (The Correction's buyback) when
M3 arrives — the hardcoded `anchor-slab` id should become data by then.