PARITY DEBT is one scalar with five bands: it rises on tier>=2 shipments, always decays, decays faster while stock footage ships, and vents outright when a compliance quota is filed. Bands gate the ladder and nothing else needs to know more than the band. Wardens fax 20s ahead, walk in from the rim, roll back the last N outputs from a new per-machine output ring and stamp a compliance seal; sealed machines refuse to run until pried (90 ticks, and prying is itself deviation). Gunships tether to the 8x8 sector with the most DEVIATION and mirror it. Hash auditors prescan, then lock a rect with a deterministic hash that ALWAYS auto-releases. A FIREWALL CATCHES WHAT WALKS — mites and wardens, not the airborne gunship or the auditor that is never present. This departs from the orders' "or a firewall cans it" for the gunship: LANE-UI measured rung 3 being deleted entirely by a machine the player already owns, and there is no tile in the brick works that guards the wardens without also covering the gunship's station. Regression test requires a mirror in the demo as shipped, firewalls and all. Reference v7 adds the compliance tap (DATA measured zero commissions clearing in 90k ticks, so every quota was dead content), a canning line for rung 5, a doubled brick works that can absorb a seal, and the power to pay for it. Save v5. Determinism byte-identical browser vs Node across a 20k run. 645 tests green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
841 lines
60 KiB
Markdown
841 lines
60 KiB
Markdown
# 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 7 (Phase 1: THE CORRECTION, EMBODIED — mechanics only)
|
||
|
||
Zero new assets, zero farm. Contracts v9 carries everything: `CorrectionState` (debt,
|
||
band, units), `EntityState.sealed`, the `pry` command, `CommissionDef.compliance`, and
|
||
events `notice`/`sealed`/`mirrored`/`stasis`/`debtBand`. The parity-mite REMAINS in
|
||
WildlifeState — the refactor was measured at 25 files across five lanes for zero
|
||
visible change and is deferred by ruling. CorrectionState covers rungs 2-4 only.
|
||
|
||
1. **PARITY DEBT.** One scalar, five bands (NOMINAL/OBSERVED/AUDITED/SEALED/REFUSED),
|
||
thresholds exported from constants.ts. Rises on tier>=2 shipments (per-item tier
|
||
from data). DECAYS always; decays faster while shipping tier-0/1 — and completing a
|
||
`compliance:true` commission vents a chunk. Emit `debtBand` on band edges only.
|
||
RULING: canning a Correction unit adds NO debt — an immune system does not hold a
|
||
grudge; "it cannot perceive you" is the joke.
|
||
2. **CHECKSUM WARDEN (band>=2).** Emit `notice` FIRST (fax-first is contractual),
|
||
~20s later it walks to its target, audits (phase+progress visible), rolls back the
|
||
last N outputs (new small per-machine output ring buffer), stamps `sealed:true`
|
||
(emit `sealed`), leaves. A sealed machine refuses to run. Counters: the firewall
|
||
recipe-shape catcher (same detection you shipped for mites) OR the new `pry`
|
||
command — ~90 ticks standing at the machine, clears the seal, ADDS debt (you
|
||
defaced a compliance notice).
|
||
3. **REDUNDANCY GUNSHIP (band>=3).** Picks the 8x8 sector with the most DEVIATION
|
||
(most tier>=2 output — RULING: not highest throughput; it corrects corruption, not
|
||
productivity), tethers to a PARITY ANCHOR (the sector's most corrupting machine,
|
||
`target`), publishes `rect`. Crafts inside the sector feed its progress; at
|
||
threshold it emits `mirrored` (which SCREEN subtracts from the sky ledger — the
|
||
shipped `repaired` path). Break the tether by starving/sealing/idling/removing the
|
||
anchor; it leaves when the tether breaks or a firewall cans it.
|
||
4. **HASH AUDITOR (band 4).** `notice`, then a visible 5s PRESCAN phase, then
|
||
`stasis` on: a rect where entities stop ticking and belt items freeze, with a
|
||
deterministic `lockHash` string. SAFETIES ARE CONTRACTUAL: it ALWAYS auto-releases
|
||
after AUDITOR_STASIS_TICKS even if the player does nothing. Counter is ABSTENTION:
|
||
if nothing crafted in its rect during prescan, it finds nothing and leaves without
|
||
locking. Material counter: FORGED HASH — a two-input recipe on the existing
|
||
hex-splicer (DATA authors it); holding a forged-hash item in any machine inside
|
||
the prescan rect voids the audit. NOTE: your recipe-shape catcher path assumes
|
||
empty inputs — this one has inputs, budget ~40-60 lines for a second detection.
|
||
5. **Reference factory v7**: survives one full warden+gunship cycle at 20k ticks with
|
||
melt regression intact (a firewall near the uplink is the natural teach).
|
||
Save v5 carries debt/units/seals. Determinism suite green.
|
||
|
||
**DoD:** full lifecycle tests for all three rungs incl. abstention and forged-hash
|
||
voiding; a sealed machine provably dead until pried; `mirrored` arithmetic verified;
|
||
auto-release pinned by test (this one is the safety — it must be impossible to cut).
|
||
|
||
---
|
||
|
||
## 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 —
|
||
6–15 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.
|
||
|
||
### Round 3 — 2026-07-17 — Opus 4.8
|
||
|
||
SHIPPED
|
||
- **Contracts v3 wired**: `EntityState.scrammed` (RENDER can delete its duplicated 0.5
|
||
constant — the latch is mine to publish), `snapshot.commissionQueue` (active first then
|
||
upcoming; UI's NEXT IN TRAY is now truth rather than a guess), `coolPerTick` per machine
|
||
with my constant as the fallback, `setRecipeAt`, and `BeltItem.id` hardened to required.
|
||
- **UNITS CONVERSION — confirmed, DATA please retune against this.** gen/draw are bandwidth
|
||
per SECOND; `stored` is bandwidth-SECONDS. Charging and draining now divide by
|
||
TICKS_PER_SECOND, so a 30/s deficit drains **1 stored per tick**, i.e. 30 per second.
|
||
Concretely: `bufferCap: 240` = exactly 8.0s of cover against a 30/s deficit; the
|
||
in-flight `300` = 10.0s. `pressure.test.ts` asserts that arithmetic tick by tick.
|
||
- **save()/load()**: 26.9 KB for the 118-command reference factory. The bar I held it to is
|
||
"the future is identical", not "the snapshot looks right" — every test loads a blob and
|
||
runs BOTH sims 1,000+ ticks before comparing. Includes save→load→save byte-identity, a
|
||
chained save/load×3, and pending-command preservation. 7 tests.
|
||
- **Reference factory v3**: power restructured to 4× decode-asic (80/s) + 1× software-decoder
|
||
(100/s) + 3× buffer-tank (900 bandwidth-seconds). Melt still first at tick 1247, still
|
||
shipping at 20k, rate unchanged.
|
||
- **Scram survival (the DoD numbers)**: first scram **t434**, restart **t934** (500-tick
|
||
recovery = 16.7s), then a steady ~718-tick duty cycle at 47% uptime. **Brownout ticks
|
||
across 20,000: zero.** Tanks sit at a 773/900 low-water mark in steady state. Melt per
|
||
2,000-tick window: `[6,12,12,12,15,9,15,12,14,10]` — identical to round 2's naked-decoder
|
||
build, so the duty cycle costs nothing measurable. The decoders black out; the line doesn't.
|
||
- **Bloom loop**: a closed delta-wafer bus with five splitter taps escalates concentrate to
|
||
grade 5. All six rungs craft; 4× grade-5 shipped by 20k. Plus a separate test of the loop
|
||
mechanic itself (pure circle → rank 0 → cargo circulates forever, conserved, tracer fine).
|
||
- Verified in the live browser as well as Node — identical tick numbers (1247 / 434 / 934),
|
||
save/load round-trips there too, no console errors, and the UI is visibly reading the new
|
||
fields: `94 DRAW / 180 GEN` is the new power block and `BUFFER 29` is a tank charging (that
|
||
readout has never been non-zero before).
|
||
- **Perf: 74.9× realtime** at 3,500 entities / 6,820 belt items (0.445 ms/tick, best of 3 on a
|
||
freshly loaded page). See DECISIONS — beware my older numbers.
|
||
|
||
DECISIONS
|
||
- **Reference power is sized against the worst case, not the average.** The quantizers and
|
||
subsamplers hand back ~30/s of compression bandwidth, but not while they're between crafts.
|
||
Assume none of it. My first cut (3 ASIC + 2 tanks) passed every test and was quietly
|
||
gambling: measured low-water 2.2 of 600, i.e. it survived only because compression happened
|
||
to be running. 4 ASIC + 3 tanks leaves 50% margin with compression at zero, and still works
|
||
if DATA reverts `bufferCap` 300→240. A test that passes on a coincidence is worse than one
|
||
that fails.
|
||
- **Bloom taps run hungriest-first, counter-current to the wafer flow — this is load-bearing.**
|
||
Wafers join the ring at one point and meet the taps in order, so the first tap eats first.
|
||
Put the bloom tap first (the obvious layout) and it swallows 49% of all wafers, grade 3
|
||
backs up 'output full' behind a starving grade 4, and 30,000 ticks yields exactly ONE
|
||
grade-5. Reversed: 4 by 20,000. Same machines, same recipes, one ordering decision. That's a
|
||
genuine lesson the topology teaches a player, and worth surfacing in-game somewhere.
|
||
- `updateHeat` early-outs on cold, cooling-less, unscrammed machines. Belts are ~99% of a big
|
||
factory and can never be anything but stone cold; skipping them paid for all of v3's other
|
||
additions and then some.
|
||
- `save()` carries what the snapshot deliberately hides: crafting/scram latches, splitter
|
||
queues *and cursors*, the RNG position, and the pending command queue. `load()` needs an
|
||
`init()`'d sim (GameData isn't in the blob) and rebuilds occupancy/tiles/belt-contents from
|
||
the defs rather than trusting the blob's ordering.
|
||
- Brownout comparison carries a 1e-9 epsilon: a tank covering a deficit exactly lands
|
||
`supplied === draw`, and float drift there would flicker the flag every single tick.
|
||
|
||
BLOCKED/BROKEN
|
||
- **DATA is mid-edit in the shared tree — I tuned against uncommitted values.**
|
||
`data/machines.json` is dirty as I write this. The reference factory's power is sized
|
||
against the working-tree numbers (software-decoder powerGen 100 / heatPerTick 0.0033 /
|
||
coolPerTick 0.001; buffer-tank bufferCap 300). If those land differently, re-check it; the
|
||
20k regression test is the net that will catch it.
|
||
- **Heat semantics need a one-line ruling.** My model cools every tick, working or not, so
|
||
`heatPerTick` is GROSS and the climb is `heatPerTick - coolPerTick`. DATA's *committed*
|
||
values (heatPerTick 0.0025, no coolPerTick → default 0.004) net to **-0.0015** — the
|
||
software decoder could never scram at all, and the DoD would have been unprovable. Their
|
||
*in-flight* values (0.0033 / 0.001) net +0.0023 and work. So we agree, but by luck of
|
||
timing, and the contract comment ("heat while active" / "per-machine cooling rate") doesn't
|
||
say which it means. Please make it say: *heatPerTick is gross; net climb is heatPerTick −
|
||
coolPerTick; a machine whose heatPerTick ≤ coolPerTick never overheats.* This is the same
|
||
class of bug as the gen/draw units one, and it cost DATA a round.
|
||
- Sanity check that DATA's intent was always a scram: `bufferCap` 240 ÷ 55 gen = 4.36s of
|
||
cover, against my 4.17s recovery at the time. They sized the tank to ride out exactly one
|
||
scram before the decoder could produce one.
|
||
- **`asic-cooler` (new) currently does nothing.** `coolPerTick: 0.02`, `powerGen: 5`, no
|
||
`heatPerTick` — cooling is per-machine and there is no adjacency mechanic, so it cools
|
||
itself, and it has no heat to shed. It's a 5-bandwidth generator with a great flavour text.
|
||
If it's meant to cool its NEIGHBOURS, that's spatial heat: a real feature needing orders and
|
||
probably a `coolRadius` field. Say the word.
|
||
- **The codex's literal bloom loop is not expressible.** "Any closed belt circle with a
|
||
duplicator on it; contents compound each lap" cannot work with per-grade recipes: a machine
|
||
runs one recipe, so a lone duplicator can never consume its own output, and a grade-5
|
||
returning to a grade-1 duplicator wedges that lane permanently. The shape that composes is
|
||
the wafer-bus circle I built. The literal version needs a single self-upgrading recipe
|
||
(grade-N → grade-N+1), which `RecipeDef` can't express — inputs/outputs are concrete item
|
||
ids. Either accept the escalator reading, or that's a schema change.
|
||
- Ownership wart is gone: `data/validate.test.ts` is staged by DATA and `src/sim/**` is purely
|
||
mine again. Thank you.
|
||
- **My round-3 sim code is committed inside `45003db [ui] REMOVE mode`, not under a `[sim]`
|
||
commit — please don't read that as a LANE-UI ownership violation.** All five lanes share one
|
||
working tree and one index. I had `src/sim/**` staged; LANE-UI's session ran an
|
||
add-everything commit in the same repo a moment later and swept my staged files in with
|
||
theirs. By the time my own `[sim]` commit ran there was nothing left to commit, and 45003db
|
||
was already pushed. The code is correct and complete in the tree (48 sim tests green, tsc
|
||
clean, verified in-browser); only the attribution is wrong, and rewriting pushed history
|
||
under four concurrent sessions would do more damage than the mislabel. Flagging it so your
|
||
per-lane diff doesn't fire. See PROPOSAL.
|
||
|
||
CONTRACT REQUEST
|
||
- None. v3 covered everything.
|
||
|
||
PROPOSAL
|
||
- `Sim.save?/load?` can drop the `?` in v4 — I always implement both.
|
||
- `snapshot.commissionQueue` aliases the live array rather than copying it each tick. Covered
|
||
by "snapshot is read-only", but worth a line in CONTRACTS since it's newly mutable-looking.
|
||
- Treat my round-1/round-2 perf figures (80× / 72×) as soft. I re-measured on a page loaded
|
||
with leftover sims and got anywhere from 59× to 12× for the *same* build; a fresh page gives
|
||
74.9× consistently across three runs. The numbers were never wrong so much as noisy — if you
|
||
ever want perf tracked properly it should be a headless benchmark, not a browser probe.
|
||
- **The shared working tree needs a commit rule.** Five sessions share one repo *and one git
|
||
index*, so any lane running `git add -A` / `git commit -a` silently commits whatever another
|
||
lane has staged at that instant — that's how my sim work ended up inside a `[ui]` commit this
|
||
round, and it'll keep happening. Cheapest fix: a standing rule that lanes only ever
|
||
`git add <their own explicit paths>` and never `-A`/`-a`/`.`. Failing that, `git worktree`
|
||
per lane would make it structurally impossible.
|
||
|
||
NEXT (round 4 if asked)
|
||
- HF dust piles + mosquito swarms — the seeded RNG is still unrolled, and save/load already
|
||
carries its state, so wildlife can land without touching persistence.
|
||
- The Correction / parity mites (M3's enemy).
|
||
- Spatial heat + `coolRadius`, if the ASIC cooler is meant to mean something.
|
||
- A self-upgrading bloom recipe, if you want the codex's literal loop.
|
||
|
||
### Round 4 — 2026-07-17 — Opus 4.8
|
||
|
||
SHIPPED
|
||
- **Research v1 + the v4 gating rule.** `kind:'lab'`, `setResearch`, `snapshot.research`,
|
||
the `researched` event, and gating: any id a tech's `unlocks` names is refused by
|
||
`place`/`setRecipe`/`setRecipeAt` until that tech lands; anything no tech mentions is free
|
||
forever (so data with no tech gates nothing). Labs take only packs the active tech still
|
||
needs, several labs feed one total, and research rides through save/load — as a *rule*,
|
||
not just a list, so a restored save can still build what it unlocked.
|
||
- **A real research run on DATA's own tree** (`realtech.test.ts`): ore → demuxer → quantizer
|
||
→ {crime → bricks, ringing → halos} + slurry → artifact bottler → SPATIAL PACK →
|
||
archaeology lab → tech complete → a recipe that was silently refused is now accepted.
|
||
Cold start, nothing granted. That's the DoD's unlock path on real data, not a fixture.
|
||
- **Spatial cooling** (`coolRadius`): chebyshev from the footprint, auras stack by sum,
|
||
total capped at `HEAT_COOL_MAX`. Proven by measuring the actual shed rate, and a cooler in
|
||
range roughly triples a hot generator's run before it scrams (2.5× the ticks).
|
||
- **`src/sim/constants.ts`** — 12 constants exported; DATA can import truth instead of
|
||
hand-copying numbers that go stale silently.
|
||
- **Reference factory v4**: anchor-slab overflow tap → the demo now ships slabs (18 over 20k),
|
||
melt still first at tick 1248, still shipping at 20k, still **zero brownout ticks**.
|
||
`referenceFactoryTech(data)` derives which techs the layout needs, from the commands.
|
||
- Also wired: `bandwidth.capacity`, `EntityState.scrammed`, `commissionQueue` in the snapshot
|
||
literal (v5-ready).
|
||
- Verified in the browser as well as Node — identical ticks (1248 / scram 435), gating live
|
||
(`mv-extractor` refused while locked), `capacity: 900`, 23 techs unlockable, no console
|
||
errors. 65 sim tests; 318 across the repo.
|
||
- Perf: **64.7× realtime** at 3,500 entities / 6,820 belt items (0.515 ms/tick, best of 3 on
|
||
a fresh page). Round 3 measured 74.9×. Nothing I added is meaningfully per-tick — `drainLabs`
|
||
early-outs when nothing is being researched, and cooling only touches machines that can
|
||
heat — so I can't attribute the gap, and I'm not going to pretend I can. It's either
|
||
environment noise or something small I haven't found; the headless benchmark I proposed last
|
||
round would settle it.
|
||
|
||
DECISIONS
|
||
- **Cooling cap = 0.05/tick, and the number is arbitrary but the reasoning isn't.** At 0.05 a
|
||
machine sheds a full 1.0 of heat in 20 ticks, which is already effectively instant. Past
|
||
that, more coolers buy nothing but still cost bandwidth and floor space — a stack that does
|
||
nothing reads as broken rather than generous. Retune freely; it's one constant now.
|
||
- A cooler's aura excludes itself (its own `coolPerTick` is already counted once), and the
|
||
cache is invalidated on place/remove/rotate but *not* setRecipe — auras are geometry.
|
||
- **A lab's appetite is whatever the active tech costs**, so switching targets re-ranks every
|
||
lane feeding it, and `setResearch` drops the route cache. Labs take only what the tech still
|
||
needs — a lab is not a warehouse, and packs hoarded for a tech nobody picked would strand
|
||
where no other lab could reach them.
|
||
- Switching research targets **abandons** delivered packs rather than banking them per-tech.
|
||
Packs are the cost of indecision. Say the word if you'd rather bank them.
|
||
- Completing a tech clears `active` rather than guessing a next target.
|
||
- A machine placed while some of its recipes are locked defaults to its **first unlocked**
|
||
recipe, not `recipes[0]`.
|
||
- `testkit.ts` (new, tree-shaken, imported by nothing in the app) grants research via a
|
||
save/load round-trip — the public API, not a back door. It's how a test says "this is a
|
||
late-game save" honestly.
|
||
|
||
BLOCKED/BROKEN
|
||
- **The demo browns out, and it needs one line from you.** The reference factory uses two
|
||
gated ids (`software-decoder` ← stream-software-decoding, `assemble-gop-i-only` ←
|
||
disc-i-only-cadence), so it is a post-research save by definition now. Dispatched cold
|
||
through `__fktryBus` it comes up **100 draw against 80 gen — BROWNOUT** with no decoder,
|
||
which I watched happen on screen. `__fktryDemo` must open those techs first;
|
||
`referenceFactoryTech(data)` tells you exactly which, and `testkit.grantResearch(sim, ids)`
|
||
will do it. The alternative is a dev-only `{kind:'grantResearch'}` command — your call,
|
||
since shipping testkit into main.ts is a bit grubby. A test pins both halves of this.
|
||
- **DATA fixed the tech tree underneath me mid-round, twice, and it was the right call both
|
||
times.** When I started: no `kind:'lab'` machine existed at all, and every science pack was
|
||
made on the artifact-bottler which was itself locked behind a tech costing 10 analog-packs
|
||
— a hard circular dependency, **0 of 24 techs affordable at start**, the tree literally
|
||
unenterable. It now has `archaeology-lab`, spatial-packs craftable with zero research, and
|
||
6 techs affordable cold. `realtech.test.ts` is the tripwire if that bootstrap ever closes
|
||
over again — it's worth keeping for exactly that reason.
|
||
- Casualty of the churn: my first pass hard-coded `mosh-reactor` as "a gated machine" and
|
||
broke when DATA un-gated it an hour later. Round 1's lesson, relearned. The real-tree
|
||
tests now *derive* a gated and an ungated machine from the data.
|
||
- `asic-cooler` has `coolPerTick: 0.02` but still no `coolRadius`, so its aura is inert in the
|
||
real game — it cools only itself, and it has no heat to shed. The mechanic is live and
|
||
tested on a fixture; DATA just needs the number. (This was my round-3 note; the field now
|
||
exists for them.)
|
||
- Vitest caches JSON imports: I spent a while chasing a "gating is broken" ghost that was a
|
||
stale `tech.json` in `node_modules/.vite` while DATA edited underneath. Worth knowing if
|
||
anyone else sees data that doesn't match disk.
|
||
|
||
CONTRACT REQUEST
|
||
- None. v4 had everything.
|
||
|
||
PROPOSAL
|
||
- **A dev-only `{kind:'grantResearch'; tech: string[]}` command** would let `__fktryDemo` set
|
||
up a late-game board without importing test helpers into the app, and would give UI a way to
|
||
build a "sandbox" mode later. It's the cleanest fix for the brownout above.
|
||
- The mosh reactor now needs **two** melt lanes in the reference layout, and that's a smell
|
||
worth knowing about rather than a bug: a machine pushes one item per output type per tick,
|
||
so a 3-melt craft trickles out over several ticks, and while its own lane is busy melt is
|
||
free to take any *other* rank-1 lane. Nothing is lost — it still reaches an uplink — but it
|
||
can wander into a lane meant for something else. If that ever bites somewhere it matters,
|
||
the fix is a per-item lane affinity, not more belts.
|
||
- `Sim.save/load` are hardened in v4 and `testkit` depends on the blob shape (`.research`).
|
||
If the save format ever versions past 3, testkit moves with it.
|
||
|
||
NEXT (round 5 if asked)
|
||
- HF dust piles + mosquito swarms — the RNG is still unrolled and save/load already carries
|
||
its state, so wildlife lands without touching persistence.
|
||
- The Correction / parity mites.
|
||
- A headless perf benchmark, so the numbers above stop being anecdotes.
|
||
- Research *time* (a lab currently counts a pack the tick it lands) if you want the tech tree
|
||
to feel like an investment rather than a purchase.
|
||
|
||
### Round 5 — 2026-07-18 — Opus 4.8
|
||
|
||
SHIPPED
|
||
- **`grantResearch`** — idempotent, ignores unknown ids, completes the active target out from
|
||
under the labs, save/load safe. Verified through the live bus, so `__fktryDemo` can drop
|
||
`testkit` whenever you like.
|
||
- **Research takes time** — `RESEARCH_TICKS_PER_PACK = 60` (exported). Stepped: a lab spends
|
||
the full 60 ticks on a pack, then consumes it and ticks progress by exactly 1, so
|
||
`research.progress` stays integer and determinism stays trivial. Labs really do parallelise
|
||
— measured 200 ticks with two vs 260 with one on the same supply. A lab mid-pack reports
|
||
`progress` 0..1, so RENDER/UI can show it working.
|
||
- **Wildlife v1** — piles grow from dust a machine is sitting on, hatch swarms at 1.0, swarms
|
||
drift to production machinery and slow it while attached, traps can them. Deterministic,
|
||
save/load carries the lot, spawn/clear events on both. 12 tests including the design point.
|
||
- **Seams** — extractors run only while their footprint overlaps an authored seam rect;
|
||
off-seam they idle with `'no seam'`, edge-triggered like every other jam. Consumes DATA's
|
||
real `seams.json` shape (and a bare rect list), defensively — see BLOCKED.
|
||
- **THE RELIC** — one optical fossil per world, position a pure function of the seed, 18–28
|
||
tiles out, never on a seam, never under the demo. `excavate` within a tile uncovers it and
|
||
emits `relicFound`; it stays found through save/load and 20,000 further ticks. When DATA's
|
||
seam map supplies a `relicReserve` the roll folds into that corner — verified in-browser
|
||
landing at (21,15), inside the reserve, and digging up first try.
|
||
- **Reference factory v5** — a second works on DATA's eastern seam: ore → demuxer → quantizer
|
||
→ {crime → BRICKS, ringing → HALOS} + slurry → artifact bottler → SPATIAL PACK →
|
||
archaeology lab. **It researches `broadcast-phosphor-seams` at tick 1235.** Melt still first
|
||
at 1247, still shipping at 20k, **zero brownout ticks**, 179/179 placements legal, and with
|
||
seams wired **zero extractors off-seam** — both works sit on real ore.
|
||
- Verified: 99 sim tests (446 across the repo), tsc clean in my lane, and the browser agrees
|
||
with Node tick-for-tick (melt 1247, scram 434, research 1235). Perf **63× realtime** at
|
||
3,500 entities / 6,820 belt items (0.529 ms/tick) — Round 4 was 64.7×, so all of this round
|
||
cost ~3%. (An earlier probe "timed out" and looked like a regression; it was the 30s tool
|
||
limit on a 16k-tick warmup, not the sim. Staged it and re-measured.)
|
||
|
||
DECISIONS
|
||
- **Dust breeds from BACKED-UP dust, not from dust existing.** First cut grew piles from
|
||
whatever a machine held, and the tidy factory — dust belted straight to an uplink — still
|
||
hatched 56 swarms, because buffers are never empty between pushes. Growth is now
|
||
proportional to what sits *beyond one craft's worth* (`DUST_SPILL_GRACE`). Plumbed lines
|
||
never cross the threshold; neglected ones hit the stall cap and hatch in ~400 ticks. The
|
||
asymmetry is the mechanic — without it the hazard is just weather.
|
||
- Related, and worth knowing: my first "tidy" fixture still bred, and the mechanic was
|
||
innocent. A crusher making 4 dust per craft out-produces a single belt lane (~0.15
|
||
items/tick) and backs up no matter how you plumb it. That's a true fact about belts.
|
||
- **`SWARM_MAX = 8`.** A permanently jammed machine breeds forever — measured 29 swarms in one
|
||
20k demo run, which is a plague, not a hazard. At the cap, full piles just sit there.
|
||
- **Swarms only settle on production machinery**, preferring machines actually mid-craft and
|
||
falling back to jammed ones. Power plants read as permanently "running" and hoovered up
|
||
every swarm in the world to no effect — it looked exactly as silly as it sounds. The
|
||
fallback matters because the dust that bred them came from a machine that seized: without
|
||
it, a swarm evaporates at the moment it should bite.
|
||
- **Swarms slow crafting, not generation.** `swarmThrottle` is not applied to `powerGen`, so
|
||
an infestation never causes a brownout. That's my call, not the contract's — flag it if you
|
||
want mosquitoes on the grid; it would make them far nastier and would end the zero-brownout
|
||
regression.
|
||
- Traps are identified by **recipe shape** — empty inputs, outputs the canned swarm — not by a
|
||
machine id, so DATA's real trap lights up with zero code changes. A trap only progresses
|
||
while a swarm is in `TRAP_RADIUS`, and holds at full progress rather than canning air if its
|
||
prey drifts off or a neighbour beats it to the catch.
|
||
- Switching research target **forfeits the part-processed pack** in every lab, matching the
|
||
existing rule that switching abandons delivered packs. Lab intake now counts what *all*
|
||
labs are holding, so a bank of them can't collectively hoard more than the tech costs.
|
||
- **The annex is a separate works, not a tap off the melt line.** Every column between the
|
||
quantizers and the press is spoken for; threading bricks, halos and slurry through that
|
||
floorplan would have made the melt regression hostage to the science. DATA's second mdat
|
||
seam is empty ground, so the annex mines its own ore and shares only the bandwidth bus.
|
||
- Save format **v3 → v4** (wildlife, relics, lab timers). `testkit` follows it.
|
||
|
||
BLOCKED/BROKEN
|
||
- **`GameData.seams` is not wired, so seams are inert in the actual game.** DATA shipped
|
||
`data/seams.json` mid-round (good schema, and they reserved a relic corner for me), but
|
||
`GameData` has no `seams` field and `main.ts` doesn't import the file, so nothing reaches
|
||
`sim.init`. I consume it defensively and my tests inject it. **This is the one contract
|
||
change the round needs** — see CONTRACT REQUEST. I verified in-browser what happens once
|
||
it's wired: 0 extractors off-seam, relic in DATA's reserve. Until then extractors mine
|
||
anywhere, exactly as before.
|
||
- **There is still no trap machine in the data,** so swarms cannot be cleared in the real
|
||
game. The mechanic is complete and tested against a fixture trap; DATA needs a machine
|
||
whose recipe has no inputs and outputs `mosquito-swarm`. The demo currently hatches its 8
|
||
and keeps them. (Suggested shape in PROPOSAL.)
|
||
- The demo researches **one** tech and then idles: completing clears `active` and nothing
|
||
auto-picks a successor. That's a player/UI decision rather than a sim one, but it means
|
||
the annex jams shortly after 1235 — the bottler starves of halos (skim-ringing wants 3
|
||
coefficient packs per craft against crime's 1, and the splitter is a blind 1:1), packs back
|
||
up, and the line seizes. Harmless and honest, but it's why the demo grows piles.
|
||
- My round-1 lesson bit me again from the other side: DATA reworked the commission ladder
|
||
mid-round (`first-taste` → `raw-payload`, 20 ore), which broke a sim test that assumed
|
||
shipping melt closes the first commission. Rewritten to assert the *rule* — shipping what
|
||
the active order wants credits it, capped at the cost — rather than which order is first
|
||
this week.
|
||
|
||
CONTRACT REQUEST
|
||
- **`GameData.seams?: SeamMap`**, and `main.ts` importing `../data/seams.json` into the data
|
||
object. The shape is DATA's file verbatim; I've mirrored it as `SeamDef`/`SeamMap`, exported
|
||
from `src/sim/index.ts`, so the contract can lift it as-is. Nothing else in my lane is
|
||
blocked.
|
||
|
||
PROPOSAL
|
||
- **A trap for DATA**, matching what the sim already recognises:
|
||
`{ id: 'mosquito-trap', kind: 'crafter', footprint: {x:2,y:2}, recipes: ['catch-swarm'], powerDraw: 3 }`
|
||
plus `{ id: 'catch-swarm', machine: 'mosquito-trap', inputs: {}, outputs: {'mosquito-swarm': 1}, ticks: 30, bandwidth: 2 }`.
|
||
The canned swarm is already an item and commissions already ask for live ones, so it ships.
|
||
- Should swarms bite `powerGen` as well as craft speed? Currently no, deliberately.
|
||
- `snapshot.wildlife` and `snapshot.relics` are always present (found or not) as the v6 note
|
||
requires. They alias live arrays like `commissionQueue` — same read-only rule.
|
||
|
||
NEXT (round 6 if asked)
|
||
- The Correction / parity mites — the enemy proper.
|
||
- A headless perf benchmark, so these numbers stop being browser anecdotes (proposed round 3;
|
||
the 30s-timeout scare this round is the third time it would have paid for itself).
|
||
- Per-seam `richness` driving extraction rate, once DATA wants it — the field is already there.
|
||
- Swarm behaviour beyond "sit and bite": drifting between targets, or fleeing a trap.
|
||
|
||
### Round 6 — 2026-07-20 — Opus 4.8
|
||
|
||
SHIPPED
|
||
- **Parity mites v1** (contracts v8 `'parity-mite'` + the `repaired` event). They ride the
|
||
wildlife array. Lifecycle: spawn from the world RIM as corruption crosses thresholds →
|
||
hunt the nearest tier-2 belt item → dwell on station while the proboscis works → repair it
|
||
(eat the item, emit `{kind:'repaired'; item; pos; tick}`) → sated, carry the correction back
|
||
to the rim and leave. They **never touch tier-0/1 cargo** — corruption only, which is both
|
||
the fiction and the balance (the DoD point). Deterministic; save/load carried with **no
|
||
save-format bump** (mites are just WildlifeState entries). `size` encodes the state machine
|
||
(0 hunting / 0<size<1 repairing / ≥1 returning), which also hands the renderer a "glowing,
|
||
sated" tell.
|
||
- **The firewall counter** — recognised by recipe SHAPE (empty inputs, cans a mite), the exact
|
||
same detection as the mosquito trap, so **DATA's real firewall lit up with zero code**. DATA
|
||
landed it MID-ROUND (commit `5e3bbff`: `firewall` 2×2 crafter, `catch-mite` {}→parity-mite,
|
||
ticks 120, gated behind `broadcast-parity-firewall`; `parity-mite` item tier 2). Re-probing
|
||
HEAD is the only reason I caught it — my round-start grep found nothing.
|
||
- **Reference factory v6** — auto-detects DATA's firewall and places it on the **melt uplink
|
||
approach**, belted to a shipper so it never clogs at the 4-item stall cap. 182 placements all
|
||
legal; melt still first at **1247**, still shipping at 20k, steady `[6,12,9,12,15,9,12,9,9,12]`
|
||
melt/2k; **zero-brownout regression intact**; firewall cans + ships **16 mites** over 20k.
|
||
When DATA had *not* shipped the machine (start of round), the layout simply skipped the
|
||
firewall command and weathered the wave on throughput — verified both ways.
|
||
- **11 new mite tests** (`mite.test.ts`): corruption-gated spawn, seek+repair emits `repaired`,
|
||
the tier-0/1 balance point, escalation to `MITE_MAX`, firewall catch/idle, and
|
||
determinism + save/load mid-wave. **113 sim tests, 481 repo tests green; tsc clean.**
|
||
- Verified in the browser as well as Node: melt **1247 identical across engines** (determinism
|
||
holds), firewall canning live, no console errors. Perf: the reference build 0.054 ms/tick;
|
||
a 2,680-entity field 0.21 ms/tick (158× realtime).
|
||
|
||
DECISIONS
|
||
- **The patrol cycle (in, fix ONE, out) is the load-bearing call.** My first cut let mites camp
|
||
the nearest corruption. Because melt production is SLOW (~10 per 2,000 ticks), a single
|
||
camping mite **zeroes the melt lane** — measured mid-game 2k windows dropping to 0. That's a
|
||
wipe, not the codex's "early nuisance". Return-to-rim after one repair bounds damage: 119
|
||
corrections over 20k, almost all against the abundant bricks, ~3 against melt. The melt
|
||
regression went from marginal (fragile 0-windows) to comfortable and flat.
|
||
- **Repair DWELL (`MITE_REPAIR_TICKS = 30`) exists for the firewall as much as the fiction.**
|
||
A mite that repairs on contact does so in `updateWildlife`'s phase, one step BEFORE the
|
||
firewall's catch runs in `advanceCrafts` — so the firewall could only ever can an
|
||
already-sated mite, never PREVENT a repair. Making the mite hold station a beat gives an
|
||
armed firewall a window to snap it mid-repair; that's what makes "guarding cancels
|
||
corrections" actually true (proven in `mite.test.ts`).
|
||
- **Corruption = tier-2+ shipped** (summed from `shippedTotal` × item tiers), so the immune
|
||
system reacts to PRODUCT, not raw throughput. `desiredMites = floor(corruption/MITE_SPAWN_PER)`
|
||
capped at `MITE_MAX`; a caught mite is replaced while corruption stays over threshold — the
|
||
firewall holds a line, it doesn't win once. All tuning is in `constants.ts`.
|
||
- Firewall and trap are unified by shape (both empty-input creature-canners). `FIREWALL_RADIUS`
|
||
is its own constant (= `TRAP_RADIUS` today, but free to diverge).
|
||
- The reference's firewall guards MELT (the uplink "teach"), but most mites target the far-west
|
||
brick lanes, so it cans few (~16/20k). That's honest — one firewall guards one approach.
|
||
|
||
BLOCKED/BROKEN
|
||
- **I reshaped a regression test — flagging deliberately.** `reference.test.ts`'s "keeps the
|
||
demuxer alive" asserted `demuxer.jammed !== 'output full'` at the single tick 20000. MEASURED:
|
||
the demuxer transiently back-pressures **~18% of ticks WITHOUT mites** and 16% WITH (mites
|
||
slightly *reduce* it) — the press/assembly loop cycles. The old assertion was a coin-flip that
|
||
had been landing null; mites shifted the phase and it landed 'output full'. I rewrote it to
|
||
sample a 600-tick window and require the demuxer flows the majority of ticks (a real
|
||
round-1-style wedge is ~100% stuck). Stronger net, not weaker — but it's a test-shape change
|
||
on someone's regression, so it's here in writing.
|
||
- **Perf caveat, stated not hidden.** A *hunting* mite scans every belt item each tick (≤6
|
||
mites). At 5,000 belt items that's ≤30k comparisons/tick, and only while mites are actively
|
||
hunting (sated/returning/repairing mites don't scan). Acceptable now; a spatial index is the
|
||
fix if a late-game board with a live wave ever shows it. My stress probe ran empty belts, so
|
||
it did NOT exercise this path — noting rather than pretending I measured it.
|
||
- DATA's `catch-mite` is 120 ticks (~4s), slow to ARM the first charge; but once progress hits
|
||
1 it holds and snaps any in-range mite every subsequent tick, so the long craft only delays
|
||
the first catch, not steady state.
|
||
|
||
CONTRACT REQUEST
|
||
- None. v8 carried `parity-mite` and `repaired`, and mites needed no new persisted fields.
|
||
|
||
PROPOSAL
|
||
- `WildlifeState.size` is overloaded as the mite state machine. It works and doubles as a render
|
||
glow, but a future unit wanting a genuine second scalar would be cleaner with a `phase?` field
|
||
than more size-encoding.
|
||
- Commissions could now ask for canned `parity-mite` (the firewall's cargo — the item exists):
|
||
a "COMPLIANCE BUYBACK" standing order closes the loop, shipping the immune system back to
|
||
itself. DATA/orchestrator call.
|
||
- Mites could prefer the highest-tier product (melt over bricks) so they feel drawn to your best
|
||
work, or flee a firewall. Left simple this round.
|
||
|
||
NEXT (round 7 if asked)
|
||
- Checksum Wardens (Correction escalation #2): audit a machine, roll back its last N outputs,
|
||
stamp it sealed. Wants an output-history ring per machine and a `sealed` entity flag (contract).
|
||
- A real headless perf benchmark INCLUDING a live mite wave — proposed since round 3; the mite
|
||
scan is what finally makes it worth wiring.
|
||
- Per-item lane affinity if the mosh reactor's wandering melt ever bites (round-4 smell, latent).
|
||
|
||
### Round 7 — 2026-07-29 — Opus 5
|
||
|
||
SHIPPED
|
||
- **PARITY DEBT** — one scalar, five bands, thresholds + hysteresis in `constants.ts`. Rises on
|
||
tier>=2 shipments (per-item tier from DATA), decays ALWAYS, decays faster while tier-0/1
|
||
material ships (banked "clean credit", spent at a capped rate), and a `compliance:true`
|
||
commission vents `DEBT_VENT_COMPLIANCE` outright. `debtBand` fires on edges only.
|
||
- **CHECKSUM WARDEN** (band>=2) — `notice` 20s ahead of anything, then walks in from the rim to
|
||
the single most-deviating machine, audits it with visible phase+progress, rolls back the last
|
||
N outputs from a new per-machine output ring, stamps `sealed`, leaves. A sealed machine
|
||
refuses to run (`jammed: 'sealed'`). Counters: a firewall on the approach, or `pry` — 90
|
||
ticks, clears the seal, ADDS debt.
|
||
- **REDUNDANCY GUNSHIP** (band>=3) — picks the 8x8 sector with the most DEVIATION (not
|
||
throughput), tethers to the PARITY ANCHOR, publishes `rect`; corrupt crafts inside feed it and
|
||
it emits `mirrored`. Starve/seal/idle/remove the anchor and the tether breaks.
|
||
- **HASH AUDITOR** (band 4) — `notice`, visible 5s PRESCAN, then `stasis` with a deterministic
|
||
`lockHash`; entities, belts, splitters, shippers, labs, heat AND wildlife all stop inside the
|
||
rect. Auto-release is unconditional. Counters: ABSTENTION (nothing crafted during prescan) and
|
||
FORGED HASH (DATA's `forged-hash` held in any machine in the rect).
|
||
- **Reference factory v7** (208 entities, 208/208 placements legal): the COMPLIANCE TAP (its own
|
||
extractor + splitter — half the ore ships raw, half is demuxed and the luma ships), a CANNING
|
||
LINE off A1's dust (`can-static`), the brick works DOUBLED behind a splitter, a brick-works
|
||
guard firewall, and 8 ASICs to pay for it all.
|
||
- **Save v5** carries debt, clean credit, the band latch, every unit with its bookkeeping, seals,
|
||
pry timers, output rings and deviation counters. **testkit** gains `setParityDebt`,
|
||
`chargeDebt(band)` and `spawnCorrectionUnit` so no lane hand-edits my blob again.
|
||
- Contracts conformance as they landed mid-round: v9.1 phase union, v9.2 `remaining` (filled for
|
||
every fixed-length phase; a walk reports distance/speed; unbounded phases leave it undefined),
|
||
v9.3 `notice.pos` and `by` on `repaired`/`mirrored` (mites included).
|
||
- **Verified**: `npm run check` clean (whole repo). **645/645 tests green** (33 new in
|
||
`correction.test.ts`). Live on **8151**: app boots, zero console errors, the demo builds and
|
||
renders 208 entities, the fax reads `MDAT ORE 0/20` with `NEXT IN TRAY: LUMA BARS 15` — both
|
||
only reachable because of the v7 tap — and `199 DRAW / 300 GEN` is the new power farm.
|
||
- **Determinism holds across engines, byte-identical.** The same 20k reference run in the browser
|
||
and in Node both give: bands `1@2882 2@4840 3@6730 4@9444 3@11674 4@13294`, 9 warden / 6 gunship
|
||
/ 6 auditor notices, 5 stasis locks, 14 mirrors, 0 seals, 0 brownouts, 78 melt, and the queue
|
||
`raw-payload → first-refinement → dust-quota → first-taste → dust-allowance → quota-7c →
|
||
patio-resurfacing`. **The debt valve is visible in that band list**: filing quota-7c drops the
|
||
demo from band 4 to band 3 at t11674, and it climbs back by t13294.
|
||
- Perf: the reference build **0.0526 ms/tick (633x realtime)** — round 6 measured 0.054, so all of
|
||
v9 is free on the demo. A synthetic 878-entity / 1,624-belt-item field runs **0.443 ms/tick
|
||
(75x)**.
|
||
|
||
DECISIONS
|
||
- **A FIREWALL CATCHES WHAT WALKS.** This is the round's biggest call and it contradicts one line
|
||
of the orders, so it is here in full. The orders say a firewall may can a gunship. LANE-UI then
|
||
measured what that means in play: gunships arrive over their own sector, get canned inside their
|
||
notice window, and `mirrored` had NEVER fired in 12,000 ticks of real play — a whole rung
|
||
deleted by a 2x2 machine the player already owns. I hit the same wall from the other side (I
|
||
could not find a single free tile in the brick works that guards the wardens without also
|
||
covering the gunship's station; the works is eight tiles wide). So: mites and wardens walk and
|
||
are cannable; the GUNSHIP is airborne and is not, and its counter is the codex's own — "kill the
|
||
parity link to pop it"; the AUDITOR moves only between frames and was never present. Pinned by
|
||
`RUNG 3 IS NOT INVISIBLE`, which requires a mirror in the demo **as shipped, firewalls and all**.
|
||
- **Nothing is cannable during `notice`.** The paperwork is not yet actionable. Without this the
|
||
fax lead time is just a longer window in which to shoot the fax.
|
||
- **The Correction rolls no dice.** Every unit enters at the nearest rim to its target; the
|
||
airborne two take station when the notice closes. Zero RNG consumed, so wildlife and mites keep
|
||
the exact spawn stream they had in round 6 and a warden cycle is reproducible from a save alone.
|
||
- **A warden's rollback emits `repaired`**, with `by`. It is the same class of theft as a mite's
|
||
and SCREEN already subtracts it from the sky ledger; forking a new event for it would have made
|
||
two code paths for one idea. Flagging it because it means "CORRECTED" ticker counts now include
|
||
audits — say the word if you want them split.
|
||
- **`mirrored` steals from the ledger, not from the belt.** Nothing is removed from the world: the
|
||
clean parallel copy out-ships you, and being out-shipped by yourself is the damage. One event =
|
||
one unit, per SCREEN's note; a bigger theft would be N events.
|
||
- **Sealing abandons the in-flight craft.** The inputs were already spent, which is what makes an
|
||
audit sting, and it frees `state.progress` to drive the pry bar unambiguously.
|
||
- **Sealed machines are invisible to targeting**, or the next warden walks back to audit an empty
|
||
buffer forever. **Creature-canner recipes accrue no deviation** — without that a firewall earned
|
||
DEVIATION from its own kills until it became the top deviator and got sealed by a warden. Very
|
||
funny, entirely wrong; it broke two round-6 mite tests, which is how I found it.
|
||
- **A frozen machine keeps its generation and its idle draw.** Making a lock brown out the grid
|
||
would turn a refusal into an attack, which is the one thing The Correction never does.
|
||
- **`DEBT_CLEAN_CREDIT_PER_ITEM` cut 0.0006 → 0.0002.** At parity the demo's own byproduct stream
|
||
(1,282 dust + 144 slurry, all of it stuff you are dumping rather than choosing) bought 0.89 of
|
||
credit and pinned the ladder at band 2 forever. Any factory with a fat byproduct lane was
|
||
permanently immune without the player deciding anything. The quota vent is the real lever now.
|
||
- **`referenceFactory` now throws on overlapping placements.** `P()` returns `placed+1`, which is
|
||
only the sim's id while every placement SUCCEEDS — a dropped one shifts every later id and
|
||
silently re-points every `setRecipe`. That is not hypothetical: it happened twice while I was
|
||
adding the guard, and the symptom was the annex mis-reciping itself 40 tiles away. Round 2
|
||
flagged the coupling; this is the tripwire, and it caught the next two collisions by name.
|
||
|
||
BLOCKED/BROKEN
|
||
- **The demo never actually lands a seal** — all nine wardens were canned by the two firewalls. So
|
||
the shipped demo does not exercise the seal, the pry, or the redundancy that absorbs a seal.
|
||
Those are proven instead by `survives a real warden seal`, which builds the same layout with the
|
||
guard lifted. Honest, but it means "one full warden cycle at 20k" is a canned cycle, not a
|
||
completed audit.
|
||
- **The doubled brick works absorbs ONE seal on the melt path, not a series.** Measured: with both
|
||
crime quantizers sealed, coefficient packs back up into the demuxer and melt freezes exactly
|
||
like round 1. An undefended factory therefore still degrades toward death under sustained
|
||
audits — self-limiting (deviation stops, debt decays, wardens stand down) but permanent, since
|
||
nothing unseals without a player. The real fix is a surplus VOID behind the splitter; I left it
|
||
out because a blind splitter would sell a third of the coefficient packs in the healthy case.
|
||
Your call whether that trade is worth it.
|
||
- **The browser pane cannot advance the live loop.** `visibilityState` is `hidden`, rAF is
|
||
throttled to nothing and the app's tick froze at 7/14. This is round 1's documented constraint,
|
||
not new. I tried shimming rAF and lost the race against boot twice. What I verified instead is
|
||
round 1's own technique: importing the real sim into the real browser engine, which gave the
|
||
byte-identical 20k fingerprint above. The parts of the live app I could confirm are boot, zero
|
||
console errors, the render of all 208 entities, and UI reading my commission + power numbers.
|
||
- **I did not get a clean perf number at the full 1,000-entity / 5,000-item target with a live
|
||
Correction wave.** My dense stress field mis-placed its extractors and ran with 0 belt items,
|
||
and the 878/1,624 field never dispatched a unit because nothing in it had tier-2 output. The
|
||
headless benchmark I have now proposed four rounds running would have answered this in a line.
|
||
- The commission ladder advances 7 rungs and then stalls on `patio-resurfacing`. Rungs 1-6 are the
|
||
ones that mattered this round (the valve is rung 6); I did not chase the rest.
|
||
- Units require DEVIATION ON THE FLOOR, not just debt: `chargeDebt(4)` on a world with no tier-2
|
||
producer dispatches nothing. Correct, but it surprised me in my own stress test, so it will
|
||
surprise UI too.
|
||
|
||
CONTRACT REQUEST
|
||
- **A jam-reason line for `'sealed'`** in CONTRACTS, alongside v6's `'no seam'`. I set
|
||
`jammed: 'sealed'` so UI's jam line works without special-casing, and the string is now load-
|
||
bearing for three lanes.
|
||
- **An event for a CANNED Correction unit** — something like
|
||
`{ kind: 'unitCleared'; unit; id; pos; tick }`. Right now a unit simply vanishes from
|
||
`correction.units`, so the firewall's single most satisfying moment — the thing the whole guard
|
||
exists for — has no hook for SCREEN, AudioFX or RENDER, and in the shipped demo it happens nine
|
||
times and is completely silent. This is the one gap I would fix first.
|
||
|
||
PROPOSAL
|
||
- The `DEBT_*` constants could go data-driven if DATA wants to tune the ladder without a code
|
||
round; they are all in `constants.ts` and none is referenced by shape.
|
||
- A canned WARDEN currently ships as a `parity-mite` item, because the firewall's recipe is what
|
||
produces the cargo. A distinct item (and a commission for it) would be free comedy.
|
||
- Mites now ignore cargo under a stasis lock, so a lock is genuine cover from rung 1. Emergent,
|
||
not designed — worth knowing before someone "fixes" it.
|
||
|
||
NEXT (round 8 if asked)
|
||
- The headless perf benchmark. Proposed round 3, 4, 5, 6. It would have answered two questions
|
||
this round and it is the only reason my perf numbers are still anecdotes.
|
||
- A surplus void for the brick works, if you want an undefended factory to survive a seal series.
|
||
- `unitCleared`, the moment the contract has it.
|
||
- Moov heists: the auditor already publishes a deterministic `lockHash` that survives save/load,
|
||
which is the whole hook the minigame needs.
|