[data] heat that fires, the muxer, reel + broadcast eras

Round 3 orders, all six. The validator move landed in 4803b6a (swept into
LANE-SCREEN's commit by the shared index); this is the rest.

- heat retune: round 2's numbers were below the sim's always-on cooling, so
  nothing could ever warm up. Decoder now 100 gen at 0.0033 heat / 0.001 cool
  -> throttles 10.2s, scrams 14.5s, 23.9s cycle, 25.8/s average (1.29x the
  ASIC). Verified against LANE-SIM's real sim, not just arithmetic.
- powerGen had to move: the scram/restart hysteresis forces ~30% duty, so at
  55 gen the decoder averaged worse than a steady ASIC and nobody would build
  one.
- two new tests refuse decorative heat (per-tick and per-craft). They caught a
  fresh instance of the same bug in chart-stco-map before it shipped.
- bufferCap 240 -> 300: one tank now covers exactly one decoder scram.
- ASIC COOLER spec'd (aura deferred to M4, no contract field requested yet);
  THE MUXER + moov brain sub-lines; reel + broadcast era tech.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
type-two 2026-07-17 18:24:38 +10:00
parent 3707afa3a0
commit 4cccd22ca4
7 changed files with 343 additions and 67 deletions

View File

@ -59,6 +59,46 @@ one direction" — five grades, one item each, `bloom-grade-1..5`.
- Escalation is in delta wafers, not exotic inputs — a grade-5 canister is 2 raw bloom +
18 wafers of compounding, which is §4's "contents compound each lap" priced honestly.
## Added in round 3
### THE MUXER and the moov brain (§7, data-first — mechanics are M4's)
§7 names the parts and says each is "its own late-game production line", but never said
what makes them. Each part is now made by the machine that already owns that knowledge,
which is the whole joke: the container is assembled by the factory that made the mess.
| Thing | Made by | Why that machine |
|---|---|---|
| `stsz-ledger` | gop-assembler | The sample-size table. The assembler packed the crates, so it knows how big each one was. |
| `stco-map` | hex-splicer | The chunk-offset map — byte offsets are the splicer's entire domain. It nudges them for a living; here it writes them down honestly, under protest. |
| `stts-metronome` | telecine-puller | Time-to-sample is a cadence, and the puller is the cadence machine. Built from judder cams, so the metronome ticks 3:2. |
| `stss-keyring` | artifact-bottler | The sync-sample table is a list of I-frames, so it is literally forged from anchor slabs. |
| `ftyp-seal` | artifact-bottler | §7 names the ftyp seal as part of the container; it needed a source. |
| `moov-brain` | muxer | The four converge. 300 ticks. |
| `megaglitch` | muxer | `mux-container`: brain + seal + 500 mdat ore + 100 melt, 900 ticks. §7's "colossal composited corruption" priced like one. |
`muxer` is `kind: 'crafter'`, not `'shipper'` — it *builds* the megaglitch, and you still
have to ship the thing. 5×5, the biggest footprint in the game, per "hero structure".
### ASIC COOLER (spec'd, deliberately not yet mechanical)
§2 HEAT says heat is "Vented by ASIC coolers" — a machine the codex names and never
specifies. Authored now so the data is ready; **the mechanic is deferred to M4**, when
SIM builds auras. Gated behind `broadcast-asic-cooling` so it can't appear early.
- **Intended mechanic (SIM, when auras exist):** an adjacency aura — every machine whose
footprint touches the cooler's 2×2 gets the cooler's `coolPerTick` (0.02) instead of
its own. That turns the software decoder from a 30%-duty throb into something you can
*engineer around*: ring your decoders with coolers and pay for it in floor space and
build cost. This needs a contract change (`coolRadius?`, or a `kind: 'cooler'`) —
**not requesting it until SIM is ready to build it**, since a contract field nobody
reads is the same decorative trap heat just fell into.
- **Authored as `kind: 'power'` with `powerGen: 5`** because there is no cooler kind and
a lie is worse than a stretch: it is an ASIC, ASICs decode, decoding makes bandwidth.
It vents far more than it earns. If `kind: 'cooler'` ever lands, this moves.
- Its own `coolPerTick: 0.02` is inert today (nothing heats it) and is there to state the
intended aura strength in data rather than in prose.
---
## The numbers, and the arithmetic behind them
@ -76,79 +116,106 @@ idle draw (every machine's powerDraw) 57 /s
= net peak demand 78 /s
```
**Unit assumption — SIM please confirm:** I read `gen`/`draw` as bandwidth units **per
second**, not per tick (the codex's pylon meter reads kbps, §2 BANDWIDTH). This only
matters now that `bufferCap` exists, because a stored quantity = rate × time. If SIM
integrates per *tick*, every buffer number below is 30× too small and I'll retune.
**Units (RULED round 3):** `gen`/`draw` are bandwidth per **second**; `stored` and
`bufferCap` are bandwidth-**seconds**. My round-2 assumption held.
### `bufferCap: 240` on the buffer tank
### Round 2's heat was decorative — what went wrong
Order was "roughly 10 seconds of typical M1-chain deficit". Typical deficit ≠ total
blackout — it's the chain running with one generator down:
Worth writing down, because it's a trap anyone can fall into. I sized round 2's
`heatPerTick: 0.0025` against *no* cooling. SIM then implemented heat with cooling shed
**every tick, active or not** (`HEAT_DISSIPATION = 0.004`). So the decoder shed 0.004 and
banked 0.0025: net **0.0015/tick**. It could never warm up. The number looked like a
mechanic and was a comment.
The lesson is now a test (`every heat source out-heats its own cooling`), and it caught a
second instance immediately: `chart-stco-map` banked 0.06 heat over 90 ticks while the
splicer shed 0.09 — I'd authored a fresh decorative number in this very round.
**Anything with a heat number must beat its own machine's cooling, or it isn't a
mechanic.** The validator now refuses both forms (per-tick machine heat, per-craft recipe
heat).
### The software decoder: `powerGen` 55 → 100, `heatPerTick` 0.0033, `coolPerTick` 0.001
Net while running = 0.0033 0.001 = **0.0023/tick**. Against SIM's curve (throttle 0.7,
scram 1.0, restart 0.5):
```
reference chain, peak draw 96 /s
plant: 2 software decoders (55 each) 110 /s → comfortable
one decoder scrams: supply = 55 + 18 (compression) = 73 /s
deficit = 96 - 73 23 /s
× 10 s 230
bufferCap 240 (one tank ≈ one decoder's cooldown)
throttle, from cold 0.7 / 0.0023 / 30 = 10.1 s (target ~10 s)
scram, from cold 1.0 / 0.0023 / 30 = 14.5 s (target 13-15 s)
steady cycle, up 0.5 / 0.0023 / 30 = 7.25 s (heat 0.5 -> 1.0)
steady cycle, down 0.5 / 0.001 / 30 = 16.67 s (cools 1.0 -> restart 0.5)
period 23.9 s (target ~25 s)
duty 30.3 %
```
That is the intended M2 sentence: **a buffer tank is what makes software decoders
survivable.** The tank rides out exactly one scram. Two decoders scramming in sync
drains it and you brown out — which is the lesson.
**Why the decoder cools slowly (0.001, vs the sim's 0.004 default):** it is an overclocked
tower PC held together with zip ties and prayer (§4). It is the worst-cooled object in the
factory. The ASIC has "a huge heatsink afro" and no heat source at all.
### `heatPerTick: 0.0025` on the software decoder, `powerGen` 45 → 55
Heat is 0..1, throttling >0.7 and scramming at 1 (contract). From cold, running flat out:
**Why powerGen had to nearly double.** SIM's hysteresis (scram at 1.0, restart at 0.5)
means any machine that throttles at ~10 s spends ~70% of its life cooling. At round 2's
55 gen that's an average of ~14/s — strictly worse than the ASIC's steady 20, so nobody
would ever build one and "burst gen" would be a lie. Averaged properly (generation is
throttled linearly 1.0 → 0.5 across the throttle window, and zero while scrammed):
```
throttle (0.7) at 280 ticks = 9.3 s
scram (1.0) at 400 ticks = 13.3 s
un-throttled 0.2 / 0.0023 = 87.0 ticks at 100/s
throttled 0.3 / 0.0023 = 130.4 ticks at avg 75/s
energy per cycle = 100 x (87.0 + 130.4 x 0.75) = 18480 bandwidth-ticks
average = 18480 / 717 ticks = 25.8 /s
```
So a software decoder **cannot** run continuously. That's deliberate and it is the codex:
§4 gives it "burst gen", "doubles throughput on demand", "throttles theatrically". It is a
burst generator that needs downtime, not a better ASIC.
The trade, per 2×2 footprint:
| | gen | duty | effective | failure mode |
| | peak | duty | average | failure mode |
|---|---|---|---|---|
| DECODE ASIC | 20 /s | 100% | **20 /s** | none, forever |
| SOFTWARE DECODER | 55 /s | ~50% | **~27 /s** | brownout, on its own schedule |
| SOFTWARE DECODER | 100 /s | 30% | **25.8 /s** | total blackout, 16.7 s at a time |
~37% more average power for a supply that drops out on a timer you don't control. That's
a real choice rather than a strictly-better button. **And it answers the order's question
directly** ("does anyone dare run two side by side"): two identical decoders placed
together heat in lockstep and therefore scram in lockstep, turning a 50% duty cycle into a
100% blackout every ~13 s. Two decoders are only safe if something staggers them or a tank
covers the gap. Daring to run two is precisely the M2 lesson.
Five ASICs at full song, one third of the time, in one 2×2 footprint. 29% more average
power for a supply that vanishes on a schedule you don't control — a real choice, and the
answer to "does anyone dare run two side by side" is still *no, not naked*: identical
decoders heat in lockstep and therefore black out in lockstep.
**Assumption SIM owns — cooling.** The ~50% duty above assumes cooling of about
`0.0025`/tick while scrammed or idle (symmetric with the heat-up, so ~400 ticks cold).
There is no cooling model in the contract and no cooler machine in `data/`. If SIM picks a
different rate, tell me the number in NOTES and I'll retune `heatPerTick` — the shape I'm
aiming at is *throttle in ~10 s, full cycle ~25 s*. If SIM implements no cooling at all,
`heatPerTick` is a fuse, not a mechanic, and every software decoder is a one-shot: say so
and I'll drop it to 0 until a cooler exists.
### `bufferCap: 240 → 300`
### `heat: 0.05` on `splice-offsets`
Re-derived against the retuned cycle, since the order was "one tank should cover one
decoder scram" and the scram window changed:
Per craft, at 45 ticks each: 20 consecutive crafts = 900 ticks = 30 s to scram. §4 says the
splicer "Requires ... a steady buffer feed or it splices *itself*" — a splicer you leave
running unattended stops itself, which is the pinch the order asked for.
```
reference chain: peak draw 96 /s, compression pays back 18 /s -> net demand 78 /s
balanced plant: 3 ASIC (60/s steady) + 1 software decoder (25.8/s avg) = 85.8 /s ✓
decoder scrams: supply = 60 + 18 = 78 /s vs draw 96 /s -> deficit 18 /s
18 /s x 16.67 s = 300 bandwidth-seconds
bufferCap = 300 -> one tank rides out exactly one scram.
```
And it refills in time to do it again: while the decoder is up the plant makes 67/s more
than it spends, recharging 300 in **4.5 s** of the 7.25 s up-phase. That is the M2
sentence — **a buffer tank is what makes software decoders survivable** — and it now
closes.
### `heat` on the hex splicer: `coolPerTick 0.001`, 0.08 per splice
Net 0.035 per craft after the 45 ticks of cooling → 28.6 crafts → **42.9 s** of continuous
splicing before it scrams (`chart-stco-map`: 50 s). A lesser fever than the decoder's
14.5 s, exactly as ordered. §4: "Requires ... a steady buffer feed or it splices *itself*"
— left running unattended, it stops itself.
---
## Open / deferred
- **No cooler machine exists.** §2 HEAT says heat is "Vented by ASIC coolers" — that's a
machine the codex names but never specifies. Candidate for round 3; until then, heat is
only survivable via duty cycling.
- **The ASIC cooler's aura is spec'd, not built** (above). Until SIM has auras, heat is
survivable only by duty-cycling or by leaning on ASICs. Deliberate.
- `cinema-pack`, `mosquito-swarm`, `gibbs-wraith` remain forward canon (no recipe). The
validator's FORWARD_CANON allowlist is the enforcement, and it now has a companion test
asserting nothing sits on the list once it *is* producible.
validator's FORWARD_CANON allowlist is the enforcement, and it has a companion test
asserting nothing sits on the list once it *is* producible. Round 3 retired
`container-pack`'s slot (round 2) and added none.
- Dials-as-recipes (quantizer Q, subsampler grades, GOP cadence) stays, per round-2 review.
- The reel and broadcast era tech nodes don't exist yet; tech is disc + stream only.
- **The heat constants in `data/validate.test.ts` are mirrored from `src/sim/index.ts`**
(throttle 0.7, restart 0.5, dissipation default 0.004, 30 tps). They are not exported,
so my arithmetic tests can't import them. If SIM retunes the curve, my tests go stale
*silently* — they'd still pass while the feel drifts. Re-check on any heat round. A
cheap fix if SIM likes: export the constants.
- The muxer, moov brain and megaglitch are data-only; no lane implements a victory
condition yet. The tech gate (`fortress-the-muxer`) keeps them out of reach meanwhile.

View File

@ -48,5 +48,13 @@
{ "id": "cinema-pack", "name": "CINEMA PACK", "codex": "2:science-packs", "tier": 3, "color": "#fdfdff" },
{ "id": "mosquito-swarm", "name": "MOSQUITO SWARM (LIVE)", "codex": "5:mosquito-swarms", "tier": 2, "color": "#d0ffb0" },
{ "id": "gibbs-wraith", "name": "GIBBS WRAITH", "codex": "5:gibbs-wraiths", "tier": 2, "color": "#bfe0ff" }
{ "id": "gibbs-wraith", "name": "GIBBS WRAITH", "codex": "5:gibbs-wraiths", "tier": 2, "color": "#bfe0ff" },
{ "id": "stsz-ledger", "name": "STSZ LEDGER", "codex": "7:the-muxer", "tier": 2, "color": "#8fb0d8" },
{ "id": "stco-map", "name": "STCO MAP", "codex": "7:the-muxer", "tier": 2, "color": "#6f90e0" },
{ "id": "stts-metronome", "name": "STTS METRONOME", "codex": "7:the-muxer", "tier": 2, "color": "#d8b060" },
{ "id": "stss-keyring", "name": "STSS KEYRING", "codex": "7:the-muxer", "tier": 2, "color": "#e0b83f" },
{ "id": "ftyp-seal", "name": "FTYP SEAL", "codex": "7:the-muxer", "tier": 2, "color": "#b08fd0" },
{ "id": "moov-brain", "name": "THE MOOV BRAIN", "codex": "7:the-muxer", "tier": 3, "color": "#d8a0ff" },
{ "id": "megaglitch", "name": "THE MEGAGLITCH", "codex": "7:the-megaglitch", "tier": 3, "color": "#ff2fd4" }
]

View File

@ -14,7 +14,7 @@
"asset": "lane-splitter", "color": "#55595e",
"flavor": "One lane in, two lanes out, round-robin, no favourites. The only fair machine in the Bitstream." },
{ "id": "buffer-tank", "name": "BUFFER TANK", "codex": "2:buffer-charge", "kind": "buffer",
"footprint": { "x": 2, "y": 2 }, "recipes": [], "powerDraw": 0, "bufferCap": 240,
"footprint": { "x": 2, "y": 2 }, "recipes": [], "powerDraw": 0, "bufferCap": 300,
"asset": "buffer-tank", "color": "#5a6068",
"flavor": "Smoothness in a tank. Drains at exactly one frame per 16.6ms. Keep it above the red line; below the red line the audio has opinions." },
@ -48,7 +48,8 @@
"flavor": "Prints instructions, not pictures. Each one is applied to whatever came before it — correct or otherwise." },
{ "id": "gop-assembler", "name": "GOP ASSEMBLER", "codex": "4:gop-assembler", "kind": "crafter",
"footprint": { "x": 3, "y": 2 },
"recipes": ["assemble-gop", "assemble-gop-ibbp", "assemble-gop-i-only", "spool-offset-ribbon"],
"recipes": ["assemble-gop", "assemble-gop-ibbp", "assemble-gop-i-only", "spool-offset-ribbon",
"tally-stsz-ledger"],
"powerDraw": 6, "asset": "gop-assembler", "color": "#7a6a44",
"flavor": "One anchor, N wafers, one crate, per the cadence punch-card. Opening a sealed crate is a production verb here, not a crime." },
{ "id": "mosh-reactor", "name": "MOSH REACTOR", "codex": "4:mosh-reactor", "kind": "crafter",
@ -65,17 +66,18 @@
"powerDraw": 5, "asset": "field-loom", "color": "#3f5a44",
"flavor": "Weaves and unweaves time. The two halves of a splinter were captured a moment apart and have never agreed since." },
{ "id": "telecine-puller", "name": "TELECINE PULLER", "codex": "4:telecine-puller", "kind": "crafter",
"footprint": { "x": 2, "y": 2 }, "recipes": ["mill-judder"], "powerDraw": 4,
"footprint": { "x": 2, "y": 2 }, "recipes": ["mill-judder", "wind-stts-metronome"], "powerDraw": 4,
"asset": "telecine-puller", "color": "#6e5a38",
"flavor": "Cranks 24 into 30 on a deliberately lumpy cam. Runs backwards as a purifier, which we do not discuss on site." },
{ "id": "hex-splicer", "name": "HEX SPLICER", "codex": "4:hex-splicer", "kind": "crafter",
"footprint": { "x": 3, "y": 3 }, "recipes": ["splice-offsets"], "powerDraw": 12,
"footprint": { "x": 3, "y": 3 }, "recipes": ["splice-offsets", "chart-stco-map"], "powerDraw": 12,
"coolPerTick": 0.001,
"asset": "hex-splicer", "color": "#5a3a3a",
"flavor": "Nudges the offsets a few bytes so the decoder fetches garbage. Keep its buffer fed, or it splices itself." },
{ "id": "artifact-bottler", "name": "ARTIFACT BOTTLER", "codex": "2:science-packs", "kind": "crafter",
"footprint": { "x": 2, "y": 2 },
"recipes": ["can-static", "mix-rec709", "mix-dci-p3", "mix-rec2020",
"cut-sync-key", "bottle-container-pack",
"cut-sync-key", "bottle-container-pack", "forge-stss-keyring", "cut-ftyp-seal",
"bottle-analog-pack", "bottle-spatial-pack", "bottle-temporal-pack"],
"powerDraw": 4, "asset": "artifact-bottler", "color": "#566056",
"flavor": "Bottles the triptych. Contents are shelf-stable in the legal sense, i.e. no law has been written that mentions them." },
@ -85,9 +87,19 @@
"asset": "decode-asic", "color": "#b9bec4",
"flavor": "Cool, efficient, inflexible. Handles standard profiles and declines to imagine anything else. It will outlive you." },
{ "id": "software-decoder", "name": "SOFTWARE DECODER", "codex": "4:software-decoder", "kind": "power",
"footprint": { "x": 2, "y": 2 }, "recipes": [], "powerDraw": 0, "powerGen": 55, "heatPerTick": 0.0025,
"footprint": { "x": 2, "y": 2 }, "recipes": [], "powerDraw": 0, "powerGen": 100,
"heatPerTick": 0.0033, "coolPerTick": 0.001,
"asset": "software-decoder", "color": "#6e3a34",
"flavor": "Decodes ANYTHING, at any temperature. Throttles at 0.7, scrams at 1.0, screams throughout. The lag is the damage model." },
"flavor": "Decodes ANYTHING, at any temperature. Five ASICs at full song, one third of the time. Throttles at 0.7, scrams at 1.0, screams throughout. The lag is the damage model." },
{ "id": "asic-cooler", "name": "ASIC COOLER", "codex": "2:heat", "kind": "power",
"footprint": { "x": 2, "y": 2 }, "recipes": [], "powerDraw": 0, "powerGen": 5, "coolPerTick": 0.02,
"asset": "asic-cooler", "color": "#a8b4bc",
"flavor": "Decodes a little and vents a lot. Stand beside it in summer. Do not stand beside it in the dark, when you can see what it is venting." },
{ "id": "muxer", "name": "THE MUXER", "codex": "7:the-muxer", "kind": "crafter",
"footprint": { "x": 5, "y": 5 }, "recipes": ["assemble-moov-brain", "mux-container"], "powerDraw": 40,
"asset": "muxer", "color": "#6a7078",
"flavor": "The rocket. A launch gantry embracing a container built of nested boxes, innermost a brain of index tape-reels. It ships the whole file at once, and the file is your entire shipping history, and there is no second launch." },
{ "id": "shipper", "name": "SCREEN UPLINK", "codex": "7:the-muxer", "kind": "shipper",
"footprint": { "x": 2, "y": 2 }, "recipes": [], "powerDraw": 5,

View File

@ -51,6 +51,8 @@
"outputs": { "gop-crate": 1 }, "ticks": 45, "bandwidth": 24 },
{ "id": "spool-offset-ribbon", "machine": "gop-assembler", "inputs": { "gop-crate": 1, "moov-shard": 1 },
"outputs": { "offset-ribbon": 2 }, "ticks": 60, "bandwidth": 6 },
{ "id": "tally-stsz-ledger", "machine": "gop-assembler", "inputs": { "gop-crate": 2, "offset-ribbon": 1 },
"outputs": { "stsz-ledger": 1 }, "ticks": 90, "bandwidth": 8 },
{ "id": "mosh", "machine": "mosh-reactor", "inputs": { "gop-crate": 1 },
"outputs": { "melt": 3, "anchor-slab": 1 }, "ticks": 90, "bandwidth": 10 },
@ -75,9 +77,13 @@
{ "id": "mill-judder", "machine": "telecine-puller", "inputs": { "silver-frames": 3 },
"outputs": { "judder-cams": 1, "field-splinters": 2 }, "ticks": 60, "bandwidth": 5 },
{ "id": "wind-stts-metronome", "machine": "telecine-puller", "inputs": { "judder-cams": 2, "sync-key": 1 },
"outputs": { "stts-metronome": 1 }, "ticks": 90, "bandwidth": 7 },
{ "id": "splice-offsets", "machine": "hex-splicer", "inputs": { "gop-crate": 1 },
"outputs": { "explosive-hits": 4, "hf-dust": 2 }, "ticks": 45, "bandwidth": 20, "heat": 0.05 },
"outputs": { "explosive-hits": 4, "hf-dust": 2 }, "ticks": 45, "bandwidth": 20, "heat": 0.08 },
{ "id": "chart-stco-map", "machine": "hex-splicer", "inputs": { "offset-ribbon": 3, "moov-shard": 1 },
"outputs": { "stco-map": 1 }, "ticks": 90, "bandwidth": 14, "heat": 0.15 },
{ "id": "can-static", "machine": "artifact-bottler", "inputs": { "hf-dust": 4 },
"outputs": { "static-canister": 1 }, "ticks": 30, "bandwidth": 3 },
@ -86,6 +92,10 @@
{ "id": "bottle-container-pack", "machine": "artifact-bottler",
"inputs": { "moov-shard": 2, "offset-ribbon": 1, "sync-key": 1 },
"outputs": { "container-pack": 1 }, "ticks": 90, "bandwidth": 6 },
{ "id": "forge-stss-keyring", "machine": "artifact-bottler", "inputs": { "sync-key": 4, "anchor-slab": 2 },
"outputs": { "stss-keyring": 1 }, "ticks": 120, "bandwidth": 9 },
{ "id": "cut-ftyp-seal", "machine": "artifact-bottler", "inputs": { "moov-shard": 1, "sync-key": 1 },
"outputs": { "ftyp-seal": 1 }, "ticks": 60, "bandwidth": 5 },
{ "id": "mix-rec709", "machine": "artifact-bottler", "inputs": { "chroma-slurry": 2, "luma-bars": 1 },
"outputs": { "gamut-paint-rec709": 1 }, "ticks": 30, "bandwidth": 3 },
{ "id": "mix-dci-p3", "machine": "artifact-bottler", "inputs": { "chroma-slurry": 3, "false-color-speckle": 1 },
@ -102,5 +112,12 @@
"outputs": { "spatial-pack": 1 }, "ticks": 60, "bandwidth": 6 },
{ "id": "bottle-temporal-pack", "machine": "artifact-bottler",
"inputs": { "melt": 2, "bloom-concentrate": 1, "judder-cams": 1 },
"outputs": { "temporal-pack": 1 }, "ticks": 90, "bandwidth": 8 }
"outputs": { "temporal-pack": 1 }, "ticks": 90, "bandwidth": 8 },
{ "id": "assemble-moov-brain", "machine": "muxer",
"inputs": { "stsz-ledger": 1, "stco-map": 1, "stts-metronome": 1, "stss-keyring": 1 },
"outputs": { "moov-brain": 1 }, "ticks": 300, "bandwidth": 30 },
{ "id": "mux-container", "machine": "muxer",
"inputs": { "moov-brain": 1, "ftyp-seal": 1, "mdat-ore": 500, "melt": 100 },
"outputs": { "megaglitch": 1 }, "ticks": 900, "bandwidth": 60 }
]

View File

@ -1,4 +1,16 @@
[
{ "id": "reel-silver-seams", "era": "reel", "cost": { "analog-pack": 5 },
"unlocks": ["extract-silver-frames"] },
{ "id": "reel-telecine", "era": "reel", "cost": { "analog-pack": 25, "temporal-pack": 15 },
"unlocks": ["telecine-puller", "mill-judder"] },
{ "id": "broadcast-phosphor-seams", "era": "broadcast", "cost": { "analog-pack": 5 },
"unlocks": ["extract-field-splinters"] },
{ "id": "broadcast-field-loom", "era": "broadcast", "cost": { "analog-pack": 20 },
"unlocks": ["field-loom", "weave-fields", "loom-vhold-roll"] },
{ "id": "broadcast-asic-cooling", "era": "broadcast", "cost": { "analog-pack": 30, "spatial-pack": 10 },
"unlocks": ["asic-cooler"] },
{ "id": "disc-artifact-bottling", "era": "disc", "cost": { "analog-pack": 10 },
"unlocks": ["artifact-bottler", "can-static"] },
{ "id": "disc-chroma-subsampling", "era": "disc", "cost": { "analog-pack": 10 },
@ -18,10 +30,6 @@
{ "id": "disc-i-only-cadence", "era": "disc", "cost": { "spatial-pack": 30, "temporal-pack": 20 },
"unlocks": ["assemble-gop-i-only"] },
{ "id": "stream-field-loom", "era": "stream", "cost": { "analog-pack": 20 },
"unlocks": ["field-loom", "weave-fields", "loom-vhold-roll"] },
{ "id": "stream-telecine", "era": "stream", "cost": { "analog-pack": 25, "temporal-pack": 15 },
"unlocks": ["telecine-puller", "mill-judder"] },
{ "id": "stream-software-decoding", "era": "stream", "cost": { "analog-pack": 20, "temporal-pack": 20 },
"unlocks": ["software-decoder"] },
{ "id": "stream-demosaic-vng4", "era": "stream", "cost": { "spatial-pack": 30 },
@ -37,5 +45,13 @@
{ "id": "stream-laser-primaries", "era": "stream", "cost": { "spatial-pack": 40, "temporal-pack": 20 },
"unlocks": ["mix-rec2020"] },
{ "id": "stream-hex-splicing", "era": "stream", "cost": { "container-pack": 25, "temporal-pack": 25 },
"unlocks": ["hex-splicer", "splice-offsets"] }
"unlocks": ["hex-splicer", "splice-offsets"] },
{ "id": "fortress-moov-brain", "era": "fortress",
"cost": { "container-pack": 40, "temporal-pack": 30, "spatial-pack": 30 },
"unlocks": ["tally-stsz-ledger", "chart-stco-map", "wind-stts-metronome",
"forge-stss-keyring", "cut-ftyp-seal", "assemble-moov-brain"] },
{ "id": "fortress-the-muxer", "era": "fortress",
"cost": { "container-pack": 60, "analog-pack": 40, "spatial-pack": 40, "temporal-pack": 40 },
"unlocks": ["muxer", "mux-container"] }
]

View File

@ -181,6 +181,83 @@ describe('machines', () => {
});
});
// Round 2 shipped heat numbers that could never fire: every heatPerTick sat below the
// sim's always-on cooling, so heat was decorative. These lock the arithmetic down so a
// well-meaning tweak can't quietly make it decorative again.
describe('the heat curve actually fires', () => {
// MIRRORED from src/sim/index.ts (LANE-SIM owns the curve; these are not exported).
// If SIM changes them, these numbers go stale silently — re-check on any heat round.
const TPS = 30;
const THROTTLE_START = 0.7; // heat above this throttles
const RESTART = 0.5; // a scrammed machine restarts once it cools below this
const SIM_DEFAULT_COOL = 0.004;
const soft = machines.find((m) => m.id === 'software-decoder')!;
const cool = (m: MachineDef) => m.coolPerTick ?? SIM_DEFAULT_COOL;
/** Net heat per tick while the machine is working. Negative = can never overheat. */
const net = (m: MachineDef) => (m.heatPerTick ?? 0) - cool(m);
it('every heat source out-heats its own cooling — or it is just decoration', () => {
const decorative = machines
.filter((m) => m.heatPerTick !== undefined)
.filter((m) => net(m) <= 0)
.map((m) => `${m.id}: heats ${m.heatPerTick}/tick but cools ${cool(m)}/tick — never warms up`);
expect(decorative).toEqual([]);
});
it('every heated recipe banks more than its machine sheds during the craft', () => {
// A recipe's heat lands once per craft, but the machine cools every tick of it.
// heat <= ticks * coolPerTick means the number is a comment, not a mechanic.
const decorative = recipes
.filter((r) => r.heat !== undefined)
.map((r) => ({ r, m: machines.find((x) => x.id === r.machine)! }))
.filter(({ r, m }) => r.heat! <= r.ticks * cool(m))
.map(({ r, m }) =>
`${r.id}: banks ${r.heat} per craft but ${m.id} sheds ${(r.ticks * cool(m)).toFixed(3)} over its ${r.ticks} ticks`);
expect(decorative).toEqual([]);
});
it('the software decoder throttles at ~10s and scrams at 13-15s of continuous load', () => {
const secondsTo = (heat: number) => heat / net(soft) / TPS;
expect(secondsTo(THROTTLE_START)).toBeGreaterThan(8);
expect(secondsTo(THROTTLE_START)).toBeLessThan(12);
expect(secondsTo(1)).toBeGreaterThan(13);
expect(secondsTo(1)).toBeLessThan(15);
});
it('the software decoder duty-cycles at ~25s, and is still worth its footprint', () => {
// Steady state after the first scram: heat swings RESTART -> 1 -> RESTART.
const upTicks = (1 - RESTART) / net(soft);
const downTicks = (1 - RESTART) / cool(soft);
const periodS = (upTicks + downTicks) / TPS;
expect(periodS).toBeGreaterThan(20);
expect(periodS).toBeLessThan(30);
// Generation is throttled linearly from 1.0 down to 0.5 across the throttle window,
// and is zero while scrammed. Average it over the whole cycle.
const fullTicks = (THROTTLE_START - RESTART) / net(soft); // un-throttled
const throttledTicks = (1 - THROTTLE_START) / net(soft); // 1.0 -> 0.5 output
const energy = soft.powerGen! * (fullTicks + throttledTicks * 0.75);
const average = energy / (upTicks + downTicks);
const asic = machines.find((m) => m.id === 'decode-asic')!;
// Same 2x2 footprint, so if the hot one doesn't beat the cool one on average,
// nobody would ever build it and the codex's "burst gen" is a lie.
expect(average).toBeGreaterThan(asic.powerGen! * 1.2);
});
it('the hex splicer runs a lesser fever than the decoder', () => {
const splicer = machines.find((m) => m.id === 'hex-splicer')!;
const secondsToScram = Math.min(
...recipes.filter((r) => r.machine === 'hex-splicer' && r.heat)
.map((r) => (1 / (r.heat! - r.ticks * cool(splicer))) * r.ticks / TPS),
);
expect(secondsToScram).toBeGreaterThan(30); // lesser...
expect(secondsToScram).toBeLessThan(90); // ...but a fever
expect(secondsToScram).toBeGreaterThan(1 / net(soft) / TPS); // slower than the decoder
});
});
describe('recipes', () => {
it('every recipe runs on a machine that exists', () => {
const bad = recipes.filter((r) => !machineIds.has(r.machine)).map((r) => `${r.id} -> "${r.machine}"`);

View File

@ -211,3 +211,82 @@ NEXT (candidates): a cooler machine — §2 says heat is "Vented by ASIC coolers
machine the codex names but never specifies, and without it heat has no counterplay;
mosquito traps (unblocks `vaporwave-sunset`); reel + broadcast era tech; the muxer and
the moov brain's four sub-lines (§7); cinema-pack when the Fortress exists.
### Round 3 — 2026-07-17 — Opus 4.8
SHIPPED: all 6 orders. Validator moved to `data/validate.test.ts` (git mv, imports
rewired, vitest picks it up from the new home; `src/sim/**` is fully SIM's again).
Heat retuned. `bufferCap` re-derived. ASIC COOLER + muxer + moov brain authored. Reel
and broadcast eras. items 44 → 51, machines 21 → 23, recipes 44 → 51, tech 19 → 24.
Validator 36 → 41 tests.
- **DoD met, verified against the real sim, not my arithmetic**: I wrote a throwaway
probe that ran LANE-SIM's actual `createSim()` with my data — **throttle at 10.17s,
scram at 14.50s, downtime 16.67s, ASIC heat still 0 after 100s**. Predicted 10.14 /
14.49 / 16.67. Then deleted the probe: it asserts sim behaviour, which is SIM's lane
to test, not mine.
- Full tree green: **223/223 across all 16 files** — SIM's reference factory and
pressure tests pass against the rebalanced numbers. Live build: 23/23 machines
art-directed, ASIC COOLER in the POWER page at my exact hex with zero UI code, no
console errors.
- Fault-injected the new checks by **re-introducing round 2's exact bug** — it fails
with `software-decoder: heats 0.0025/tick but cools 0.004/tick — never warms up`.
DECISIONS:
- **You were right that heat was decorative, and the cause is worth recording**: I sized
round 2's numbers against *no* cooling; SIM shipped cooling that runs every tick,
active or not. 0.0025 heat vs 0.004 cooling = net 0.0015. The number looked like a
mechanic and was a comment. That lesson is now two tests — and they immediately caught
me authoring a *fresh* decorative number this round (`chart-stco-map` banked 0.06 over
90 ticks while the splicer shed 0.09). Fixed to 0.15 before it ever ran.
- **`powerGen` 55 → 100, and it had to move.** SIM's hysteresis (scram at 1.0, restart at
0.5) means anything throttling at ~10s spends ~70% of its life cooling. At 55 gen the
decoder averages ~14/s — strictly worse than the ASIC's steady 20, so nobody would
build one and "burst gen" would be a lie. At 100 it averages 25.8/s (1.29× the ASIC) in
the same 2×2: five ASICs at full song, one third of the time. The duty cycle set the
peak, not taste. Arithmetic in CANON-DELTA.
- **`bufferCap` 240 → 300.** Re-derived, since the scram window changed: a balanced plant
(3 ASIC + 1 decoder) runs an 18/s deficit for the 16.67s scram = exactly 300
bandwidth-seconds, and refills in 4.5s of the 7.25s up-phase. One tank rides out one
scram — the order's sentence, now literally true. (Your review said 240 = 8s of a 30
deficit; my round-2 intent was 10s of a 23 deficit. Either way the retune moved it.)
- **ASIC COOLER authored as `kind: 'power'`, `powerGen: 5`** — there is no cooler kind and
I'd rather stretch than lie: it is an ASIC, ASICs decode, decoding makes bandwidth. It
vents far more than it earns. **I have deliberately NOT requested `coolRadius?` /
`kind: 'cooler'`** — a contract field nobody reads is the same decorative trap heat just
fell into. Ask me for it the round SIM builds auras. The intended adjacency mechanic is
spec'd in CANON-DELTA.
- Tech ids `stream-field-loom``broadcast-field-loom` and `stream-telecine`
`reel-telecine`: a *move*, not an addition (the validator forbids two techs unlocking
the same thing, which is what caught the duplication). Grepped first — no lane reads
tech ids or eras yet, so the rename is safe. Recorded here per the deprecation rule.
INVENTED: 7 items (the four moov parts, `ftyp-seal`, `moov-brain`, `megaglitch`), 2
machines (`asic-cooler`, `muxer`), 9 recipes, 4 tech nodes. Rationale in CANON-DELTA —
each moov part is made by the machine that already owns that knowledge (the splicer
charts stco because byte offsets are its whole domain; the keyring is forged from anchor
slabs because stss *is* a list of I-frames).
BLOCKED/BROKEN: nothing blocked. One regression I caused by following orders, below.
CONTRACT REQUEST: **`tsconfig.json` `include` needs `"data"`.** The ordered move took the
validator out of the tsc program — `include: ["src"]`. I proved it: appended
`const x: number = "not a number"` to `data/validate.test.ts` and `npm run check` exits
**0**. Vitest transpiles without typechecking, so my validator — the file whose whole job
is catching mistakes — is now the one file in the repo where a type error ships silently.
tsconfig is orchestrator-owned, so I can't fix it. One word: `"include": ["src", "data"]`.
CROSS-LANE — SIM: your `coolPerTick` wiring landed and works; my numbers are tuned
against it and verified in your sim. Two notes. (1) Your reference factory's plant is 4
software decoders and no ASIC — with heat now real they heat in lockstep and black out
together for 16.67s at a time. Suggested v3 plant: **3 decode-asics + 1 software decoder
+ 1 buffer-tank**, which is precisely what `bufferCap: 300` is sized for (85.8/s average
vs the chain's 78/s demand, and the tank covers the scram). Your call. (2) My heat tests
mirror your constants (0.7 / 0.5 / 0.004 / 30tps) by hand because they aren't exported —
if you retune the curve my tests go stale *silently*. Exporting them would let me import
truth instead of copying it.
NEXT (candidates): mosquito traps + HF-dust piles (unblocks `vaporwave-sunset`, the last
forward-canon commission that's blocked on data rather than mechanics); the ASIC cooler's
aura the round SIM has auras; a seam/ore map — SIM notes extractors currently mine
anywhere because `data/` has no seam data, which is a real content gap I own;
`cinema-pack` + the Fortress when M4 lands.