From f015a6628306a35a09c6fe106b51cce29da905f0 Mon Sep 17 00:00:00 2001 From: type-two Date: Fri, 17 Jul 2026 16:04:27 +1000 Subject: [PATCH] [data] full codex catalog + data validator MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Transcribe the §2-§6 codex into machine-readable canon and add the validator that keeps five parallel lanes from drowning in dangling ids. - validator (28 tests): recipes point back at their machine, every item/tech/ commission reference resolves, ids unique + kebab-case, colors hex, footprints >=1x1, plus a reachability pass proving MELT is craftable from raw ore. - items 9->36, machines 7->21, recipes 4->35, tech 0->17, commissions 1->10. - mosh rewired to canon (gop-crate -> melt + reclaimed anchor slab); all seed recipe ids preserved so nothing dangles mid-round. Co-Authored-By: Claude Opus 4.8 --- fktry/data/commissions.json | 41 +++- fktry/data/items.json | 34 +++- fktry/data/machines.json | 48 ++++- fktry/data/recipes.json | 87 ++++++++- fktry/data/tech.json | 38 +++- fktry/lanes/LANE-DATA.md | 69 +++++++ fktry/src/sim/data-validate.test.ts | 287 ++++++++++++++++++++++++++++ 7 files changed, 595 insertions(+), 9 deletions(-) create mode 100644 fktry/src/sim/data-validate.test.ts diff --git a/fktry/data/commissions.json b/fktry/data/commissions.json index 0020b79..9e35662 100644 --- a/fktry/data/commissions.json +++ b/fktry/data/commissions.json @@ -1,4 +1,41 @@ [ - { "id": "first-taste", "flavor": "One (1) unit of MELT. We hear you make it fresh. No questions will be answered.", - "wants": { "melt": 1 }, "rewardBandwidth": 50 } + { "id": "first-taste", + "flavor": "One (1) unit of MELT. We hear you make it fresh. No questions will be answered.", + "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 }, + + { "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": "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": "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": "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": "gibbs-wraith-willingly", + "flavor": "One (1) GIBBS WRAITH, willingly.", + "wants": { "gibbs-wraith": 1 }, "rewardBandwidth": 900 }, + + { "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 }, + + { "id": "quarterly-sins", + "flavor": "Quarterly order: 200 MELT, 40 EXPLOSIVE HITS, 12 MOIRE CRYSTAL. Ship the HITS last and ship them running. Our loading dock is a formality and our insurer knows this.", + "wants": { "melt": 200, "explosive-hits": 40, "moire-crystal": 12 }, "rewardBandwidth": 3200 } ] diff --git a/fktry/data/items.json b/fktry/data/items.json index 2a8e803..971e7f5 100644 --- a/fktry/data/items.json +++ b/fktry/data/items.json @@ -1,11 +1,43 @@ [ { "id": "mdat-ore", "name": "MDAT ORE", "codex": "2:mdat-ore", "tier": 0, "color": "#1a1a22" }, + { "id": "bayer-grit", "name": "BAYER GRIT", "codex": "2:bayer-grit", "tier": 0, "color": "#3fbf5f" }, + { "id": "field-splinters", "name": "FIELD SPLINTERS", "codex": "2:field-splinters", "tier": 0, "color": "#6ef58a" }, + { "id": "silver-frames", "name": "SILVER FRAMES", "codex": "2:silver-frames", "tier": 0, "color": "#b0803a" }, + { "id": "luma-bars", "name": "LUMA BARS", "codex": "2:luma-bars", "tier": 1, "color": "#c0c0c0" }, { "id": "chroma-slurry", "name": "CHROMA SLURRY", "codex": "2:chroma-slurry", "tier": 1, "color": "#ff3fd4" }, + { "id": "chroma-422", "name": "CHROMA SLURRY (4:2:2)", "codex": "2:chroma-slurry", "tier": 1, "color": "#e03fc0" }, + { "id": "chroma-420", "name": "CHROMA SLURRY (4:2:0)", "codex": "2:chroma-slurry", "tier": 1, "color": "#b83fa8" }, + { "id": "color-plate", "name": "COLOR PLATE", "codex": "4:demosaicer", "tier": 1, "color": "#d8b0ff" }, { "id": "coefficient-pack", "name": "COEFFICIENT PACK", "codex": "2:coefficient-packs", "tier": 1, "color": "#3fffe0" }, { "id": "hf-dust", "name": "HF DUST", "codex": "2:hf-dust", "tier": 1, "color": "#e8e0ff" }, + { "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": "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": "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": "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" }, + { "id": "moire-crystal", "name": "MOIRE CRYSTAL", "codex": "3:moire-crystal", "tier": 2, "color": "#7fffd4" }, + { "id": "false-color-speckle", "name": "FALSE-COLOR SPECKLE", "codex": "3:false-color-speckle", "tier": 2, "color": "#d940ff" }, + { "id": "judder-cams", "name": "JUDDER CAMS", "codex": "3:judder-cams", "tier": 2, "color": "#9aa0a8" }, + { "id": "explosive-hits", "name": "EXPLOSIVE HITS", "codex": "3:explosive-hits", "tier": 2, "color": "#ff6a00" }, + { "id": "gamut-paint-rec709", "name": "GAMUT PAINT: REC709", "codex": "3:gamut-paints", "tier": 2, "color": "#8a8f96" }, + { "id": "gamut-paint-dci-p3", "name": "GAMUT PAINT: DCI-P3", "codex": "3:gamut-paints", "tier": 2, "color": "#ff2f6a" }, + { "id": "gamut-paint-rec2020", "name": "GAMUT PAINT: REC2020", "codex": "3:gamut-paints", "tier": 2, "color": "#00ff5a" }, + + { "id": "analog-pack", "name": "ANALOG PACK", "codex": "2:science-packs", "tier": 2, "color": "#57d96b" }, + { "id": "spatial-pack", "name": "SPATIAL PACK", "codex": "2:science-packs", "tier": 2, "color": "#4fa8ff" }, + { "id": "temporal-pack", "name": "TEMPORAL PACK", "codex": "2:science-packs", "tier": 2, "color": "#ffb43f" }, + { "id": "container-pack", "name": "CONTAINER PACK", "codex": "2:science-packs", "tier": 2, "color": "#c07fff" }, + { "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" } ] diff --git a/fktry/data/machines.json b/fktry/data/machines.json index 4bc2794..7bdd8d4 100644 --- a/fktry/data/machines.json +++ b/fktry/data/machines.json @@ -1,16 +1,60 @@ [ { "id": "seam-extractor", "name": "SEAM EXTRACTOR", "codex": "4:seam-extractor", "kind": "extractor", - "footprint": { "x": 2, "y": 2 }, "recipes": ["extract-mdat"], "powerDraw": 2, "asset": "seam-extractor" }, + "footprint": { "x": 2, "y": 2 }, + "recipes": ["extract-mdat", "extract-bayer-grit", "extract-field-splinters", "extract-silver-frames"], + "powerDraw": 2, "asset": "seam-extractor" }, + { "id": "belt", "name": "STREAM BELT", "codex": "4:belts", "kind": "belt", "footprint": { "x": 1, "y": 1 }, "recipes": [], "powerDraw": 0, "beltSpeed": 2, "asset": "belt" }, + { "id": "lane-splitter", "name": "LANE SPLITTER", "codex": "4:demuxer", "kind": "splitter", + "footprint": { "x": 1, "y": 1 }, "recipes": [], "powerDraw": 1, "asset": "lane-splitter" }, + { "id": "buffer-tank", "name": "BUFFER TANK", "codex": "2:buffer-charge", "kind": "buffer", + "footprint": { "x": 2, "y": 2 }, "recipes": [], "powerDraw": 0, "asset": "buffer-tank" }, + { "id": "demuxer", "name": "DEMUXER", "codex": "4:demuxer", "kind": "crafter", "footprint": { "x": 2, "y": 2 }, "recipes": ["demux-ore"], "powerDraw": 3, "asset": "demuxer" }, + { "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" }, + { "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" }, { "id": "quantizer", "name": "QUANTIZER", "codex": "4:quantizer", "kind": "crafter", - "footprint": { "x": 2, "y": 2 }, "recipes": ["quantize"], "powerDraw": 4, "asset": "quantizer" }, + "footprint": { "x": 2, "y": 2 }, "recipes": ["quantize", "quantize-crime", "skim-ringing"], + "powerDraw": 4, "asset": "quantizer" }, + { "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" }, + { "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" }, + { "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" }, + { "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" }, { "id": "mosh-reactor", "name": "MOSH REACTOR", "codex": "4:mosh-reactor", "kind": "crafter", "footprint": { "x": 3, "y": 3 }, "recipes": ["mosh"], "powerDraw": 8, "asset": "mosh-reactor" }, + { "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" }, + { "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" }, + { "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" }, + { "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" }, + { "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", + "bottle-analog-pack", "bottle-spatial-pack", "bottle-temporal-pack"], + "powerDraw": 4, "asset": "artifact-bottler" }, + { "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" }, + { "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" }, + { "id": "shipper", "name": "SCREEN UPLINK", "codex": "7:the-muxer", "kind": "shipper", "footprint": { "x": 2, "y": 2 }, "recipes": [], "powerDraw": 5, "asset": "shipper" } ] diff --git a/fktry/data/recipes.json b/fktry/data/recipes.json index 60f0c6c..dd453d7 100644 --- a/fktry/data/recipes.json +++ b/fktry/data/recipes.json @@ -1,6 +1,87 @@ [ { "id": "extract-mdat", "machine": "seam-extractor", "inputs": {}, "outputs": { "mdat-ore": 1 }, "ticks": 30, "bandwidth": 2 }, - { "id": "demux-ore", "machine": "demuxer", "inputs": { "mdat-ore": 2 }, "outputs": { "luma-bars": 1, "chroma-slurry": 1 }, "ticks": 45, "bandwidth": 3 }, - { "id": "quantize", "machine": "quantizer", "inputs": { "luma-bars": 1 }, "outputs": { "coefficient-pack": 1, "hf-dust": 1 }, "ticks": 30, "bandwidth": -6 }, - { "id": "mosh", "machine": "mosh-reactor", "inputs": { "coefficient-pack": 2, "chroma-slurry": 1 }, "outputs": { "melt": 1, "anchor-slab": 1 }, "ticks": 90, "bandwidth": 10 } + { "id": "extract-bayer-grit", "machine": "seam-extractor", "inputs": {}, "outputs": { "bayer-grit": 1 }, "ticks": 45, "bandwidth": 3 }, + { "id": "extract-field-splinters", "machine": "seam-extractor", "inputs": {}, "outputs": { "field-splinters": 1 }, "ticks": 40, "bandwidth": 3 }, + { "id": "extract-silver-frames", "machine": "seam-extractor", "inputs": {}, "outputs": { "silver-frames": 1 }, "ticks": 60, "bandwidth": 4 }, + + { "id": "demux-ore", "machine": "demuxer", "inputs": { "mdat-ore": 3 }, + "outputs": { "luma-bars": 2, "chroma-slurry": 1, "mv-flux": 1 }, "ticks": 45, "bandwidth": 3 }, + + { "id": "demosaic-rggb", "machine": "demosaicer", "inputs": { "bayer-grit": 4 }, + "outputs": { "color-plate": 1 }, "ticks": 60, "bandwidth": 4 }, + { "id": "demosaic-vng4", "machine": "demosaicer", "inputs": { "bayer-grit": 6 }, + "outputs": { "color-plate": 1, "false-color-speckle": 2 }, "ticks": 75, "bandwidth": 5 }, + { "id": "demosaic-stripes", "machine": "demosaicer", "inputs": { "bayer-grit": 4, "field-splinters": 1 }, + "outputs": { "moire-crystal": 1 }, "ticks": 90, "bandwidth": 6 }, + + { "id": "press-color-plate", "machine": "dct-press", "inputs": { "color-plate": 1 }, + "outputs": { "coefficient-pack": 3 }, "ticks": 45, "bandwidth": 4 }, + { "id": "press-anchor-slab", "machine": "dct-press", "inputs": { "coefficient-pack": 6, "chroma-420": 1 }, + "outputs": { "anchor-slab": 1 }, "ticks": 120, "bandwidth": 12 }, + + { "id": "quantize", "machine": "quantizer", "inputs": { "luma-bars": 1 }, + "outputs": { "coefficient-pack": 1, "hf-dust": 1 }, "ticks": 30, "bandwidth": -6 }, + { "id": "quantize-crime", "machine": "quantizer", "inputs": { "coefficient-pack": 1 }, + "outputs": { "macroblock-bricks": 2, "hf-dust": 2 }, "ticks": 20, "bandwidth": -18 }, + { "id": "skim-ringing", "machine": "quantizer", "inputs": { "coefficient-pack": 3 }, + "outputs": { "ringing-halos": 1, "hf-dust": 1 }, "ticks": 40, "bandwidth": -4 }, + + { "id": "subsample-422", "machine": "subsampler", "inputs": { "chroma-slurry": 2 }, + "outputs": { "chroma-422": 1 }, "ticks": 24, "bandwidth": -4 }, + { "id": "subsample-420", "machine": "subsampler", "inputs": { "chroma-422": 2 }, + "outputs": { "chroma-420": 1 }, "ticks": 24, "bandwidth": -8 }, + { "id": "pan-speckle", "machine": "subsampler", "inputs": { "hf-dust": 40 }, + "outputs": { "false-color-speckle": 1 }, "ticks": 200, "bandwidth": 8 }, + + { "id": "extract-motion", "machine": "mv-extractor", "inputs": { "gop-crate": 1 }, + "outputs": { "mv-flux": 6, "hf-dust": 1 }, "ticks": 60, "bandwidth": 5 }, + { "id": "cast-delta-wafer", "machine": "p-caster", "inputs": { "mv-flux": 2 }, + "outputs": { "delta-wafer": 1 }, "ticks": 15, "bandwidth": 2 }, + { "id": "cast-janus-wafer", "machine": "p-caster", "inputs": { "mv-flux": 3, "delta-wafer": 1 }, + "outputs": { "janus-wafer": 1 }, "ticks": 30, "bandwidth": 4 }, + + { "id": "assemble-gop", "machine": "gop-assembler", "inputs": { "anchor-slab": 1, "delta-wafer": 6 }, + "outputs": { "gop-crate": 1 }, "ticks": 60, "bandwidth": 6 }, + { "id": "assemble-gop-ibbp", "machine": "gop-assembler", + "inputs": { "anchor-slab": 1, "delta-wafer": 3, "janus-wafer": 2 }, + "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": "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 }, + "outputs": { "bloom-concentrate": 1 }, "ticks": 60, "bandwidth": 8 }, + + { "id": "weave-fields", "machine": "field-loom", "inputs": { "field-splinters": 2 }, + "outputs": { "luma-bars": 1 }, "ticks": 30, "bandwidth": 3 }, + { "id": "loom-vhold-roll", "machine": "field-loom", "inputs": { "field-splinters": 6 }, + "outputs": { "v-hold-roll": 1, "static-canister": 1 }, "ticks": 75, "bandwidth": 6 }, + + { "id": "mill-judder", "machine": "telecine-puller", "inputs": { "silver-frames": 3 }, + "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 }, + + { "id": "can-static", "machine": "artifact-bottler", "inputs": { "hf-dust": 4 }, + "outputs": { "static-canister": 1 }, "ticks": 30, "bandwidth": 3 }, + { "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 }, + "outputs": { "gamut-paint-dci-p3": 1 }, "ticks": 45, "bandwidth": 5 }, + { "id": "mix-rec2020", "machine": "artifact-bottler", + "inputs": { "chroma-slurry": 4, "false-color-speckle": 3, "moire-crystal": 1 }, + "outputs": { "gamut-paint-rec2020": 1 }, "ticks": 90, "bandwidth": 12 }, + + { "id": "bottle-analog-pack", "machine": "artifact-bottler", + "inputs": { "field-splinters": 2, "v-hold-roll": 1, "static-canister": 1 }, + "outputs": { "analog-pack": 1 }, "ticks": 60, "bandwidth": 6 }, + { "id": "bottle-spatial-pack", "machine": "artifact-bottler", + "inputs": { "macroblock-bricks": 2, "ringing-halos": 1, "chroma-slurry": 2 }, + "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 } ] diff --git a/fktry/data/tech.json b/fktry/data/tech.json index fe51488..be6b712 100644 --- a/fktry/data/tech.json +++ b/fktry/data/tech.json @@ -1 +1,37 @@ -[] +[ + { "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": "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 }, + "unlocks": ["cast-janus-wafer", "assemble-gop-ibbp"] }, + { "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 }, + "unlocks": ["demosaic-vng4", "pan-speckle"] }, + { "id": "stream-bloom-loops", "era": "stream", "cost": { "spatial-pack": 20, "temporal-pack": 30 }, + "unlocks": ["bloom-duplicator", "bloom-grade-3"] }, + { "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 }, + "unlocks": ["mix-rec2020"] }, + { "id": "stream-hex-splicing", "era": "stream", "cost": { "container-pack": 25, "temporal-pack": 25 }, + "unlocks": ["hex-splicer", "splice-offsets"] } +] diff --git a/fktry/lanes/LANE-DATA.md b/fktry/lanes/LANE-DATA.md index cdcdf4e..2b4b493 100644 --- a/fktry/lanes/LANE-DATA.md +++ b/fktry/lanes/LANE-DATA.md @@ -69,3 +69,72 @@ to a codex section. NOTES list what you invented beyond the codex. ### Round N — YYYY-MM-DD — [your model] SHIPPED: / DECISIONS: / INVENTED: / BLOCKED/BROKEN: / CONTRACT REQUEST: / NEXT: ``` + +### Round 1 — 2026-07-17 — Opus 4.8 + +SHIPPED: +- Validator `src/sim/data-validate.test.ts`, 28 tests. Fault-injected a recipe with a + dangling item + a machine that doesn't list it — both caught by id and named. It is + not a test that only knows how to pass. +- items 9 → 36, machines 7 → 21, recipes 4 → 35, tech 0 → 17, commissions 1 → 10. +- Verified: `tsc` clean, `vitest run` 52/52 green (LANE-SCREEN's 24 included), and the + live dev build boots with no console errors. All 21 machines appear in LANE-UI's + build bar with zero code changes from them. + +DECISIONS: +- **All four seed recipe ids kept alive** (`extract-mdat`, `demux-ore`, `quantize`, + `mosh`) so nothing dangles for a lane mid-round. `mosh` keeps its id but is rewired + to canon: `gop-crate 1 → melt 3 + anchor-slab 1`. The winched-out I-frame is + recovered (§4 mosh reactor: "recoverable — sell it!"), so slabs circulate and delta + wafers are what actually burns. Net economy: 6 wafers → 3 melt. +- `quantize` left at the seed's exact ratio so the M1 chain stays easy to build. +- **M1 chain is longer than the masterplan says — please rule on this.** My orders + spell out `slab + wafers → gop-crate` and `gop-crate → mosh`, but MASTERPLAN M1 + describes extractor → belts → demuxer → quantizer → mosh → shipper (seven machines). + Those can't both be true: reaching a crate needs dct-press (slab), p-caster (wafers) + and gop-assembler in between. I followed the orders since they're the more specific + and more recent instruction, and all three run fine as generic crafters. If M1 must + stay at seven machines, say so and I'll add a shortcut `mosh-raw` recipe next round + rather than weaken the canon path. +- Machine params that the codex describes as dials (quantizer Q, subsampler grade taps, + GOP cadence templates) are modelled as **separate recipes per setting**, not as a + per-entity parameter. Cheapest thing that works against the current contract; if SIM + ever wants a real dial, that's a contract change, not a data change. +- Two absurd-but-honest ratios, as ordered: `assemble-gop-i-only` burns 8 premium slabs + for 1 crate (I-only genuinely is the worst cadence — the joke balances itself, nobody + sane builds it), and `pan-speckle` takes 40 HF DUST for 1 speckle (§3 says "panned + like gold", so it's priced like gold panning). Both give hf-dust real sinks. + +INVENTED (codex is silent; written in its voice — kill any of these if you disagree): +- `press-anchor-slab` on the DCT PRESS — the press run in reverse is an IDCT, i.e. a + complete picture. Consistent with the field loom and telecine puller, which the codex + explicitly runs backwards. This is where I-frames come from. +- Items `color-plate` (the demosaicer's "full-color sheet", §4) and `v-hold-roll` + (bottled from the §1 Broadcast Shelf v-hold faults, so the ANALOG PACK's "combing + + v-hold roll + static" triptych maps to three real items). +- Machines `artifact-bottler` (§2 calls the packs "a bottled artifact triptych" but + names no bottling plant) and `lane-splitter` (contract has `kind: 'splitter'` and + belts need routing; the codex only ever calls the DEMUXER "the first splitter"). + +BLOCKED/BROKEN: nothing blocked. Known-incomplete, deliberately: +- Bloom is one grade, not the graded 1-5 line §3 sells by intensity. +- `container-pack` / `cinema-pack` / `mosquito-swarm` / `gibbs-wraith` exist as items + with no recipe — forward canon, per orders. The validator's reachability test has an + explicit FORWARD_CANON allowlist so these four can't hide a real dangling id; delete + entries from it as recipes land. +- `stream-hex-splicing` costs container-packs nothing currently produces. Intentional + dead-end until the moov/container line exists (§6 heists). + +CONTRACT REQUEST: `MachineDef.heatPerTick?: number` — the SOFTWARE DECODER is +`kind: 'power'`, and power machines carry no recipes, but `RecipeDef.heat` is the only +heat hook in the contract. So the one machine the codex defines *by* its heat (§4: +"decodes ANYTHING, at any temperature") has no way to declare it. It's shipped as a +plain 45-gen power machine and is currently strictly better than the ASIC, which is +backwards. Alternative if you prefer no contract churn: let power machines run a +recipe with empty outputs — but I'd rather not put `outputs: {}` in the data, and my +validator rejects it today. + +NEXT (Round 2 candidates, not self-assigned): bloom grades 1-5 as real items; the +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. diff --git a/fktry/src/sim/data-validate.test.ts b/fktry/src/sim/data-validate.test.ts new file mode 100644 index 0000000..09fb727 --- /dev/null +++ b/fktry/src/sim/data-validate.test.ts @@ -0,0 +1,287 @@ +/** + * LANE-DATA territory (the one file this lane owns inside src/sim/). + * Validates data/*.json against the contracts. It is the handshake that keeps five + * parallel lanes from drowning in dangling ids: if a lane references a machine, + * recipe, item or tech that data doesn't define, this test names it. + */ +import { describe, expect, it } from 'vitest'; +import type { CommissionDef, ItemDef, MachineDef, MachineKind, RecipeDef, TechDef } from '../contracts'; + +import itemsJson from '../../data/items.json'; +import machinesJson from '../../data/machines.json'; +import recipesJson from '../../data/recipes.json'; +import techJson from '../../data/tech.json'; +import commissionsJson from '../../data/commissions.json'; + +const items = itemsJson as unknown as ItemDef[]; +const machines = machinesJson as unknown as MachineDef[]; +const recipes = recipesJson as unknown as RecipeDef[]; +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]+)*$/; +const HEX = /^#[0-9a-f]{6}$/; + +const KINDS: MachineKind[] = ['extractor', 'crafter', 'belt', 'splitter', 'power', 'buffer', 'shipper']; +const ERAS: TechDef['era'][] = ['reel', 'broadcast', 'disc', 'stream', 'fortress']; + +const itemIds = new Set(items.map((i) => i.id)); +const machineIds = new Set(machines.map((m) => m.id)); +const recipeIds = new Set(recipes.map((r) => r.id)); + +/** Collect ids that appear more than once. */ +function duplicates(ids: string[]): string[] { + const seen = new Set(); + const dupes = new Set(); + for (const id of ids) (seen.has(id) ? dupes : seen).add(id); + return [...dupes]; +} + +/** Item ids referenced by a count map that no item defines. */ +function unknownItems(counts: Record): string[] { + return Object.keys(counts).filter((id) => !itemIds.has(id)); +} + +describe('ids and formatting', () => { + it('every collection has unique ids', () => { + expect(duplicates(items.map((i) => i.id))).toEqual([]); + expect(duplicates(machines.map((m) => m.id))).toEqual([]); + expect(duplicates(recipes.map((r) => r.id))).toEqual([]); + expect(duplicates(tech.map((t) => t.id))).toEqual([]); + expect(duplicates(commissions.map((c) => c.id))).toEqual([]); + }); + + it('every id is kebab-case', () => { + const bad = [...items, ...machines, ...recipes, ...tech, ...commissions] + .map((d) => d.id) + .filter((id) => !KEBAB.test(id)); + expect(bad).toEqual([]); + }); + + it('every item and machine points at a codex section', () => { + const bad = [...items, ...machines] + .filter((d) => !CODEX_REF.test(d.codex)) + .map((d) => `${d.id} -> "${d.codex}"`); + expect(bad).toEqual([]); + }); +}); + +describe('items', () => { + it('every colour is a 6-digit lowercase hex', () => { + const bad = items.filter((i) => !HEX.test(i.color)).map((i) => `${i.id} -> "${i.color}"`); + expect(bad).toEqual([]); + }); + + it('every tier is 0..3', () => { + const bad = items.filter((i) => !Number.isInteger(i.tier) || i.tier < 0 || i.tier > 3) + .map((i) => `${i.id} -> ${i.tier}`); + expect(bad).toEqual([]); + }); + + it('every item has a display name', () => { + expect(items.filter((i) => !i.name.trim()).map((i) => i.id)).toEqual([]); + }); +}); + +describe('machines', () => { + it('every kind is a known MachineKind', () => { + const bad = machines.filter((m) => !KINDS.includes(m.kind)).map((m) => `${m.id} -> "${m.kind}"`); + expect(bad).toEqual([]); + }); + + it('every footprint is at least 1x1 in whole tiles', () => { + const bad = machines.filter((m) => { + const { x, y } = m.footprint ?? { x: 0, y: 0 }; + return !Number.isInteger(x) || !Number.isInteger(y) || x < 1 || y < 1; + }).map((m) => m.id); + expect(bad).toEqual([]); + }); + + it('every listed recipe exists and points back at the machine', () => { + const missing: string[] = []; + const mismatched: string[] = []; + for (const m of machines) { + for (const rid of m.recipes) { + const r = recipes.find((x) => x.id === rid); + if (!r) missing.push(`${m.id} lists unknown recipe "${rid}"`); + else if (r.machine !== m.id) mismatched.push(`${m.id} lists "${rid}" but that recipe runs on "${r.machine}"`); + } + } + expect(missing).toEqual([]); + expect(mismatched).toEqual([]); + }); + + it('belts declare a belt speed and power machines declare generation', () => { + const bad = [ + ...machines.filter((m) => m.kind === 'belt' && !(m.beltSpeed! > 0)).map((m) => `${m.id} has no beltSpeed`), + ...machines.filter((m) => m.kind === 'power' && !(m.powerGen! > 0)).map((m) => `${m.id} has no powerGen`), + ]; + expect(bad).toEqual([]); + }); + + it('every machine declares a non-negative idle draw and an asset key', () => { + const bad = machines + .filter((m) => !(m.powerDraw >= 0) || !KEBAB.test(m.asset)) + .map((m) => `${m.id} -> draw ${m.powerDraw}, asset "${m.asset}"`); + expect(bad).toEqual([]); + }); +}); + +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}"`); + expect(bad).toEqual([]); + }); + + it('every recipe is listed by the machine it runs on', () => { + const orphans = recipes.filter((r) => { + const m = machines.find((x) => x.id === r.machine); + return m && !m.recipes.includes(r.id); + }).map((r) => `${r.id} is not listed by ${r.machine}`); + expect(orphans).toEqual([]); + }); + + it('every input and output item exists', () => { + const bad: string[] = []; + for (const r of recipes) { + for (const id of unknownItems(r.inputs)) bad.push(`${r.id} consumes unknown item "${id}"`); + for (const id of unknownItems(r.outputs)) bad.push(`${r.id} produces unknown item "${id}"`); + } + expect(bad).toEqual([]); + }); + + it('every recipe produces something in whole positive counts', () => { + const bad: string[] = []; + for (const r of recipes) { + if (Object.keys(r.outputs).length === 0) bad.push(`${r.id} has no outputs`); + for (const [id, n] of [...Object.entries(r.inputs), ...Object.entries(r.outputs)]) { + if (!Number.isInteger(n) || n < 1) bad.push(`${r.id} -> "${id}" count ${n}`); + } + } + expect(bad).toEqual([]); + }); + + it('every recipe takes a whole positive number of ticks', () => { + const bad = recipes.filter((r) => !Number.isInteger(r.ticks) || r.ticks < 1) + .map((r) => `${r.id} -> ${r.ticks} ticks`); + expect(bad).toEqual([]); + }); + + it('only extractors run input-free recipes', () => { + const bad = recipes.filter((r) => Object.keys(r.inputs).length === 0) + .filter((r) => machines.find((m) => m.id === r.machine)?.kind !== 'extractor') + .map((r) => `${r.id} conjures items on non-extractor ${r.machine}`); + expect(bad).toEqual([]); + }); + + it('every heat value is a 0..1 fraction per craft', () => { + const bad = recipes.filter((r) => r.heat !== undefined && !(r.heat >= 0 && r.heat <= 1)) + .map((r) => `${r.id} -> heat ${r.heat}`); + expect(bad).toEqual([]); + }); +}); + +describe('tech', () => { + it('every era is a known era', () => { + const bad = tech.filter((t) => !ERAS.includes(t.era)).map((t) => `${t.id} -> "${t.era}"`); + expect(bad).toEqual([]); + }); + + it('every cost is paid in items that exist, in whole positive counts', () => { + const bad: string[] = []; + for (const t of tech) { + for (const id of unknownItems(t.cost)) bad.push(`${t.id} costs unknown item "${id}"`); + for (const [id, n] of Object.entries(t.cost)) { + if (!Number.isInteger(n) || n < 1) bad.push(`${t.id} -> "${id}" count ${n}`); + } + if (Object.keys(t.cost).length === 0) bad.push(`${t.id} is free`); + } + expect(bad).toEqual([]); + }); + + it('every unlock names a machine or a recipe that exists', () => { + const bad: string[] = []; + for (const t of tech) { + if (t.unlocks.length === 0) bad.push(`${t.id} unlocks nothing`); + for (const u of t.unlocks) { + if (!machineIds.has(u) && !recipeIds.has(u)) bad.push(`${t.id} unlocks unknown "${u}"`); + } + } + expect(bad).toEqual([]); + }); + + it('no two techs unlock the same thing', () => { + expect(duplicates(tech.flatMap((t) => t.unlocks))).toEqual([]); + }); +}); + +describe('commissions', () => { + it('every want names an item that exists', () => { + const bad: string[] = []; + for (const c of commissions) { + for (const id of unknownItems(c.wants)) bad.push(`${c.id} wants unknown item "${id}"`); + } + expect(bad).toEqual([]); + }); + + it('every commission wants a whole positive quantity of something', () => { + const bad: string[] = []; + for (const c of commissions) { + if (Object.keys(c.wants).length === 0) bad.push(`${c.id} wants nothing`); + for (const [id, n] of Object.entries(c.wants)) { + if (!Number.isInteger(n) || n < 1) bad.push(`${c.id} -> "${id}" count ${n}`); + } + } + expect(bad).toEqual([]); + }); + + it('every commission has fax text and a positive reward', () => { + const bad = commissions + .filter((c) => !c.flavor.trim() || !(c.rewardBandwidth > 0)) + .map((c) => `${c.id} -> reward ${c.rewardBandwidth}`); + 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. + const FORWARD_CANON = new Set([ + 'mosquito-swarm', 'gibbs-wraith', 'container-pack', 'cinema-pack', + ]); + const have = producible(); + const orphans = items + .map((i) => i.id) + .filter((id) => !have.has(id) && !FORWARD_CANON.has(id)); + expect(orphans).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); + return ids.length > 0 && ids.every((o) => r.outputs[o] > r.inputs[o]) && + Object.keys(r.inputs).every((i) => r.outputs[i] !== undefined); + }).map((r) => `${r.id} multiplies its own inputs`); + expect(bad).toEqual([]); + }); +});