From 3e704824477fe7531dd103b176414332d824b330 Mon Sep 17 00:00:00 2001 From: type-two Date: Fri, 17 Jul 2026 17:20:36 +1000 Subject: [PATCH] [data] art direction, heat economy, container + bloom lines MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Round 2 orders, all seven. - data/CANON-DELTA.md: ledger of every invention beyond the codex, with the rationale and the arithmetic behind the numbers. - colour + flavor on all 21 machines (kills UI's hardcoded lookup and RENDER's black-on-black); desaturated industrial per style guide §8. - heat: software decoder 45->55 gen at 0.0025/tick (throttles 9.3s, scrams 13.3s) so it's a burst generator, not a better ASIC; a pinch on the splicer. - bufferCap 240 = 10s of the reference chain's one-decoder-down deficit. - commission queue reordered into a ramp + 3 standing orders, incl. THE CORRECTION BUYBACK (the sellout loop: anchors clean THE SCREEN). - container line (moov-shard/offset-ribbon/sync-key) unblocks the hex splicer; bloom grades 1-5. bloom-concentrate kept — LANE-SCREEN hardcodes it. - validator 28 -> 36 tests, covering every v2 field; FORWARD_CANON pruned. Co-Authored-By: Claude Opus 4.8 --- fktry/data/CANON-DELTA.md | 154 ++++++++++++++++++++++++++++ fktry/data/commissions.json | 26 ++++- fktry/data/items.json | 11 +- fktry/data/machines.json | 80 +++++++++++---- fktry/data/recipes.json | 23 ++++- fktry/data/tech.json | 6 +- fktry/lanes/LANE-DATA.md | 67 ++++++++++++ fktry/src/sim/data-validate.test.ts | 105 ++++++++++++++++--- 8 files changed, 425 insertions(+), 47 deletions(-) create mode 100644 fktry/data/CANON-DELTA.md diff --git a/fktry/data/CANON-DELTA.md b/fktry/data/CANON-DELTA.md new file mode 100644 index 0000000..9fe8543 --- /dev/null +++ b/fktry/data/CANON-DELTA.md @@ -0,0 +1,154 @@ +# CANON-DELTA — everything in `data/` that isn't in the codex yet + +LANE-DATA owns this file. `../../docs/FKTRY_LORE.md` is canon; this is the ledger of +where the data has run ahead of it. The orchestrator absorbs these into the lore doc in +one pass later. Until then, **this is the single place canon-drift is allowed to live**. + +Rule for adding an entry: the codex was silent, I invented in its voice, and I wrote +down *why it is consistent with the codex* — not just what I did. If a future lore pass +disagrees, the codex wins and the data changes. + +--- + +## Canonized in round 1 (orchestrator: "CANONIZED", round 2 review) + +| Thing | Kind | Why it fits | +|---|---|---| +| `press-anchor-slab` | recipe (dct-press) | The press run backwards is an IDCT, and an IDCT *is* a complete picture. §4 already runs the field loom and the telecine puller in reverse, so reversibility is established machine behaviour. This is where I-frames come from. | +| `color-plate` | item | §4 demosaicer literally outputs "a smooth colour sheet"; it needed an id. | +| `v-hold-roll` | item | §1 Broadcast Shelf has "v-hold faults that make whole regions periodically roll upward". Bottling one gives the ANALOG PACK's "combing + v-hold roll + static" triptych three real ingredients. | +| `artifact-bottler` | machine | §2 calls the science packs "a bottled artifact triptych" but names no bottling plant. Something has to hold the bottle. | +| `lane-splitter` | machine | The contract has `kind: 'splitter'` and belts need routing, but §4 only ever calls the DEMUXER "the first splitter" — which is a refinery, not a junction. | + +## Added in round 2 + +### The container line (unblocks `stream-hex-splicing`) + +§2 names the CONTAINER PACK's contents ("moov shard + offset ribbon + sync key") and §7 +names the moov brain's parts, but nothing said where they come from. The chain now +mirrors what the parts actually *are* in a real container: + +| Thing | Kind | Why it fits | +|---|---|---| +| `moov-shard` | item | The index, not the payload. | +| `offset-ribbon` | item | The stco map — a ribbon of byte offsets. | +| `sync-key` | item | The stss table lists which frames are keyframes, i.e. I-frames. | +| `demux-container` | recipe (demuxer) | The demuxer is the machine that separates index from payload. 8 ore → 2 shards: the index is a rounding error next to the mdat, which is the joke and also the truth. | +| `spool-offset-ribbon` | recipe (gop-assembler) | The assembler is the only machine that knows where each frame starts, because it put them there. | +| `cut-sync-key` | recipe (artifact-bottler) | Cut from an anchor slab, because stss is a list of anchors. | +| `bottle-container-pack` | recipe | Assembles the §2 triptych. | + +Chicken-and-egg check: §4 says the hex splicer "Requires CONTAINER PACK research", so the +container line deliberately does **not** route through the splicer. It is demuxer → +assembler → bottler, all machines you already own. + +### The bloom line + +§3 sells bloom "by intensity grade; grade-5 bloom visibly tries to escape its canister in +one direction" — five grades, one item each, `bloom-grade-1..5`. + +- `bloom-concentrate` is **kept** and redefined as the raw, ungraded draw-off from a loop + (`tap-bloom`). It was not deleted because LANE-SCREEN hardcodes it in + `src/screen/corruptionMap.ts:54` and its test — deleting it would have broken another + lane's suite mid-round. Grading now consumes it. +- The recipe id `bloom-grade-3` survives from round 1 but now produces the *item* + `bloom-grade-3`, so the id finally means what it says. `tap-bloom` is the renamed + round-1 `bloom-grade-3` recipe. **Deprecation notice:** no recipe id was retired, but + `bloom-grade-3` changed its inputs/outputs entirely. Nothing outside `data/` referenced + it (checked: only my own `tech.json`). +- 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. + +--- + +## The numbers, and the arithmetic behind them + +### The reference M1 chain (measured, not guessed) + +Computed from `data/*.json` against the machine list in SIM's `meltChain()` +(`src/sim/sim.test.ts`), with the two recipes that test hand-dials applied: + +``` +idle draw (every machine's powerDraw) 57 /s ++ active recipe draw at full tilt 39 /s += peak draw 96 /s +- compression paid back (quantize -6, 4:2:2 -4, 4:2:0 -8) 18 /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. + +### `bufferCap: 240` on the buffer tank + +Order was "roughly 10 seconds of typical M1-chain deficit". Typical deficit ≠ total +blackout — it's the chain running with one generator down: + +``` +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) +``` + +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. + +### `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: + +``` +throttle (0.7) at 280 ticks = 9.3 s +scram (1.0) at 400 ticks = 13.3 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 | +|---|---|---|---|---| +| DECODE ASIC | 20 /s | 100% | **20 /s** | none, forever | +| SOFTWARE DECODER | 55 /s | ~50% | **~27 /s** | brownout, on its own schedule | + +~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. + +**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. + +### `heat: 0.05` on `splice-offsets` + +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. + +--- + +## 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. +- `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. +- 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. diff --git a/fktry/data/commissions.json b/fktry/data/commissions.json index 9e35662..6c01e45 100644 --- a/fktry/data/commissions.json +++ b/fktry/data/commissions.json @@ -4,33 +4,49 @@ "wants": { "melt": 1 }, "rewardBandwidth": 50 }, { "id": "dust-allowance", - "flavor": "12 STATIC CANISTERS. Brine grade, not display grade. Our previous supplier shipped display grade and we no longer speak of them.", - "wants": { "static-canister": 12 }, "rewardBandwidth": 120 }, + "flavor": "12 STATIC CANISTERS, brine grade, not display grade. Our previous supplier shipped display grade and we no longer speak of them. Standing order; the jars come back empty and we do not explain why.", + "wants": { "static-canister": 12 }, "rewardBandwidth": 120, "repeat": true }, { "id": "patio-resurfacing", "flavor": "48 MACROBLOCK BRICKS for a patio. Client has been advised the patio will not be flat. Client insists the patio has never been flat. Proceed.", "wants": { "macroblock-bricks": 48 }, "rewardBandwidth": 220 }, + { "id": "house-gray-audit", + "flavor": "30 LUMA BARS and 4 GAMUT PAINT: REC709. Deliver to Compliance. This is a legitimate order from a legitimate buyer and we are legally required to state that we are not The Correction.", + "wants": { "luma-bars": 30, "gamut-paint-rec709": 4 }, "rewardBandwidth": 260 }, + { "id": "haunted-doorway", "flavor": "9 RINGING HALOS, hung not stacked. They are for a doorway. The doorway is for nothing. Do not ask what comes through it; it is already through.", "wants": { "ringing-halos": 9, "gamut-paint-rec709": 2 }, "rewardBandwidth": 300 }, - { "id": "house-gray-audit", - "flavor": "30 LUMA BARS and 4 GAMUT PAINT: REC709. Deliver to Compliance. This is a legitimate order from a legitimate buyer and we are legally required to state that we are not The Correction.", - "wants": { "luma-bars": 30, "gamut-paint-rec709": 4 }, "rewardBandwidth": 260 }, + { "id": "correction-buyback", + "flavor": "Good morning. Our records show 5 unreferenced ANCHOR SLABS in your sector. We would like to purchase them. We pay well, we pay immediately, and we pay every time. This is not an audit. You are not in trouble. There is no trouble. Your file looks so much better already. We will be here.", + "wants": { "anchor-slab": 5 }, "rewardBandwidth": 450, "repeat": true }, { "id": "wrong-on-purpose", "flavor": "16 JUDDER CAMS installed in 4 BLOOM CONCENTRATE canisters. We want the movement to arrive slightly after the thing that is moving. Yes. That. Exactly that.", "wants": { "judder-cams": 16, "bloom-concentrate": 4 }, "rewardBandwidth": 480 }, + { "id": "melt-retainer", + "flavor": "STANDING ORDER: 25 MELT per collection, indefinitely. Our client is redecorating. Our client has not specified an end date. We have stopped asking our client things.", + "wants": { "melt": 25 }, "rewardBandwidth": 560, "repeat": true }, + { "id": "vaporwave-sunset", "flavor": "1 MELTED ANCHOR SLAB + 3 MOSQUITO SWARMS (live) + 40 bbl CHROMA SLURRY (4:2:0) = 1 VAPORWAVE SUNSET. Deliver before the beat drops.", "wants": { "melt": 1, "anchor-slab": 1, "mosquito-swarm": 3, "chroma-420": 40 }, "rewardBandwidth": 700 }, + { "id": "escape-attempt", + "flavor": "One (1) BLOOM, GRADE 5. Do not seal the canister on the side it is pulling toward — we have learned this. Deliver upright, deliver quickly, and do not deliver it to us in person.", + "wants": { "bloom-grade-5": 1 }, "rewardBandwidth": 850 }, + { "id": "gibbs-wraith-willingly", "flavor": "One (1) GIBBS WRAITH, willingly.", "wants": { "gibbs-wraith": 1 }, "rewardBandwidth": 900 }, + { "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": "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 }, diff --git a/fktry/data/items.json b/fktry/data/items.json index 971e7f5..c37d7a7 100644 --- a/fktry/data/items.json +++ b/fktry/data/items.json @@ -14,13 +14,22 @@ { "id": "mv-flux", "name": "MOTION VECTOR FLUID", "codex": "2:motion-vector-fluid", "tier": 1, "color": "#3fd8ff" }, { "id": "v-hold-roll", "name": "V-HOLD ROLL", "codex": "1:the-broadcast-shelf", "tier": 1, "color": "#8fdc7a" }, + { "id": "moov-shard", "name": "MOOV SHARD", "codex": "2:science-packs", "tier": 1, "color": "#9a7fd0" }, + { "id": "offset-ribbon", "name": "OFFSET RIBBON", "codex": "2:science-packs", "tier": 1, "color": "#7fa8ff" }, + { "id": "sync-key", "name": "SYNC KEY", "codex": "2:science-packs", "tier": 1, "color": "#ffd24f" }, + { "id": "anchor-slab", "name": "I-FRAME (ANCHOR SLAB)", "codex": "2:i-frame", "tier": 1, "color": "#fff2a0" }, { "id": "delta-wafer", "name": "P-FRAME (DELTA WAFER)", "codex": "2:p-frame", "tier": 1, "color": "#7fb0ff" }, { "id": "janus-wafer", "name": "B-FRAME (JANUS WAFER)", "codex": "2:b-frame", "tier": 1, "color": "#b08fff" }, { "id": "gop-crate", "name": "GOP CRATE", "codex": "2:gop-crate", "tier": 1, "color": "#a07840" }, { "id": "melt", "name": "MELT", "codex": "3:melt", "tier": 2, "color": "#ff7a3f" }, - { "id": "bloom-concentrate", "name": "BLOOM CONCENTRATE", "codex": "3:bloom-concentrate", "tier": 2, "color": "#ffb0e0" }, + { "id": "bloom-concentrate", "name": "BLOOM CONCENTRATE (RAW)", "codex": "3:bloom-concentrate", "tier": 2, "color": "#ffb0e0" }, + { "id": "bloom-grade-1", "name": "BLOOM, GRADE 1", "codex": "3:bloom-concentrate", "tier": 2, "color": "#ffc4e8" }, + { "id": "bloom-grade-2", "name": "BLOOM, GRADE 2", "codex": "3:bloom-concentrate", "tier": 2, "color": "#ff9ed8" }, + { "id": "bloom-grade-3", "name": "BLOOM, GRADE 3", "codex": "3:bloom-concentrate", "tier": 2, "color": "#ff77c8" }, + { "id": "bloom-grade-4", "name": "BLOOM, GRADE 4", "codex": "3:bloom-concentrate", "tier": 2, "color": "#ff4fb8" }, + { "id": "bloom-grade-5", "name": "BLOOM, GRADE 5", "codex": "3:bloom-concentrate", "tier": 2, "color": "#ff1fa8" }, { "id": "macroblock-bricks", "name": "MACROBLOCK BRICKS", "codex": "3:macroblock-bricks", "tier": 2, "color": "#6f8fa0" }, { "id": "ringing-halos", "name": "RINGING HALOS", "codex": "3:ringing-halos", "tier": 2, "color": "#cfe8ff" }, { "id": "static-canister", "name": "STATIC CANISTER", "codex": "3:static-canisters", "tier": 2, "color": "#d8d8d8" }, diff --git a/fktry/data/machines.json b/fktry/data/machines.json index 7bdd8d4..ffdfb91 100644 --- a/fktry/data/machines.json +++ b/fktry/data/machines.json @@ -2,59 +2,95 @@ { "id": "seam-extractor", "name": "SEAM EXTRACTOR", "codex": "4:seam-extractor", "kind": "extractor", "footprint": { "x": 2, "y": 2 }, "recipes": ["extract-mdat", "extract-bayer-grit", "extract-field-splinters", "extract-silver-frames"], - "powerDraw": 2, "asset": "seam-extractor" }, + "powerDraw": 2, "asset": "seam-extractor", "color": "#8a7a32", + "flavor": "The miner. Chews mdat seams and spits raw payload. The drill head is CRT-certified. Operators are not." }, { "id": "belt", "name": "STREAM BELT", "codex": "4:belts", "kind": "belt", - "footprint": { "x": 1, "y": 1 }, "recipes": [], "powerDraw": 0, "beltSpeed": 2, "asset": "belt" }, + "footprint": { "x": 1, "y": 1 }, "recipes": [], "powerDraw": 0, "beltSpeed": 2, + "asset": "belt", "color": "#4a4e52", + "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" }, + "footprint": { "x": 1, "y": 1 }, "recipes": [], "powerDraw": 1, + "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, "asset": "buffer-tank" }, + "footprint": { "x": 2, "y": 2 }, "recipes": [], "powerDraw": 0, "bufferCap": 240, + "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." }, { "id": "demuxer", "name": "DEMUXER", "codex": "4:demuxer", "kind": "crafter", - "footprint": { "x": 2, "y": 2 }, "recipes": ["demux-ore"], "powerDraw": 3, "asset": "demuxer" }, + "footprint": { "x": 2, "y": 2 }, "recipes": ["demux-ore", "demux-container"], "powerDraw": 3, + "asset": "demuxer", "color": "#4e5a62", + "flavor": "The first splitter. Cleaves raw ore into brightness, colour, and pure directional intent." }, { "id": "demosaicer", "name": "DEMOSAICER", "codex": "4:demosaicer", "kind": "crafter", "footprint": { "x": 3, "y": 2 }, "recipes": ["demosaic-rggb", "demosaic-vng4", "demosaic-stripes"], - "powerDraw": 5, "asset": "demosaicer" }, + "powerDraw": 5, "asset": "demosaicer", "color": "#6a5f52", + "flavor": "Weaves checkerboard grit into full colour. Feed it stripes and it will jam and extrude MOIRÉ — as designed, as warned, as invoiced." }, { "id": "dct-press", "name": "DCT PRESS", "codex": "4:dct-press", "kind": "crafter", "footprint": { "x": 3, "y": 3 }, "recipes": ["press-color-plate", "press-anchor-slab"], - "powerDraw": 8, "asset": "dct-press" }, + "powerDraw": 8, "asset": "dct-press", "color": "#3f4348", + "flavor": "Spatial into frequency. Slams pixel blocks into coefficient decks; run it backwards and an entire picture falls out of the slot." }, { "id": "quantizer", "name": "QUANTIZER", "codex": "4:quantizer", "kind": "crafter", "footprint": { "x": 2, "y": 2 }, "recipes": ["quantize", "quantize-crime", "skim-ringing"], - "powerDraw": 4, "asset": "quantizer" }, + "powerDraw": 4, "asset": "quantizer", "color": "#5e5148", + "flavor": "The crusher, the moneymaker, the sin. Every bit it destroys becomes room to move. The dial goes to CRIME." }, { "id": "subsampler", "name": "SUBSAMPLER", "codex": "4:subsampler", "kind": "crafter", "footprint": { "x": 2, "y": 3 }, "recipes": ["subsample-422", "subsample-420", "pan-speckle"], - "powerDraw": 4, "asset": "subsampler" }, + "powerDraw": 4, "asset": "subsampler", "color": "#4a5560", + "flavor": "Spins colour down to grade and vents the difference as bandwidth. Leaks by design. Place downwind of anything you are fond of." }, { "id": "mv-extractor", "name": "MV EXTRACTOR", "codex": "4:mv-extractor-and-p-caster", "kind": "crafter", - "footprint": { "x": 2, "y": 2 }, "recipes": ["extract-motion"], "powerDraw": 5, "asset": "mv-extractor" }, + "footprint": { "x": 2, "y": 2 }, "recipes": ["extract-motion"], "powerDraw": 5, + "asset": "mv-extractor", "color": "#454b52", + "flavor": "Watches the footage until it knows where the picture wants to go, then pipes the wanting off as fluid." }, { "id": "p-caster", "name": "P-CASTER", "codex": "4:mv-extractor-and-p-caster", "kind": "crafter", "footprint": { "x": 2, "y": 2 }, "recipes": ["cast-delta-wafer", "cast-janus-wafer"], - "powerDraw": 4, "asset": "p-caster" }, + "powerDraw": 4, "asset": "p-caster", "color": "#4d545c", + "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"], - "powerDraw": 6, "asset": "gop-assembler" }, + "footprint": { "x": 3, "y": 2 }, + "recipes": ["assemble-gop", "assemble-gop-ibbp", "assemble-gop-i-only", "spool-offset-ribbon"], + "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", - "footprint": { "x": 3, "y": 3 }, "recipes": ["mosh"], "powerDraw": 8, "asset": "mosh-reactor" }, + "footprint": { "x": 3, "y": 3 }, "recipes": ["mosh"], "powerDraw": 8, + "asset": "mosh-reactor", "color": "#52333e", + "flavor": "A decoder sealed in a vessel and starved of anchors. The I-frame leaves through the side airlock. The hallucination leaves through the front." }, { "id": "bloom-duplicator", "name": "P-FRAME DUPLICATOR", "codex": "4:bloom-loop", "kind": "crafter", - "footprint": { "x": 1, "y": 1 }, "recipes": ["bloom-grade-3"], "powerDraw": 6, "asset": "bloom-duplicator" }, + "footprint": { "x": 1, "y": 1 }, + "recipes": ["tap-bloom", "bloom-grade-1", "bloom-grade-2", "bloom-grade-3", "bloom-grade-4", "bloom-grade-5"], + "powerDraw": 6, "asset": "bloom-duplicator", "color": "#5a5060", + "flavor": "Stamps the same displacement onto the same cargo every lap. Grade 5 is visibly trying to leave its canister in one direction." }, { "id": "field-loom", "name": "FIELD LOOM", "codex": "4:field-loom", "kind": "crafter", "footprint": { "x": 3, "y": 2 }, "recipes": ["weave-fields", "loom-vhold-roll"], - "powerDraw": 5, "asset": "field-loom" }, + "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, "asset": "telecine-puller" }, + "footprint": { "x": 2, "y": 2 }, "recipes": ["mill-judder"], "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, "asset": "hex-splicer" }, + "footprint": { "x": 3, "y": 3 }, "recipes": ["splice-offsets"], "powerDraw": 12, + "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", "bottle-analog-pack", "bottle-spatial-pack", "bottle-temporal-pack"], - "powerDraw": 4, "asset": "artifact-bottler" }, + "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." }, { "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" }, + "footprint": { "x": 2, "y": 2 }, "recipes": [], "powerDraw": 0, "powerGen": 20, + "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": 45, "asset": "software-decoder" }, + "footprint": { "x": 2, "y": 2 }, "recipes": [], "powerDraw": 0, "powerGen": 55, "heatPerTick": 0.0025, + "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." }, { "id": "shipper", "name": "SCREEN UPLINK", "codex": "7:the-muxer", "kind": "shipper", - "footprint": { "x": 2, "y": 2 }, "recipes": [], "powerDraw": 5, "asset": "shipper" } + "footprint": { "x": 2, "y": 2 }, "recipes": [], "powerDraw": 5, + "asset": "shipper", "color": "#46525e", + "flavor": "Composites whatever you hand it onto THE SCREEN. There is no undo. The absence of undo is the product." } ] diff --git a/fktry/data/recipes.json b/fktry/data/recipes.json index dd453d7..b2fb2d8 100644 --- a/fktry/data/recipes.json +++ b/fktry/data/recipes.json @@ -6,6 +6,8 @@ { "id": "demux-ore", "machine": "demuxer", "inputs": { "mdat-ore": 3 }, "outputs": { "luma-bars": 2, "chroma-slurry": 1, "mv-flux": 1 }, "ticks": 45, "bandwidth": 3 }, + { "id": "demux-container", "machine": "demuxer", "inputs": { "mdat-ore": 8 }, + "outputs": { "moov-shard": 2, "hf-dust": 1 }, "ticks": 90, "bandwidth": 5 }, { "id": "demosaic-rggb", "machine": "demosaicer", "inputs": { "bayer-grit": 4 }, "outputs": { "color-plate": 1 }, "ticks": 60, "bandwidth": 4 }, @@ -47,12 +49,24 @@ "outputs": { "gop-crate": 2 }, "ticks": 75, "bandwidth": 8 }, { "id": "assemble-gop-i-only", "machine": "gop-assembler", "inputs": { "anchor-slab": 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": "mosh", "machine": "mosh-reactor", "inputs": { "gop-crate": 1 }, "outputs": { "melt": 3, "anchor-slab": 1 }, "ticks": 90, "bandwidth": 10 }, - { "id": "bloom-grade-3", "machine": "bloom-duplicator", "inputs": { "delta-wafer": 4, "mv-flux": 2 }, + { "id": "tap-bloom", "machine": "bloom-duplicator", "inputs": { "delta-wafer": 4, "mv-flux": 2 }, "outputs": { "bloom-concentrate": 1 }, "ticks": 60, "bandwidth": 8 }, + { "id": "bloom-grade-1", "machine": "bloom-duplicator", "inputs": { "bloom-concentrate": 2 }, + "outputs": { "bloom-grade-1": 1 }, "ticks": 45, "bandwidth": 6 }, + { "id": "bloom-grade-2", "machine": "bloom-duplicator", "inputs": { "bloom-grade-1": 1, "delta-wafer": 2 }, + "outputs": { "bloom-grade-2": 1 }, "ticks": 60, "bandwidth": 8 }, + { "id": "bloom-grade-3", "machine": "bloom-duplicator", "inputs": { "bloom-grade-2": 1, "delta-wafer": 3 }, + "outputs": { "bloom-grade-3": 1 }, "ticks": 75, "bandwidth": 11 }, + { "id": "bloom-grade-4", "machine": "bloom-duplicator", "inputs": { "bloom-grade-3": 1, "delta-wafer": 5 }, + "outputs": { "bloom-grade-4": 1 }, "ticks": 90, "bandwidth": 15 }, + { "id": "bloom-grade-5", "machine": "bloom-duplicator", "inputs": { "bloom-grade-4": 1, "delta-wafer": 8 }, + "outputs": { "bloom-grade-5": 1 }, "ticks": 120, "bandwidth": 22 }, { "id": "weave-fields", "machine": "field-loom", "inputs": { "field-splinters": 2 }, "outputs": { "luma-bars": 1 }, "ticks": 30, "bandwidth": 3 }, @@ -63,10 +77,15 @@ "outputs": { "judder-cams": 1, "field-splinters": 2 }, "ticks": 60, "bandwidth": 5 }, { "id": "splice-offsets", "machine": "hex-splicer", "inputs": { "gop-crate": 1 }, - "outputs": { "explosive-hits": 4, "hf-dust": 2 }, "ticks": 45, "bandwidth": 20 }, + "outputs": { "explosive-hits": 4, "hf-dust": 2 }, "ticks": 45, "bandwidth": 20, "heat": 0.05 }, { "id": "can-static", "machine": "artifact-bottler", "inputs": { "hf-dust": 4 }, "outputs": { "static-canister": 1 }, "ticks": 30, "bandwidth": 3 }, + { "id": "cut-sync-key", "machine": "artifact-bottler", "inputs": { "anchor-slab": 1, "moov-shard": 1 }, + "outputs": { "sync-key": 2 }, "ticks": 45, "bandwidth": 4 }, + { "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": "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 }, diff --git a/fktry/data/tech.json b/fktry/data/tech.json index be6b712..6ee4497 100644 --- a/fktry/data/tech.json +++ b/fktry/data/tech.json @@ -27,7 +27,11 @@ { "id": "stream-demosaic-vng4", "era": "stream", "cost": { "spatial-pack": 30 }, "unlocks": ["demosaic-vng4", "pan-speckle"] }, { "id": "stream-bloom-loops", "era": "stream", "cost": { "spatial-pack": 20, "temporal-pack": 30 }, - "unlocks": ["bloom-duplicator", "bloom-grade-3"] }, + "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-optical-flow", "era": "stream", "cost": { "temporal-pack": 25 }, "unlocks": ["mv-extractor", "extract-motion"] }, { "id": "stream-laser-primaries", "era": "stream", "cost": { "spatial-pack": 40, "temporal-pack": 20 }, diff --git a/fktry/lanes/LANE-DATA.md b/fktry/lanes/LANE-DATA.md index c3aa800..cceb87b 100644 --- a/fktry/lanes/LANE-DATA.md +++ b/fktry/lanes/LANE-DATA.md @@ -144,3 +144,70 @@ NEXT (Round 2 candidates, not self-assigned): bloom grades 1-5 as real items; th container line (moov shard / offset ribbon / sync key → CONTAINER PACK) which unblocks the hex splicer; mosquito traps so `vaporwave-sunset` becomes shippable; buffer-tank and muxer data; reel + broadcast era tech nodes. + +### Round 2 — 2026-07-17 — Opus 4.8 + +SHIPPED: all 7 orders. `data/CANON-DELTA.md`; colour + flavor on all 21 machines; +heat economy; `bufferCap`; commission queue with 3 standing orders; container line; +bloom grades 1-5. items 36 → 44, machines 21, recipes 35 → 44, tech 17 → 19, +commissions 10 → 14. Validator 28 → 36 tests. +- Verified: my 36 pass; **every other lane's suite is green** (140/141 total — the one + failure is SIM's, see BLOCKED). Live build: 21/21 machines art-directed with zero + gaps, my hexes confirmed on the real DOM swatches (ASIC `rgb(185,190,196)`, software + decoder `rgb(110,58,52)`), no console errors, fax opens on FIRST TASTE with DUST + ALLOWANCE next in tray. +- Fault-injected all six new checks (missing colour, absurd heatPerTick, buffer with no + cap, non-buffer with a cap, wrong queue head, standing order wanting live mosquitoes) + — all six failed by name, then restored. Same standard as round 1. + +DECISIONS: +- **`bloom-concentrate` kept, not replaced.** It is now the raw ungraded draw-off that + feeds grading. I was about to retire it for the grade-1..5 line until I grepped: + LANE-SCREEN hardcodes it in `src/screen/corruptionMap.ts:54` and its test. Deleting it + would have broken their suite mid-round. The standing rule earned its keep. +- The round-1 recipe id `bloom-grade-3` survives but now produces the *item* + `bloom-grade-3` — the id finally means what it says. The old recipe body moved to + `tap-bloom`. Nothing outside `data/` referenced it (checked). +- Container line routes demuxer → gop-assembler → bottler, deliberately **not** through + the hex splicer, since §4 gates the splicer behind CONTAINER PACK research. Otherwise + the tech tree eats its own tail. +- Machine colours are all desaturated industrial per §8 ("ALL saturation belongs to + media/artifacts"). The items carry the colour; the factory is grime. Two eras get a + tint where the codex insists: field loom phosphor-green, telecine brass, ASIC + stream-era white, software decoder hot-red. + +INVENTED: 3 items (`moov-shard`, `offset-ribbon`, `sync-key`), 6 recipes, 2 tech nodes, +2 commissions. All logged with rationale in `data/CANON-DELTA.md` rather than here. + +BLOCKED/BROKEN: +- **SIM's M1 chain test is red, and it is not mine.** `sim.test.ts:456` asserts + `activeCommission === 'first-taste'` after 4000 ticks, but SIM's new queue advances on + completion, so it reads `dust-allowance`. I confirmed this by stashing my entire round + 2 diff and re-running: identical failure. Their assertion needs updating to their own + new behaviour — I didn't touch their file. My validator now pins `commissions[0]` as + `first-taste` so the queue head stays load-bearing for them. +- `npx tsc --noEmit` reports 2 errors in `src/render/index.ts` (94, 96) from RENDER's + uncommitted work. Also not mine — a JSON file cannot cause "Expected 3 arguments". + +CROSS-LANE — SIM, two things I need from you: +1. **Units.** I read `gen`/`draw` as bandwidth **per second** (codex §2 meters read + kbps). `bufferCap: 240` is built on that. If you integrate per *tick*, my buffer + number is 30× too small — say so and I'll retune. Arithmetic is in CANON-DELTA. +2. **Cooling is yours and it doesn't exist yet.** `heatPerTick: 0.0025` makes a software + decoder throttle at 9.3s and scram at 13.3s of continuous running, which is the codex + ("burst gen ... throttles theatrically"). That only becomes a *mechanic* if something + cools it; with no cooling it's a one-shot fuse. I assumed ~0.0025/tick while + scrammed/idle (~50% duty → effective 27/s vs the ASIC's steady 20/s, in the same 2×2 + footprint). Tell me your rate and I'll retune to hit "throttle ~10s, full cycle ~25s". + **Heads up:** your `meltChain()` reference factory runs on 3 software decoders and no + ASIC. Once scram lands they will heat in lockstep and black out together every ~13s. + That's the honest answer to "does anyone dare run two side by side" — but it will slow + your reference factory, so consider an ASIC base + one decoder for burst, or a buffer + tank (which is exactly what `bufferCap: 240` is sized to cover: one scram). + +CONTRACT REQUEST: none this round. + +NEXT (candidates): a cooler machine — §2 says heat is "Vented by ASIC coolers", a +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. diff --git a/fktry/src/sim/data-validate.test.ts b/fktry/src/sim/data-validate.test.ts index 09fb727..fcda2ba 100644 --- a/fktry/src/sim/data-validate.test.ts +++ b/fktry/src/sim/data-validate.test.ts @@ -43,6 +43,20 @@ function unknownItems(counts: Record): string[] { return Object.keys(counts).filter((id) => !itemIds.has(id)); } +/** Everything an extractor can pull, closed over every recipe that can then run. */ +function producible(): Set { + const have = new Set(); + for (;;) { + const before = have.size; + for (const r of recipes) { + 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; + } +} + describe('ids and formatting', () => { it('every collection has unique ids', () => { expect(duplicates(items.map((i) => i.id))).toEqual([]); @@ -126,6 +140,43 @@ describe('machines', () => { .map((m) => `${m.id} -> draw ${m.powerDraw}, asset "${m.asset}"`); expect(bad).toEqual([]); }); + + // v2 art direction: RENDER prefers our colour over one derived from outputs, and UI's + // inspector prints our flavor instead of a hardcoded lookup. Both need full coverage + // or a machine goes black-on-black / voiceless. + it('every machine is art-directed: a hex colour and a line of flavor', () => { + const bad = machines + .filter((m) => !m.color || !HEX.test(m.color) || !m.flavor?.trim()) + .map((m) => `${m.id} -> color ${m.color ?? 'MISSING'}, flavor ${m.flavor ? 'ok' : 'MISSING'}`); + expect(bad).toEqual([]); + }); + + it('only buffers declare a capacity, and they all do', () => { + const bad = [ + ...machines.filter((m) => m.kind === 'buffer' && !(m.bufferCap! > 0)) + .map((m) => `${m.id} is a buffer with no bufferCap`), + ...machines.filter((m) => m.kind !== 'buffer' && m.bufferCap !== undefined) + .map((m) => `${m.id} is not a buffer but declares bufferCap`), + ]; + expect(bad).toEqual([]); + }); + + it('machine heat is a sane 0..1 per-tick trickle, never an instant scram', () => { + const bad = machines.filter((m) => m.heatPerTick !== undefined) + .filter((m) => !(m.heatPerTick! > 0 && m.heatPerTick! <= 0.05)) + .map((m) => `${m.id} -> heatPerTick ${m.heatPerTick}`); + expect(bad).toEqual([]); + }); + + it('the ASIC stays cool and the software decoder does not', () => { + // The codex's whole point (§4): cool-and-inflexible vs hot-and-mighty. If this + // flips, the power choice has stopped being a choice. + const asic = machines.find((m) => m.id === 'decode-asic')!; + const soft = machines.find((m) => m.id === 'software-decoder')!; + expect(asic.heatPerTick).toBeUndefined(); + expect(soft.heatPerTick).toBeGreaterThan(0); + expect(soft.powerGen!).toBeGreaterThan(asic.powerGen!); + }); }); describe('recipes', () => { @@ -242,32 +293,46 @@ describe('commissions', () => { .map((c) => `${c.id} -> reward ${c.rewardBandwidth}`); expect(bad).toEqual([]); }); + + it('opens on first-taste — the queue order is the difficulty ramp', () => { + // SIM's M1 chain test asserts activeCommission === 'first-taste' on a fresh sim, + // so the head of this list is load-bearing, not decorative. + expect(commissions[0].id).toBe('first-taste'); + }); + + it('rewards climb monotonically down the queue', () => { + const slips = commissions + .map((c, i) => ({ c, prev: commissions[i - 1] })) + .filter(({ c, prev }) => prev && c.rewardBandwidth < prev.rewardBandwidth) + .map(({ c, prev }) => `${c.id} (${c.rewardBandwidth}) pays less than ${prev.id} (${prev.rewardBandwidth})`); + expect(slips).toEqual([]); + }); + + it('has standing orders, and they are the ones that can actually repeat', () => { + const standing = commissions.filter((c) => c.repeat); + expect(standing.length).toBeGreaterThanOrEqual(2); + // A repeating order must be re-fillable forever: nothing it wants may be + // forward canon, or the factory jams on a standing order it can never clear. + const bad = standing + .flatMap((c) => Object.keys(c.wants).map((w) => ({ c, w }))) + .filter(({ w }) => !producible().has(w)) + .map(({ c, w }) => `standing order ${c.id} wants unproducible "${w}"`); + expect(bad).toEqual([]); + }); }); describe('economy reachability', () => { - /** Items an extractor can pull, closed over every recipe that can then run. */ - function producible(): Set { - const have = new Set(); - for (;;) { - const before = have.size; - for (const r of recipes) { - 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('the M1 flow reaches MELT from raw ore', () => { expect([...producible()]).toContain('melt'); }); it('every item is either producible or declared forward canon', () => { // Forward canon: named in the codex and shippable one day, but nothing makes them yet. - // Keep this list short and delete entries as recipes land. + // Keep this list short and delete entries as recipes land. Round 2 retired + // container-pack (the moov line exists now); wildlife needs traps, cinema needs + // the Fortress. const FORWARD_CANON = new Set([ - 'mosquito-swarm', 'gibbs-wraith', 'container-pack', 'cinema-pack', + 'mosquito-swarm', 'gibbs-wraith', 'cinema-pack', ]); const have = producible(); const orphans = items @@ -276,6 +341,14 @@ describe('economy reachability', () => { expect(orphans).toEqual([]); }); + it('the forward-canon allowlist has no stowaways', () => { + // The allowlist is a licence to be unreachable. The moment a recipe makes one of + // these, it must leave the list, or it stops proving anything. + const have = producible(); + const retired = ['container-pack'].filter((id) => !have.has(id)); + expect(retired).toEqual([]); + }); + it('no recipe is a free lunch: every craft that only recycles its own output loses something', () => { const bad = recipes.filter((r) => { const ids = Object.keys(r.outputs).filter((o) => r.inputs[o] !== undefined);