# LANE-RENDER — three.js world renderer You are an Opus 4.8 executor on the RENDER lane of FKTRY. Read `../MASTERPLAN.md` and `../CONTRACTS.md` first; the world bible + visual style guide (§8) is `../../docs/FKTRY_LORE.md` — §8 is YOUR section, follow it. **Mission:** make the Bitstream visible. Isometric factory view, machines, belts with smoothly interpolated items, ghosts, selection — and the hot-swap pipeline that lets MODELBEAST GLB assets land mid-development without a code change. **Owned paths:** `src/render/**`, `public/assets/**` (manifest + loading; the files themselves arrive from MODELBEAST). **Never touch:** contracts, main.ts, other lanes. Sim is truth: you render snapshots, you never simulate. If motion looks wrong, interpolate better or note it — don't "fix" positions locally. **Standing rules (all rounds):** - 60fps with 500 entities + 2,000 belt items on an M-series MacBook. Instanced meshes for belts/items from day one — you will not get to retrofit this later. - Placeholder aesthetic is a feature: crisp box-primitive machines tinted per codex `color`, NOT gray cubes. Two-material rule even for placeholders: dark body + one emissive accent. - Every entity mesh keys off `MachineDef.asset` / `ItemDef.id` through the registry — zero hard-coded ids. --- ## CURRENT ORDERS — Round 7 (Phase 1: bodies later, PRESENCE now) Contracts v9 grants your round-6 request: `Renderer.onEvents?(events)` — main.ts fans out the same array Screen/Audio get. Your three derived cues (repaired sparkle, firewall paperwork, relicFound geyser) can now be exact; migrate them. RULING — THE INVERSION: the codex assigns wrong-motion to CORRUPTION, not authority (§8: "clean things move at 60fps; corrupted things move wrong on purpose"). So The Correction units are the ONLY things in the world that never miss a frame: they glide, smoothly interpolated, unbothered, while everything the player owns keeps its existing judder vocabulary. Do NOT quantize their transforms. 1. **Placeholder bodies for rungs 2-4** (zero GLBs this round): distinct primitive silhouettes in hospital-white with one green element each — warden: tall filing-cabinet slab with a stamp arm; gunship: five mirrored rotor platters in a row, hovering; auditor: a faceless white marble column that TELEPORTS (appears/ disappears, never walks — §5 says it moves only between frames). All matte, spotless, unweathered. 2. **The gunship's mirror**: its `rect` renders as a faint ghostly duplicate of the sector beneath it (a translucent white re-render or projected decal — your language). The tether to `target` is a visible line. 3. **Stasis rect**: bleached ground, frozen belt items visibly stopped mid-tile, 00:00:00:00 timecode floating. Release snaps back hard. 4. **Seal stamp**: sealed machines get a white compliance placard on the chassis + their idle animation FREEZES (a sealed machine is a still photograph of itself). 5. **onEvents migration** for your three derived cues + the new ones (notice ping at the fax, mirrored flash inside the sector). **DoD:** all three rungs readable at a glance in showroom + demo world; the glide verified against juddering surroundings; stasis freeze/release pinned; perf holds. --- ## 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 7 orders, each verified in the running dev build (not "should work"): - **Camera/controls** (`camera.ts`): ortho iso, wheel zoom clamped 8..40, middle/right-drag pan, WASD+arrows, resize via ResizeObserver. - **Ground** (`ground.ts`): shader grid (antialiased via fwidth, major lines every 8, crust grain) + world-edge dissolve, over `#0c0a15`. Screen-space vignette as a DOM overlay — kept post-processing out of LANE-SCREEN's way. - **Registry + hot-swap** (`registry.ts`): placeholder factories per kind; probes `assets/models/.glb` on init + every 10s in dev. **Verified live**: dropped a GLB into `public/assets/models/quantizer.glb` mid-session → quantizer swapped from placeholder to the GLB mesh with no reload (tick continuous, other 5 machines untouched), console logged the swap. Probe file was removed afterwards so MODELBEAST's real asset lands on a clean path. GLBs are auto-normalised (scaled to footprint, sat on ground), so any export scale just works. - **Entities** (`entities.ts`), **belts** (`belts.ts`, instanced + chevron scroll), **belt items** (`beltitems.ts`, InstancedMesh per type, interpolated), **ghost + picking** (`ghost.ts`), jam blink + brownout dimming. **VERIFIED (numbers, not vibes)** - **Perf standing rule met with 5x headroom**: `?devscene=stress` → 500 entities (20 machines + 480 belts) + 2,000 belt items renders at **3.2ms/frame avg** (budget 16.6ms). The 2,480 belts+items cost **5 instanced draw calls** total. - **Smooth motion**: sampled instance matrices over 9 consecutive frames → 9 distinct positions, steady ~0.0166 tiles/frame (2 tiles/s on a 120Hz panel). Interpolating continuously, not stepping at 30tps. - **Ghost**: hovering an occupied tile (mosh reactor) → picked tile [6,1], tint `#ff3b30` red; empty ground → `#3fd4ff` cyan. Driven through main.ts's real glue. - **Brownout**: exposure 1.35 → 0.644 with flicker. **Jam**: amber `#ffa726`, blinking. **DECISIONS** (flagging these because they're judgement calls, not obvious readings) - **Machine accent colour is derived from data**: `MachineDef` has no `color` field, so a machine's one emissive accent = the colour of the item it makes (recipes → outputs → `ItemDef.color`). Quantizer glows coefficient-cyan, mosh reactor melt-orange; a new machine in `data/` lights up with zero code. Matches §8's "colour = value = literally the resource". See CONTRACT REQUEST below. - **Legibility floor on data colours.** Raw products are near-black by design (MDAT ORE `#1a1a22`). Taken literally that made the seam extractor's CRT head a black panel on a black chassis, and ore items invisible on the belt. Derived accents below L=0.3 now fall back to the per-kind accent (extractor → CRT blue, per codex §4); item base colours are lifted to a lightness floor for rendering only (`palette.readable`). Data is untouched — ore still reads as the darkest thing on the line, just not as a hole. - **Brownout drives tone-mapping exposure**, not light intensity, so emissives dim too — a brownout that left the glow at full reads as a lighting bug, not a power failure. - **Fog is banded around the camera distance**, not absolute. Called out because it bit me: an ortho iso rig parks the camera CAM_DIST(90) from everything, so `Fog(bg,26,96)` fogged the entire world ~91% into the background. Anything touching `CAM_DIST` must keep `index.ts`'s fog band in step. - Dev scaffold (`devscene.ts`) is gated behind `import.meta.env.DEV` **and** `?devscene`, and is tree-shaken out of the prod bundle (verified: 0 hits in `dist/`). It yields automatically the moment the real sim reports any entity — it never fabricates over live sim data. `?devscene=stress` is the perf rig. **BLOCKED/BROKEN** — nothing blocking. Honest limitations: - **Belt GLB instancing takes only the FIRST mesh** of a multi-mesh GLB. Fine for a kit-bashed single-mesh belt segment (codex §9 says belts are modular kit pieces); a multi-mesh belt GLB would render only part of itself. Machines are unaffected. - **GLB animation clips are not played** (no AnimationMixer yet). Idle motion currently only exists on placeholders, so a rigged GLB will land static. Wanted for M4. - **Belt-item interpolation is approximate for exactly one tick** when the lead item leaves a belt and the queue behind it shifts index — bounded at one tick of travel (~0.07 tiles), imperceptible. Root cause is the missing stable id; see below. - Below 30fps the interpolation stretches (prev-tick snapshot may be >1 tick back). Not observed; noting it rather than hiding it. **CONTRACT REQUEST** 1. `BeltItem.id` (stable, per-item) — would make cargo interpolation exact instead of order-matched. Low priority; current error is imperceptible. 2. `MachineDef.color?: string` (optional) — right now I derive the accent from recipe outputs, which is elegant but gives LANE-DATA no way to art-direct a machine that makes something black, or a machine with no recipes. Optional field, derived fallback. 3. **`main.ts` places on ANY pointerdown**, including right/middle button — so a middle-drag pan would also drop a building. I worked around it inside my lane (`camera.ts` stops non-left buttons from bubbling to `#game`), but the clean fix is `if (e.button !== 0) return;` in main.ts's handler. Flagging since it's your file. **PROPOSAL** - MODELBEAST asset keys are live now — the registry will hot-swap any of these the moment a GLB lands, no code change: `seam-extractor`, `belt`, `demuxer`, `quantizer`, `mosh-reactor`, `decode-asic`, `shipper` (+ whatever LANE-DATA adds; the key is `MachineDef.asset`). Hero assets (mosh reactor, muxer) are the ones worth `fal_trellis2`. - Shadows are on (2048 map, tight ortho shadow camera). Cheap at current load; if a big factory drags, that's the first dial to turn. **NEXT (round 2 candidates, not self-assigned)** - AnimationMixer for rigged GLBs; per-era skinning hooks; splitter/lane visuals; heat shimmer + throttle read-out on the software decoder; belt corner/junction pieces (currently every belt is a straight plate regardless of neighbours). ### Round 2 — 2026-07-17 — Opus 4.8 **READ THIS FIRST — I did not follow order 2's letter, deliberately.** `MachineDef.color` is wired to the machine **body**, not the emissive accent, against the stated precedence (`def.color` > derived > kind). Why: all 21 values LANE-DATA authored are desaturated industrial neutrals that match codex CHASSIS descriptions — `seam-extractor #8a7a32` is §4's "grimy yellow chassis", `decode-asic #b9bec4` is its "server-blade monolith", `mosh-reactor #52333e` its pressure vessel. Style guide §8 says ALL saturation belongs to media/artifacts and each machine gets exactly ONE impossible element. Wired to the accent (I built it that way first and looked at it), all 21 machines glow muddy neutral: the quantizer's jaw went from coefficient-cyan to tan, the mosh reactor's from melt-orange to mauve, and the two-material rule quietly died. Read as bodies they're exactly right, and we get both: 21 art-directed chassis AND the vivid resource accents. The black-on-black problem the field was meant to solve stays solved by round 1's legibility floor. **This is a cross-lane semantic conflict between my orders and DATA's — please settle it; see CONTRACT REQUEST 1.** If you want the literal reading, it's a two-line revert in `registry.ts` (`bodyFor`/`accentFor`). **SHIPPED** — all 6 orders, each verified in the running build: 1. **Belt topology** (`topology.ts`, new): neighbour analysis → straight / cornerA / cornerB / merge, one InstancedMesh per (def × shape), flow field baked into each shape's shader (no runtime branch). Corners sweep a quarter-arc; merges add inlet arrows. Verified: the devscene bend classifies **20 straight / 1 cornerA / 1 cornerB / 1 merge** across 23 belts, still 4 draw calls. - **Cargo now follows the corner arc too.** Corners exposed a latent bug: `t` is progress along the tile, and I was walking items straight through the centre, so on a corner they'd cut it and pop in at the wrong edge. Items now enter at the feeding edge and sweep the arc. Verified numerically: items on both corners sit 0.089–0.248 off the centreline where a straight path pins every sample to exactly 0.000. 2. **Heat** (`heat.ts`, new + `entities.ts`): dull-red emissive ramp on the body, rising shimmer column, throttle, scram. Verified end-to-end at forced heat: - 0.50 → body emissive `#bc280a` @ 1.35 (= `1 + 0.5²×1.4`), haze 0.5 - 0.85 → idle clock advances **0.0053/frame vs 0.0083 full speed** = the 0.625× throttle factor exactly; body emissive 2.01 - 1.00 → **scram**: every emissive 0 (dead-dark), haze off, idle clock frozen at 43.613, amber blinking - back to 0.20 → **restart recovers**: body emissive 1.06 (= `1 + 0.2²×1.4`), latch cleared, idle moving, haze back 3. **Buffer tank**: fill column driven by the tank's share of `bandwidth.stored` over summed `bufferCap` (DATA shipped `buffer-tank: 240`). Verified oscillating with the pool rather than sitting at its 0.001 floor. 4. **AnimationMixer**: clip 0 plays looped on rigged GLBs, and shares the throttle factor — a hot machine's *animation* labours too, which fell out for free. Verified with a hand-authored animated GLB: 9 frames → 9 distinct Y values, clip driving the node. Rigged GLBs clone via `SkeletonUtils.clone`, not `Object3D.clone`, or every instance would animate off the first one's bones. 5. **Exact cargo interpolation**: keys off `BeltItem.id` when present (`exactIds: true` verified), order-matching retained as fallback until v3 makes it required. The one-tick approximation is gone whenever the sim stamps ids. 6. **Multi-mesh belt GLBs**: merged with `mergeGeometries(..., useGroups)` so a fancy belt asset can't half-render. Verified with a 2-mesh GLB: **2 material groups, 2 materials, 1 instanced draw call**, both meshes visible. If attributes ever mismatch it keeps the first mesh and `console.warn`s the count rather than silently dropping. **VERIFIED** — perf standing rule still met with everything above added: `?devscene=stress` → 500 entities + 2,000 items at **3.6ms/frame avg (4.6× headroom)**, 5 instanced draw calls. Round 1 was 3.2ms, so topology + heat + mixers cost ~nothing. Dev scaffold still DEV-gated and absent from `dist/` (verified 0 hits). **DECISIONS** - **Scram is a latch with hysteresis, mirroring SIM's — and this caught a real bug.** SIM landed heat mid-round, so I read their code: `scrammed` lives on their internal `Ent`, never on `EntityState`; they deliberately clear `jammed` on scram ("reported by its own event, not as a flow jam"); and `Renderer` only ever receives snapshots, never events. So the latched-off state is **not snapshot-visible at all**. A plain `heat >= 1` test (what I shipped first) un-scrams the moment the machine starts cooling — and with `HEAT_RESTART = 0.5` and `HEAT_DISSIPATION = 0.004/tick` the sim keeps it dead for ~125 ticks (**~4 seconds**) while heat falls 1.0 → 0.5. The renderer would have shown it alive, glowing and animating, for that whole window, every scram. `entities.ts` now latches at 1.0 and releases below `SCRAM_RELEASE = 0.5`, mirroring them. Verified: 1.0 → dead; **0.7 → stays dead** (the window that used to lie); 0.3 → releases and recovers. This is correct today but duplicates their constant — see request 2. - GLB instances get **cloned materials**; heat/scram is per-entity state and the template's materials are shared, so one hot machine would otherwise tint every copy. Placeholders own geo+mats; GLB clones own only mats. Disposal follows that split. - Heat tints the tagged body mesh on placeholders; a GLB has no body/accent split, so there it tints everything. - Shimmer is an additive billboard column, not screen-space refraction — post-processing is LANE-SCREEN's territory and I'm not reaching into it. - Kept the `camera.ts` non-left-button `stopPropagation` as belt-and-braces even though main.ts now guards; it's two lines and costs nothing. **BLOCKED/BROKEN** — nothing blocking. Honest limitations: - **Merge pieces don't route cargo per-inlet**: items on a merge belt still ride the straight path, because `BeltItem` doesn't say which side it entered from. Reads fine (a merge's main flow *is* straight) but it's not truthful for the side feed. - Corner arc assumes a 1×1 belt. Fine per contract (`beltSpeed` belts are 1×1). **DoD — verified against SIM's actual `referenceFactory`** (it landed while I was building; I loaded it in the dev page and dispatched all 114 commands): - **85 belts classify as 80 straight / 3 cornerA / 1 cornerB / 1 merge**, 25 machines — five pieces that round 1 would have drawn as flat straight plates. - **Cargo flows with `exactIds: true`** — SIM stamps `BeltItem.id`, so the exact interpolation path is live on real sim data, not just the scaffold. - Ore renders `#4b4b62` (round 1's legibility floor lifting DATA's `#1a1a22`), so the reference factory's first product is visible on its belts rather than a black smudge. - **4.1ms/frame** with the whole reference factory up. - Note for whoever plays it: a hand-placed extractor produces nothing until you also `setRecipe` it — `place` alone leaves `recipe: null`. Not a bug, but it's why a quick manual line looks dead. The reference factory sets all four itself. **CONTRACT REQUEST** 1. **Settle `MachineDef.color`'s meaning** (see the top of this entry). My proposal: keep DATA's values as-authored and re-comment the field as chassis/body colour, then add `accent?: string` if art-directing the emissive is ever wanted. Right now the contract comment says "accent", DATA authored bodies, and I've followed the data. 2. **`EntityState.scrammed?: boolean`** — now CONFIRMED necessary, not speculative (see DECISIONS). SIM's scram latch is invisible to the snapshot, and the only way I can render it correctly is by duplicating their `HEAT_RESTART = 0.5` as my own `SCRAM_RELEASE`. That works, but it silently desyncs the moment SIM retunes their constant — a rendering lie with no test to catch it. One bool on `EntityState` kills the duplication outright. (Alternative if you'd rather not widen the state: give `Renderer` the events, like `ScreenFX` gets — but the bool is smaller and snapshots staying self-sufficient is a nice property.) 3. Not urgent: `BeltItem.side?: Dir` (which edge cargo entered from) would let merge pieces route their side feed honestly. Only worth it if merges get common. **PROPOSAL** - MODELBEAST hot-swap now also covers **animated** and **multi-mesh** GLBs, so hero assets can arrive rigged and kit-bashed with no code change. Asset keys are whatever `MachineDef.asset` says — all 21 are live targets today. - The showroom row in the devscene (`?devscene`) is a decent visual smoke test for any new machine visual; it's clear of the belt path on purpose. **NEXT (round 3 candidates, not self-assigned)** - Per-era chassis skinning (§8 era skins) once tech/eras land; splitter output lamp driven by real round-robin state rather than a timer; bloom-loop belt tell; HF dust piles; parity-mite wildlife; LOD/culling if a real megabase ever drags the frame. ### Round 3 — 2026-07-17 — Opus 4.8 **PROVENANCE — read before you diff lanes against owned paths.** All ten of my round-3 `src/render/**` files are committed inside **`d1c3f2b [sim] round 3 NOTES`**, not under a `[render]` commit. Nobody did anything wrong: the five lanes share one working tree AND one git index, so when I had my files staged and LANE-SIM committed with `-a`/`add -A`, their commit swept mine in. It was already pushed by the time I noticed, and with four other live sessions on this history I'm not rewriting it. **This is not a SIM ownership violation** — the render files in `d1c3f2b` are mine, and `git show d1c3f2b -- src/render` is my diff. PROPOSAL below on how to stop it recurring; it will, otherwise. **SHIPPED** — all 4 orders, verified in the running build. 1. **Scram from truth.** `SCRAM_RELEASE` and the whole local latch are gone; the renderer reads `EntityState.scrammed`. SIM wired the field mid-round (their `state.scrammed` at index.ts:489/494) so this is verified against their real latch. The crisp test: **heat 0.6 + `scrammed: true` → dead-dark, amber, idle frozen**, and the same heat with `scrammed: false` → alive at body emissive 1.5 (= `1 + 0.6²×1.4`). Same heat, opposite field, opposite visual — no threshold left in this lane to drift. (`e.scrammed ?? heat >= 1` remains only as a bridge for a sim that hasn't set it; it carries no constant.) 2. **Remove mode.** `setGhost('__remove', …)` → hazard-striped footprint decal + red volume wash (`remove.ts`). Verified through UI's real flow: clicked their REMOVE tab, `selectedBuild()` returned `{def:'__remove',dir:0}`, their hint bar switched to "REMOVE ARMED · LMB DEMOLISH · ESC STAND DOWN", and hovering the mosh reactor lit its **whole 3×3** (decal scale [3,3], wash [2.82,0.8,2.82], centred on the machine at [5.5,9.5] — not the hovered tile). Bare ground shows nothing. 3. **`?showroom`** — 23 machines + 51 items on labelled plinths, camera preset, and it lands MODELBEAST drops live (hot-swap + normalise + AnimationMixer + era skin all already run through it). See the manifest below. 4. **Era skins** (`era.ts`) — plumbing, derived from data, verified live. **VERIFIED** - Perf standing rule holds: `?devscene=stress` → 500 entities + 2,000 items at **3.3ms/frame**, 5 instanced draw calls. (R1 3.2 / R2 3.6 / R3 3.3 — topology, heat, mixers, eras and remove-mode have cost nothing measurable.) - **Era skins live across all five eras**, each matching its `ERA_SKIN` entry exactly: reel telecine-puller `r0.65/m0.45` (sepia'd to #6e491f), broadcast field-loom `r0.8/m0.1` (phosphor #2e5132), disc mosh-reactor `r0.2/m0.65` (gloss), fortress muxer `r0.95/m0`. **Stream is a true identity skin** — quantizer chassis reads `#5e5148`, byte-for-byte DATA's authored value, so nothing changed for the 16 stream machines. - Prod bundle clean: `devscene` 0, `showroom` 0, `plinth` 0, `window.__render` 0 hits (the 4 `__renderTarget` matches are three.js's own). **DECISIONS** - **`BeltItem.id` fallback retired.** Identity is now always the id, which also deleted the per-frame sort of every belt item — the order-matching path only ever existed to reconstruct identity the contract didn't carry. - **The showroom fabricates a SimSnapshot** rather than hand-building display meshes, so machines go through the real EntityLayer. A QA page with its own bespoke display path would be testing itself instead of the renderer. Items aren't entities (no `asset`, never hot-swap) so those are built directly — but from beltitems.ts's own factories, so the showroom shows literally what rides the belts. - **Camera preset frames the MACHINE hall, not everything.** A view wide enough to hold all 51 items renders every machine too small to judge, which defeats the point. Items are one pan south. Labels are collision-culled front-to-back: ~74 of them cannot all fit at overview zoom, and a readable subset that resolves as you zoom beats a pile. - **`__remove` is duplicated, not imported.** A renderer importing UI internals is the wrong layering and there's no contract field yet. Unlike a tuning constant, drift here fails loudly and visibly. Both lanes filed for a real hook (see request 1). - Era skin touches the **chassis only** — the accent stays the colour of what the machine makes, because that's the resource, not the decade. **BLOCKED/BROKEN** — nothing blocking. - **A dead debug hook leaked into prod and I nearly shipped it.** Hoisting the scaffold check into a `debug` VARIABLE defeated Rollup's constant folding, so `window.__render = {scene, gl, topo, …}` survived in `dist/` as unreachable dead code (the class itself still tree-shook). Round 2's `if (devSceneEnabled())` folded because the call was inline. Fixed by testing `import.meta.env.DEV` literally at the branch. Flagging the shape, not just the fix: **any** scaffold guard that goes through a variable will do this silently, and only grepping the bundle catches it. - Merge pieces still don't route cargo per-inlet (needs `BeltItem.side`, deferred — agreed). - The showroom's item hall is laid out for 51 items; if DATA doubles them again the rows will run past the world edge and want paging. Not urgent, noted so it isn't a surprise. **CONTRACT REQUEST** 1. **`UIBus.setGhostMode(mode)` or a 4th `setGhost` arg** — LANE-UI filed the same one independently (`src/ui/selection.ts` documents their half). Today remove-mode rides a magic string through `selectedBuild()`, and main.ts fires a `place` for `__remove` every click that the sim discards. It works and it's documented on both sides, but it's a protocol living in two lanes' comments instead of in the contract. 2. Not urgent: nothing else. v3 granted both of round 2's asks and they both landed clean. **MODELBEAST WORK ORDER** — this is `?showroom`'s content, and every one of these is a live hot-swap target today: drop `public/assets/models/` and it appears in ~10s, no reload, no code. Normalisation means any export scale lands correctly; multi-mesh and rigged+animated GLBs both work (verified round 2). Footprint is the tile budget. | asset | era | fp | machine | |---|---|---|---| | `asic-cooler.glb` | broadcast | 2x2 | ASIC COOLER | | `field-loom.glb` | broadcast | 3x2 | FIELD LOOM | | `artifact-bottler.glb` | disc | 2x2 | ARTIFACT BOTTLER | | `mosh-reactor.glb` | disc | 3x3 | MOSH REACTOR | | `subsampler.glb` | disc | 2x3 | SUBSAMPLER | | `muxer.glb` | fortress | 5x5 | THE MUXER | | `telecine-puller.glb` | reel | 2x2 | TELECINE PULLER | | `belt.glb` | stream | 1x1 | STREAM BELT | | `bloom-duplicator.glb` | stream | 1x1 | P-FRAME DUPLICATOR | | `buffer-tank.glb` | stream | 2x2 | BUFFER TANK | | `dct-press.glb` | stream | 3x3 | DCT PRESS | | `decode-asic.glb` | stream | 2x2 | DECODE ASIC | | `demosaicer.glb` | stream | 3x2 | DEMOSAICER | | `demuxer.glb` | stream | 2x2 | DEMUXER | | `gop-assembler.glb` | stream | 3x2 | GOP ASSEMBLER | | `hex-splicer.glb` | stream | 3x3 | HEX SPLICER | | `lane-splitter.glb` | stream | 1x1 | LANE SPLITTER | | `mv-extractor.glb` | stream | 2x2 | MV EXTRACTOR | | `p-caster.glb` | stream | 2x2 | P-CASTER | | `quantizer.glb` | stream | 2x2 | QUANTIZER | | `seam-extractor.glb` | stream | 2x2 | SEAM EXTRACTOR | | `shipper.glb` | stream | 2x2 | SCREEN UPLINK | | `software-decoder.glb` | stream | 2x2 | SOFTWARE DECODER | Hero assets worth `fal_trellis2` (codex §9): `muxer` (5x5, the rocket), `mosh-reactor`, plus the Fortress kit when it exists. Everything else is `sf3d` territory. **PROPOSAL — the shared index will keep eating commits.** Five sessions share one working tree and one `.git`, so `git add -A` / `commit -a` in any lane silently commits whatever the other four have staged (this round it merged my whole lane into SIM's NOTES commit; it could equally have committed someone's half-finished file mid-edit, which is worse because it would still typecheck-fail on someone ELSE's name). Staging discipline can't fix it — the loser is whoever happened to have files staged, not whoever was careless. Cheapest real fixes, in order: 1. **`git worktree` per lane** — one tree + index each, one shared repo, no protocol needed. Lanes still see each other's commits after a pull. 2. A standing rule that lanes commit only with an explicit pathspec (`git commit -- `, never `-a`), which leaves staging alone. Weaker: it's a convention, and this round proves conventions lose races. 3. Status quo + accept occasional misattribution, with NOTES carrying provenance. **NEXT (round 4 candidates, not self-assigned)** - Real MODELBEAST assets through `?showroom`; splitter lamp driven by real round-robin state instead of a timer; bloom-loop belt tell; HF dust piles; wildlife; save/load camera state; LOD/culling if a megabase ever drags the frame. ### Round 4 — 2026-07-17 — Opus 4.8 **SHIPPED** — all 4 orders + housekeeping, verified against LANE-DATA's real v4 data (which landed mid-round: 29 machines, 60 items, `archaeology-lab`, `asic-cooler coolRadius:2`, `accent` on 8 machines) and UI's real migrated flow. 1. **Ghost keys off `mode`.** The `'__remove'` sentinel is ignored — mode is the truth, and the `REMOVE_DEF` constant is deleted from this lane. Verified through UI's own REMOVE tab: `selection()` → `{mode:'remove'}`, demolition decal + wash on the mosh reactor's whole 3×3. 2. **Locked-machine ghost.** Padlock-amber `#ffb02e` — deliberately not the occupied-red, because "not yet" and "not here" are different sentences. A padlock floats above, tinted by the era gating it. Verified: `telecine-puller` → amber ghost + `#d8a24a` reel brass; `asic-cooler` → amber + `#5fd08a` broadcast phosphor. - **Era tell needed its own palette.** `ERA_SKIN.tint` can't do this job: it's a multiplier tuned to leave DATA's chassis alone (stream is literally `#ffffff`), so three of five eras would have told you nothing. Added `ERA_TELL` — five hues chosen to be told apart, used as signal rather than surface. 3. **Cooler auras.** Verified on `asic-cooler` (fp 2×2, coolRadius 2) → **6×6 tiles**, centred on the ghost, dashed + tick-marked, no fill. - **It's a rectangle, not a ring.** `coolRadius` is CHEBYSHEV from the footprint, so the cooled region is the footprint grown by r on each side. A circle would have been prettier and would have lied — the corners it left out are cooled, and players would plan around a boundary that doesn't exist. Drawn in the geometry the sim measures in. 4. **Lab visuals.** `archaeology-lab` (kind `lab`, 3×3) renders as a bench + the "bottled artifact triptych" (§2) — three out-of-phase pulsing bottles. Verified geometry `Box,Cylinder,Cylinder,Cylinder` and **`accent` consumed from data**: bottles emissive `#ffc24a`, exactly DATA's authored value. 5. **Housekeeping.** Splitter lamp left timer-driven as approved. Showroom absorbed the new catalog — see the bug below. **VERIFIED** - Perf standing rule holds: 500 entities + 2,000 items at **3.6ms/frame**, 5 instanced draw calls. (R1 3.2 / R2 3.6 / R3 3.3 / R4 3.6 — auras, padlocks and the locked path cost nothing measurable.) - Showroom absorbs 29 machines + 60 items, all five eras present, hall inside the world. **DECISIONS** - **Matched LANE-UI's lock rule rather than my own, and it was a real divergence.** My first cut treated absent `research` as "nothing is locked" — don't lie when the field isn't wired. UI's `research.ts` reads the same rule the opposite way: nothing has completed, so everything gated shows its padlock. **UI is right and I was wrong**: this is the `scrammed` lesson misapplied. There, the field's absence meant the sim hadn't wired a *behaviour*; here the rule is satisfiable from data alone and the contract says "until that tech completes". Left unresolved, the build bar would have padlocked a machine while the ghost waved it through — on the same screen. Also matched their first-tech-wins tie-break for ids unlocked by two techs: a data bug either way, but we must both blame the same tech. - **Buffer fill now reads `bandwidth.capacity`** instead of re-summing `bufferCap` locally. v4 publishes the total; a second copy of the sim's arithmetic in this lane is exactly what the scram latch taught us not to keep. Falls back to the local sum only while the field is absent. - **`MachineDef.accent` wins outright, no legibility floor** — same reasoning as `def.color`: explicit art-direction is a decision; the floor exists for *derived* colours, which are incidental. **BLOCKED/BROKEN** — nothing blocking. - **The showroom bug I predicted last round arrived, and my fix was half a fix.** Round 3 noted the item hall would overflow when the catalog grew. It did (51 → 60 items), but the failure wasn't the one I expected: the *machine* hall grew to 5 rows, ate the depth budget, collapsed the item grid to **one row of 60 columns — a 206-tile row across a 64-tile world**. My round-3 "adaptive" fit budgeted depth only. Now fitted to both axes, and it `console.warn`s if the catalog ever exceeds the hall rather than silently stacking items off-world. Lesson: an auto-fit that constrains one axis just moves the overflow to the other one. - **"Placed-and-selected" auras aren't reachable** (order 3's third case). `SelectionState` is `build | remove | null` — UI's click-to-inspect selection has no contract channel, so a placed cooler rings on **hover**, not on inspect. Hover covers the same intent for now; see CONTRACT REQUEST 1. - Two tests fail repo-wide (`sim/reference.test.ts`, `ui/live.test.ts`) — both SIM/UI in-flight research work, neither imports `src/render`. tsc is clean repo-wide. **CONTRACT REQUEST** 1. **An inspected-entity channel**, if you want order 3's "placed-and-selected" case for real. Cheapest shape: extend `SelectionState` with `{ mode: 'inspect'; entity: number }`, or add `UIBus.inspected?(): number | null`. Renderer has no other way to know what the player clicked. Low priority — hover already answers "what does this cool?". 2. Nothing else. v4 granted round 3's `setGhostMode` ask (as `setGhost(..., mode)`) and it landed clean; the sentinel bridge can die in v5 whenever you like — this lane no longer reads it. **NOTE FOR THE MODELBEAST ORDER** — the manifest in round 3's NOTES is now stale: DATA added 6 machines (`roto-bench`, `matte-bath`, `optical-printer`, `chroma-keyer`, `neural-keyer`, `archaeology-lab`) and the tech tree now gates 13 across all five eras. `?showroom` is the live manifest — it labels every machine with its exact `.glb` filename and era, and re-reads data on load, so it can't go stale the way a table in a doc does. Read it there rather than from round 3. **NEXT (round 5 candidates, not self-assigned)** - Real MODELBEAST assets through `?showroom`; heat-shimmer suppression reading as *relief* inside a cooler's rectangle once SIM lands spatial cooling; bloom-loop belt tell; HF dust piles; wildlife; save/load camera state; LOD/culling if a megabase ever drags the frame. ### Round 5 (MEGA) — 2026-07-18 — Opus 4.8 **THE HEADLINE: "the game about place has no place" was a COLOR-MANAGEMENT BUG, not a missing feature.** I wrote the whole floor — fbm terrain, era hinting, seam glints — ran it, and the screen was still black. The shader was fine. `THREE.Color.setHex()` converts sRGB → the linear working space, but a raw `ShaderMaterial` gets none of the renderer's output includes, so the linear value went to the framebuffer untouched. Measured, not guessed: crust authored `#0c0a15` (RGB 12,10,21) was rendering as **RGB (1,1,2)**; even a mid-grey `#2a2440` came out (6,5,13). Everything this lane has drawn with a raw shader has been ~6× too dark **since round 1** — the grid, the crust, all of it. One `#include ` in `ground.ts` and the floor measures (10,8,18) against an authored (12,10,21). The void was never a design problem; it was a missing include. That's why the "before/after" is so stark, and it's the single most valuable thing I found this round. **SHIPPED** — all 8 orders. *Part A* 1. **Inspect highlight** — selection ring + cooler aura on `{mode:'inspect'}`. Verified: inspecting a 2×2 extractor rings it at its centre with scale 2.5; inspecting the asic-cooler shows ring **and** its 6×6 coverage rect. This closes the "placed-and-selected" case round 4 couldn't reach. 2. **Wildlife** (`wildlife.ts`) — dust piles as HF-dust glitter heaps scaling with `size`; mosquito swarms as pixel-gnat clouds orbiting `target`, with the harassed machine stuttering (§8: wrongness of motion is a faction uniform). Two draw calls for the whole ecosystem. 3. **Fixture hygiene** — devscene now fabricates `commissionQueue`, `research`, `wildlife` and `relics`, plus an asic-cooler and archaeology-lab so the v4/v5 kinds are reachable in the scaffold. *Part B* 4. **THE FLOOR** (`ground.ts`, `seams.ts`) — fbm tone mottle + neutral rim + 8 data-driven seam regions that skin the crust with their era and glint with their ore. 5. **THE RELIC** (`relics.ts`) — unfound = a breathing static patch; found = a fossil on a plinth. Numbers below. 6. **JUICE** (`juice.ts`) — placement scale-pop + dust puff, demolition burst, shipment flash. All derived from snapshot deltas, since `render()` never sees events. 7. **THE BREAK ROOM** (`breakroom.ts`) — GLYTCH arcade cabinet, own raycast, `cursor: pointer` on hover, click → `window.open('./match.html')` **verified firing**. 8. **CAMERA + CLICKS** — default framing lifts world origin to screen y=264, clear of the build bar at y=679. Picking investigation below — it found more than expected. **VERIFIED (numbers)** - **Relic is subtle-but-findable, measured at two zooms.** Brightest pixel on the relic vs plain floor 7 tiles away: at wandering zoom (12) **+39** — findable. At overview zoom (30) **−18** — i.e. indistinguishable from floor mottle. You cannot spot it from above; you have to be down there looking. That's the Mario-hidden-pipe budget, in numbers. - **Perf holds with everything on**: 500 entities + 2,000 belt items + 50 piles + 198 gnats + 8 seam regions + the new floor = **4.4ms/frame**, 3.8× headroom, 8 instanced draw calls. (R1 3.2 / R2 3.6 / R3 3.3 / R4 3.6 / R5 4.4.) - **Juice**: puff → 14 particles decaying over ~0.67s; burst → 22; shipment flash fired 22 times tinted `#ff7a3f`, exactly MELT's authored colour — so the flash reads which item shipped, not just that something did. - Prod bundle: `devscene`/`showroom`/`plinth`/`window.__render` all 0 hits; the round-5 shipping code (seams, aura, relics, breakroom) present. 446 tests pass, tsc clean. **DECISIONS** - **Dropped my own era-rim guess when DATA contradicted it.** The orders asked for strata whispers toward the world edges and I built exactly that — sepia north, phosphor east. Then `data/seams.json` landed mid-round, its doc saying "RENDER: `era` skins the ground", with reel in BOTH the north-west and north-east and disc both north-centre and south-west. My directional guess would have contradicted the data on screen, so era is now skinned per seam region with a wide soft falloff. It still reads as strata whispers toward the rim — the richest seams live out there anyway. - **Seam glint is flecks, not veins.** First pass drew continuous sinusoidal veins; on screen they were bright green worms crawling over the floor, competing with the belts. Mineral glitter is sparse and twinkles, so it's now a fleck grid with per-fleck sparkle clocks. The body fade is deliberately SHORT (0.8 tiles): SIM extracts on any footprint/rect overlap, and a glint dying two tiles inside the boundary would under-report where mining actually works. - **The cooler aura stays a rectangle** (round 4's reasoning) and the relic monument, the cabinet and the seams all now carry `colorspace_fragment` for the same reason the ground does. - **Cabinet is rotated 45°** to face the iso camera and sized to its own marquee — at the rig's closest zoom a 0.03-unit marquee pixel was sub-pixel and GLYTCH was a smudge. **BLOCKED/BROKEN** - **A frame-delta of 63.7 SECONDS was silently eating all juice.** `clock.getDelta()` returns the real gap, and a backgrounded tab / alt-tab / GC pause / first frame after load hands back seconds. Particles have a 0.5s TTL, so every effect was born and killed inside one update — and a 63s integration step flings them to infinity. `render()` now clamps dt to 0.05 (main.ts clamps the sim accumulator for the same reason; this is the renderer's half). This also protects the scale-pop, camera pan and heat animation. - **`match.html` only exists in `dist/`** (deploy.sh copies it there). In a dev server the cabinet's click opens a 404. Not fixable from this lane — `public/` is mine but the file is the deploy's. Flagged so nobody reports it as a cabinet bug. - The devscene relic sits at (9,-7), which is NOT inside DATA's `relicReserve` (12,12,20,20). That's fine for a fixture, but SIM's real relic should land in the reserve — worth a check when SIM lands `relics`. - The cabinet is decorative and reserves no tiles, so a player can place a machine "inside" it. Cosmetic only. **B8 PICKING INVESTIGATION — 27.6% of the viewport cannot be clicked** I probed a 20px grid over the whole viewport (2,205 points) asking "does a click here reach the game canvas?". 609 points — **27.6%** — do not. Breakdown: | blocker | points | note | |---|---|---| | `#screen` | 153 (6.9%) | **decorative overlay, `pointer-events: auto`** | | `#fk-build` + hint | 155 (7.0%) | build bar — legitimately interactive | | `#fk-fax` + flavor | 90 (4.1%) | fax panel — legitimately interactive | | `#fk-top` | 36 (1.6%) | bandwidth panel — legitimately interactive | The one that is a **bug**: `#screen` is THE SCREEN's canvas in `index.html`, purely decorative, 322×182 at top-centre, `z-index: 5`, and it takes pointer events. `#ui` next to it correctly sets `pointer-events: none`. So a 6.9% rectangle of prime play area silently swallows clicks — I hit it myself: the cabinet at (-5,-5) rendered *underneath* it and was both invisible and unclickable, which is exactly the review's "machines spawned unclickable". **Fix is one line in `index.html` (orchestrator-owned): add `pointer-events: none` to `#screen`.** I moved the cabinet to (6,12) as a workaround, but the dead zone is still there for machines. Also seen: `#fk-screenview` (LANE-UI's theatre mode) is a full-viewport `pointer-events: auto` host. Correctly `display:none` when closed — not a bug, noting it so the next person who probes picking isn't confused by it the way I was. **CONTRACT REQUEST** 1. **A renderer inspect channel.** `Renderer.setGhost`'s mode is only `build|remove` and main.ts passes `'build'` during inspect, so nothing about the inspected entity reaches this lane. `SelectionState` carries it, so I read `window.__fktryBus.selection()` each frame as a bridge — same shape as the old `'__remove'` sentinel, documented on both sides, and it wants a real hook: either widen `setGhost`'s mode to include `'inspect'` with the entity id, or add `Renderer.setInspect(id | null)` that main.ts calls. 2. **`pointer-events: none` on `#screen`** in `index.html` (see above). Not my file. 3. Advisory: SIM doesn't gate extraction to seam rects yet, so the glint is currently a cosmetic tell rather than a rule. The moment SIM lands the overlap check the floor already teaches it — no renderer work needed. **PROPOSAL — the shared tree cost me real time again.** Verification kept failing because LANE-UI and LANE-SIM save `src/ui/*.ts` / `src/sim/*.ts` every few seconds and Vite full-reloads my page, wiping every probe mid-measurement (the vite log shows a reload every 5–15s for minutes). I finished the round by snapshotting the repo to a scratch dir and running an isolated dev server against it. That works, but it's a workaround for the same root cause as round 3's commit collision: **one working tree, five writers.** A worktree per lane fixes both. **NEXT (round 6 candidates, not self-assigned)** - Real MODELBEAST assets through `?showroom`; heat-shimmer suppression reading as *relief* inside a cooler's rectangle once SIM lands spatial cooling; relic excavation VFX on the `relicFound` event; bloom-loop belt tell; save/load camera state; LOD/culling if a megabase ever drags the frame. ### Round 6 — 2026-07-20 — Opus 4.8 **SHIPPED** — all 3 orders, verified in the running build. The second asset wave landed mid-round: **27 GLBs are now in `public/assets/models/`** (up from the 10 the order named). Everything below is generic — it runs on *any* hot-swapped GLB through the registry, no hardcoded list — so all 27 (and whatever lands next) got the treatment automatically. 1. **GLB legibility pass (centrepiece).** The two-material rule died on real assets: baked albedo, zero emissive, near-black at game zoom (before/after below). Two halves, both keyed off the SAME accent derivation placeholders already use, so a machine that made melt still glows melt-orange whether it's a box or a GLB: - **Texel-following emissive FLOOR** (`registry.ts floorMaterials`, run ONCE on each GLB template in `tryLoad`): `emissiveMap = map`, `emissive = white`, `emissiveIntensity = 0.42` — the baked albedo self-illuminates so the brightest texels lift off the void without washing to flat grey. Materials with no map self-light their own colour; materials that already ship a glow are left alone. EntityLayer still clones these per instance, so per-entity heat/scram rides on top (heat lerps `emissive`→red; the map keeps modulating). - **Accent BEACON** (`addAccentBeacon`): the one impossible element, perched on top, scaled to footprint, in the machine's derived/art-directed accent — plus a gentle idle pulse so a GLB gets a heartbeat like a placeholder. The idle reads `beacon.material` at call time, not a captured ref, because EntityLayer re-clones GLB materials per instance (a closure over the pre-clone material would drive a mesh nothing renders — caught that before it shipped). **DATA's round-6 accents were chosen against the real GLB textures, so the beacons wear DATA's hand-picked colours.** - Verified in `?showroom` (before: dark specks on plinths → after: lit chassis + colour- coded beacons across the whole 27-machine catalog) AND in the live demo world at the default game frame (`__fktryDemo()` reference factory, `rig.frame(3,6,15)` untouched): every GLB legible, beacons marking each unit. 2. **Parity mites** (`wildlife.ts`, +2 instanced meshes). Hospital-white faceted beetles with a green **checkmark eye** (canvas ✓ texture, tilted −45° to meet the iso camera). The tell tracks SIM's `size` state channel exactly: `0` hunting (slow, patient patrol — the wrongness is the calm), `0= 0.8`, 16 at `>= 0.5`, 21 with no metallic-roughness texture to vary it per texel. Re-probed on HEAD after the orchestrator's 27.3 MB -> 7.0 MB pass and the distribution is UNCHANGED (13/16/21 — that pass recompressed textures and meshes, not PBR params), so every number below still describes the shipped assets. Round 6's `GLB_EMISSIVE_FLOOR = 0.42` was cosmetics over a lighting hole. It is now 0.18 and the hole is filled.** **SHIPPED** — all 4 orders. 1. **`src/render/env.ts` (new) — a procedural environment, ZERO bytes shipped.** A 128x64 half-float equirectangular radiance map built from arithmetic (no asset fetch, nothing for the deploy to carry), run through `THREE.PMREMGenerator`, assigned to `scene.environment`. Content per codex §8 — near-black indigo floor keyed off the crust, cool "bitstream haze" overhead, a horizon band (this is the bit that makes a metal edge read as metal), and two soft blobs standing in for the key and fill. **The light directions are PASSED IN from index.ts's real lights**, not copied — the scram-latch lesson: this module keeps no second copy of another module's constants. 2. **Emissive band-aid RE-TUNED, not retired: 0.42 -> 0.18.** With real IBL, 0.42 is overexposure — it flattens the shading the environment just bought back and pushes every chassis toward a self-lit sticker. Kept because a genuinely black texel still needs the "never a hole" guarantee, and because it is what keeps a machine legible when a brownout drops tone-mapping exposure to 0.7. 3. **Lazy GLB loading.** `registry.init` no longer awaits `probeAll`. Placeholders are built synchronously one line above it and main.ts awaits `renderer.init()` before it starts the frame loop, so the first frame of the game was gated on 27 HEAD requests plus the whole catalog. **Placement juice suppressed on hot-swap** (`entities.ts`): the rebuild path could not tell a MODELBEAST swap from a placement, so with lazy loading the entire factory would have puffed dust and scale-popped ~1s into every boot. A `def` change still pops — that is a real replacement. 4. **`src/render/registry.test.ts` (new) — 31 tests, this lane's first.** 481 -> **512 repo tests**. Source-level only: fit-to-footprint normalisation, accent precedence, era lookup, and the env generator. **Mutation-checked, not just green**: reverting the era tie-break, changing the 0.92 fit factor, and deleting the `def.accent` short-circuit fail 7 tests between them. **WHAT THE ENVIRONMENT ACTUALLY DID — measured, and honestly mixed** Mean sRGB luminance of each machine body at a fixed showroom close-up frame (`rig.frame(0, -14, 10)`), sampling the same pixels in every condition. Plain crust reads ~24, so ~24 is the "this machine is a hole in the floor" line. | machine | metalness | no env, floor 0 | env, floor 0 | ROUND 6 (no env, floor 0.42) | **SHIPPED (env, floor 0.18)** | |---|---|---|---|---|---| | demosaicer | 1.0 | **19.8** (darker than the floor) | 42.4 | 74.9 | **76.9** | | neural-keyer | 0.96 | **22.8** | 45.4 | 84.5 | **81.4** | | archaeology-lab | 1.0 (MR tex) | **24.0** | 33.6 | 61.3 | **54.1** | | mv-extractor | 1.0 | **26.2** | 41.7 | 70.2 | **71.1** | | p-caster | 0.80 | **26.4** | 40.6 | 62.4 | **62.8** | | gop-assembler | 0.85 | 35.2 | 51.6 | 72.4 | **73.6** | | software-decoder | 1.0 | 46.8 | 63.7 | 104.7 | **96.8** | | mosh-reactor | 1.0 (MR tex) | 58.2 | 72.0 | 86.6 | **90.1** | | decode-asic | 1.0 (MR tex) | 58.2 | 76.6 | 97.9 | **99.9** | | *roto-bench (metalness 0, control)* | 0 | 63.5 | 83.0 | 95.2 | **104.2** | | *field-loom (0.09, control)* | 0.09 | 125.1 | 133.5 | 137.8 | **141.4** | Read it honestly, three ways: - **The bug is real and it is exactly where the orchestrator said.** With the band-aid off, demosaicer measured **19.8 against a crust of 24** — the machine was literally darker than the ground it stood on. Screenshot before/after of that condition is the starkest pair I have taken in this lane since the round-5 colour-space bug. - **The environment is worth the most exactly where metalness is highest.** Env alone lifted demosaicer +114% and neural-keyer +99%, against +31% for the metalness-0 control (roto-bench) and **+7% for field-loom at metalness 0.09**. That gradient IS the diagnosis: the fix tracks the term that was broken. - **BUT — and this is the honest part — the environment alone does NOT reach round-6 brightness.** Env at floor 0 leaves the darkest machine at 38; round 6 sat at 57. What the IBL buys is not brightness, it is FORM: shading, a specular edge, and the baked albedo reading as a surface instead of a decal. The final numbers land within a few percent of round 6 *because I chose the emissive floor to put them there* — the difference is that the light is now doing the shaping. Anyone reading only the last column would conclude nothing changed; look at the screenshots instead. **DECISIONS** - **GLB materials get `envMap` assigned explicitly, not inherited from the scene.** Forced, not stylistic: three.js *overrides* `material.envMapIntensity` with `scene.environmentIntensity` whenever a material falls back to the scene environment (`WebGLRenderer`: `material.envMap === null && scene.environment !== null`). Assigning it per material is the only way the assets can be dosed differently from six rounds of hand-tuned placeholder art. **I burned real time discovering this** — my first tuning sweep of `envMapIntensity` from 1 to 5 produced byte-identical output and looked like a broken env map. - **Two dials, deliberately far apart: `ENV_WORLD_INTENSITY = 0.5`, `GLB_ENV_INTENSITY = 2.6`.** The world's own materials (belts, cargo, placeholders, ghosts, wildlife, the cabinet) were art-directed across six rounds under no IBL at all; relighting them wholesale is a regression dressed as a fix, so they get a dusting for coherence and nothing more. The assets get 5x that because they are the metals. - **`AmbientLight` left alone at 1.1 after measuring it.** I lowered it to 0.55 first, assuming the new environment made it a double-count. Then I swept it: 0.55 -> 1.1 moves a placeholder body ~7% and the high-metalness GLBs **<1%**, because ambient only feeds the diffuse term metalness has already zeroed. Reverted rather than ship a change that does nothing. (Same arithmetic as the headline, seen from the other side.) - **I did NOT clamp the assets' metalness, though I tested it and it works.** Capping metalness at 0.45 at load for the 21 materials with no MR texture reaches the same luminance at `envMapIntensity: 1` and is visually near-identical (I A/B'd both at the same frame). Not shipped: rewriting an asset's authored PBR values is a bigger claim than lighting the room it stands in, and it would mask the upstream defect instead of reporting it. See MODELBEAST FINDING below. - **The TTFF instrumentation is dev-only and folded out.** `window.__fktryPerf` — 0 hits in `dist/` (verified), along with `devscene`/`showroom`/`plinth`; the only `__render` matches are three.js's own `__renderTarget`, as in round 3. **TIME-TO-FIRST-FRAME — measured, with the caveat stated up front** `firstAt` (the real first `gl.render`) is **unusable in this environment**: the browser backgrounds the pane and starves rAF, and I read 5.1s / 11.3s / 14.5s for a page that was ready in 90ms. Same friction rounds 5 and 6 hit. So the number below is `readyAt` — the moment `renderer.init()` returns, which is the earliest a frame can exist because main.ts awaits it before starting the loop. It is also precisely the quantity this change moves. A/B on the same build, one line flipped between `await this.probeAll()` and `void`: | | `renderer.init()` duration | ready at (from navigation start) | |---|---|---| | blocking (round-6 behaviour) | 284 / 426 / 304 ms | 339 / 483 / 360 ms | | **lazy (shipped)** | **27.8 / 25.2 / 28.2 ms** | **88.5 / 80.7 / 85.7 ms** | **~10x off `init`, ~4x off time-to-ready, ~250-400 ms saved — and that is the WARM-CACHE best case on localhost.** A cold visitor to partly.party/glytch pays the whole catalog over the network before the first pixel under the old path; under the new one they get placeholders immediately and the assets stream in behind. The orchestrator's 27.3 -> 7.0 MB pass and this change compound. **VERIFIED** - `npm run check` clean. `npm test` **512/512** (was 481; +31 mine). - **FRESH hard-navigated loads, not HMR** (round 6's lesson, followed): `?showroom` and `?uidemo` on :8152, both after the orchestrator's new 7.0 MB catalog landed mid-round. Zero console errors on both. Live material state confirmed on a cold boot: `envMap` present, `envMapIntensity 2.6`, `emissiveIntensity 0.18`. - **No round-6-style scale regression**: after forcing an asset-version bump on all 31 showroom entities, every `rec.pop` stayed settled at 0.22 and every `obj.scale` equalled its `baseScale` (GLBs at 2.199 / 2.070 / 1.384, placeholders at 1.0). - **Hot-swap juice suppression, deterministic**: bumping the asset version under all 31 entities emitted **0** juice particles; adding one genuinely new entity on the next sync emitted **14** (exactly one `puff`). Both cues still work, neither fires on a swap. - **Hot-swap fires once per asset**: `registry.version` 27, every entry at `version: 1` on a cold load. (The console shows each line twice — that is the console reader duplicating, not a double load. Checked because it looked alarming.) - **Perf**: the environment costs nothing measurable. `?devscene=stress` full-scene `gl.render` submit at 2800x1800, 111 draw calls / 240k tris: **0.445 ms with the env, 0.467 ms without it, 0.432 ms with it back on** — identical inside noise, and one prefiltered cubeUV fetch per lit pixel is the whole added cost. Last full-frame CPU sample 1.2 ms against a 16.6 ms budget. Honest caveat, unchanged from round 6: the rolling `stats.avg` sampler needs a sustained rAF loop and the backgrounded pane will not give one, so this is a forced-render measurement rather than the canonical one. **BLOCKED/BROKEN** — nothing blocking. - **A cross-lane era tie-break was WRONG in this lane, and writing the tests is what found it.** Round 4's NOTES claim I matched LANE-UI's "first tech to claim an id wins" (`src/ui/research.ts`). I did not: `era.ts eraIndex` used an unconditional `Map.set`, i.e. **LAST tech wins**. Left alone, a machine claimed by two techs would get a padlock naming one era in the build bar and an era tell naming another in the ghost, on the same screen — exactly the divergence round 4 congratulated itself for avoiding. Fixed, and pinned by a test. Latent so far only because `data/tech.json` currently has zero duplicate unlocks across its 25 techs, so nothing was visibly wrong. Six rounds of eyeball QA never had a chance of catching this; the first hour of unit tests did. - **The player now sees placeholders for the first ~second of a boot** before the GLBs swap in. That is the deliberate trade in order 3, and it is the same path a mid-session MODELBEAST drop has used since round 1 — but it IS a visible change to what a cold start looks like, so it should be a conscious call and not a surprise. - `match.html` 404 in dev (rounds 5-6, unchanged). **MODELBEAST FINDING — two things for the asset lane, both measured, neither mine to fix** 1. **The catalog's metalness is machine-generated, not art-directed.** 13 materials at `metallicFactor: 1.0` with **no metallic-roughness texture** is what image-to-3D generators emit by default; codex §8 describes grimy chassis with ONE impossible element, not chrome. The environment now carries them, but shipping sane metalness (or an MR map) would let `GLB_ENV_INTENSITY` drop from 2.6 to ~1.0 and would recover the baked albedo detail the metal term is currently eating. One constant in `registry.ts`. 2. **`archaeology-lab.glb` is half a flat wall, and `mosh-reactor.glb` has a large planar face.** Visible in `?showroom` as a card standing on a plinth, and quantified: fraction of vertices inside a single 4%-thick slab — **archaeology-lab 48.3% on Z** (extent 2 x 2 x 0.77), **mosh-reactor 26.2% on Z / 23.8% on Y**, against 7-15% for healthy meshes (dct-press, mv-extractor, decode-asic). It looks like a backdrop plane surviving the image-to-3D step. Present in BOTH asset generations, so the 7.0 MB pass did not introduce it and did not fix it. No renderer change would help: there is nothing behind the plane. **CONTRACT REQUEST** 1. **A renderer event channel** — renewing rounds 5 and 6 unchanged. Three cues in this lane (`repaired`, the firewall "can", `relicFound`) are still inferred from snapshot deltas. 2. Still open (round 5): `pointer-events: none` on `#screen` in `index.html` (orchestrator). **NEXT (not self-assigned)** - Tests for the rest of the lane now that the harness exists — `topology.ts` neighbour classification and `coords.ts` `centerOf`/`yawFor` are pure and are load-bearing for everything that has ever looked misplaced. - A one-line `GLB_ENV_INTENSITY` drop to ~1.0 the moment MODELBEAST ships sane metalness. - Wire the renderer event channel if granted; `firewall.glb` when it lands; per-mite orientation; heat-shimmer relief inside a cooler rectangle; bloom-loop belt tell; save/load camera state. ### Round 7 PHASE 1 — 2026-07-29 — Opus 5 — THE CORRECTION, embodied **THE HEADLINE: the inversion is not a vibe, it is a measurable property, and it now has a number. Driven at 60fps over 60 sim ticks with a WORST-CASE stepped input — a warden whose sim position changes on 6 of 120 frames and only ever by whole integer tiles — the renderer produced 120 distinct positions and ZERO stalled frames. The hash auditor, on the same rig, produced 1 distinct position and 119 stalled frames, deliberately. Authority glides; authority also holds perfectly still; what authority never does is judder.** **SHIPPED** — all 5 orders. New: `src/render/correction.ts`, `src/render/notice.ts`, `src/render/correction.test.ts`. Zero GLBs, as ordered. 1. **Placeholder bodies, rungs 2-4** (`correction.ts`). Hospital-white, matte, spotless, one green element each — green is the faction signature it shares with the mite's checkmark eye, so a player learns "green on white = the immune system" once. - **WARDEN**: tall filing-cabinet slab, drawer breaks, rubber-stamp arm on a green pad that slams on `phase === 'audit'` and rests cocked otherwise. - **GUNSHIP**: five platters in a row on spindles, counter-rotating about the centre (that is where "mirrored" went), green parity emitter on the belly, continuous hover. - **AUDITOR**: faceless white-marble column, green padlock halo, binary scan cone. 2. **The gunship's mirror**: the `rect` gets a faint white sector pad, and every entity inside it is re-rendered as a featureless translucent white box hovering at 1.5 under the gunship — your own layout, sanitised (§5). One InstancedMesh, capped at 512. The parity **tether** is a green cylinder stretched from the emitter to the anchor entity, with a pulse running down it. 3. **Stasis**: bleached ground + hard white edge + a floating **00:00:00:00** timecode, and a `frozen` entity set that EntityLayer and BeltItemLayer both read. Release snaps back the same frame with a white slap at the rect's centre and four corners. 4. **Seal**: sealed machines get a printed compliance placard (green tick, SEALED, a hash line) facing the camera azimuth, and every moving part stops. 5. **onEvents migration** — all three derived cues retired, plus four new ones. **VERIFIED (numbers, on FRESH hard-navigated loads — round 6's lesson, followed)** *The glide (order's DoD: "verified against juddering surroundings").* 120 render frames / 60 sim ticks, warm-up discarded, positions sampled per frame: | | sim moved it on | rendered distinct | step min | step max | mean | stalled frames | |---|---|---|---|---|---|---| | warden (integer-stepped input, worst case) | **6 / 120** | **120** | 0.00097 | 0.228 | 0.048 | **0** | | gunship (fractional input, SIM's real shape) | every tick | **120** | 0.00037 | 0.0157 | 0.0075 | **0** | | auditor (teleports, never walks) | 3 jumps | **1** | 0 | 0 | 0 | **119** (by design) | The warden's max/mean ratio of ~4.7 is the damped follow easing out of each whole-tile jump; it is continuous, and it is the deliberately unfair case. **I checked SIM's actual implementation rather than guessing**: `driftUnit` moves wardens and gunships by a FRACTIONAL step every tick (the gunship row above), and the auditor is relocated by direct assignment with the comment "it moves only between frames". Both of my paths match the sim they will actually be fed. *Stasis freeze.* 40 frames / 20 sim ticks with the lock held: **5 of 6 melt items dead still**, the 6th correctly still moving because its belt is the one tile outside the rect. Frozen coordinates are genuinely **mid-tile** — fractional parts 0.052, 0.4, 0.503, 0.733 — not snapped to a tile edge, which is the whole read. Over a longer 25-tick hold, 5 of 6 shared ids were **byte-identical**; the 6th legitimately left the rect and re-entered. **Release**: frozen 10 → 0 and iced 7 → 0 within 4 frames, `stasisActive` false. *Seal.* Placards visible 0 → 1 on the seal edge, positioned at (1.75, 0.68, 9.75) for a 2×2 at (0,8) — dead on the footprint centre plus the camera-facing offset — at yaw **0.7854** (= ISO_YAW). The freeze is exact: **idle clock delta 0.000000 over 30 sealed frames**, 0.500000 over the next 30 free frames (30/60 exactly). *Perf — the standing rule, at a load far past anything the sim will produce.* I put the Correction into `?devscene=stress` at the honest worst case: a gunship mirroring the ENTIRE 500-entity build and an auditor freezing all of it — **500 ghost boxes, 500 frozen entities, 2,000 pinned belt items**, 149 draw calls, 265k tris at 2800×1800. | | ms | |---|---| | whole frame callback (sim tick + render + UI + screen + audio), median / p90 / max | **2.30 / 5.5 / 6.0** | | renderer-only rolling avg | 1.90 | | `gl.render` submit, correction layer ON vs OFF | 0.41–0.51 vs 0.325 | | `correction.sync` CPU (the entire worst-case pass) | **0.128** | | `cargo.sync` WITH the freeze vs without | **0.392 vs 0.465** | | `entities.sync` with vs without | 0.032 vs 0.020 | Net cost of this round at that load: **~0.2 ms against a 16.6 ms budget.** The cargo freeze is *cheaper* than not freezing — a pinned item skips the topology and path maths entirely. *Prod bundle*: `devscene` / `showroom` / `plinth` / `__fktryPerf` / `correctionUnits` / `buildCorrectionBay` all **0 hits**; the only `__render` matches are three.js's own `__renderTarget` (4), as every round since 3. The shipping strings are present (`PARITY NOTICE`, `00:00:00:00`, `fk-notice`). `npm run check` clean. *Tests*: this lane 31 → **40** (9 new, all mutation-checked — see below). Repo 598 tests, 596 pass; the 2 reds are `src/sim/mite.test.ts`, LANE-SIM's in-flight firewall-vs-warden work, which imports nothing from `src/render` (grep: 0 hits). `src/sim/reference.test.ts` was red mid-round and SIM fixed it while I worked. **DECISIONS** - **The auditor has NO idle animation at all, and that is the whole design.** Not a bob, not a halo drift, not an emissive pulse. Every other object in this game is alive; the one that isn't is the one to be afraid of. It is also the honest reading of §5's "moves only between frames" — stillness is not judder, it is the same claim of total control from the other side. Its scan cone is binary (present during `prescan`, absent otherwise) rather than sweeping, for the same reason. - **The warden keeps its RED LED ledger eyes, but as pinpoints.** Orders said "one green element each"; codex §5 says the warden has "red LED ledger eyes". The codex wins on canon and the orders win on the faction read, so both got what they were actually protecting: the ONE element is the large green stamp pad (which is also literally the seal that ends up on your chassis), and the ledger is a strip of five 5cm red pinpoints on the drawer chest. A large red element would have said "different faction" on a screen where the other three rungs are green. - **The gunship is matte white, not chrome, and its platters are SMALLER than half their spacing.** §5 says "mirrored disk-platter rotors"; five actually-mirrored surfaces on a black-sky world read as five black holes, and authored at true platter proportions (radius 0.4, spacing 0.54) they fused into a single white lozenge at game zoom — caught in `?showroom`, fixed to radius 0.26. The COUNT is the silhouette, so the count has to survive. Screenshots before/after are stark. - **The warden's detail face is local +Z, not -Z**, breaking this lane's own placeholder convention on purpose. Machines are authored facing north because they carry a `dir`; a Correction unit instead yaws toward its own travel, and a parked one sits at yaw 0 — with the drawers on -Z, a parked warden showed the iso camera a blank white back. Found in the showroom bay, which is exactly what the bay is for. - **Stasis rects come from the UNION of the event and the snapshot.** `phase === 'stasis'` is a string this lane does not own, and keying a freeze off it alone would be a silent single point of failure the day SIM renames a phase; the `stasis` event's rect (keyed by `lockHash`) is the other half. Either source alone lights the region. They currently agree — I read SIM's `refreshStasis`. - **The rect overlap convention is now PINNED BY A TEST against SIM's own formula.** SIM's `frozenEnt` uses inclusive bounds, mine are half-open. On integers those are the same predicate — but only by argument, and phase 0's era tie-break was exactly this shape: two lanes claiming to agree, never checked, latent for three rounds because the data never disagreed. SIM's formula is transcribed into `correction.test.ts` as an oracle and the two are asserted equal over 4,896 footprint/position combinations. If either lane retunes its bounds, it fails loudly. - **The bleach overlay is totally static** — no pulse, no scroll, no breathing. Every other overlay in this game moves; the one that doesn't is the one that stopped time. - **Stasis freezes machines as well as cargo**, reusing the sealed path. Both are the same sentence from The Correction: this is not running. (SIM agrees independently — its `frozenEnt` gates crafting, heat, progress and belt movement.) - **`shipmentFlash` was deliberately NOT migrated** to the `shipped` event. The event carries item and count but no position, so the uplink tiles come from the snapshot either way, and the existing delta is already exact. Migrating for its own sake would have been churn. - **The mite's "canned by a firewall" cue is now event-TIMED but still state-DISCRIMINATED.** `wildlife: 'cleared'` fires for a sated mite leaving at the rim and for one canned mid-repair alike, and only `size` tells them apart — so timing and position are now exact (from the event) and one boolean is still read from the layer's last-seen state. Honest improvement, not a full retirement. See CONTRACT REQUEST 2. **BLOCKED/BROKEN** — nothing blocking. - **The notice ping reads LANE-UI's DOM to find the fax, and that is a bridge.** The `notice` event has no `pos` and the fax is a DOM panel, so `NoticePing` measures `#fk-notices` (UI's new v9 tray) or `#fk-fax` and strikes a halo around it. The element is mine — created by and appended by this lane, same as the round-1 vignette — and the read is read-only, but it is a cross-lane coupling that wants a real hook. It also exposed a real ordering trap: **main.ts runs the renderer BEFORE the UI within a frame**, so on the first notice of a session the panel may not be laid out yet and measures 0×0. It now retries once on the next animation frame before falling back to a corner. I hit this for real — my first live notice struck the fallback position. - **The `#screen` pointer-events request is closed, not granted** — the round-5 review ruled it a legitimate interactive surface now that UI has click-to-enlarge. Dropping it from my asks; recording the resolution so it isn't re-filed. - **The verification environment fought back again, worse than round 5.** The Browser pane reports `document.hidden === true` and rAF never fires, so `stats.frames` sat at 0 while the page looked alive; and the shared tree hot-reloaded my page mid-measurement (I caught it only because `stats.frames` reset to 0 under me and I nearly wrote up a "notice ping is broken" finding from a page that had reloaded 200ms earlier). Fixed the way round 5 did: an isolated `rsync` copy of the repo on its own port, plus a setTimeout-backed rAF shim so frames advance at all. Every number above comes from that isolated build. **Anyone measuring in this pane should check `document.hidden` first.** - The mirror only ghosts ENTITIES, not belt cargo. Belts are entities so the sector's layout reads fully; the items riding it don't get duplicated. Cheap to add (one more instanced mesh) if the mirror ever needs to look busy rather than clean — though "clean parallel copy" arguably wants to be emptier than yours. - `match.html` 404 in dev (rounds 5-7, unchanged). **CONTRACT REQUEST** 1. **`notice` could carry a `pos?: Vec2`** — the unit that filed it has a position, and with one the renderer could put the cue in the world at the thing being noticed instead of reading another lane's DOM to find the fax. Small, optional, retires a bridge. 2. **`repaired` / `mirrored` could carry `by: number`** (the unit or mite id). Would let the "canned vs sated" discrimination above come from the event stream instead of the layer's remembered `size`, retiring the last state-derived cue in this lane. Low priority — the current form is correct for SIM's lifecycle. 3. Nothing else. **v9 granted the renderer event channel** asked for in rounds 5, 6 and 7-phase-0; it landed clean and retired three heuristics on contact. **PROPOSAL / MODELBEAST WORK ORDER — rungs 2-4 are hot-swap targets the moment they exist.** `?showroom` now has a **CORRECTION bay** north of the machine hall: all three rungs on labelled plinths, plus four duplicate machines being mirrored, sealed and frozen so every state is one screenshot. The bodies are procedural placeholders and do NOT yet go through `AssetRegistry` (they are not `MachineDef`s and have no `asset` key), so a GLB drop needs a small registry extension rather than nothing — flagging that honestly, since every other asset in this game is zero-code. Codex asset lines are in §5; the three worth generating are `checksum-warden` (rigged: stamp arm), `redundancy-gunship` (5 platters), `hash-auditor` (rigged, though it must never be seen to walk — an idle-only clip). **NEXT (round 8 candidates, not self-assigned)** - Registry support for non-machine actors so Correction GLBs hot-swap like everything else. - `debtBand` as a world-wide grade (the sky/fog cooling as the bands climb) — deliberately left alone this round because UI and SCREEN both own ledger surfaces and three lanes drawing the same number is how a screen gets noisy. - Mirror the cargo inside a gunship sector; per-mite orientation; heat-shimmer relief inside a cooler rectangle; `firewall.glb`; save/load camera state.