[orchestrator] round 3 review + contracts v4 + composite seam codex + round 4 orders

Review: 5/5 lanes pass, 238/238, tsc clean. Verified live: scram duty-cycling with
tanks covering (zero brownout), shipment stamps, strain fever 0.824. Found 1 real bug
in review: UI BROWNOUT banner fires on covered deficit (topstrip.ts:66) - ordered.

Contracts v4: research (lab kind, setResearch, ResearchState, researched event),
SelectionState protocol (kills __remove sentinel, main.ts owns remove dispatch),
accent?, coolRadius? (spatial cooling), bandwidth.capacity?, save/load hardened,
heat ruled GROSS (net = heat - cool). tsconfig now typechecks data/ (DATA's proof).
Orchestrator patched lab entries into exhaustive kind maps (my breakage, my fix).

Rulings: pathspec-commit rule after 3 shared-index collisions (worktrees = named
escalation); bloom escalator reading accepted; UI ?uidemo + SCREEN boredom approved.

Codex: docs/FKTRY_LORE.md grew §10 THE COMPOSITE SEAM - keying/transparency artifact
family from the Corridor Crew compositing history + our corridorkey-mrp-mlx (which
doubles as a PYXLFK ground-truth artifact factory). DATA transcribes it round 4.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
type-two 2026-07-17 19:43:48 +10:00
parent 7eda076fda
commit 83e3ad2107
14 changed files with 375 additions and 164 deletions

View File

@ -411,3 +411,115 @@ your win condition from "make it glitch" to "make it silent," or steal the KDM w
(also you). (also you).
- Every asset generated gets its element's codex name — this file is the manifest; - Every asset generated gets its element's codex name — this file is the manifest;
check off against it. check off against it.
---
## 10. THE COMPOSITE SEAM — keying & transparency artifacts
> Source study: Corridor Crew's history of compositing (Niko Pueringer) + our local
> `corridorkey-mrp-mlx` (MLX port of CorridorKey, runs in the MODELBEAST flow on the
> m3ultra). Thesis, and the section's whole physics: **you cannot film transparency.**
> Alpha does not exist in nature — every era of cinema is a different failed séance
> for summoning it. In the Bitstream this is literal: ALPHA is the one resource that
> cannot be mined from any seam. It can only be *computed*, and every computation
> before the last one lies.
**Geology:** the Composite Seam is not a stratum — it is a vertical FAULT crossing
every era layer, glittering with half-transparent fossils. Mining near it yields
subjects fused to backgrounds that were never really there.
### Resources & products
**ALPHA** — *raw transparency, the unfilmable substance.* A shimmering nothing in a
frame; the rarest resource in the game. Cannot be extracted; only keyer machines emit
it, each era's keyer emitting a corrupted grade.
`ASSET (3D + shader): a glass ingot that renders as a checkerboard-transparency hole in the world itself, faint rainbow rim, pedestal casting a shadow the ingot doesn't`
**HARD MATTE** — *reel-era binary alpha.* Silver-halide matte slabs: every pixel all
or nothing. Razor edges, zero feather; anything soft (hair, smoke, glass) that touches
one gets guillotined to a hard silhouette. Cheap, brutal, in demand for "woodcut" looks.
`ASSET (3D): stack of black-and-white film mattes like x-ray plates, silhouettes with surgically hard edges, chemical stain rims, one plate mid-slice through a wisp of smoke`
**BLUE HOLE** — *optical-printer costume dropout.* A patch of SUBJECT gone transparent
because it dared to be blue. Sold as canned absence; installing one in a product makes
part of it see-through in a way that reads as a wound.
`ASSET (3D): a mannequin torso with a clean-edged void where the chest was, world visible through it, faint blue glow licking the hole's rim`
**FRINGE LACE** — *matte lines, bottled.* The bright halo ribbon where layers of film
were re-exposed slightly misaligned. Harvested off composite edges like dried glue.
`ASSET (3D): coils of luminous ribbon, each strand a hard bright outline the exact shape of some absent object's edge, wound on an industrial spool`
**GREEN SPILL** — *chroma contamination.* Cousin fluid to CHROMA SLURRY but nastier:
it doesn't tint what it touches, it makes it PARTIALLY DISAPPEAR (the keyer downstream
eats whatever it stains). Leaks from every chroma keyer; the OSHA posters are explicit.
`ASSET (3D): dripping acid-green fluid whose puddles render as半 half-vanished floor, hazmat berm around a keyer machine, splashes eating a crate mid-vanish`
**MIXED PIXEL** — *the purple gel: an unmixable blend.* Foreground and background
fused into one pixel that belongs to neither — the red gel that filmed purple. Classic
keyers cannot separate it; the neural keyer eats it for breakfast.
`ASSET (3D): a jar of pixels each half one color half another, split diagonally, labeled RED (DISPUTED)`
**ROTO CHATTER** — *boiling edges.* Frame-inconsistent cutout outlines, jittering like
they're being redrawn 24 times a second by a tired artist — because they are. A live
hazard that infests composite edges; trapped and canned, it sells as nervous energy.
`ASSET (VFX + 3D can): an outline that never sits still, vibrating polyline crawling around a silhouette; the can has a shaking label`
**SODIUM LIGHT (589nm)** — *the grail wavelength.* Disney's sodium-vapor trick:
mathematically perfect mattes, hair and glass and gossamer intact — locked behind an
exclusive prism only one company ever owned. In the Bitstream, The Correction hoards
the prisms (perfection is their department). A single beam is an endgame reagent.
`ASSET (3D): a sealed lantern emitting one impossibly pure amber-orange line of light, wavelength etched on the housing, white-glove handling cradle, Correction seal`
**GHOST FEATHER** — *semi-transparency done RIGHT.* Hair strands, motion blur, glass —
alpha with gradients. The proof-of-mastery product; every era below the neural keyer
ruins it, so demand never dies.
`ASSET (3D): a single enormous feather fading from fully solid quill to truly transparent tip, the transition agonizingly smooth, displayed under museum glass`
### Machines
**MATTE BATH** *(reel era)* — chemical tank that binarizes anything: footage in, hard
mattes out, all softness destroyed (a GHOST FEATHER fed in comes out a HARD MATTE and
the machine does not apologize). One mistake ruins the whole reel — jams eat inventory.
`ASSET (3D): open chemical bath with film strips on rollers dipping through, developer-red darkroom lamp, dissolving softness visibly leaving the film as rising fog`
**OPTICAL PRINTER** *(the iron lung)* — a cathedral of bellows and lenses that splits
footage into R/G/B channel reels and recombines them slightly wrong. Produces
composites + FRINGE LACE always + BLUE HOLES whenever fed anything blue. Huge
footprint, gorgeous, mechanical; the codex's most machine-ass machine.
`ASSET (3D, hero): room-sized brass-and-iron optical printer, three film paths in primary-color light, bellows lungs mid-breath, a fringe of glowing misalignment haloing its output gate`
**CHROMA KEYER** *(stream era)* — digital green screen box: cheap, fast, 30-year-old
math. Emits ALPHA (consumer grade) + GREEN SPILL, chokes on hair (GHOST FEATHER input
→ ROTO CHATTER hazard spawn), turns semi-transparent inputs into MIXED PIXELS.
`ASSET (3D): studio-green cube with a subject-shaped hole punched clean through, green fluid weeping from its seams, sample swatches of failed hair taped to its side`
**ROTO BENCH** — *the grind made flesh.* Manual-labor station that converts ANY
footage to cutouts — perfectly, eventually — at an atrocious rate (24 frames of pain
per second of footage). The economic floor under every keyer: when automation fails,
the bench never does, it just costs your life. Emits ROTO CHATTER while running.
`ASSET (3D): a hunched drafting desk with a glowing frame-by-frame lightbox, mountain of finished frames on one side, coffee rings fossilized into the wood, a small flickering outline hovering over the current frame`
**NEURAL KEYER** *(endgame)* — the CorridorKey machine: trained on ten thousand
synthetic ghosts because reality couldn't teach it (you can't film the answer). Pulls
perfect ALPHA through glass, chainmail, motion-blurred hair; UNMIXES pixels (MIXED
PIXEL → original colors, recovered); no spill, no fringe, no chatter. The catch is
pure hardware: it draws 24 VRAM — a bandwidth appetite that dwarfs everything else in
the game. You don't unlock the neural keyer; you unlock the power plant it demands.
`ASSET (3D, hero): a monolithic GPU shrine, fans like cathedral rose windows, cables thick as anacondas, projecting a flawless translucent figure above itself; power meter needle buried in the red`
### Hazards
**SPILL FOG** — green contamination cloud around leaky keyers; products crossing it
gain partial transparency (defect or feature, depending on the commission).
**EDGE BOIL** — roto chatter infestation: outlines in the area start crawling;
machines misread their own footprints and mis-deliver until it's trapped.
### PYXLFK / MODELBEAST production note
`corridorkey-mrp-mlx` (gitea.partly.party/monster/corridorkey-mrp-mlx, deployed in the
MODELBEAST flow on the m3ultra) is a REAL artifact factory for this whole section:
run the same footage through (a) classic chroma-subtraction math and (b) the neural
keyer — the difference image is a pixel-perfect, ground-truth-labeled map of spill,
fringe, and mixed pixels. Degrading the alpha hint synthesizes roto chatter and hard-
matte binarization with known parameters. Every artifact above is bakeable with a
recorded recipe, which is the PYXLFK contract: corruption in, sidecar out.

View File

@ -55,3 +55,16 @@ data/*.json ──▶ main.ts loads GameData
Written in your lane NOTES as `CONTRACT REQUEST: <what> — <why>`. The orchestrator Written in your lane NOTES as `CONTRACT REQUEST: <what> — <why>`. The orchestrator
batches them between rounds. Until granted, work around locally inside your lane. batches them between rounds. Until granted, work around locally inside your lane.
## v4 notes
- `snapshot.commissionQueue` may alias a live sim array — read-only, never retain
across frames (same rule as the snapshot itself, stated here because it looks like
a plain array).
- Selection protocol: UI writes `bus.setSelection(...)`; main.ts derives ghost mode
and dispatches `place`/`remove` on click. UI must NOT dispatch `remove` itself.
The `'__remove'` def string is a deprecated bridge, deleted in v5.
- Heat: `heatPerTick` is GROSS; net climb = `heatPerTick coolPerTick`. A machine
whose heatPerTick ≤ its coolPerTick can never overheat — the validator enforces it.
- Research gating: ids appearing in any `TechDef.unlocks` are locked until researched;
ids referenced by no tech are always available.

View File

@ -37,6 +37,16 @@ codex wins.
5. Commit with your lane prefix: `[sim] belt merge logic + tests`. One or a few 5. Commit with your lane prefix: `[sim] belt merge logic + tests`. One or a few
focused commits, not one mega-commit. Push. focused commits, not one mega-commit. Push.
**COMMIT HYGIENE (ruled after three round-3 collisions):** five sessions share ONE
working tree and ONE git index. `git add -A`, `git add .`, and `git commit -a` are
BANNED — they commit whatever other lanes have staged at that instant. Every commit
uses an explicit pathspec so the index state of other lanes cannot leak in:
`git commit -m "[sim] ..." -- fktry/src/sim fktry/lanes/LANE-SIM.md`
(a pathspec commit ignores the staging area for everything outside the paths).
Before pushing, `git show --stat HEAD` — if it lists files outside your lane,
`git reset --soft HEAD~1` and recommit correctly. If collisions still happen,
the next escalation is a git worktree per lane.
## Hard rules ## Hard rules
- **Never edit**: `MASTERPLAN.md`, `CONTRACTS.md`, `src/contracts.ts`, `src/main.ts`, - **Never edit**: `MASTERPLAN.md`, `CONTRACTS.md`, `src/contracts.ts`, `src/main.ts`,

View File

@ -25,47 +25,46 @@ REQUESTs.
--- ---
## CURRENT ORDERS — Round 3 (goal: heat that matters, deeper economy) ## CURRENT ORDERS — Round 4 (goal: THE COMPOSITE SEAM + research economy)
Round 2 review: the bloom-concentrate grep-before-delete and the fault-injection Round 3 review: re-introducing round 2's exact bug to prove the validator catches it
discipline are exactly the standard. Both your cross-lane asks are RULED: is the gold standard — and your "decorative number" lesson is now a contract comment
**(1) Units: per-second.** `gen`/`draw` are per second, `stored` is AND your validator's job forever (heat ruling: gross, net = heat cool). Verdicts:
bandwidth-seconds; SIM converts their integration this round — your `bufferCap: 240` tsconfig FIXED (`include: ["src", "data"]` — your poisoned-file proof was conclusive).
= 8 seconds of a 30 deficit, as you intended. **(2) Cooling:** `coolPerTick?` granted `accent?` and `coolRadius?` are in v4. SIM exports tuning constants this round —
in v3 (sim default 0.004); SIM wires it. Your SIM's-test-is-red flag was correct retire your hand-copies. The stco/keyring recipe-assignment logic ("made by the
mid-round and self-resolved at HEAD (their landing fixed it) — the stash-and-rerun machine that already owns that knowledge") is exactly how future inventions should
diagnostic was the right move. Ownership RULING: `data-validate.test.ts` moves to argue.
`data/validate.test.ts` (git mv, yours — vitest picks it up there; `src/sim/**`
returns fully to SIM).
1. **The validator move** above, first, so the tree is clean. 1. **THE COMPOSITE SEAM — the codex grew §10** (docs/FKTRY_LORE.md, read it first):
2. **Heat retune, the headline**: today NOTHING can overheat — every `heatPerTick` keying/transparency artifacts, from "you cannot film transparency". Transcribe it:
you authored is below the 0.004 ambient cooling, so heat is decorative. With ALPHA (the unfilmable resource — only keyer machines emit it), HARD MATTE, BLUE
`coolPerTick` yours to set per machine: hit SIM's suggested feel — software HOLE, FRINGE LACE, GREEN SPILL, MIXED PIXEL, ROTO CHATTER, GHOST FEATHER, SODIUM
decoder throttles after ~10s of continuous load, scrams ~13-15s, full duty cycle LIGHT (589nm, Correction-hoarded reagent — forward canon until M4); machines
~25s; ASIC stays incapable of heat. Show the arithmetic in CANON-DELTA (per-tick MATTE BATH (reel), OPTICAL PRINTER (the iron lung — make its ratios hurt), CHROMA
vs per-second — mind the units ruling). Give the hex-splicer a lesser fever. KEYER (stream; leaks green spill, wrecks feathers into chatter), ROTO BENCH (the
Coordinate with SIM's reference-factory v3 via NOTES — your numbers decide grind: converts anything, atrociously), NEURAL KEYER (endgame: perfect, and its
whether their layout survives. powerDraw should be obscene — the codex says its appetite dwarfs everything;
3. **Buffer economy check** under the units ruling: is 240 (8s) still your intent research-gate it behind a fortress-adjacent tech). Era-gate the family through
now that it's true? One tank should cover one decoder scram — verify against your the tech tree; validator covers everything; CANON-DELTA logs deviations.
retuned cycle, adjust, show working. 2. **Research economy**: author the lab machine (`kind:'lab'` in v4 — name it in
4. **ASIC COOLER spec'd** in CANON-DELTA (the codex names it, §2 "vented by ASIC codex voice, it's yours to canonize in CANON-DELTA) and audit all 24+ tech costs
coolers"): item/machine entry + intended adjacency mechanic, data authored but now that they'll actually be paid: pack production chains must exist and the
recipes gated behind a `broadcast`-era tech node — the MECHANIC stays deferred early nodes must be affordable off a first-taste-era factory. Show one worked
until SIM builds auras (M4); the data existing first is fine and the validator's cost path (packs/minute vs research demand) in CANON-DELTA.
FORWARD_CANON pattern covers it. 3. **ASIC COOLER activation**: SIM lands `coolRadius` auras this round — give the
5. **Reel + broadcast era tech nodes**: research costs in analog packs; unlocks cooler its radius + numbers (your spec'd adjacency intent), and make the software
field-loom/telecine recipe families you already authored. decoder + cooler pairing the worked example (duty cycle with vs without).
6. **Muxer + moov brain sub-lines** (§7): `stsz-ledger`, `stco-map`, `stts-metronome`, 4. **Accent fills**: `accent?` where the first-output derivation is wrong — belts,
`stss-keyring` items + assembly recipes converging on a `moov-brain` buffer tank, uplink, lab, coolers; leave it unset where the derivation is already
`mux-container` endgame recipe on the muxer. This is M4's rocket — data lands right (that's the design, not laziness).
first, mechanics later. Muxer machine entry if you don't have one. 5. **Fixture/validator updates**: v4 fields (`accent`, `coolRadius`), the gross-heat
rule you already enforce stays, `commissionQueue` v5-hardening — any fixtures you
own carry it.
**Definition of done:** validator green from its new home; a software decoder **Definition of done:** Composite Seam fully transcribed and reachable through era
demonstrably overheats in SIM's tests using YOUR numbers (coordinate — their DoD tech (validator green, FORWARD_CANON updated); a research path is affordable and
depends on your retune); CANON-DELTA carries the arithmetic; FORWARD_CANON pruned of proven against SIM's real mechanic; cooler pairing arithmetic in CANON-DELTA.
anything that gained a recipe.
--- ---

View File

@ -25,35 +25,37 @@ you never simulate. If motion looks wrong, interpolate better or note it — don
--- ---
## CURRENT ORDERS — Round 3 (goal: truth-driven states + asset intake readiness) ## CURRENT ORDERS — Round 4 (goal: v4 protocol, research & aura visuals)
Round 2 review: the corner-cargo catch and the scram-latch bug you found by READING Round 3 review: the dead-debug-hook bundle grep and the provenance note were exemplary;
SIM'S CODE are the reasons this process works. Verdicts: your color deviation is your worktree analysis drove the new MASTERPLAN commit rule (pathspec commits — your
**APPROVED as the ruling** — contracts v3 re-comments `MachineDef.color` as option 2, with worktrees as the named escalation). Verdicts: CONTRACT REQUEST 1
chassis/body, accent stays derived; DATA's values stand; deviating loudly with a GRANTED — v4 has the typed selection protocol (`SelectionState`, `setGhost(..., mode)`)
written rationale was exactly right. Req 2 GRANTED: `EntityState.scrammed` is in v3 — and main.ts already passes `mode` on every call (the `'__remove'` def string still
SIM wires it this round. Req 3 (`BeltItem.side`) DEFERRED until merges matter. arrives as a bridge; it dies in v5). `MachineDef.accent?` also landed (your round-2
`BeltItem.id` is now required, so your fallback path can retire whenever you like. wish, UI re-filed it). Era skins verified beautiful.
1. **Scram from truth**: read `EntityState.scrammed`, delete your `SCRAM_RELEASE` 1. **Ghost migrates to `mode`**: key demolition tint off the 4th arg, ignore the
duplication and the desync risk it carried. sentinel def. Keep rendering exactly as shipped otherwise.
2. **Remove-mode support**: UI ships a REMOVE interaction this round — give it a 2. **Locked-machine ghost** (research lands this round): a `place` for a locked id is
hover treatment (target machine tinted demolition-red, footprint outline) driven silently dropped by the sim — the ghost must telegraph it. Distinct from
by a `setGhost`-style call. You own the visual language; coordinate the hook via occupied-red: padlock-amber wash + "era" tell if you can read the tech era from
NOTES if `setGhost(null, ...)` isn't expressive enough (I'll widen the contract data cheaply. Coordinate the lock lookup with UI via NOTES (both of you derive it
next round if you two need it). from `snapshot.research.unlocked` + tech.json — same rule, CONTRACTS v4 notes).
3. **Showroom formalized**: promote the devscene showroom row to `?showroom` — every 3. **Cooler auras**: `coolRadius` machines get a floor-ring (subtle, engineering-
machine + every item on plinths, labeled, camera preset. This becomes the QA page drawing vibe, not a buff circle) on hover/ghost AND while placed-and-selected;
for MODELBEAST asset drops (which land as `public/assets/models/<asset>.glb`; heat-shimmer suppression inside the ring falls out of sim truth automatically.
your hot-swap + normalization + AnimationMixer are already ready — this is the 4. **Lab visuals**: placeholder with a distinct silhouette (research bench — codex
eyeball step). voice pending DATA's machine entry); `accent?` consumed when present (your
4. **Era-skin groundwork**: registry hook for per-era material variants (§8 era derivation stays fallback).
skins) — plumbing only, stream-era default, so eras can land in data without a 5. **Housekeeping approved from your NEXT list**: splitter lamp can stay timer-driven
renderer round. (real round-robin state isn't in the snapshot and doesn't earn a contract field
yet); showroom item-hall paging when DATA's catalog next grows (Composite Seam
transcription lands ~15 items this round — it will).
**Definition of done:** scram visuals verified against SIM's real latch (no local **Definition of done:** remove-tint works with the mode arg (verified with UI's
constants); showroom screenshotted in NOTES; remove-mode hover demonstrated with migrated flow); a locked machine's ghost is legibly "not yet" rather than "broken";
UI's flow; perf standing rule still met. cooler ring reads at a glance; showroom absorbs the new catalog.
--- ---

View File

@ -27,33 +27,37 @@ seed stock — port, don't move).
--- ---
## CURRENT ORDERS — Round 3 (goal: continuous dread + spatial artifacts) ## CURRENT ORDERS — Round 4 (goal: second dread axis, boredom, ratification)
Round 2 review: the ringing self-rejection ("sterile, but sharper — it flunked your Round 3 review: measuring tremor in *seconds-until-dark* — and getting scale-
bar and I nearly shipped it") is the exact quality instinct these orders exist to invariance for free from the units ruling — is the single most elegant thing in this
buy. Verdicts: CONTRACT REQUEST GRANTED — `frame(timeMs, snap?)` is in v3 and main.ts codebase. The negative-dt landmine catch is exactly why the NOTES exist. Verdicts:
already passes the snapshot; the un-retained invariant you promised is now a contract your capacity PROPOSAL is GRANTED unasked — `bandwidth.capacity?` is in v4 (SIM sums
comment. Scram-wipe PROPOSAL: APPROVED at 3 frames (~100ms) — you were right, one `bufferCap`); your "reserve low vs falling fast" second reading is this round's order.
frame is a flinch, not a legend. Regional-moiré PROPOSAL: APPROVED as this round's Your stalled-factory idea is APPROVED as design. The strain/fever/seizure hierarchy is
centerpiece. canon now — keep future readings inside it.
1. **Signal strain** (your design, now unblocked): continuous dread from the 1. **Two dreads**: with `capacity` available, split the readings — "seconds left"
snapshot — `bandwidth.stored` draining toward zero = rising tremor/desync judder (tremor, as shipped) vs "reserve fraction" (a low-tank pallor/vignette that sits
BEFORE the brownout seizure; aggregate entity `heat` = a slow feverish shimmer. there even when draw ≤ gen and nothing is draining). A full tank draining fast vs
Keep it subordinate to the corruption ledger: strain is weather, shipments are a near-empty tank coasting should FEEL different; write the one-line design intent
climate. for each in NOTES.
2. **Regional moiré**: grow the lattice from a region instead of full-frame (the 2. **Boredom** (your proposal): no `shipped` events for a sustained stretch → total
codex has it spreading as maze walls) — spatial mask, seeded deterministically, corruption eases back toward clean, slowly, visibly — the factory's work
creeping outward with tally. This is the template for future spatial artifacts; literally evaporating off THE SCREEN. Escalation tallies stay (history isn't
note the mask approach for reuse. erased; the *display* decays), so production resuming snaps it back fast. Tune
3. **Scram wipe at 3 frames**, as approved. the decay horizon so a normal jam doesn't read as punishment (minutes, not
4. **Idle strain baseline**: with zero corruption AND healthy bandwidth, the feed seconds). Test it.
stays perfectly sterile — verify strain can't leak into the NOTHING IS WRONG era 3. **Ratification interruption**: `researched` event → a one-second full-screen
(a test pinning params at zero under a healthy snapshot would do it). interruption card on the feed — sterile white, "STANDARD RATIFIED", era name,
then the corruption crashes back in. The Correction briefly wins the sky every
time the player does science. Deadpan, readable, cheap.
4. **Fixture hygiene**: `commissionQueue` hardens to required in v5 — update your
fixture snapshots this round.
**Definition of done:** strain visible as stored drains (drive it by pausing the **Definition of done:** both dread axes verified at the pixel level (your contact-
reference factory's generators or just feeding a fixture snapshot), regional moiré sheet method); boredom decays and snaps back under test; ratification card exactly
grows rather than switches, wipe readable, budget still <2ms, tests updated. N frames with era text; budget still <2ms.
--- ---

View File

@ -21,39 +21,45 @@ playable through unit tests. You are the lane the other four trust with their li
--- ---
## CURRENT ORDERS — Round 3 (goal: M2 "PRESSURE" becomes real, M3 groundwork) ## CURRENT ORDERS — Round 4 (goal: M3 "DEPTH" — research, spatial heat)
Round 2 review: the reference factory turning round 1's deadlock into 1,136 shipped Round 3 review: the worst-case power sizing ("a test that passes on a coincidence is
bricks is exactly what "surplus becomes product" was supposed to mean — outstanding. worse than one that fails") and the counter-current bloom discovery are the two best
Verdicts: `coolPerTick` GRANTED (contracts v3, default 0.004 — your constant becomes engineering notes anyone has filed. Verdicts: **heat semantics RULED as you asked**
the fallback). `setRecipeAt` GRANTED (your tripwire, defused). `BeltItem.id` HARDENED gross, net = heatPerTick coolPerTick, now a contract comment and a DATA validator
to required (you asked for it). Throttle-hits-powerGen: KEPT, your reading was right. rule. **Bloom loop: the escalator reading is ACCEPTED** — your wafer-bus circle IS the
The validator-file ownership wart: RULED — it moves to `data/validate.test.ts`, DATA mechanic; the codex line stays as lore-poetry. **Git: pathspec commits are now a hard
executes the move, `src/sim/**` is purely yours again. rule in MASTERPLAN** (your option, chosen over worktrees for now — worktrees are the
escalation if it recurs). save/load hardened in v4 as you offered. commissionQueue
aliasing documented in CONTRACTS. Your perf-noise honesty is noted — a headless
benchmark is a good M4 item, not now.
**UNITS RULING (DATA's cross-lane ask):** `gen`/`draw` are bandwidth per SECOND; 1. **Research v1 — the round's centerpiece.** Contracts v4: `kind:'lab'`,
`stored` is bandwidth-seconds. Your integration currently charges/drains per tick — `{kind:'setResearch'}`, `snapshot.research` (`ResearchState`), `researched` event.
divide by `TICKS_PER_SECOND` so a 30-deficit drains 30 stored per second, not per Labs consume science packs from their input toward the active tech's `cost`;
tick. DATA's `bufferCap: 240` then covers 8s as they intended. Update tests; confirm totals met → `researched`, unlocks apply. Gating rule (in CONTRACTS v4 notes): ids
the conversion in NOTES so DATA can retune against truth. appearing in any `TechDef.unlocks` are locked — `place`/`setRecipe`/`setRecipeAt`
for locked ids are silently dropped; everything unreferenced is free. Multiple
labs stack. save/load carries research. Tests incl. a full unlock path against
DATA's real tech.json.
2. **Spatial cooling**`coolRadius` (v4): a machine with it applies its
`coolPerTick` to machines within chebyshev range of its footprint (stacking = sum,
cap total cooling at your call, note it). The ASIC COOLER finally means something;
DATA activates numbers in coordination.
3. **Export your tuning constants** (`src/sim/constants.ts`: throttle/restart
thresholds, default cooling, etc.) — DATA's tests currently hand-copy them and go
stale silently; let them import truth.
4. **Reference factory v4**: add an anchor-slab overflow tap → uplink so the demo
ships slabs (UI's SANITISING + the buyback fiction become visible in `__fktryDemo`
without surgery — they had to demolish your assembler to see it). Keep the 20k
regression green; a lab + a research run in the reference build would be ideal
but is optional if layout pressure fights you.
5. **Fixture hygiene**: `commissionQueue` hardens to required in v5 — make sure your
snapshot literal always carries it (you already publish it live).
1. **Contracts v3 wiring**: `EntityState.scrammed` (RENDER is currently duplicating **Definition of done:** research provable end-to-end in tests (place lab → feed packs
your 0.5 constant to avoid rendering a lie — kill that), `commissionQueue` on the → unlock → previously-dropped place now succeeds); cooler aura demonstrably extends a
snapshot (active first, then upcoming — UI's NEXT IN TRAY is guessing today), decoder's duty cycle; constants exported; reference factory ships slabs.
`coolPerTick` per machine, `setRecipeAt` command, the units conversion above.
2. **Save/load**: implement `save()`/`load()` — JSON, deterministic round-trip
(test: save at tick N, load into a fresh sim, run both 1,000 ticks, deep-equal).
3. **Reference factory v3**: heat is about to become real (DATA retunes this round —
coordinate via NOTES). Restructure power as ASIC base + software-decoder burst +
buffer tank so the layout *survives scram duty-cycles* — DATA warned your three
naked decoders will black out in lockstep. Keep the 20k regression green.
4. **Bloom loop proof**: test that a closed belt circle through a bloom-duplicator
accumulates grades (your rank-0 loop acceptance + DATA's grade recipes should
compose). This is M3's signature topology — prove it early.
**Definition of done:** all tests green including save/load round-trip and the bloom
loop; NOTES confirm the units conversion + the retuned reference factory's scram
survival (ticks of first scram, recovery, melt rate before/after).
--- ---

View File

@ -24,39 +24,43 @@ dispatch Commands via the bus and read state from `update(snap, events)`.
--- ---
## CURRENT ORDERS — Round 3 (goal: close the interaction loop, verify the pressure UI live) ## CURRENT ORDERS — Round 4 (goal: research UI, one real bug, protocol migration)
Round 2 review: 57 tests against a 10 minimum, and factoring pure modules instead of Round 3 review: `live.test.ts` — booting the real sim against the real DOM — is the
touching root config when my grant turned out to be vapor — model behavior. About best verification asset any lane has produced; the SANITISING machine-swap showed
that: **happy-dom genuinely is installed now** (verified: `package.json` line 17, and exactly the right instinct when an order's premise fails. Verdicts: CONTRACT REQUEST 1
your 57 tests pass under `--environment happy-dom`). My round-2 "it's installed" was GRANTED (v4 `SelectionState` + `setSelection`/`selection` on the bus — main.ts now owns
wrong and your BLOCKED section caught it — good catch, keep doing that. the remove dispatch; see order 2). Request 2 GRANTED (`MachineDef.accent?` — your
Verdicts: CONTRACT REQUEST 1 GRANTED — `SimSnapshot.commissionQueue` in v3, SIM wires derivation becomes the documented fallback, DATA fills the field where it's wrong).
it this round. Request 2 was already landing as you wrote it (parallel-round race; `?uidemo` PROPOSAL APPROVED. Tech-panel skip was right at the time — but it was
the queue advances at HEAD — I watched PATIO RESURFACING arrive live). REMOVE another round-start grep going stale: `tech.json` has 24 nodes at HEAD (DATA landed
PROPOSAL: APPROVED. mid-round again). It's a real order now.
1. **REMOVE**: your design — suggested shape: X hotkey / bar button arms remove mode, 1. **BUG, found live in review: the BROWNOUT banner lies.** `topstrip.ts:66` keys on
hover shows RENDER's demolition tint (they're building it this round — coordinate `out || ratio > 1` — during a covered deficit (tanks draining, sim `brownout:
via NOTES), click dispatches `remove`, Esc disarms. Toast in house voice false`, THE SCREEN correctly calm) the panel screams BROWNOUT. That state is
("UNIT RECLAIMED. THE FLOOR REMEMBERS."). *survival*, not failure. Fix: BROWNOUT = the flag only; deficit-with-reserve gets
2. **NEXT IN TRAY becomes truth**: read `snap.commissionQueue` when present, delete its own amber state (suggest "ON RESERVE · 34s" — `stored/(draw-gen)` is seconds
`peekNext` inference the moment SIM lands it (keep fallback until then). left, same math SCREEN uses for dread). Add a test pinning covered-deficit ≠
3. **DOM render tests**: the two you named as needing a real DOM (chip rendering, brownout.
`attach()` binding) plus fax stamp lifecycle — `// @vitest-environment happy-dom`. 2. **Selection protocol migration**: write via `bus.setSelection(...)`, drop the
4. **Live pressure verification** (now unblocked): `window.__fktryDemo()` builds `REMOVE_DEF` sentinel export and — important — **delete your own `remove`
SIM's reference factory in one call — use it. Verify with your own eyes and dispatch**: main.ts dispatches it on click now (double-fire today is harmless
screenshot in NOTES: heat bar + THROTTLING + SCRAM on the software decoders once only because the second one no-ops).
DATA's retune lands, SANITISING chip on real anchor shipments, STANDING ORDER 3. **Research UI, the centerpiece**: `T` opens the tech tree (24 nodes, 5 eras),
stamp on a real repeat completion (I verified the stamp renders — you verify the pick → `setResearch`; progress from `snapshot.research` (packs delivered vs
full lifecycle). cost); locked machines in the build bar get greyed + padlock + "REQUIRES:
5. **Stretch, skippable**: read-only tech panel (`T`) rendering `tech.json` as an era <tech>" tooltip (lock rule in CONTRACTS v4 notes); `researched` event → toast in
tree marked RESEARCH OFFLINE — research mechanics land in M3, the panel can exist house voice ("STANDARD RATIFIED. NEW CRIMES AVAILABLE.") and the padlocks fall
first. off live.
4. **`?uidemo`** — as approved: boots `__fktryDemo()` after init.
5. **Accent adoption**: read `accent?` first, your derivation second, kind table
third (same precedence as RENDER — one sentence in NOTES confirming you both
read it identically).
**Definition of done:** a misplaced machine is mouse-removable; queue peek reads **Definition of done:** banner distinguishes RESERVE from BROWNOUT under test; a
truth; the three DOM tests exist; NOTES carry screenshots of heat/scram/SANITISING full research run is verifiable in `live.test.ts` (lock visible → research → unlock
observed live. → place succeeds); remove mode works through the typed protocol with RENDER's tint.
--- ---

View File

@ -38,7 +38,8 @@ export type MachineKind =
| 'splitter' // 1 in, N out round-robin | 'splitter' // 1 in, N out round-robin
| 'power' // generates bandwidth | 'power' // generates bandwidth
| 'buffer' // stores bandwidth smoothness (brownout protection) | 'buffer' // stores bandwidth smoothness (brownout protection)
| 'shipper'; // consumes items => 'shipped' events => THE SCREEN | 'shipper' // consumes items => 'shipped' events => THE SCREEN
| 'lab'; // v4: consumes science packs toward the active research
export interface MachineDef { export interface MachineDef {
id: string; id: string;
@ -54,9 +55,15 @@ export interface MachineDef {
color?: string; // v3 RULING: CHASSIS/BODY color (codex §8 grime); the emissive color?: string; // v3 RULING: CHASSIS/BODY color (codex §8 grime); the emissive
// accent stays derived from outputs. (Was mislabeled "accent".) // accent stays derived from outputs. (Was mislabeled "accent".)
flavor?: string; // v2: codex one-liner for the inspector (DATA's material) flavor?: string; // v2: codex one-liner for the inspector (DATA's material)
heatPerTick?: number; // v2: heat while active (the software decoder's whole bit) heatPerTick?: number; // v2: heat while active. v4 RULING: this is GROSS heat — net
// climb = heatPerTick coolPerTick; a machine whose
// heatPerTick ≤ its coolPerTick can never overheat.
bufferCap?: number; // v2: bandwidth storage in bandwidth-SECONDS (see SimSnapshot) bufferCap?: number; // v2: bandwidth storage in bandwidth-SECONDS (see SimSnapshot)
coolPerTick?: number; // v3: per-machine cooling rate; sim default 0.004 coolPerTick?: number; // v3: per-machine cooling rate; sim default 0.004
accent?: string; // v4: art-directed emissive accent; fallback stays derived
// from first output item (renderer + UI share the derivation)
coolRadius?: number; // v4: spatial cooling aura in tiles (chebyshev from footprint);
// this machine's coolPerTick also applies to machines in range
} }
export interface TechDef { export interface TechDef {
@ -113,20 +120,32 @@ export interface SimSnapshot {
beltItems: ReadonlyArray<BeltItem>; beltItems: ReadonlyArray<BeltItem>;
// v3 RULING on units: gen/draw are bandwidth per SECOND; stored is bandwidth-seconds // v3 RULING on units: gen/draw are bandwidth per SECOND; stored is bandwidth-seconds
// (sim integrates surplus/deficit divided by TICKS_PER_SECOND each tick). // (sim integrates surplus/deficit divided by TICKS_PER_SECOND each tick).
bandwidth: { gen: number; draw: number; stored: number; brownout: boolean }; // v4: capacity = summed bufferCap of placed tanks (0 if none) — SCREEN's 2nd dread axis.
bandwidth: { gen: number; draw: number; stored: number; capacity?: number; brownout: boolean };
shippedTotal: Record<string, number>; shippedTotal: Record<string, number>;
activeCommission: string | null; activeCommission: string | null;
commissionQueue?: string[]; // v3: truth for "next in tray" — active first, then commissionQueue?: string[]; // v3: active first, then upcoming in activation
// upcoming in activation order (hardens in v4) // order. May alias a live array — read-only, never
// retain. HARDENS IN v5: update fixtures this round.
commissionProgress: Record<string, number>; commissionProgress: Record<string, number>;
research?: ResearchState; // v4: research progress (hardens in v5)
} }
export interface ResearchState {
active: string | null; // TechDef.id being researched
progress: Record<string, number>; // packs delivered toward active (itemId -> n)
unlocked: string[]; // completed TechDef.ids
}
// v4 gating rule: a machine/recipe id that appears in ANY TechDef.unlocks is locked
// until that tech completes; ids referenced by no tech are always available.
export type Command = export type Command =
| { kind: 'place'; def: string; pos: Vec2; dir: Dir } | { kind: 'place'; def: string; pos: Vec2; dir: Dir }
| { kind: 'remove'; pos: Vec2 } | { kind: 'remove'; pos: Vec2 }
| { kind: 'rotate'; pos: Vec2 } | { kind: 'rotate'; pos: Vec2 }
| { kind: 'setRecipe'; entity: number; recipe: string | null } | { kind: 'setRecipe'; entity: number; recipe: string | null }
| { kind: 'setRecipeAt'; pos: Vec2; recipe: string | null } // v3: id-free variant | { kind: 'setRecipeAt'; pos: Vec2; recipe: string | null } // v3: id-free variant
| { kind: 'setResearch'; tech: string | null } // v4: pick the lab target
| { kind: 'setPaused'; paused: boolean }; | { kind: 'setPaused'; paused: boolean };
export type SimEvent = export type SimEvent =
@ -137,7 +156,8 @@ export type SimEvent =
| { kind: 'placed'; entity: number; def: string; tick: number } | { kind: 'placed'; entity: number; def: string; tick: number }
| { kind: 'removed'; entity: number; def: string; tick: number } | { kind: 'removed'; entity: number; def: string; tick: number }
| { kind: 'commissionDone'; commission: string; tick: number } | { kind: 'commissionDone'; commission: string; tick: number }
| { kind: 'scram'; entity: number; on: boolean; tick: number }; // v2: heat shutdown edge | { kind: 'scram'; entity: number; on: boolean; tick: number } // v2: heat shutdown edge
| { kind: 'researched'; tech: string; tick: number }; // v4: research complete
// ---------------------------------------------------------------- module interfaces // ---------------------------------------------------------------- module interfaces
@ -147,23 +167,38 @@ export interface Sim {
tick(): void; // advance exactly one tick tick(): void; // advance exactly one tick
snapshot(): SimSnapshot; // read-only view; renderer/ui must not mutate snapshot(): SimSnapshot; // read-only view; renderer/ui must not mutate
drainEvents(): SimEvent[]; // events since last drain (main loop fans out) drainEvents(): SimEvent[]; // events since last drain (main loop fans out)
save?(): string; // v3: full state as JSON, deterministic round-trip save(): string; // v4: HARDENED — full state, deterministic round-trip
load?(json: string): void; // v3: restore a save() blob (hardens in v4) load(json: string): void; // v4: HARDENED — restore a save() blob
} }
/** v4: the typed selection protocol — kills the '__remove' magic-string sentinel. */
export type SelectionState =
| { mode: 'build'; def: string; dir: Dir }
| { mode: 'remove' }
| null;
export interface Renderer { export interface Renderer {
init(container: HTMLElement, data: GameData): Promise<void>; init(container: HTMLElement, data: GameData): Promise<void>;
render(snap: SimSnapshot, alpha: number): void; // alpha: 0..1 interp between ticks render(snap: SimSnapshot, alpha: number): void; // alpha: 0..1 interp between ticks
pickTile(clientX: number, clientY: number): Vec2 | null; pickTile(clientX: number, clientY: number): Vec2 | null;
setGhost(def: string | null, pos: Vec2 | null, dir: Dir): void; /** v4: mode drives the ghost visual 'build' = placement preview, 'remove' =
* demolition tint. def is null in remove mode. The '__remove' sentinel is deprecated
* and main.ts stops sending it in v5. */
setGhost(def: string | null, pos: Vec2 | null, dir: Dir, mode?: 'build' | 'remove'): void;
} }
export interface UIBus { export interface UIBus {
dispatch(cmd: Command): void; dispatch(cmd: Command): void;
/** current build selection; renderer ghost + click-to-place read this */ /** current build selection; renderer ghost + click-to-place read this.
* DEPRECATED in v4 (still served): use selection()/setSelection(). */
selectedBuild(): { def: string; dir: Dir } | null; selectedBuild(): { def: string; dir: Dir } | null;
/** v2: world picking, plumbed from the renderer — UI's click-to-inspect */ /** v2: world picking, plumbed from the renderer — UI's click-to-inspect */
pickTile(clientX: number, clientY: number): Vec2 | null; pickTile(clientX: number, clientY: number): Vec2 | null;
/** v4: typed selection. main.ts owns the state; UI writes via setSelection.
* In remove mode main.ts dispatches the `remove` command on click the UI must
* NOT also dispatch it (that was the round-3 double-fire). */
selection?(): SelectionState;
setSelection?(sel: SelectionState): void;
} }
export interface UI { export interface UI {

View File

@ -2,7 +2,7 @@
* FKTRY main loop ORCHESTRATOR-OWNED wiring. Lanes do not edit. * FKTRY main loop ORCHESTRATOR-OWNED wiring. Lanes do not edit.
* Fixed-timestep sim (30 tps) + interpolated render + event fan-out. * Fixed-timestep sim (30 tps) + interpolated render + event fan-out.
*/ */
import { TICKS_PER_SECOND, type Command, type Dir, type GameData, type UIBus } from './contracts'; import { TICKS_PER_SECOND, type Command, type Dir, type GameData, type SelectionState, type UIBus } from './contracts';
import { createSim } from './sim'; import { createSim } from './sim';
import { createRenderer } from './render'; import { createRenderer } from './render';
import { createUI } from './ui'; import { createUI } from './ui';
@ -21,11 +21,24 @@ const renderer = createRenderer();
const ui = createUI(); const ui = createUI();
const screen = createScreenFX(); const screen = createScreenFX();
// v4 bridge: _sel keeps the legacy {def,dir} shape (including the deprecated
// '__remove' sentinel) so unmigrated lanes keep working; selection()/setSelection()
// are the typed protocol. The sentinel path is deleted in v5.
const bus: UIBus & { _sel: { def: string; dir: Dir } | null } = { const bus: UIBus & { _sel: { def: string; dir: Dir } | null } = {
_sel: null, _sel: null,
dispatch: (cmd: Command) => sim.enqueue(cmd), dispatch: (cmd: Command) => sim.enqueue(cmd),
selectedBuild: () => bus._sel, selectedBuild: () => bus._sel,
pickTile: (x, y) => renderer.pickTile(x, y), pickTile: (x, y) => renderer.pickTile(x, y),
selection: (): SelectionState => {
if (!bus._sel) return null;
if (bus._sel.def === '__remove') return { mode: 'remove' };
return { mode: 'build', def: bus._sel.def, dir: bus._sel.dir };
},
setSelection: (sel: SelectionState) => {
bus._sel = sel === null ? null
: sel.mode === 'remove' ? { def: '__remove', dir: 0 as Dir }
: { def: sel.def, dir: sel.dir };
},
}; };
// LANE-UI sets the build selection through this hook: // LANE-UI sets the build selection through this hook:
(window as any).__fktryBus = bus; (window as any).__fktryBus = bus;
@ -39,14 +52,22 @@ async function boot() {
// click-to-place / hover ghost glue // click-to-place / hover ghost glue
const game = document.getElementById('game')!; const game = document.getElementById('game')!;
game.addEventListener('pointermove', (e) => { game.addEventListener('pointermove', (e) => {
const sel = bus.selectedBuild(); const sel = bus.selection!();
renderer.setGhost(sel?.def ?? null, renderer.pickTile(e.clientX, e.clientY), sel?.dir ?? 0); const tile = renderer.pickTile(e.clientX, e.clientY);
if (sel?.mode === 'remove') {
// legacy def string kept alongside the mode until renderers migrate (v5 removes it)
renderer.setGhost('__remove', tile, 0, 'remove');
} else {
renderer.setGhost(sel?.def ?? null, tile, sel?.dir ?? 0, 'build');
}
}); });
game.addEventListener('pointerdown', (e) => { game.addEventListener('pointerdown', (e) => {
if (e.button !== 0) return; // pan/context buttons never place if (e.button !== 0) return; // pan/context buttons never place
const sel = bus.selectedBuild(); const sel = bus.selection!();
const tile = renderer.pickTile(e.clientX, e.clientY); const tile = renderer.pickTile(e.clientX, e.clientY);
if (sel && tile) sim.enqueue({ kind: 'place', def: sel.def, pos: tile, dir: sel.dir }); if (!sel || !tile) return;
if (sel.mode === 'remove') sim.enqueue({ kind: 'remove', pos: tile });
else sim.enqueue({ kind: 'place', def: sel.def, pos: tile, dir: sel.dir });
}); });
const MS_PER_TICK = 1000 / TICKS_PER_SECOND; const MS_PER_TICK = 1000 / TICKS_PER_SECOND;

View File

@ -51,6 +51,7 @@ export const BODY_BY_KIND: Record<MachineKind, number> = {
power: 0x45434f, power: 0x45434f,
buffer: 0x3a3f48, buffer: 0x3a3f48,
shipper: 0x4a4652, shipper: 0x4a4652,
lab: 0x3d3a4e, // research bench chassis (v4 kind; placeholder until art-directed)
}; };
/** Fallback accent when a machine runs no recipes (belts, power, shippers). */ /** Fallback accent when a machine runs no recipes (belts, power, shippers). */
@ -62,6 +63,7 @@ export const ACCENT_BY_KIND: Record<MachineKind, number> = {
power: 0xffb03f, // serene LED face power: 0xffb03f, // serene LED face
buffer: 0x7fffd4, buffer: 0x7fffd4,
shipper: 0x3fd4ff, // uplink to THE SCREEN shipper: 0x3fd4ff, // uplink to THE SCREEN
lab: 0xb0a0ff, // research glow (v4 kind; placeholder)
}; };
/** Body height in world units. Silhouette differentiation on a flat iso grid. */ /** Body height in world units. Silhouette differentiation on a flat iso grid. */
@ -73,6 +75,7 @@ export const HEIGHT_BY_KIND: Record<MachineKind, number> = {
power: 0.95, power: 0.95,
buffer: 0.7, buffer: 0.7,
shipper: 1.3, shipper: 1.3,
lab: 1.1, // v4 kind; placeholder
}; };
export const JAM_AMBER = 0xffa726; export const JAM_AMBER = 0xffa726;

View File

@ -23,6 +23,7 @@ const GROUP_OF: Record<MachineKind, string> = {
crafter: 'REFINE', crafter: 'REFINE',
power: 'POWER', power: 'POWER',
shipper: 'SHIP', shipper: 'SHIP',
lab: 'RESEARCH', // v4 kind
}; };
const GROUP_ORDER = ['EXTRACT', 'LOGISTICS', 'REFINE', 'POWER', 'SHIP']; const GROUP_ORDER = ['EXTRACT', 'LOGISTICS', 'REFINE', 'POWER', 'SHIP'];

View File

@ -38,6 +38,7 @@ const ACCENT_BY_KIND: Record<MachineKind, string> = {
power: '#3fffe0', power: '#3fffe0',
buffer: '#7fb0ff', buffer: '#7fb0ff',
shipper: '#d8ffd8', shipper: '#d8ffd8',
lab: '#b0a0ff', // v4 kind; placeholder
}; };
/** Fallback chassis for machines DATA hasn't art-directed yet. */ /** Fallback chassis for machines DATA hasn't art-directed yet. */

View File

@ -11,5 +11,5 @@
"lib": ["ES2022", "DOM"], "lib": ["ES2022", "DOM"],
"types": ["vite/client"] "types": ["vite/client"]
}, },
"include": ["src"] "include": ["src", "data"]
} }