glytch/fktry/lanes/LANE-RENDER.md
type-two c97dd80a9b [render] round 4 NOTES
Verification numbers; the lock-rule divergence with UI (they were right, I was
wrong — the scrammed lesson misapplied); the showroom overflow postmortem; one
contract request (an inspected-entity channel for placed-and-selected auras).

Notes that round 3's MODELBEAST manifest is stale — ?showroom is the live one.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 20:12:04 +10:00

34 KiB
Raw Blame History

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 4 (goal: v4 protocol, research & aura visuals)

Round 3 review: the dead-debug-hook bundle grep and the provenance note were exemplary; your worktree analysis drove the new MASTERPLAN commit rule (pathspec commits — your option 2, with worktrees as the named escalation). Verdicts: CONTRACT REQUEST 1 GRANTED — v4 has the typed selection protocol (SelectionState, setGhost(..., mode)) and main.ts already passes mode on every call (the '__remove' def string still arrives as a bridge; it dies in v5). MachineDef.accent? also landed (your round-2 wish, UI re-filed it). Era skins verified beautiful.

  1. Ghost migrates to mode: key demolition tint off the 4th arg, ignore the sentinel def. Keep rendering exactly as shipped otherwise.
  2. Locked-machine ghost (research lands this round): a place for a locked id is silently dropped by the sim — the ghost must telegraph it. Distinct from occupied-red: padlock-amber wash + "era" tell if you can read the tech era from data cheaply. Coordinate the lock lookup with UI via NOTES (both of you derive it from snapshot.research.unlocked + tech.json — same rule, CONTRACTS v4 notes).
  3. Cooler auras: coolRadius machines get a floor-ring (subtle, engineering- drawing vibe, not a buff circle) on hover/ghost AND while placed-and-selected; heat-shimmer suppression inside the ring falls out of sim truth automatically.
  4. Lab visuals: placeholder with a distinct silhouette (research bench — codex voice pending DATA's machine entry); accent? consumed when present (your derivation stays fallback).
  5. Housekeeping approved from your NEXT list: splitter lamp can stay timer-driven (real round-robin state isn't in the snapshot and doesn't earn a contract field yet); showroom item-hall paging when DATA's catalog next grows (Composite Seam transcription lands ~15 items this round — it will).

Definition of done: remove-tint works with the mode arg (verified with UI's migrated flow); a locked machine's ghost is legibly "not yet" rather than "broken"; cooler ring reads at a glance; showroom absorbs the new catalog.


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/<key>.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.0890.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.warns 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/<asset> 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 -- <paths>, 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.warns 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 <asset>.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.