- The Correction gets a voice: parity-mite arrival/swat copy, repaired-event
toasts ("IT LEFT A RECEIPT."), a CORRECTED ticker in the top strip, and a
per-unit FIREWALL LOG in the inspector. All clinical white-green (codex §8),
hidden until the first edit so the enemy is never signposted early.
- Tuner dial draws anonymous station ticks from the speaker adapter's
listStations() (MHz, empty pre-gesture) — landmarks that mark THAT, not WHAT.
- ABOUT/CREDITS footer on the tech panel: repo, codex, and a standing
synthetic-voice disclosure slot for the voice-cloning plan.
- 213 UI tests (was 194): new corrections.ts + mite.test.ts, extended tuner
and techpanel suites.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
46 KiB
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):
#uiroot haspointer-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,
#d8ffd8on dark, hot accents#ff3fd4/#3fffe0(match GLYTCH's palette; see../../index.htmlfor reference). No rounded corners. This is machinery, not a startup.
CURRENT ORDERS — Round 6 (goal: the enemy has a face, the dial has teeth)
Your tuner dock now drives the real engine (orchestrator adapter — the dead-dial finding from the voice review is fixed and live-verified, station names and all).
- Mite surfacing: toasts in voice ("A PARITY MITE IS EDITING YOUR MELT.",
"IT LEFT A RECEIPT."),
repairedevents in the ticker, firewall inspector shows catches. The Correction is polite; the copy should make players furious. - Tuner tick marks: the adapter now passes
listStations()through — draw station ticks on the dial band so hunting has landmarks (keep the hunt: ticks mark THAT something is there, not WHAT). - Credits line (from the voice-cloning plan, cheap now): an ABOUT/credits entry reachable from the tech panel footer — repo, codex, and the standing disclosure slot for future synthetic-voice credit.
DoD: a mite raid is narratable from UI alone; ticks render from real station data; credits reachable in-game.
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).
- 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.
- Top strip — bandwidth meter (cyan → amber ≥85% → red + flashing BROWNOUT over 100%), buffer, shipped ticker + per-item chips, TICK / HALTED.
- Inspector — name, codex flavor, INTAKE/OUTPUT chips w/ live buffers, process bar, heat readout w/ THROTTLING, jam reason in amber. Esc or X closes.
- Commission fax — fax-paper card under THE SCREEN, wants as chips w/
1/1progress, reward line, and a FAX SENT rubber stamp oncommissionDone. - 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.tsso 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, andUIBushas onlydispatch/selectedBuild— so the UI cannot reachrenderer.pickTile, which is the only thing that knows the camera. LANE-RENDER has a workingpickTilethis round; I just can't call it.pick.tsopportunistically probeswindow.__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. Nojsdom/happy-dominstalled 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. ?uifixtureis 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: spacedelivers an emptyevent.key, so pause could only be exercised via a dispatched' 'KeyboardEvent (works: HALTED renders) and viadispatch({setPaused})(works). Real spacebar sends' 'per spec; the handler matches that.
CONTRACT REQUEST:
UIBus.pickTile(clientX, clientY): Vec2 | null(or pass theRenderertoui.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.MachineDef.color: string— why:ItemDefhascolor,MachineDefdoesn'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 nowpalette.tsand 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.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.jsdom(orhappy-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.
- Click-to-inspect via
bus.pickTile. Thewindow.__fktryRendererprobe and the TAB fallback are gone. - 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. - Inspector — recipe picker (
setRecipe), heat bar w/ THROTTLING/SCRAM states, jam wording viajamText(). - Fax — NEXT IN TRAY peek, STANDING ORDER stamp on
repeat, SANITISING chip on shipped anchor-slabs. - 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.tspins it at 1/9/14/40 crafters. - Logic was pulled out of the DOM modules into
pages.ts/chipspec.ts/ a purecreateKeyRouter, which is why there are 57 tests without happy-dom (below). - Data-first everywhere:
MachineDef.flavorand.colorwin, 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-domis NOT installed — order 5 says "GRANTED — happy-dom is installed"; it is not inpackage.jsonand not innode_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 inchipspec.test.ts) andattach()'s window binding (the routing is covered inhotkeys.test.ts). Install it and I'll add the render tests.- STANDING ORDER stamp is built but unobservable. Data has 3
repeatcommissions (dust-allowance, correction-buyback, melt-retainer) but the sim pins the active commission tocommissions[0](first-taste, not repeat) and never advances —commissionDoneEmittedlatches 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
activeCommissionbut 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-decoderhasheatPerTick(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/andscreen/continuously; vite logged 16 page reloads in ~10 minutes, wiping the world mid-test every time. I verified onvite build+vite previewinstead. Not a complaint — just: if a round asks for hands-on verification, expect this. - During the round I hit LANE-SCREEN's
glitchStack.tsand LANE-RENDER'sindex.tsmid-edit and broken (tsc errors, dead rAF loop). Both self-resolved.src/uiwas clean throughout; the one failing test at the time was LANE-SIM's ownsim.test.ts. - Harness note (unchanged): browser-automation
key: spacesends an emptyevent.key; space verified via a dispatched' 'event. Real spacebar sends' 'per spec.
CONTRACT REQUEST:
SimSnapshot.commissionQueue: string[](ornextCommission: string | null) — why: NEXT IN TRAY currently guesses from data order. Low cost, removes a UI lie.- 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. happy-domactually 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.
- REMOVE mode —
Xor the REMOVE button beside the tabs arms it (button goes hazard red, hint turns red,[ ] PAGE · X REMOVEin 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. - 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. - 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. - 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/1amber, 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 >= 1got wrong, now read offEntityState.scrammedper 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 matchescommissionQueue[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
__removeghost sentinel (LANE-RENDER: this is the hook — please read). The UI has no Renderer reference; the only channel to the ghost isbus.selectedBuild(), which main.ts feeds tosetGhost. So while remove is armed,_selreads{ def: '__remove', dir: 0 }— key your demolition tint off that def id. It is safe because sim's place handler isconst def = defs.get(cmd.def); if (def) ..., so main.ts's click-to-place fires aplacefor__remove, matches no machine, and does nothing; the real removal is dispatched by the UI. Exported asREMOVE_DEFfromsrc/ui/selection.tsand 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
coloris chassis, not accent. DATA's chassis values are all desaturated neutrals — correct for the world, but 21 icons in#4a4e52/#3f4348is an unreadable grey wall. So an icon is body (datacolor) + 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, notheat >= 1. Re-deriving SIM's hysteresis here would silently desync the moment they retuneHEAT_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 shipshf-dust,chroma-slurry,macroblock-bricks,melt— zero slabs, withdct-presssitting onoutput 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 whatlive.test.tsdoes. 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 pausesrequestAnimationFrame, 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 reportedhidden: truetoo. Hencelive.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
commissionQueueas unpublished. My first grep forsnap.commissionQueuefound nothing and I wrote the fallback on that basis; SIM landed it minutes later (parallel-round race — same thing that happened withflavor/colorlast 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.jsonis 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:
UIBus.setGhostMode(mode: 'build' | 'remove' | null)(or letsetGhosttake a mode) — why: remove-mode hover currently rides a magic def id throughselectedBuild(), and main.ts fires a junkplacecommand 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.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.
- 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/(draw−gen)— the same arithmetic SCREEN drives dread with, and honest because v3 ruledstoredis bandwidth-SECONDS. Four states now: ok / tight / reserve / brownout, all pure inpower.tsand 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 v4bandwidth.capacity. - Selection protocol migrated. Writes go through
bus.setSelection(),REMOVE_DEFis deleted, and myremovedispatch 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 servesetSelection(it's optional in the contract) and is tested. - Research UI.
Topens 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 withREQUIRES: <TECH>, can't be picked by click or hotkey, and the padlock falls off in the same frameresearchedlands.researched→ "STANDARD RATIFIED. NEW CRIMES AVAILABLE." ?uidemo— boots the reference factory after init. It already paid for itself.- 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-runsapplyUnlocks). Everything after that is the sim's realdrainLabs → researched → applyUnlockspath, unmocked. Reason in BLOCKED: nothing in the game can currently make a science pack. - With no
ResearchStateat 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.tsno 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-decodinggatessoftware-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:?uidemobuilds 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 inlive.test.tsnow 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 needfield-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.tsis 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
modereachessetGhostvia 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).
Round 5 (MEGA) — 2026-07-18 — Opus 4.8
SHIPPED: All nine orders. New modules: power.ts was round 4; this round adds
radio.ts (SPEAKER bridge + echo stand-in), tuner.ts (the dock), screenview.ts
(enlarge), and tests tuner.test.ts, wildlife.test.ts, screenview.test.ts.
194 UI tests (was 145); full suite 446/446; npm run check clean.
- Scrolling tab strip — one row, never wraps, active tab scrolled into view on
[/]. Found and fixed a real bug doing it: see the hitbox finding below. - Inspect mode published —
setSelection({mode:'inspect',entity})on open, cleared on close (X, Esc, or the unit being demolished under it). Holding a tool supersedes it and shuts the panel, so panel and highlight can't disagree. - Wildlife surfaced — infestation toasts in voice; MOSQUITO EXPOSURE + severity in
the inspector, read from
snap.wildlifebytarget; hatch warnings deduped one per pile id. - Research tests migrated to SIM's
testkit.grantResearch/grantAllResearch— my round-4 hand-rolled save-blob edit is gone, and with it the coupling to SIM's schema. Tint + highlight verified, see below. - First-brownout mercy toast — once per session, on the first brownout event.
- Camera hint in the idle line; tab hitbox adjudicated (below).
no seamjam voice — "THERE IS NOTHING DOWN THERE. MOVE." SIM emits this reason at index.ts:498, so it's live copy, not speculative.- THE SCREEN click-to-enlarge — ~2.5×, dimmed backdrop, Esc/click closes.
- THE TUNER DOCK — hidden until the fossil is excavated, then slides in with dial (drag + ←/→), AM/FM/SW, volume + mute, and a station readout in voice.
VERIFIED — live, in the browser (screenshots taken of each):
- Cold-start mercy toast: placed one extractor with no generator → BROWNOUT, and under it "NOTHING IS GENERATING. THE COMMITTEE SUGGESTS: POWER." That's the DoD's opening requirement.
- THE SCREEN enlarge: the live WebGL sky at 2.5× centred over a dimmed HUD; Esc
restored its
styleattribute tonulland its dock rect to y=10, w=322 — exactly as found. - THE TUNER DOCK: excavated SIM's real seeded fossil → dock slid in bottom-right with "AUDIO BUS RESTORED. THE BITSTREAM HAS A VOICE.", dial, FM active, VOL/MUTE, hint line.
- RENDER's demolition tint — the item owed since round 3 is CLOSED. Armed remove, hovered a machine, and it renders outlined in demolition red. Screenshot taken.
- Build bar with padlocks on the POWER page (DECODE ASIC free; SOFTWARE DECODER and ASIC COOLER greyed + 🔒 + REQUIRES:), reference factory healthy at GEN 220 / DRAW 158.
VERIFIED — headless (live.test.ts, real sim + real DOM, 19 tests): mercy toast fires
once and stays once across a second brownout; inspect selection published on open and
dropped both on Esc and when the inspected unit is demolished; the tuner stays hidden
until relics[0].found flips and then reveals with the ceremony toast and the toast
standoff; full research loop (lock → pick → deliver → unlock → place) now driven through
grantResearch and the lab's real per-pack timing.
DECISIONS:
- THE SCREEN is enlarged in place, not mirrored. I built the mirror first; it rendered
black. THE SCREEN is a WebGL canvas, and
drawImagefrom a WebGL context withoutpreserveDrawingBufferreturns nothing. Asking SCREEN to enable that flag would cost them a buffer copy every frame for an occasional feature, so I enlarge the real element with inline styles and restore itsstyleattribute verbatim on close (tested, including the case where SCREEN later adds inline styles of their own). - The radio bridge is guarded (
radio.ts).src/audiois still the stub — noradioexport exists — so the dock resolves a control at runtime and otherwise drives a silent echo stand-in, which is why every dial is testable today. LANE-SCREEN: publishwindow.__fktrySpeaker = { radio }(or tell me the export name you prefer) and the dock drives real audio with zero change here. The interface I'm driving is exactly your documented one:setFrequency / setBand / volume / mute / getState / subscribe, withgetState()returning{freqMHz, band, volume, muted, station}—station: string|nullis what flips the readout to "SIGNAL LOCKED".isLive()reports which one is attached. - Wildlife exposure shows the swarm's severity (
WildlifeState.size), not a sim-confirmed throughput number — SIM owns the real penalty and doesn't expose one. Labelled as exposure rather than a promise about output.
BLOCKED/BROKEN (read this part):
- The tab-click bug was REAL, and I fixed it. Reproduced with a genuine mouse click:
a click on POWER did nothing.
elementFromPointat the button's own centre returned the strip container, not the button — the horizontal scrollbar was being painted over the lower half of every tab and swallowing clicks there, which is exactly why keyboard paging still worked. Fixed by giving the scrollbar its own lane (padding-bottom: 7px≥ scrollbar height); the full button height now hit-tests to the button, and a real click activates POWER. Separately,computerclicks viarefdeliver no events to the page at all (document-level capture recorded nothing); coordinate clicks work fine. So: real bug, now fixed, plus a harness quirk on top of it. - DATA's commission queue head-of-line blocks, and it stalls the whole game. The v5
queue opens with
raw-payload(20 raw MDAT ORE), but the reference factory refines every scrap of its ore and never ships it — so the head commission can never complete and all 19 behind it are unreachable. Probed: 12k ticks ships hf-dust, chroma-slurry, macroblock-bricks, melt and anchor-slab, andcommissionDonefires zero times. My tests now build the obvious player rig (extractor → belt → uplink) to pay it. Worth a design look: either the opening commission should want something the tutorial factory ships, or an unsatisfiable head shouldn't block the tray. - Machines only push onto belts, never into an adjacent machine. Cost me a while to find; noting it because it's non-obvious and unstated anywhere in CONTRACTS.
- RENDER doesn't consume the inspect selection yet — no
inspectanywhere insrc/render. I publish it correctly (verified live:{mode:'inspect',entity:105}), so the highlight lights up whenever they read it. Not owed by me any more; owed to me. - Stale ghost on keyboard disarm: main.ts only refreshes the ghost on
pointermove, so Esc-ing out of remove mode leaves the demolition tint on screen until the mouse moves. Visible in my inspect screenshot. Cosmetic, and it's main.ts's glue, not mine. - SIM landed wildlife AND relics mid-round, again — my round-start grep found neither. Fifth round running. It's why my wildlife/relic UI could be verified against the real sim at all, so this is a good problem, but the pattern is now completely reliable: I re-probe a running sim before writing NOTES, never a grep from round-start.
- Three of my live tests failed mid-round from SIM/DATA changes, not my code (proved by
stashing my diff and re-running). Also hit
DUST_SPILL_GRACE is not definedfrom SIM mid-edit, which resolved itself between two consecutive commands. - Harness, for the record: the browser pane backgrounds the tab, which pauses rAF and
CSS transitions. I lost time reading a transitioned
bottomthat never advanced and briefly thought the CSS was broken;transition:noneproved it computes correctly. Same root cause as the paused sim loop from rounds 3-4.
CONTRACT REQUEST: none. v5/v6 gave me everything these orders needed.
PROPOSAL: UIBus.audio?: { radio?: RadioControl } — the tuner currently reaches THE
SPEAKER through a global handshake because ui.init never sees the audio module, which is
the same shape of problem as pickTile in round 1 and the __remove sentinel in round 3.
One optional field on the bus would make it typed and testable instead of a documented
convention. Not blocking: the guarded bridge works.
NEXT (suggested): wire the real radio the moment SCREEN publishes it (and describe how it sounds in NOTES, since they're doing the same); inspect highlight once RENDER reads the selection; a station-locked readout screenshot once there are real stations; commission head-of-line blocking is a design question for you, not a UI fix.
Round 6 — 2026-07-20 — Opus 4.8
SHIPPED: All three orders. New modules: corrections.ts (session tally of The
Correction's edits) + mite.test.ts. Extended: voice.ts, topstrip.ts, inspector.ts,
tuner.ts, radio.ts, techpanel.ts, toasts.ts, index.ts, style.ts, and the tuner/
techpanel test files. 213 UI tests (was 194); every src/ui/** file green; npm run check reports zero errors in my lane (the one tsc error is SCREEN's, below).
- Mite surfacing — the enemy has copy now. Replaced the v8 placeholder in
wildlifeToast: a mite arriving is "A PARITY MITE IS EDITING YOUR MELT.", swatting one is "MITE SWATTED. IT FORGIVES YOU. IT ALWAYS FORGIVES YOU." (the Correction's manners are the insult).repairedevents (v8) now toast in the house voice — "THE CORRECTION IMPROVED YOUR . IT LEFT A RECEIPT." — item name resolved fromdata.itemsvia a resolver threaded throughtoasts.push → eventToast. Two more surfaces so a raid is narratable without watching the belts: a CORRECTED ticker in the top strip (session total + per-item chips, hidden until the first edit so the enemy is never signposted early), and a FIREWALL LOG in the inspector — per-unit catch count, attributed by footprint containment of eachrepairedpos. The Correction reads clinical white-green everywhere (codex §8: hospital-white, green checkmarks), calm next to the hot alarms — that's the intended menace. - Tuner tick marks. The dial now draws a hot-magenta tick per station on the current
band, positioned by
(freqMHz-min)/(max-min), from the speaker adapter'scontrol.listStations()(added optionallistStations()+StationMarktoRadioControl; the echo stand-in returns[]). Ticks are anonymous — they mark THAT a station is there, not WHAT; the readout stays the only thing that names a lock, so the hunt is intact. Rebuilt only on band/station-set change; empty pre-gesture and fills in on the first state push after the engine attaches — i.e. exactly when audio comes alive. - ABOUT/CREDITS. A footer button on the tech panel toggles a colophon: repo (gitea.partly.party/monster/glytch), codex (docs/FKTRY_LORE.md), a VOICES line, and a standing disclosure slot — "SYNTHETIC VOICE CREDIT: NONE ON FILE." — reserved for the voice-cloning plan's real credit when it ships.
VERIFIED — live, on :8153 (?uidemo): app boots, no console errors. Credits: pressed
into the DOM path with a real click event → block reveals with repo/codex/voices and the
disclosure slot (text asserted). Confirmed present-and-correctly-hidden by default:
.fk-corrected-wrap (display:none pre-mite), .fk-tuner-ticks (0 ticks pre-gesture),
.fk-ins-firewall (hidden). --fk-clinic resolves to #d6f5df. The tab backgrounds
(viewport 0x0, rAF paused) exactly as every prior round documents, so the mite raid itself
isn't drivable live here — see below.
VERIFIED — headless (mite.test.ts, real modules, happy-dom, 14 tests): the mite voice
(arrival/swat/receipt copy + name fallback); the tally (ignores non-repairs, first-seen
per-item order, footprint containment); the FIREWALL LOG (hidden at zero, singular/plural
"1 TIME"/"2 TIMES", survives no-corrections); the CORRECTED ticker (hidden until first edit,
then count + chips). Plus tuner.test.ts (+2): no ticks pre-attach, one tick per on-band
station at the right left%, anonymous. techpanel.test.ts (+3): ABOUT collapsed, reveals
repo/codex/slot, toggles shut.
DECISIONS:
repaired.posis the mite's belt tile, not a machine. SIM repairs belt items (index.ts ~1316), so the event lands on a belt tile. FIREWALL LOG therefore shows catches on whatever unit's footprint that tile falls in — usually the belt itself (1×1), sometimes a crafter if the corrected item sat on its tile. That's honest: it's "edits on THIS unit's tiles", and belts are inspectable. Not a per-machine damage total (SIM doesn't expose one).- "Firewall" is grounded, not invented. SIM already has
FIREWALL_RADIUS/miteInFirewallRange(index.ts ~1329) — firewalls can mites on the approach. Naming the inspector's catch log FIREWALL LOG matches that fiction rather than coining a term. - Item-name resolution stays data-driven (LANE-DATA's
ItemDef.namewins); the resolver falls back to dashes→spaces+uppercase so a brand-new item never crashes a toast. - Corrections state is session chrome, deliberately not persisted — the sim doesn't persist it either, and re-deriving it from a save would be a lie about what actually happened.
BLOCKED/BROKEN (read this part):
- The mite raid is reachable by play but not in this harness. Mites spawn once
corruptionShipped()/MITE_SPAWN_PERclears a threshold (index.ts ~1282), so the reference factory will eventually breed them and firerepaired— but that needs sustained runtime, and the browser pane backgrounds the tab (rAF paused, viewport 0x0), the same wall documented in rounds 3-5. So the mite surfaces are verified headless against the real UI modules, and live only for DOM presence/default-state/no-errors. If you can drive a foregrounded build to the mite threshold, the ticker/firewall/toasts light up with zero further change. npm run checkhas one error, and it is not mine:src/screen/index.ts:106("function must return a value"). SCREEN's file is modified-uncommitted in the shared tree. My lane is clean — everysrc/ui/**file type-checks. esbuild strips types so the dev build boots fine regardless.npm test: 4 failures across 3 files, none mine —src/audio/band.test.ts(SCREEN),src/sim/mite.test.ts(2) andsrc/sim/reference.test.ts(SIM). All three files are modified-uncommitted by their lanes. Confirmed by ownership: I touched nosrc/sim,src/audio, orsrc/screenfile. All 17src/ui/**test files pass (213 tests).- Re-probed HEAD live, not from a round-start grep: SIM does emit both
parity-miteandrepairedat HEAD (index.ts 1288/1321), and the adapter'slistStations()is live in main.ts — so nothing here is speculative against a contract that hadn't landed.
CONTRACT REQUEST: none. Contracts v8 (repaired) + the speaker adapter's listStations()
gave me everything these orders needed.
PROPOSAL: a ?mites dev flag (or a testkit verb) that seeds N parity-mites / forces a
repaired on boot, the mite analogue of ?uidemo. Every mite surface I built is only
observable once corruption crosses a spawn threshold that the backgrounded harness can't
reach; a one-command seed would make the raid demoable in the browser instead of only in
mite.test.ts. Small, and it'd live in main.ts (yours) or SIM's testkit, not my lane.
NEXT (suggested): fill the credits' synthetic-voice slot when the voice-cloning plan ships; name-on-hover for dial ticks only if playtesting says the hunt is too hard (default stays anonymous); a per-machine "corrected N times" once/if SIM attributes repairs to a machine rather than a belt tile; Checksum Wardens / Redundancy Gunships get the same surfacing treatment when they arrive (the corrections module generalises to any Correction edit event).