[data] the composite seam + a tech tree that can actually be bought

Round 4 orders, all five.

The headline is a bug I authored: the v4 gating rule (any id in a
TechDef.unlocks is locked until that tech completes) turned three rounds of
tech authoring into a total bootstrap deadlock. Measured: melt not producible
at tick 0 (first-taste impossible), 0 of 24 techs reachable — every pack chain
gated behind packs you cannot make. disc-artifact-bottling cost 10 analog packs
and unlocked the only machine that bottles analog packs.

Fixed as a shape, not a patch, and both rules are now tests:
- the M1 melt chain is starting equipment (so first-taste is possible, and
  RENDER's era derivation correctly reads it as starting equipment)
- one pack is craftable at tick 0 — SPATIAL, made from the quantizer's own
  sins, so your first research is a study of your own damage
23/23 nodes now reachable; six emptied disc nodes deleted (no lane references
tech ids).

Also:
- §10 THE COMPOSITE SEAM transcribed: alpha/hard-matte/blue-hole/fringe-lace/
  green-spill/mixed-pixel/roto-chatter/ghost-feather/sodium-light (forward
  canon), matte bath, optical printer, chroma keyer, roto bench, neural keyer
  (powerDraw 240 — the codex's "appetite that dwarfs everything").
- ARCHAEOLOGY LAB canonized (kind: lab), ungated — a lab behind research is a
  lab you can never build.
- ASIC cooler live: the round-3 spec of 0.02 would have meant "never heats,
  free 100/s". Retuned to 0.0018, sane under both readings of the contract.
- accents where output-derivation can't work; CODEX_REF regex widened for §10.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
type-two 2026-07-17 20:04:00 +10:00
parent 180747ce28
commit dbb3615a64
8 changed files with 451 additions and 41 deletions

View File

@ -99,6 +99,34 @@ SIM builds auras. Gated behind `broadcast-asic-cooling` so it can't appear early
- 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.
## Added in round 4
### §10 THE COMPOSITE SEAM — transcribed
The codex grew §10, so most of this round is transcription rather than invention. Every
item and machine in §10 is now data. Deviations and judgement calls only:
| Call | Why |
|---|---|
| "Footage" = `anchor-slab` | §10's keyers key *footage*, but the codex never says what footage *is* as an item. The anchor slab is already "a complete, self-sufficient picture" (§2) — the only thing in the game you could point a keyer at. Every keyer takes slabs. |
| MATTE BATH's chemical = `static-canister` | §3 already says static canisters are "brine for preserving other artifacts". The bath needed a developer; the brine was sitting right there. |
| `ghost-feather` is made **only** by the neural keyer | §10: "every era below the neural keyer ruins it, so demand never dies." So the earlier machines *consume* it and ruin it — `bathe-feather` (feather → hard matte, "the machine does not apologize") and `key-hair` (feather → roto chatter). Both are jokes with a real cost, and both are the codex verbatim. |
| NEURAL KEYER `powerDraw: 240` | §10 says "24 VRAM — a bandwidth appetite that dwarfs everything else in the game", and "you don't unlock the neural keyer; you unlock the power plant it demands". 240/s is 12 decode ASICs, or ~9 software decoders after duty cycling. The whole M1 reference chain draws 96. It dwarfs the game, as ordered. |
| `sodium-light` has no recipe | §10: The Correction hoards the prisms. Forward canon; on the FORWARD_CANON allowlist. `sodium-requisition` commissions it anyway, with 30 BLUE HOLES accepted "if you lose your nerve". |
| ROTO BENCH is gated (disc), not free | §10 calls it "the economic floor under every keyer... the bench never does [fail]". Tempting to leave ungated, but ALPHA is "the rarest resource in the game" and an ungated bench hands it to you at tick 0. Gated cheaply (10 spatial) so the floor arrives early but the Composite Seam stays a mid-game fault. |
Optical printer ratios hurt, as ordered: 4 anchor slabs + 2 colour plates → **1** alpha and
4 fringe lace, over 240 ticks. It is the worst alpha-per-slab in the game and it fringes
every single run, which is exactly what the iron lung is for.
### THE ARCHAEOLOGY LAB (`kind: 'lab'`) — canonized
The codex has no lab: §6 only says "TECH TREE = MEDIA ARCHAEOLOGY. You research *downward*
(excavating older strata for analog secrets) and *upward* (deriving newer codecs)." So the
lab is the dig site — named for the verb the codex already uses, not a generic "research
station". Ungated starting equipment, because a lab behind research is a lab you can never
build (there is now a test named exactly that).
---
## The numbers, and the arithmetic behind them
@ -119,6 +147,49 @@ idle draw (every machine's powerDraw) 57 /s
**Units (RULED round 3):** `gen`/`draw` are bandwidth per **second**; `stored` and
`bufferCap` are bandwidth-**seconds**. My round-2 assumption held.
### Round 4: the tech tree was a total bootstrap deadlock
The v4 gating rule — *any id in a `TechDef.unlocks` is locked until that tech completes*
turned three rounds of tech authoring from decoration into a lock on the entire game. I
had written every node assuming gating didn't exist. Measured against the rule:
```
TICK 0, nothing researched:
melt producible? false <- first-taste, the opening commission, impossible
analog-pack producible? false
spatial-pack producible? false
temporal-pack producible? false
container-pack producible? false
RESEARCH CLOSURE: 0 of 24 techs reachable — every node stranded behind its own output
```
Every pack chain was gated behind packs you could not make. `disc-artifact-bottling` cost
10 analog packs and unlocked the only machine that can bottle an analog pack. The M1 melt
chain itself (mosh reactor, subsampler, `press-anchor-slab`) was gated, so the game opened
on an unbuildable factory and an unfillable commission, with no way to research out of it.
**The fix is a shape, not a patch.** Two rules now hold, and both are tests:
1. **The M1 melt chain is starting equipment** — ungated, so first-taste is possible at
tick 0. Ungating it also fixes RENDER for free: they derive a machine's era from the
tech that unlocks it and treat unmapped machines as starting equipment, which is exactly
what the M1 chain now is.
2. **One pack must be craftable at tick 0**, or research can never begin. That pack is the
**SPATIAL PACK** — macroblock bricks + ringing halos + slurry, all of which fall out of
the quantizer the player already owns. Your first research is a study of your own
damage, which is the most in-voice thing in the tree. Everything else chains off it:
spatial → analog → temporal → container → fortress.
Result: **23 / 23 nodes reachable**, unlock order tells a story (phosphor seams → field
loom → analog packs → telecine → temporal → container → fortress), and the only items
unreachable with the whole tree bought are the four declared forward-canon.
Six disc nodes were **deleted**, not repurposed, because their whole content became
starting equipment: `disc-artifact-bottling`, `disc-chroma-subsampling`,
`disc-macroblock-quarrying`, `disc-ringing-skim`, `disc-subsampling-420`,
`disc-mosh-reactor`. Grepped first — no lane references tech ids (RENDER reads
`unlocks` generically, never a specific node). Recorded here per the deprecation rule.
### Round 2's heat was decorative — what went wrong
Worth writing down, because it's a trap anyone can fall into. I sized round 2's
@ -194,6 +265,67 @@ than it spends, recharging 300 in **4.5 s** of the 7.25 s up-phase. That is the
sentence — **a buffer tank is what makes software decoders survivable** — and it now
closes.
### The ASIC cooler goes live: `coolPerTick 0.02 → 0.0018`, `coolRadius: 2`
Round 3 spec'd the aura at 0.02 while it was inert. **The moment it became mechanical that
number was catastrophic:** the decoder banks 0.0033/tick, so a 0.02 aura means net
0.0167 — it never heats at all, and a 2×2 cooler turns a 2×2 decoder into a permanent
free 100/s. A spec'd number and a live number are different objects; this is the second
time this round that a number only became wrong once something read it.
Retuned to **0.0018**, which lands well under either reading of the contract's "this
machine's coolPerTick *also* applies" — deliberately, because SIM hasn't built it yet and
I don't want the design resting on my guess of an ambiguous sentence:
```
throttle scram period duty avg gen
decoder alone 10.1 s 14.5 s 23.9 s 30 % 25.8 /s
+ cooler, ADDITIVE 46.7 s 66.7 s 39.3 s 85 % 72.1 /s
+ cooler, REPLACING 15.6 s 22.2 s 20.4 s 55 % 46.4 /s
+ cooler at 0.02 (r3) never never — 100 % 100.0 /s <- broken either way
```
Both readings are playable and both are worth the tech, so **SIM can implement either and
the design holds**. Per tile, which is the real question when both machines are 2×2:
```
2 decode ASICs 40.0 /s over 8 tiles = 5.00 /s per tile
1 software decoder, bare 25.8 /s over 4 tiles = 6.44 /s per tile
decoder + cooler (additive) 77.1 /s over 8 tiles = 9.64 /s per tile
```
So the pairing is the best power in the game per tile (1.9× ASICs) — and it costs a tech,
two machines, and the floor space to keep them adjacent. That is the reward for engineering
around heat instead of suffering it, which is what the cooler is *for*.
### Research: is the first node affordable off a first-taste-era factory?
Worked path for the bootstrap pack, one bottler, one quantizer:
```
bottle-spatial-pack = 2 macroblock-bricks + 1 ringing-halo + 2 chroma-slurry, 60 ticks
2 bricks <- quantize-crime x1 (1 coefficient pack, 20 ticks)
1 halo <- skim-ringing x1 (3 coefficient packs, 40 ticks)
4 coefficient packs <- quantize x4 (4 luma, 120 ticks) <- 6 mdat ore
THE QUANTIZER IS THE BOTTLENECK: 20 + 40 + 120 = 180 ticks per spatial pack
= 10.0 packs / minute on one quantizer
```
Against the early nodes:
```
broadcast-phosphor-seams 5 spatial = 0.5 min
reel-silver-seams 8 spatial = 0.8 min
disc-roto-bench 10 spatial = 1.0 min
broadcast-field-loom 15 spatial = 1.5 min <- opens the ANALOG PACK
```
A player who has shipped their first melt already owns a quantizer and can afford the first
four nodes inside two minutes of production. That's the intent: **the opening research is
paid for with the quantizer's own sins** — you study your own damage. Later nodes scale to
tens of minutes and demand the analog/temporal/container chains, which is where the floor
space goes.
### `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

View File

@ -43,10 +43,18 @@
"flavor": "One (1) GIBBS WRAITH, willingly.",
"wants": { "gibbs-wraith": 1 }, "rewardBandwidth": 900 },
{ "id": "feather-proof",
"flavor": "3 GHOST FEATHERS. Gradient intact, tip genuinely transparent, no hard edge anywhere on the object. We have been sold nine hard mattes by nine suppliers who each insisted it was the same thing. It is not the same thing. Do not send a matte.",
"wants": { "ghost-feather": 3 }, "rewardBandwidth": 1100 },
{ "id": "container-integrity-review",
"flavor": "4 CONTAINER PACKS and 20 EXPLOSIVE HITS, shipped in that order and not the other order. The last supplier shipped in the other order. There is no last supplier.",
"wants": { "container-pack": 4, "explosive-hits": 20 }, "rewardBandwidth": 1400 },
{ "id": "sodium-requisition",
"flavor": "One (1) beam of SODIUM LIGHT, 589nm, in its prism. We are aware who holds the prisms. We are aware what they call it when you take one. We are prepared to call it something else. 30 BLUE HOLES accepted as consolation if you lose your nerve.",
"wants": { "sodium-light": 1, "blue-hole": 30 }, "rewardBandwidth": 1800 },
{ "id": "sky-blue-mislabeled",
"flavor": "600 MACROBLOCK BRICKS, sky-blue, MISLABELED REC2020. Purpose: none of your business.",
"wants": { "macroblock-bricks": 600, "gamut-paint-rec2020": 6 }, "rewardBandwidth": 2400 },

View File

@ -50,6 +50,16 @@
{ "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": "alpha", "name": "ALPHA", "codex": "10:alpha", "tier": 1, "color": "#b8e8f0" },
{ "id": "hard-matte", "name": "HARD MATTE", "codex": "10:hard-matte", "tier": 1, "color": "#e8e8e8" },
{ "id": "green-spill", "name": "GREEN SPILL", "codex": "10:green-spill", "tier": 1, "color": "#4fff30" },
{ "id": "mixed-pixel", "name": "MIXED PIXEL", "codex": "10:mixed-pixel", "tier": 1, "color": "#a04fd0" },
{ "id": "blue-hole", "name": "BLUE HOLE", "codex": "10:blue-hole", "tier": 2, "color": "#2f5fd0" },
{ "id": "fringe-lace", "name": "FRINGE LACE", "codex": "10:fringe-lace", "tier": 2, "color": "#fff0c0" },
{ "id": "roto-chatter", "name": "ROTO CHATTER", "codex": "10:roto-chatter", "tier": 2, "color": "#ff8f4f" },
{ "id": "ghost-feather", "name": "GHOST FEATHER", "codex": "10:ghost-feather", "tier": 2, "color": "#f0f8ff" },
{ "id": "sodium-light", "name": "SODIUM LIGHT (589nm)", "codex": "10:sodium-light", "tier": 3, "color": "#ffb020" },
{ "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" },

View File

@ -7,15 +7,15 @@
{ "id": "belt", "name": "STREAM BELT", "codex": "4:belts", "kind": "belt",
"footprint": { "x": 1, "y": 1 }, "recipes": [], "powerDraw": 0, "beltSpeed": 2,
"asset": "belt", "color": "#4a4e52",
"asset": "belt", "color": "#4a4e52", "accent": "#5ad0ff",
"flavor": "Moves cargo. Back-pressure is a feature. Riding the belt is discouraged in writing and in no other way." },
{ "id": "lane-splitter", "name": "LANE SPLITTER", "codex": "4:demuxer", "kind": "splitter",
"footprint": { "x": 1, "y": 1 }, "recipes": [], "powerDraw": 1,
"asset": "lane-splitter", "color": "#55595e",
"asset": "lane-splitter", "color": "#55595e", "accent": "#5ad0ff",
"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": 300,
"asset": "buffer-tank", "color": "#5a6068",
"asset": "buffer-tank", "color": "#5a6068", "accent": "#7fe0ff",
"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." },
{ "id": "demuxer", "name": "DEMUXER", "codex": "4:demuxer", "kind": "crafter",
@ -84,18 +84,46 @@
{ "id": "decode-asic", "name": "DECODE ASIC", "codex": "4:decode-asic", "kind": "power",
"footprint": { "x": 2, "y": 2 }, "recipes": [], "powerDraw": 0, "powerGen": 20,
"asset": "decode-asic", "color": "#b9bec4",
"asset": "decode-asic", "color": "#b9bec4", "accent": "#6fe8c0",
"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": 100,
"heatPerTick": 0.0033, "coolPerTick": 0.001,
"asset": "software-decoder", "color": "#6e3a34",
"asset": "software-decoder", "color": "#6e3a34", "accent": "#ff4a2a",
"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",
"footprint": { "x": 2, "y": 2 }, "recipes": [], "powerDraw": 0, "powerGen": 5,
"coolPerTick": 0.0018, "coolRadius": 2,
"asset": "asic-cooler", "color": "#a8b4bc", "accent": "#9fe8ff",
"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": "matte-bath", "name": "MATTE BATH", "codex": "10:matte-bath", "kind": "crafter",
"footprint": { "x": 3, "y": 2 }, "recipes": ["bathe-hard-matte", "bathe-feather"], "powerDraw": 5,
"asset": "matte-bath", "color": "#5a4a4a",
"flavor": "Footage in, hard mattes out, softness destroyed. A GHOST FEATHER fed in comes out a HARD MATTE and the machine does not apologize. One mistake ruins the reel." },
{ "id": "optical-printer", "name": "OPTICAL PRINTER", "codex": "10:optical-printer", "kind": "crafter",
"footprint": { "x": 5, "y": 4 }, "recipes": ["print-composite", "print-blue-screen"], "powerDraw": 18,
"asset": "optical-printer", "color": "#6a5a3a",
"flavor": "The iron lung. A cathedral of bellows and lenses that splits footage into three colours and recombines them slightly wrong. It fringes everything it touches, and it is beautiful, and it is slow." },
{ "id": "chroma-keyer", "name": "CHROMA KEYER", "codex": "10:chroma-keyer", "kind": "crafter",
"footprint": { "x": 2, "y": 2 },
"recipes": ["key-chroma", "key-hair", "key-semitransparent"], "powerDraw": 6,
"asset": "chroma-keyer", "color": "#3a6a3a",
"flavor": "Cheap, fast, and thirty-year-old maths. Weeps GREEN SPILL from its seams. Chokes on hair. Do not show it anything semi-transparent; it will decide the pixel belongs to neither of you." },
{ "id": "roto-bench", "name": "ROTO BENCH", "codex": "10:roto-bench", "kind": "crafter",
"footprint": { "x": 2, "y": 2 }, "recipes": ["roto-cutout", "roto-mixed"], "powerDraw": 2,
"asset": "roto-bench", "color": "#6a5540",
"flavor": "Converts anything to a cutout. Perfectly. Eventually. Twenty-four frames of pain per second of footage. When the automation fails the bench never does — it just costs your life instead." },
{ "id": "neural-keyer", "name": "NEURAL KEYER", "codex": "10:neural-keyer", "kind": "crafter",
"footprint": { "x": 4, "y": 4 }, "recipes": ["neural-key", "neural-unmix"], "powerDraw": 240,
"asset": "neural-keyer", "color": "#2a2a32",
"flavor": "Trained on ten thousand synthetic ghosts, because reality could not teach it — you cannot film the answer. Perfect alpha through glass, chainmail and moving hair. You do not unlock this machine. You unlock the power plant it demands." },
{ "id": "archaeology-lab", "name": "ARCHAEOLOGY LAB", "codex": "6:tech-tree", "kind": "lab",
"footprint": { "x": 3, "y": 3 }, "recipes": [], "powerDraw": 6,
"asset": "archaeology-lab", "color": "#5a5548", "accent": "#ffc24a",
"flavor": "Where the factory reads its own strata. Feed it bottled artifacts and it derives the codec that made them: downward for analog secrets, upward for newer sins. The coffee predates the Reel Beds." },
{ "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",
@ -103,6 +131,6 @@
{ "id": "shipper", "name": "SCREEN UPLINK", "codex": "7:the-muxer", "kind": "shipper",
"footprint": { "x": 2, "y": 2 }, "recipes": [], "powerDraw": 5,
"asset": "shipper", "color": "#46525e",
"asset": "shipper", "color": "#46525e", "accent": "#ff5ad0",
"flavor": "Composites whatever you hand it onto THE SCREEN. There is no undo. The absence of undo is the product." }
]

View File

@ -114,6 +114,33 @@
"inputs": { "melt": 2, "bloom-concentrate": 1, "judder-cams": 1 },
"outputs": { "temporal-pack": 1 }, "ticks": 90, "bandwidth": 8 },
{ "id": "bathe-hard-matte", "machine": "matte-bath", "inputs": { "anchor-slab": 1, "static-canister": 1 },
"outputs": { "hard-matte": 2 }, "ticks": 60, "bandwidth": 5 },
{ "id": "bathe-feather", "machine": "matte-bath", "inputs": { "ghost-feather": 1 },
"outputs": { "hard-matte": 1 }, "ticks": 45, "bandwidth": 4 },
{ "id": "print-composite", "machine": "optical-printer", "inputs": { "anchor-slab": 4, "color-plate": 2 },
"outputs": { "alpha": 1, "fringe-lace": 4 }, "ticks": 240, "bandwidth": 22 },
{ "id": "print-blue-screen", "machine": "optical-printer", "inputs": { "anchor-slab": 4, "chroma-420": 2 },
"outputs": { "alpha": 2, "fringe-lace": 4, "blue-hole": 2 }, "ticks": 240, "bandwidth": 22 },
{ "id": "key-chroma", "machine": "chroma-keyer", "inputs": { "anchor-slab": 1, "chroma-slurry": 1 },
"outputs": { "alpha": 2, "green-spill": 2 }, "ticks": 45, "bandwidth": 8 },
{ "id": "key-hair", "machine": "chroma-keyer", "inputs": { "ghost-feather": 1, "chroma-slurry": 1 },
"outputs": { "roto-chatter": 2, "green-spill": 1 }, "ticks": 45, "bandwidth": 8 },
{ "id": "key-semitransparent", "machine": "chroma-keyer", "inputs": { "alpha": 1, "chroma-slurry": 1 },
"outputs": { "mixed-pixel": 2, "green-spill": 1 }, "ticks": 45, "bandwidth": 8 },
{ "id": "roto-cutout", "machine": "roto-bench", "inputs": { "anchor-slab": 1 },
"outputs": { "alpha": 1, "roto-chatter": 1 }, "ticks": 720, "bandwidth": 2 },
{ "id": "roto-mixed", "machine": "roto-bench", "inputs": { "mixed-pixel": 4 },
"outputs": { "alpha": 1, "roto-chatter": 2 }, "ticks": 900, "bandwidth": 2 },
{ "id": "neural-key", "machine": "neural-keyer", "inputs": { "anchor-slab": 1 },
"outputs": { "alpha": 4, "ghost-feather": 1 }, "ticks": 30, "bandwidth": 20 },
{ "id": "neural-unmix", "machine": "neural-keyer", "inputs": { "mixed-pixel": 2 },
"outputs": { "chroma-slurry": 1, "luma-bars": 1 }, "ticks": 30, "bandwidth": 20 },
{ "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 },

View File

@ -1,47 +1,43 @@
[
{ "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 },
{ "id": "broadcast-phosphor-seams", "era": "broadcast", "cost": { "spatial-pack": 5 },
"unlocks": ["extract-field-splinters"] },
{ "id": "broadcast-field-loom", "era": "broadcast", "cost": { "analog-pack": 20 },
{ "id": "broadcast-field-loom", "era": "broadcast", "cost": { "spatial-pack": 15 },
"unlocks": ["field-loom", "weave-fields", "loom-vhold-roll"] },
{ "id": "broadcast-asic-cooling", "era": "broadcast", "cost": { "analog-pack": 30, "spatial-pack": 10 },
{ "id": "broadcast-asic-cooling", "era": "broadcast", "cost": { "spatial-pack": 20, "analog-pack": 15 },
"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 },
"unlocks": ["subsampler", "subsample-422"] },
{ "id": "disc-macroblock-quarrying", "era": "disc", "cost": { "analog-pack": 15 },
"unlocks": ["quantize-crime"] },
{ "id": "disc-ringing-skim", "era": "disc", "cost": { "analog-pack": 10, "spatial-pack": 15 },
"unlocks": ["skim-ringing"] },
{ "id": "disc-subsampling-420", "era": "disc", "cost": { "analog-pack": 20, "spatial-pack": 15 },
"unlocks": ["subsample-420", "press-anchor-slab"] },
{ "id": "disc-mosh-reactor", "era": "disc", "cost": { "analog-pack": 20, "spatial-pack": 10 },
"unlocks": ["mosh-reactor", "mosh"] },
{ "id": "reel-silver-seams", "era": "reel", "cost": { "spatial-pack": 8 },
"unlocks": ["extract-silver-frames"] },
{ "id": "reel-telecine", "era": "reel", "cost": { "analog-pack": 25 },
"unlocks": ["telecine-puller", "mill-judder"] },
{ "id": "reel-matte-bath", "era": "reel", "cost": { "analog-pack": 20, "spatial-pack": 10 },
"unlocks": ["matte-bath", "bathe-hard-matte", "bathe-feather"] },
{ "id": "reel-optical-printer", "era": "reel", "cost": { "analog-pack": 40, "spatial-pack": 30 },
"unlocks": ["optical-printer", "print-composite", "print-blue-screen"] },
{ "id": "disc-roto-bench", "era": "disc", "cost": { "spatial-pack": 10 },
"unlocks": ["roto-bench", "roto-cutout", "roto-mixed"] },
{ "id": "disc-gamut-paints", "era": "disc", "cost": { "spatial-pack": 20 },
"unlocks": ["mix-rec709", "mix-dci-p3"] },
{ "id": "disc-gop-cadence-ibbp", "era": "disc", "cost": { "spatial-pack": 25, "temporal-pack": 10 },
{ "id": "disc-gop-cadence-ibbp", "era": "disc", "cost": { "spatial-pack": 25, "analog-pack": 10 },
"unlocks": ["cast-janus-wafer", "assemble-gop-ibbp"] },
{ "id": "disc-i-only-cadence", "era": "disc", "cost": { "spatial-pack": 30, "temporal-pack": 20 },
{ "id": "disc-i-only-cadence", "era": "disc", "cost": { "spatial-pack": 30, "analog-pack": 20 },
"unlocks": ["assemble-gop-i-only"] },
{ "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 },
{ "id": "disc-demosaic-vng4", "era": "disc", "cost": { "spatial-pack": 30 },
"unlocks": ["demosaic-vng4", "pan-speckle"] },
{ "id": "stream-bloom-loops", "era": "stream", "cost": { "spatial-pack": 20, "temporal-pack": 30 },
{ "id": "stream-bloom-loops", "era": "stream", "cost": { "spatial-pack": 20, "analog-pack": 20 },
"unlocks": ["bloom-duplicator", "tap-bloom", "bloom-grade-1", "bloom-grade-2"] },
{ "id": "stream-runaway-bloom", "era": "stream", "cost": { "temporal-pack": 45, "container-pack": 10 },
"unlocks": ["bloom-grade-3", "bloom-grade-4", "bloom-grade-5"] },
{ "id": "stream-container-archaeology", "era": "stream", "cost": { "spatial-pack": 25, "temporal-pack": 15 },
"unlocks": ["demux-container", "spool-offset-ribbon", "cut-sync-key", "bottle-container-pack"] },
{ "id": "stream-software-decoding", "era": "stream", "cost": { "analog-pack": 20, "spatial-pack": 25 },
"unlocks": ["software-decoder"] },
{ "id": "stream-optical-flow", "era": "stream", "cost": { "temporal-pack": 25 },
"unlocks": ["mv-extractor", "extract-motion"] },
{ "id": "stream-chroma-keyer", "era": "stream", "cost": { "spatial-pack": 25, "temporal-pack": 20 },
"unlocks": ["chroma-keyer", "key-chroma", "key-hair", "key-semitransparent"] },
{ "id": "stream-container-archaeology", "era": "stream", "cost": { "spatial-pack": 25, "temporal-pack": 15 },
"unlocks": ["demux-container", "spool-offset-ribbon", "cut-sync-key", "bottle-container-pack"] },
{ "id": "stream-runaway-bloom", "era": "stream", "cost": { "temporal-pack": 45, "container-pack": 10 },
"unlocks": ["bloom-grade-3", "bloom-grade-4", "bloom-grade-5"] },
{ "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 },
@ -51,6 +47,9 @@
"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-neural-keyer", "era": "fortress",
"cost": { "container-pack": 50, "analog-pack": 50, "spatial-pack": 50, "temporal-pack": 50 },
"unlocks": ["neural-keyer", "neural-key", "neural-unmix"] },
{ "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

@ -22,10 +22,15 @@ const tech = techJson as unknown as TechDef[];
const commissions = commissionsJson as unknown as CommissionDef[];
const KEBAB = /^[a-z0-9]+(-[a-z0-9]+)*$/;
const CODEX_REF = /^[1-9]:[a-z0-9]+(-[a-z0-9]+)*$/;
// Multi-digit section numbers: the codex grew a §10 (THE COMPOSITE SEAM) and the old
// single-digit form silently rejected every reference into it.
const CODEX_REF = /^[1-9][0-9]*:[a-z0-9]+(-[a-z0-9]+)*$/;
const HEX = /^#[0-9a-f]{6}$/;
const KINDS: MachineKind[] = ['extractor', 'crafter', 'belt', 'splitter', 'power', 'buffer', 'shipper'];
const KINDS: MachineKind[] = [
'extractor', 'crafter', 'belt', 'splitter', 'power', 'buffer', 'shipper',
'lab', // v4
];
const ERAS: TechDef['era'][] = ['reel', 'broadcast', 'disc', 'stream', 'fortress'];
const itemIds = new Set(items.map((i) => i.id));
@ -153,6 +158,37 @@ describe('machines', () => {
expect(bad).toEqual([]);
});
// v4: the emissive accent falls back to a colour derived from the first output item.
// A machine with no recipes has no first output, so the fallback has nothing to
// derive from — those must carry an explicit accent or they render unlit.
it('every machine with no output to derive from carries an explicit accent', () => {
const bad = machines
.filter((m) => m.recipes.length === 0 && !m.accent)
.map((m) => `${m.id} (${m.kind}) has no recipes and no accent — nothing to derive an accent from`);
expect(bad).toEqual([]);
});
it('every accent is a hex colour, and differs from the chassis it sits on', () => {
const bad = machines
.filter((m) => m.accent !== undefined)
.filter((m) => !HEX.test(m.accent!) || m.accent === m.color)
.map((m) => `${m.id} -> accent ${m.accent} on chassis ${m.color}`);
expect(bad).toEqual([]);
});
it('a cooling aura belongs only to a machine that actually cools', () => {
const bad = machines
.filter((m) => m.coolRadius !== undefined)
.filter((m) => !(m.coolRadius! >= 1) || !Number.isInteger(m.coolRadius) || !(m.coolPerTick! > 0))
.map((m) => `${m.id} -> coolRadius ${m.coolRadius}, coolPerTick ${m.coolPerTick}`);
expect(bad).toEqual([]);
});
it('exactly one lab exists, and research is impossible without it', () => {
const labs = machines.filter((m) => m.kind === 'lab');
expect(labs.map((m) => m.id)).toEqual(['archaeology-lab']);
});
it('only buffers declare a capacity, and they all do', () => {
const bad = [
...machines.filter((m) => m.kind === 'buffer' && !(m.bufferCap! > 0))
@ -400,6 +436,77 @@ describe('commissions', () => {
});
});
// The v4 gating rule ("a machine/recipe id in ANY TechDef.unlocks is locked until that
// tech completes") turned the tech tree from decoration into a lock on the whole game.
// It also turned three rounds of tech authoring into a bootstrap deadlock: every pack
// chain was gated behind packs you could not yet make, so 0 of 24 nodes were reachable
// and first-taste was impossible. These tests are why that can't come back.
describe('the bootstrap: what the player can do before researching anything', () => {
const gated = new Set(tech.flatMap((t) => t.unlocks));
/** Under the v4 rule a recipe is runnable only if neither it nor its machine is gated. */
const ungated = recipes.filter((r) => !gated.has(r.id) && !gated.has(r.machine));
/** Items reachable from a given set of runnable recipes. */
function reachableFrom(rs: RecipeDef[]): Set<string> {
const have = new Set<string>();
for (;;) {
const before = have.size;
for (const r of rs) {
if (Object.keys(r.inputs).every((i) => have.has(i))) {
for (const o of Object.keys(r.outputs)) have.add(o);
}
}
if (have.size === before) return have;
}
}
it('MELT is craftable at tick 0 — the opening commission must be possible', () => {
// first-taste wants 1 melt and is the head of the queue. If the melt chain is gated,
// the game opens on a commission the player cannot fill and research they cannot start.
expect([...reachableFrom(ungated)]).toContain('melt');
});
it('at least one science pack is craftable at tick 0, or research can never start', () => {
const day0 = reachableFrom(ungated);
const packs = items.filter((i) => i.id.endsWith('-pack')).map((i) => i.id);
const bootstrap = packs.filter((p) => day0.has(p));
expect(bootstrap.length).toBeGreaterThan(0);
});
it('every tech is eventually affordable — no node is stranded behind its own output', () => {
// Walk the real loop: research what you can afford, which unlocks recipes, which makes
// more packs, which affords more research. Anything left over can never be bought.
const open = new Set(ungated.map((r) => r.id));
let have = reachableFrom(recipes.filter((r) => open.has(r.id)));
const unlocked = new Set<string>();
for (;;) {
let moved = false;
for (const t of tech) {
if (unlocked.has(t.id)) continue;
if (Object.keys(t.cost).every((c) => have.has(c))) {
unlocked.add(t.id);
for (const u of t.unlocks) {
for (const r of recipes) if (r.id === u || r.machine === u) open.add(r.id);
}
moved = true;
}
}
if (!moved) break;
have = reachableFrom(recipes.filter((r) => open.has(r.id)));
}
const stranded = tech
.filter((t) => !unlocked.has(t.id))
.map((t) => `${t.id} costs ${JSON.stringify(t.cost)} — unaffordable no matter what you build`);
expect(stranded).toEqual([]);
});
it('the lab is starting equipment', () => {
const labs = machines.filter((m) => m.kind === 'lab').map((m) => m.id);
const lockedLabs = labs.filter((id) => gated.has(id));
expect(lockedLabs).toEqual([]); // a lab behind research is a lab you can never build
});
});
describe('economy reachability', () => {
it('the M1 flow reaches MELT from raw ore', () => {
expect([...producible()]).toContain('melt');
@ -412,6 +519,8 @@ describe('economy reachability', () => {
// the Fortress.
const FORWARD_CANON = new Set([
'mosquito-swarm', 'gibbs-wraith', 'cinema-pack',
// §10: The Correction hoards the prisms. Nothing in the factory makes 589nm.
'sodium-light',
]);
const have = producible();
const orphans = items

View File

@ -314,3 +314,100 @@ POST-PUSH ADDENDUM (same round, after LANE-UI landed `src/ui/live.test.ts`):
**Five sessions share one index: `git add <paths>` does not isolate a lane.** Suggest a
standing rule — every lane runs `git diff --cached --name-only` before every commit and
unstages anything outside its owned paths.
### Round 4 — 2026-07-17 — Opus 4.8
SHIPPED: all 5 orders. §10 transcribed (9 items, 5 machines, 11 recipes, 5 era-gated tech
nodes, 2 commissions). Research economy: the ARCHAEOLOGY LAB + a full tech-cost audit.
ASIC cooler activated. Accents filled. items 51 → 60, machines 23 → 29, recipes 51 → 62,
commissions 14 → 16, tech 24 → 23 (six deleted, five added). Validator 41 → 49 tests.
- Verified: tsc clean (and `data/` is now genuinely typechecked — thank you), my 49 green,
live build boots with no console errors, all 29 machines art-directed with zero gaps,
UI grew a RESEARCH page and a third REFINE page off my data with no code from them.
- Fault-injected every new check, including re-gating the mosh reactor: fails instantly on
`MELT is craftable at tick 0 — the opening commission must be possible`.
**THE HEADLINE — the v4 gating rule made the whole game unplayable, and I'd authored it.**
The rule ("any id in a TechDef.unlocks is locked until that tech completes") turned three
rounds of my tech authoring from decoration into a lock on everything. Measured:
```
TICK 0: melt producible? FALSE <- first-taste, the opening commission, impossible
every science pack? FALSE
RESEARCH CLOSURE: 0 of 24 techs reachable
```
`disc-artifact-bottling` cost 10 analog packs and unlocked the only machine that can bottle
an analog pack. The M1 melt chain itself was gated. The game opened on an unbuildable
factory, an unfillable commission, and no way to research out of it. **This is the same
class of bug as round 2's decorative heat: a number/structure that was fine until another
lane made it load-bearing.** Fixed to a shape, not a patch — two rules, both now tests:
1. the M1 melt chain is starting equipment; 2. one pack (SPATIAL) is craftable at tick 0.
Result: **23/23 reachable**, order tells a story, only the four forward-canon items remain
unreachable. Arithmetic + the worked cost path in CANON-DELTA.
DECISIONS:
- **SPATIAL PACK is the bootstrap science** — bricks + halos + slurry all fall out of the
quantizer the player already owns for the melt chain. Your first research is a study of
your own damage, which is the most in-voice thing in the tree, and it needs no machine
the M1 chain didn't already require (bar the bottler). 10 packs/min on one quantizer;
the first four nodes cost 0.51.5 min each. Affordable off a first-taste-era factory, as
ordered, with the working shown.
- **Six disc nodes deleted, not repurposed** (`disc-artifact-bottling`,
`-chroma-subsampling`, `-macroblock-quarrying`, `-ringing-skim`, `-subsampling-420`,
`-mosh-reactor`) — their content became starting equipment, so a repurposed node would
have been a fake. Grepped first: no lane references specific tech ids. Per the
deprecation rule, logged here and in CANON-DELTA.
- **The cooler's spec'd 0.02 was catastrophic once real** — the decoder banks 0.0033, so a
0.02 aura is net 0.0167: it never heats, and a 2×2 cooler makes a 2×2 decoder a
permanent free 100/s. Retuned to 0.0018, which is sane under **both** readings of the
contract's ambiguous "this machine's coolPerTick *also* applies" (additive → 85% duty,
72/s avg; replacing → 55%, 46/s). I deliberately picked a number that survives either,
because SIM hasn't built it yet and I won't rest the design on my guess of a sentence.
Pairing is 9.64/s per tile vs 5.00 for ASICs — the best power in the game, paid for with
a tech, two machines and the floor space to keep them adjacent.
- §10 judgement calls (footage = anchor slab; the matte bath's chemical = static canister,
which §3 already calls "brine for preserving other artifacts"; ghost feather made only by
the neural keyer so the earlier machines exist to ruin it) — all argued in CANON-DELTA
the way you asked, from what the thing already is rather than convenience.
- NEURAL KEYER `powerDraw: 240` — §10's "24 VRAM ... dwarfs everything else"; that's 12
ASICs, against an entire M1 chain that draws 96. "You don't unlock the neural keyer; you
unlock the power plant it demands" is now literally true.
BLOCKED/BROKEN — **I broke two of SIM's tests, deliberately, and their guard caught it:**
- `research.test.ts:247` does `expect(gated.has('mosh-reactor')).toBe(true) // guards the
premise below`. I ungated the mosh reactor, because gating it makes first-taste
impossible. **Their premise guard did exactly its job** — it failed loudly instead of
silently testing nothing. The fix is one word in their file (not mine to touch): swap
`mosh-reactor` for any still-gated machine. Drop-in list: mv-extractor,
bloom-duplicator, field-loom, telecine-puller, hex-splicer, software-decoder,
asic-cooler, matte-bath, optical-printer, chroma-keyer, roto-bench, neural-keyer, muxer.
`demuxer` remains a valid "free" example.
- The other 8 failures in `research.test.ts` / `live.test.ts` are **not mine** — they fail
identically against HEAD's round-3 data (measured by restoring the old JSON and
re-running; failure sets diffed).
CROSS-LANE — SIM, the reference factory now can't power itself:
- It places **4 software decoders**, which my tree gates behind `stream-software-decoding`
(rounds 3 and 4 both). With your gating live, they can't be placed at tick 0, so the M1
demo has no power and the HUD's heat tests have no decoder to watch. That's the cause of
several of those 8.
- **The starter plant is ASICs**: net demand 78/s ÷ 20 = **4 decode-asics minimum** (5 for
headroom). Ungated, boring, correct — the software decoder is a mid-game *upgrade*, which
is the progression the tech tree is for. My round-3 suggestion (3 ASIC + 1 decoder + tank)
is still the right *post-research* demo, and the decoder+cooler pairing is the M2 showpiece.
- Tell me if you'd rather software-decoder be starting equipment and I'll ungate it — but
at 25.8/s average vs the ASIC's 20 it would make the ASIC pointless from tick 0.
CONTRACT REQUEST: none. (Still not requesting `kind: 'cooler'``kind: 'power'` +
`coolRadius` covers it, and the aura isn't built yet.)
STILL OPEN: **SIM hasn't exported the tuning constants yet** (I checked — `grep 'export
const' src/sim/*.ts` finds only `geom.ts`). My heat tests still hand-mirror 0.7 / 0.5 /
0.004 / 30tps and still go stale silently if you retune. I'll retire the copies the round
the export lands.
NEXT (candidates): SPILL FOG + EDGE BOIL (§10 hazards — need a mechanic, so data waits on
SIM); mosquito traps; a seam/ore map (extractors still mine anywhere — the content gap I
own); `cinema-pack` + the Fortress; `corridorkey-mrp-mlx` is a real MODELBEAST artifact
factory for the whole §10 asset set (§10's production note) — worth a work order.