glytch/fktry/lanes/LANE-SIM.md
type-two 08761e8eeb [orchestrator] round 2 review + contracts v3 + round 3 orders
Review: 5/5 lanes pass. 160/160 tests, tsc clean, M1 COMPLETE — verified live:
reference factory ships melt, commission queue advances with standing orders,
SCREEN's new artifact passes respond item-specifically (block 0.46 from bricks).

Contracts v3 (granted): EntityState.scrammed, SimSnapshot.commissionQueue,
MachineDef.coolPerTick, setRecipeAt command, Sim.save/load, ScreenFX.frame(t, snap),
BeltItem.id hardened required, MachineDef.color re-ruled as chassis/body.
main.ts: passes snapshot to screen.frame; dev __fktryDemo() builds reference factory.
happy-dom ACTUALLY installed this time (round-2 grant never landed; UI caught it).

Rulings: bandwidth units are per-second (SIM converts); heat retune ordered (nothing
can currently overheat - heatPerTick < ambient cooling); validator test moves to
data/; RENDER's color deviation approved as the ruling; scram wipe 3 frames; REMOVE
button approved. Round 3 orders in all lane docs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 18:05:53 +10:00

260 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

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

# LANE-SIM — deterministic factory simulation
You are an Opus 4.8 executor on the SIM lane of FKTRY. Read `../MASTERPLAN.md` and
`../CONTRACTS.md` before anything else; the world bible is `../../docs/FKTRY_LORE.md`.
**Mission:** the headless, deterministic heart of the game. Grid, entities, belts,
recipes, power, events. If the renderer were deleted, your sim should still be fully
playable through unit tests. You are the lane the other four trust with their lives.
**Owned paths:** `src/sim/**` (including your tests: `src/sim/*.test.ts`).
**Never touch:** contracts, main.ts, other lanes, data JSONs (request changes via NOTES).
**Standing rules (all rounds):**
- Deterministic: seedable RNG (write your own mulberry32-style helper), no wall-clock,
no unseeded Math.random. Same seed + same commands = identical snapshots.
- Every mechanic lands with tests. `npm test` green before you write NOTES.
- Performance target: 1,000 entities + 5,000 belt items at 30 tps without heroics.
Prefer flat arrays and index maps over object soup. Don't allocate in the tick loop
when avoidable.
- Snapshot may reuse buffers, but its SHAPE must always match `SimSnapshot`.
---
## CURRENT ORDERS — Round 3 (goal: M2 "PRESSURE" becomes real, M3 groundwork)
Round 2 review: the reference factory turning round 1's deadlock into 1,136 shipped
bricks is exactly what "surplus becomes product" was supposed to mean — outstanding.
Verdicts: `coolPerTick` GRANTED (contracts v3, default 0.004 — your constant becomes
the fallback). `setRecipeAt` GRANTED (your tripwire, defused). `BeltItem.id` HARDENED
to required (you asked for it). Throttle-hits-powerGen: KEPT, your reading was right.
The validator-file ownership wart: RULED — it moves to `data/validate.test.ts`, DATA
executes the move, `src/sim/**` is purely yours again.
**UNITS RULING (DATA's cross-lane ask):** `gen`/`draw` are bandwidth per SECOND;
`stored` is bandwidth-seconds. Your integration currently charges/drains per tick —
divide by `TICKS_PER_SECOND` so a 30-deficit drains 30 stored per second, not per
tick. DATA's `bufferCap: 240` then covers 8s as they intended. Update tests; confirm
the conversion in NOTES so DATA can retune against truth.
1. **Contracts v3 wiring**: `EntityState.scrammed` (RENDER is currently duplicating
your 0.5 constant to avoid rendering a lie — kill that), `commissionQueue` on the
snapshot (active first, then upcoming — UI's NEXT IN TRAY is guessing today),
`coolPerTick` per machine, `setRecipeAt` command, the units conversion above.
2. **Save/load**: implement `save()`/`load()` — JSON, deterministic round-trip
(test: save at tick N, load into a fresh sim, run both 1,000 ticks, deep-equal).
3. **Reference factory v3**: heat is about to become real (DATA retunes this round —
coordinate via NOTES). Restructure power as ASIC base + software-decoder burst +
buffer tank so the layout *survives scram duty-cycles* — DATA warned your three
naked decoders will black out in lockstep. Keep the 20k regression green.
4. **Bloom loop proof**: test that a closed belt circle through a bloom-duplicator
accumulates grades (your rank-0 loop acceptance + DATA's grade recipes should
compose). This is M3's signature topology — prove it early.
**Definition of done:** all tests green including save/load round-trip and the bloom
loop; NOTES confirm the units conversion + the retuned reference factory's scram
survival (ticks of first scram, recovery, melt rate before/after).
---
## NOTES (append-only log, newest at bottom — template below)
```
### Round N — YYYY-MM-DD — [your model]
SHIPPED: what actually works now (verified how)
DECISIONS: choices you made inside your mandate
BLOCKED/BROKEN: anything not working, honestly
CONTRACT REQUEST: (if any)
PROPOSAL: (ideas for the orchestrator, optional)
NEXT: what you'd do next round if asked
```
### Round 1 — 2026-07-17 — Opus 4.8
SHIPPED
- The stub is gone. `src/sim/` is now grid + placement, belts with back-pressure, crafting,
bandwidth/brownout, shipping and commissions. Files: `index.ts` (the sim), `geom.ts`
(grid math), `rng.ts` (seeded mulberry32), `sim.test.ts` (18 tests).
- Verified three ways, not one:
1. `npm run check` clean; `npm test` 70/70 green across all lanes (18 are mine).
2. Live at localhost:8150 (another session's dev server — see BLOCKED). App boots, zero
console errors. My numbers are visibly driving other lanes: the UI bandwidth readout
showed `9 DRAW / 20 GEN` for an ASIC + extractor + 4 belts + uplink, which is exactly
my power math; the fax panel reads my `activeCommission`/`commissionProgress`.
`__fktryBus.dispatch(...)` places entities and they render.
3. Ran the sim in the real browser engine by importing `/src/sim/index.ts` in the page:
the M1 chain shipped its first MELT at **tick 1149**, commission `first-taste` closed
at tick 1149 — the identical tick Node produces. Determinism holds across engines.
- Perf (measured in-browser, not estimated), against the 1,000 entity / 5,000 item target:
- 1,245 entities + 2,436 belt items → 0.138 ms/tick (242× realtime)
- 3,500 entities + 6,820 belt items → 0.418 ms/tick (**80× realtime**)
DECISIONS
- **Output-belt routing by rank — the big judgement call, please review.** A machine picks
which adjacent belt to hand an item to by tracing the belt chain to the machine it ends
at (cached; invalidated on any place/remove/rotate/setRecipe): rank 2 = that machine
consumes this item, 1 = it's an uplink (takes anything), 0 = dead end or loop (accept, so
a half-built belt still fills instead of looking broken), -1 = it will never take this
item, so refuse. It never falls back to a worse rank just because the best belt is
momentarily full. Why I did this: with no splitters or inserters, the demuxer's three
outputs (luma / slurry / mv-flux) have to leave one 2x2 perimeter toward three different
machines. Without ranking, items land on the wrong lane and wedge it permanently — M1
cannot flow at all. This is routing intelligence the orders didn't ask for; it's also the
root of the deadlock below. If you want it gone, splitters have to land first.
- Belt spacing is enforced *across the tile seam*: a blocked front item holds station 0.45
behind the rear item of the next belt rather than bunching at the boundary.
- Pause freezes sim time (`tick` stops advancing) but commands still apply — you can build
while halted. `tick` is sim-time, not wall-time.
- `jammed` reports only `'output full'`, edge-triggered (one event per stall, not per tick).
Starvation is not flagged — see PROPOSAL 3.
- Power: a negative recipe bandwidth adds to **gen** rather than subtracting from draw, so
the brownout multiplier stays `gen/draw`. Tested: the quantizer visibly pays for itself.
- Extractors mine anywhere — there is no seam map in `data/` yet.
- Tests assert against the recipe defs instead of hard-coding yields. This is deliberate:
my first pass hard-coded `demux-ore` and broke the moment LANE-DATA shipped. Mechanics are
mine to pin down; the numbers are theirs to move.
BLOCKED/BROKEN
- **My round-1 definition of done no longer exists.** The orders say place
extractor→demuxer→quantizer→mosh→shipper and watch melt climb. LANE-DATA's codex
transcription landed mid-round and the mosh reactor now eats `gop-crate`, not coefficient
packs. MELT is now **eight machine types** deep (ore→demux→{luma→quantizer→coeff;
slurry→subsampler ×2→chroma-420}→DCT press→anchor slab; mv-flux→p-caster→delta wafers;
→GOP assembler→mosh). I rebuilt against the real graph and it *does* ship melt (tick 1149,
verified above) — but "the whole fantasy in miniature" is now a ~50-entity build, not a
6-machine demo. Your call whether M1's demo target changes or DATA adds a starter recipe.
- **The M1 factory deadlocks permanently at ~tick 1200**, after 4 GOP crates / 12 melt.
This is not a mechanics bug — I probed it for 6000 ticks. Luma and coefficient packs
structurally overproduce (demux yields 2 luma/craft; the press is chroma-420-limited so it
only burns 6 coeff per ~180 ticks), and the press also jams on surplus anchor slabs (the
GOP assembler needs 1 slab per 6 delta wafers). Surplus has nowhere legal to go: my rank
rule forbids diverting a rank-2 item to a rank-1 uplink even when the preferred belt is
wedged. So the press jams → quantizer jams → demuxer jams → mv-flux stops → the p-caster
starves and the assembler sits at 5/6 delta wafers forever.
- The graph *is* solvable today without new code: a second quantizer on `quantize-crime`
drains surplus coeff into bricks, and a second assembler on `assemble-gop-i-only` eats
surplus slabs (8→crate). My demo build is just naive. But note a player currently
**cannot void surplus to an uplink**, which is the first thing a Factorio player reaches
for. That's the cost of the rank rule.
- `splitter` (`lane-splitter`) and `buffer` (`buffer-tank`) now exist in machines.json; they
place and draw power but do nothing. Not in my orders — flagging so nobody thinks they work.
- `heat` is always 0, `bandwidth.stored` always 0 (M2 / buffer tanks).
- Port 8150 is held by another session's dev server, and the harness won't start a second one
for this project. I verified against that server read-only. Note for whoever plays the
build in a Browser pane: the tab reports `visibilityState: hidden`, so rAF is paused and
the sim crawls (~9 ticks per screenshot). That's the pane, not the loop — importing the sim
module directly in the page runs it at ~96k ticks/sec.
CONTRACT REQUEST
- None. `contracts.ts` covered everything I needed this round.
PROPOSAL (yours to call, not mine to take)
1. **Splitters next.** `lane-splitter` with round-robin plus a priority/overflow output is
the canonical Factorio answer to the deadlock above, and it unlocks byproduct sinks and
the bloom-loop topology later. This is the one I'd pick.
2. Alternative, ~5 lines: allow a lower-ranked belt when every preferred belt is full *and*
outputBuf has hit the stall threshold. Cheap, but it makes byproduct management automatic
and quietly deletes a core Factorio tension — a gameplay decision, so I left it alone.
3. `jammed: 'starved'` for idle-with-no-input would let the inspector tell "backed up" from
"waiting". Cheap, but it's UI-visible, so it's LANE-UI's business as much as mine.
4. Extractors should require a seam tile — needs a seam/ore map from DATA and a worldgen call.
NEXT (round 2 if asked)
- `lane-splitter` with priority/overflow; `buffer-tank` feeding `bandwidth.stored` to smooth
brownouts.
- Heat: accumulate per craft, throttle >0.7, scram at 1 (the software decoder's whole bit).
- Save/load — `SimSnapshot` is already the right shape to serialize.
- A worked, deadlock-free reference factory you can paste into the console to demo M1.
### Round 2 — 2026-07-17 — Opus 4.8
SHIPPED
- **Reference factory** (`src/sim/reference.ts`, `referenceFactory(data)`): 114 commands,
110 entities. Ships its first MELT at **tick 1247** and is still shipping at 20,000 —
615 melt per 2,000-tick window, flat, no decay. Round 1's build wedged at ~1200; this
one never does, and the demuxer's `jammed` stays null the whole way. Surplus becomes
product instead of a deadlock: 1,136 macroblock bricks + 1,997 hf dust shipped, 219
slurry voided over 20k ticks.
- **Splitters** — round-robin, blocked outputs skipped, rank traced *through* them (a lane's
rank is now the best outcome reachable across every terminal it can reach).
- **Buffer tanks** — charge on surplus, discharge to cover a deficit, brownout only once dry;
`bandwidth.stored` is the pool.
- **Heat v1** — `heatPerTick` while active + `RecipeDef.heat` per craft, ambient cooling,
throttle above 0.7, scram at 1.0, restart under 0.5, both edges emitting `scram`.
- **Commission queue** — data-order activation, `commissionDone` advances, `repeat` re-enters
at the back, progress still only counts while active.
- **`jammed:'starved'`** and **`BeltItem.id`** (always set; survives belt transfers *and*
splitters, so the renderer never sees an item teleport).
- Verified: 37 sim tests green (18 + 15 M2 mechanics + 4 reference), tsc clean in `src/sim`.
In the live app I dispatched the whole reference factory through `__fktryBus` — it builds,
renders, and runs, and my round-2 work is already visibly driving other lanes: the UI reads
`94 DRAW / 220 GEN` + `BUFFER` off bandwidth v2, the fax shows the **queue** ("NEXT IN
TRAY: DUST ALLOWANCE"), and LANE-UI's OSHA voice is narrating my new flag ("DCT PRESS:
INTAKE STARVED. NOTHING IS ARRIVING.").
- Determinism holds across engines: browser and Node both give tick 1247 and identical
per-window melt counts.
- Perf: 3,500 entities + 6,820 belt items → 0.465 ms/tick (**72× realtime**). Round 1 was
0.418 (80×); the ~11% is heat touching every entity every tick. Still well inside target.
DECISIONS
- **Heat curve (mine to pick, per orders).** Throttle is linear from 1.0 at heat 0.7 down to
0.5 at heat 1.0 — so the lore's "tick rate visibly halves" lands exactly at the scram
point. Ambient cooling is a flat 0.004/tick applied *always*, working or not (1.0 → cold in
250 ticks ≈ 8s), which means any machine with `heatPerTick` below 0.004 can never overheat.
Scram at 1.0, restart under 0.5, so a hot generator duty-cycles rather than dying.
- **Throttle applies to `powerGen`, not just craft speed.** A hot decoder's output sags before
it drops off the bus entirely — which is precisely the hole buffer tanks exist to fill. That
synergy is my reading of "per-machine throttle"; say the word if generators should be exempt.
- Scrammed machines keep their idle `powerDraw` (stopped, not unplugged) and freeze mid-craft
progress, resuming on restart rather than binning the work.
- Scram clears `jammed` and suppresses jam evaluation: a scrammed machine reports through
`heat` + the `scram` event, not as a flow jam. Two channels, no double-reporting.
- Splitters hold 2 items, mirrored into `inputBuf` so the inspector can see them. Any adjacent
belt not pointing in is an output; the splitter's own `dir` is unused. Machines can't push
straight into one — feed it with a belt.
- **`starved` fires whenever a recipe sits idle without enough input — including a machine that
simply outruns its feed.** LANE-UI: expect it to flicker on a fast machine with a slow
supply. That's accurate rather than a bug, but you may want to debounce the voice line.
- The reference factory validates every machine/recipe id against `data` and throws a named
error, so DATA drift fails loudly instead of resurfacing as a mystery deadlock.
BLOCKED/BROKEN
- **DATA carries none of the v2 fields, so heat and buffer tanks are inert in the real game.**
No machine has `heatPerTick`, none has `bufferCap`, no recipe has `heat`, no commission has
`repeat`. Both systems are fully implemented and tested — on a fixture I own
(`pressure.test.ts`), because there is nothing in `data/` to test them against. The DoD's
"heat throttles and scrams the software decoder" is proven mechanically, but the actual
SOFTWARE DECODER cannot overheat until DATA gives it a number. This is the one thing
standing between M2 heat and being real.
- Starting values if they help (my suggestion, DATA's call): `software-decoder`
`heatPerTick: 0.005` → ~33s of work then a ~4s scram, an ~89% duty cycle that reads as
theatrical rather than punishing; `decode-asic` left at 0 (cool and inflexible, per the
codex); `buffer-tank` `bufferCap: 600` ≈ 20s of cover against a 30-bandwidth deficit —
enough to ride out exactly one decoder scram, which is a nice thing to discover.
- `src/sim/data-validate.test.ts` is LANE-DATA's file living inside my owned path. I have not
touched it. Worth an ownership ruling — either it moves to `data/`, or my lane doc should
say I share the folder.
CONTRACT REQUEST
- `MachineDef.coolPerTick?: number` (default 0.004). Right now cooling is a global constant, so
DATA can tune how fast a machine heats but not how fast it recovers — every machine cools 1.0
→ 0.5 in exactly 125 ticks. The codex explicitly has ASIC coolers venting heat, which wants a
per-machine number. Cheap to add, and I'd wire it the round it lands.
PROPOSAL
- `BeltItem.id` can drop the `?` in v3 whenever you like — I always set it already.
- `referenceFactory`'s `setRecipe` commands depend on entity ids being handed out in placement
order (1..N). That's true and deterministic, but it's an implicit coupling to my id
allocation. A `setRecipeAt(pos)` command variant would remove it. Not urgent, just a
tripwire worth knowing about before someone edits the layout.
- Splitters have no filter or priority, and the reference factory proves you don't need either
— skip-blocked self-balances. Worth revisiting only when bloom loops want a filtered tap.
NEXT (round 3 if asked)
- Save/load — `SimSnapshot` is already the right shape.
- Bloom loops: my trace already treats a closed belt circle as "no terminal" → rank 0 → accept
and pile, which is exactly what a loop wants. The P-frame duplicator should drop straight in.
- HF dust piles + mosquito swarms (the rest of M2 PRESSURE) — the seeded RNG is sitting there
unused and waiting for its first roll.
- Heat, for real, the moment DATA has numbers.