glytch/fktry/lanes/LANE-UI.md
type-two 8dd4616580 [orchestrator] MEGA round 5: the paint and the pipe
Supersedes unexecuted round-5 orders. Folds in the live first-eyes review findings
(silence, void floor, onboarding cliff, quiet payoffs) + John's hidden-radio idea.

Contracts v6: AudioFX (THE SPEAKER - all Web Audio synthesis, no asset files),
RelicState + excavate + relicFound (the optical-fossil hidden pipe: find it and the
game grows a tuner - FKTRY FM, Correction propaganda subtitled by THE SCREEN's
chyron, a shortwave numbers station reading hidden-seam coordinates, dead air).
main.ts wires audio + first-gesture unlock; src/audio stub awaits LANE-SCREEN.

Lane orders: SIM = wildlife + seams-become-real + the relic + research time;
RENDER = the floor (seam glints, strata whispers), relic shimmer (no signposting),
juice, break-room cabinet -> match.html; UI = mercy toast, tuner dock, SCREEN
click-to-enlarge, camera hints; SCREEN = era feed + THE SPEAKER (3 audio layers,
4 stations, spectral verification); DATA = the commission-ladder tutorial,
stations.json broadcast schedule, seams.json final.

Ship gate: after review, redeploy partly.party/glytch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 12:56:09 +10:00

474 lines
32 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 5 (MEGA) — supersedes the earlier round-5 text
Review verdicts you're executing: first-click punishment without guidance, camera
controls taught nowhere, THE SCREEN's payoff too quiet, and one suspect tab click.
Plus the radio — the game's sound is a *discovery*, and you build its face.
### PART A — carried from round 5
1. **Scrolling tab strip** (your design, approved).
2. **Publish inspect mode**: inspector open → `setSelection({mode:'inspect',
entity})`; clear on close; Esc order preserved (drop tool → close inspector).
3. **Wildlife surfacing**: infestation toasts in voice; MOSQUITO EXPOSURE line +
speed penalty in the inspector; one hatch-warning toast per pile, not a nag.
4. **Migrate research tests** to `grantResearch`; **verify RENDER's demolition tint
+ inspect highlight with screenshots** (third round owed — close it).
### PART B — onboarding + the radio face (new)
5. **First-brownout mercy toast** (once per session): "NOTHING IS GENERATING. THE
COMMITTEE SUGGESTS: POWER." — fires on the first brownout event only. The alarm
currently teaches nothing; this is the one hint the opening needs.
6. **Camera hint** in the idle line ("MMB/WASD PAN · WHEEL ZOOM") and a **tab
hitbox check**: in review, a direct click on the POWER tab did nothing once
(keyboard paging worked; may be harness — reproduce with a real mouse, fix or
file it as harness with evidence).
7. **New jam voice**: `'no seam'` lands from SIM — give it the line it deserves
("SEAM EXTRACTOR: THERE IS NOTHING DOWN THERE. MOVE.").
8. **THE SCREEN click-to-enlarge**: click toggles a centered ~2.5× overlay (CSS
transform + backdrop dim), Esc/click closes. The sky is the score; let people
look at it.
9. **THE TUNER DOCK (the radio's face).** Hidden until `relicFound`/`relics.found`.
Then: a dock slides in (bottom-right) — frequency dial (drag + arrow keys), band
AM/FM/SW, volume + mute, station readout in voice ("88.1 FKTRY FM — SIGNAL
LOCKED"). Controls talk to THE SPEAKER via its exported `radio` control (narrow
sanctioned import from `src/audio` — SCREEN documents the API; coordinate in
NOTES). Discovery ceremony: relicFound → "AUDIO BUS RESTORED. THE BITSTREAM HAS
A VOICE." toast + dock slide-in. Radio state persists per session only (the
fossil's `found` is the durable part, sim-side).
**Definition of done:** cold-start first-brownout shows the mercy toast; tuner dock
appears only after excavation and drives the real audio engine; enlarge verified;
tint/highlight screenshots in NOTES; tab click adjudicated.
---
## 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.
### Round 4 — 2026-07-17 — Opus 4.8
**SHIPPED:** All five orders. New: `power.ts` + `power.test.ts`, `research.ts` +
`research.test.ts`, `techpanel.ts` + `techpanel.test.ts`. **145 UI tests** (was 105);
`npm run check` clean.
1. **The BROWNOUT bug is fixed.** BROWNOUT is now the sim's flag and nothing else. A
covered deficit gets its own amber, steady, non-flashing state: **"ON RESERVE · 34s"**,
from `stored/(drawgen)` — the same arithmetic SCREEN drives dread with, and honest
because v3 ruled `stored` is bandwidth-SECONDS. Four states now: ok / tight / reserve /
brownout, all pure in `power.ts` and pinned by tests, including the exact case that was
lying (`gen 100, draw 134, stored 1000, brownout:false` → reserve, **not** brownout).
Bonus: the buffer readout is a fuel gauge now (`0/900`) using v4 `bandwidth.capacity`.
2. **Selection protocol migrated.** Writes go through `bus.setSelection()`, `REMOVE_DEF`
is deleted, and **my `remove` dispatch is gone** — main.ts owns it. Verified live: one
click, one removal, no double-fire. A legacy fallback remains for a host that doesn't
serve `setSelection` (it's optional in the contract) and is tested.
3. **Research UI.** `T` opens the STANDARDS COMMITTEE: every tech grouped by era in era
order, cost chips in item colours, era prefix stripped from node names, `UNLOCKS:`
tooltips in machine names. Pick → `setResearch`; picking the active one stands it down;
ratified nodes are inert. Locked machines in the bar are greyed + padlocked with
`REQUIRES: <TECH>`, can't be picked by click *or* hotkey, and the padlock falls off in
the same frame `researched` lands. `researched` → "STANDARD RATIFIED. NEW CRIMES
AVAILABLE."
4. **`?uidemo`** — boots the reference factory after init. It already paid for itself.
5. **Accent adopted**: `MachineDef.accent` → first-output derivation → codex kind table.
**LANE-RENDER: confirming we read this identically** — same three tiers, same order.
**VERIFIED — live, in the browser** (`?uidemo`, screenshots taken): the factory builds on
boot with no console poking; the STANDARDS COMMITTEE renders all 23 techs across REEL /
BROADCAST / DISC / STREAM / FORTRESS; clicking SOFTWARE DECODING dispatches `setResearch`
and the node goes hot magenta reading **IN COMMITTEE** with a progress bar (round-tripped
through the real sim); the POWER page shows DECODE ASIC bright and free next to SOFTWARE
DECODER and ASIC COOLER greyed with padlocks and `REQUIRES: STREAM SOFTWARE DECODING` /
`REQUIRES: BROADCAST ASIC COOLING`; `bus.selection()` reads `{mode:'remove'}` and a click
demolishes the mosh reactor once — DRAW 100→92, "UNIT RECLAIMED. THE FLOOR REMEMBERS."
**VERIFIED — headless** (`live.test.ts`, real sim + real DOM, 16 tests): the whole
research loop the DoD asked for — **lock visible → `place` silently dropped by the sim →
build a lab → pick the tech through the panel → packs delivered → `researched` → toast →
padlock falls off → the same `place` now succeeds.** Its harness is now a faithful main.ts
(typed selection; *it* dispatches place/remove), which is what caught the double-fire.
**DECISIONS:**
- Packs in the research run are teleported into the lab's intake via the sim's own
`save()`/`load()` (v4 hardened; `load()` re-runs `applyUnlocks`). Everything after that
is the sim's real `drainLabs → researched → applyUnlocks` path, unmocked. Reason in
BLOCKED: nothing in the game can currently *make* a science pack.
- With no `ResearchState` at all, the safe reading of the v4 rule is that gates **hold**
(padlocks on) rather than fall open, and the panel says RESEARCH OFFLINE instead of
rendering a tree that can't do anything. The sim always ships a ResearchState today, so
this is belt-and-braces.
- `live.test.ts` no longer hardcodes reference-factory coordinates — it finds the i-only
assembler by its recipe. reference.ts moved it from (-6,5) to (-6,6) *during this
round* and broke my own test; that's my brittleness, fixed.
**BLOCKED/BROKEN (read this part):**
- **SIM's research gating breaks SIM's own reference factory, and it's live right now.**
`stream-software-decoding` gates `software-decoder`; the sim silently drops placements
of gated machines (`if (!isAvailable(def.id)) return`); reference.ts places two software
decoders for its 200 of generation. Result at HEAD: `?uidemo` builds a factory with
**GEN 80 against DRAW 100, buffer 0/900, permanent BROWNOUT** — it can never run. My
screenshots show it. Either reference.ts should use decode-asics, or the demo should
pre-unlock, or that tech shouldn't gate the generator. (This is also why every
reference-factory test in `live.test.ts` now runs with research pre-granted.)
- **Nothing in the game can make a science pack yet, so no research can actually be
performed by playing.** Packs come only from `artifact-bottler`, whose recipes need
`field-splinters` / `v-hold-roll` / `static-canister` — a broadcast-era chain that
neither the reference factory nor any tech-free layout builds. The tree is *reachable*
on paper (18 techs have satisfiable costs, and DATA fixed the artifact-bottler deadlock
I was about to report), but unreachable in practice. Suggest reference.ts grow a
research rig — it would make this verifiable by playing rather than by save/load.
- **LANE-SIM's `reference.test.ts` is red at HEAD** ("keeps the demuxer alive by voiding
surplus rather than jamming"), and the factory layout shifted twice while I worked. Not
mine; flagging because the suite isn't green.
- **RENDER's demolition tint is still unverified from my side.** The typed `mode` reaches
`setGhost` via main.ts now, so the magic string is no longer my problem — but I never
saw the tint land.
- **The order's own numbers went stale mid-round, again — third round running.**
"24 nodes" is 23 at HEAD; `tech.json`'s era distribution changed under me; the
artifact-bottler deadlock I'd have reported as blocking was fixed by DATA while I
worked; SIM landed *all* of research after my round-start grep said it had none. I now
re-probe everything against a running sim before writing a word of NOTES, which is the
only reason this entry is accurate. Worth considering whether lanes should get a
"HEAD as of" line rather than counts baked into orders.
- Minor, mine: the tab row wraps to two lines now that RESEARCH joined (8 tabs + REMOVE).
Legible, not pretty. Real fix is a scrolling tab strip; say the word.
**CONTRACT REQUEST:** none this round. v4 gave me everything I asked for and the
`SelectionState` migration was clean.
**PROPOSAL:** `Sim.grantResearch(techIds)` or a `?unlocked` demo flag. Every research-
adjacent test I wrote needs research pre-granted, and my only route is
`JSON.parse(sim.save())` → mutate → `load()`. That works and uses public API, but it
couples my tests to SIM's save schema — if they rename `research` in the blob, my suite
breaks for no good reason.
**NEXT (suggested):** verify RENDER's tint; research rig in reference.ts so a pack economy
exists to test against; scrolling tab strip if the catalog grows again; era-skinned tech
panel once eras mean something visually (§8).