Build Brief 3: FIRE — the pan, the rib eye, the salt doctrine

P2 cut from MASTERPLAN for an Opus 4.8 session to execute cold:
M16 salt (the dose-curve bone, two salts, the timing matrix and its
named sin), M17 the pan (butter state machine, sear Field, flip, baste
— mushrooms and bell pepper as first customers), M18 rib eye (season/
cook/rest/cut/pair — the early-cut juice flood is the lesson, no UI
timer, the steak's slump is the clock), M19 the steak day. Working
agreements + every trap this session paid for (assetUrl subpath, born-
hidden panels, stale-harness guard, m4pro retries, worktree hygiene).
Asset batch seeds 180-209. Ends with: deploy, then cut Brief 4 from P3.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
type-two 2026-07-18 16:00:33 +10:00
parent db31273fc9
commit 5d29bb2d96

225
OPUS-BUILD-BRIEF-3.md Normal file
View File

@ -0,0 +1,225 @@
# TOASTSIM — Build Brief 3: FIRE (the pan, the steak, the salt doctrine)
*Read this and execute it. It is written to work cold for an Opus 4.8 session
with no other context. It cuts MASTERPLAN.md Phase P2 into buildable
milestones. Read MASTERPLAN.md §1§3 first (the design law and the bones),
then §0.5 STATUS in OPUS-BUILD-BRIEF-2.md (everything M0M15 is DONE and
gate-verified — do not rebuild any of it), then this file top to bottom.*
## 0. STATUS (update this section as you go — a cold session reads here first)
- Nothing in this brief is built yet. Brief 2 (M10M15) is fully executed:
bakery, prep bench, juicer, grater, onions, bruschetta — all routed into
days 1014, all gate-verified with numbers in the commit log.
- **LIVE 🌐** https://partly.party/toastsim/ — deploy with
`bash scripts/deploy.sh` (build `--base=/toastsim/` → rsync staging →
docker cp into `forum-nginx`). Cloudflare fronts it; curl cache-busted.
- The asset pipeline works end to end (34 GLBs + full 2D on disk). Fire new
batches FIRST, code while they render.
## 1. Working agreements (how this repo builds — non-negotiable)
1. **The design law** is MASTERPLAN §1. One game. Every mechanic rides the
bones (Field / point stats / dose curves / temp clocks / dwell commits /
judge bands) or improves them for everyone.
2. **Harness first, verify always.** Every mechanic gets deterministic
`src/dev.ts` helpers driving REAL input (study `t.chop`, `t.juice`,
`t.bruschetta`). Every milestone commits with measured numbers in the
message and the exact console calls a reviewer can replay. Typecheck
(`./node_modules/.bin/tsc --noEmit`) and `npm run build` stay clean.
If you have a browser, verify in it; if you are a worktree agent, compile
the pure sims and measure headless — the reviewer runs the live gate.
3. **Milestone discipline**: one commit per milestone, message = what you
measured. One NEW pressure per day, never two. If a feel is wrong, fixing
feel beats advancing.
4. **Failure must read as a lesson, not a lottery.** M13's knuckle retune is
the precedent: careful play must be RELIABLY safe (10/10), reckless play
reliably punished. If a punishment fires probabilistically on correct
play, that's a design bug — add the floor.
5. **Multi-agent lanes** (if built by parallel agents): pure sim + its scene
+ its `dev-*.ts` sandbox is one lane; `src/game/*` routing + `src/dev.ts`
is another; never two agents in one file. Integration points are exported
functions documented in-file (`juiceResult()` is the pattern). Reviewer
holds the browser and gates every merge live.
6. **Pre-authorized:** all tuning constants; cutting scope inside a milestone
to protect feel; procedural-vs-generated per asset. **Forbidden:** paid or
cloud dependencies, accounts, telemetry, touching `~/Documents/sandoniette`
or `~/MODELBEAST` internals (use `mb` only).
## 2. Known traps (each cost real time once — don't pay twice)
- **Asset paths must go through `assetUrl()`** (`src/scenes/assets.ts`) or
`loadProp` — a bare `'/assets/...'` works in dev and silently breaks under
the live `/toastsim/` subpath (the root `/assets/` on partly.party is
another game's and returns 200 with WRONG content).
- **Panels are born hidden.** Every scene's `Panel` gets `.hide()` in the
constructor; only `enter()` shows it (the kitchen leaked over every station
for four milestones before this was found).
- **Harness helpers must drive the real flow.** `t.roast()` standalone reads
a stale oven session if the oven wasn't opened by the day flow — drive
failure paths through the full order sequence. Guard new helpers against
this (throw or auto-route rather than return stale numbers).
- **MODELBEAST machine facts** (verified 2026-07-18): client at
`~/MODELBEAST` (NOT ~/Documents), token `MB_TOKEN` in
`~/Documents/backnforth/.env`, `MB_HOST=http://100.89.131.57:8777`.
Node m4's venvs are dead; `scripts/gen-assets.sh` already retries every
stage 10× with 60s pauses — keep that until m4pro is rebuilt.
- **Worktree agents**: symlink `node_modules` from the main checkout to
build; remove the symlink before committing. Never `git add` a worktree
directory into the main repo (it's gitignored now, but stay awake).
- **`mergePrep` merges multi-step prep orders** (worst CV, summed mess) but
no shipped order exercises it — if P2 ships a multi-step day, verify it.
## 3. The salt doctrine — build it ONCE, it outlives the steak (M16)
Salt is two objects and a timing question (MASTERPLAN §2), and the **dose
curve** underneath it is a first-class bone that capers/wasabi (Brief 2
§7.5) will reuse verbatim later. Get the shape right here.
### 3.1 `src/sim/dosing.ts` (pure, no three.js)
- `DoseCurve { under: number; band: [number, number]; ruin: number }`
score(mass): below `under` = unnoticed (0.5, not punished, just no
credit); inside `band` = 1.0; between band and `ruin` = falling fast;
past `ruin` = 0 and a named incident (`ruined: true`).
- `DoseSession`: repeated pinch gestures accumulate mass with per-pinch
variance (a pinch is ~0.080.12, seeded RNG). The SKILL is stopping.
- Two salts as data, not code: `table_salt` (fine, dissolves, `phase:
'cooking'`) and `flake_salt` (the good stuff, `phase: 'finishing'`).
- `saltTimingScore(salt, when)`: the matrix. Table during cook = 1.0;
table at the pass = 0.5 "sad little grains"; flakes at the pass = 1.0 +
small bonus; **flakes into the pan = 0.15 and the judge's named sin**
(line bank: "You cooked with the finishing salt. I can hear it weeping.").
- Integration exports, documented in-file: `doseResult()`,
`doseCriteria()`, `SALT_LINES`.
### 3.2 Scene + routing
- Pinch gesture: hold over the dish + tap (or short drag) = one pinch; the
salt cellar (fine) and salt dish (flakes) are two distinct props at the
station — WHICH one you reached for is the whole timing test.
- Wire salt into two existing moments, small: a "season it, love" modifier
on a toast day (salt on buttered toast — flakes at the pass, correct),
and into the M17 pan (below). No new day needed for M16 alone.
- Harness: `t.pinch(n)`, `t.salt(kind, when)`, `t.doseStats()`.
- **Exit bar**: dose curve measured at 5 masses (under/band-low/band-mid/
over/ruin); wrong-salt matrix all four cells measured; a 3-pinch band
landing scores 1.0 ten out of ten (variance must not push a correct pinch
count out of band — floor it like the knuckle roll).
## 4. M17 — THE PAN (the one new primitive of P2)
The pan = the toaster generalized to a continuous, visible, modulated heat
source under a Field. Everything else in P2 rides it.
### 4.1 `src/sim/pan.ts` (pure)
- `PanSession`: heat knob 19 (dwell to change, like the browning dial),
pan temp drifts toward knob setting on the temp clock (thermal lag ~8s —
the lag IS the skill).
- **Butter as medium** (MASTERPLAN §2): drop butter in → state machine on
(temp × time): `cold → melting → foaming → browned (noisette) → burnt`.
Foaming is THE window ("the moment"); noisette = flavour bonus if you
land food during it; burnt = bitterness penalty + smoke + mess. States
must be readable from sound/visual cues, not a meter.
- **Sear Field**: per-side 2D browning Field on the food (reuse the
browning math — same Field class), driven by contact + pan temp + butter
state. Flip = a gesture (drag up-and-over, dwell commits, tongs prop).
- **Basting**: spoon-tilt gesture paints hot butter over the up-face — a
spread stroke that writes into the sear Field of the side NOT touching
the pan. Comedy and skill: baste with burnt butter and you paint bitterness.
- First customers: **mushrooms and bell pepper** (already in the cast,
`roastable: true` — mushrooms brown beautifully, wet pepper spits).
- Harness: `t.pan(heat)`, `t.butterIn()`, `t.foodIn(id, side)`, `t.flip()`,
`t.baste(n)`, `t.searStats()` → per-side mean/stdev, butter state log.
- **Exit bar**: butter state timeline measured at heat 4/7/9 (foaming
window duration at each); a mushroom cooked flip-at-the-right-time gives
two sides within 0.1 mean of each other; never-flipped = one side 0.9+
burnt / one raw; baste during noisette raises up-face mean with the bonus
flag; burnt-butter baste writes bitterness. Numbers in the commit.
## 5. M18 — THE RIB EYE (the crown jewel)
Five judged acts (MASTERPLAN P2). The steak is one ingredient but the
biggest single ingredient build in the game — treat its sub-feels as gates.
### 5.1 The ingredient
- `rib_eye` in `src/sim/ingredients.ts`: `shape: 'block'`-ish (its own
silhouette is fine), `grain: { axis: number }` — NEW field, the fiber
direction. `flesh.moisture` starts 0.95 raw.
- Fridge/bench temp state (cold steak sears worse — reuse tempclock;
ticket hints "get it out early, love" — the brie lesson generalizes).
### 5.2 The acts
1. **Season** (uses M16): table salt before the pan (proper early = best,
just-before = fine), flakes only at the pass. The timing matrix judges.
2. **Cook** (uses M17): sear Field per side on the pan, doneness = interior
ramp — a 1D depth profile per Field cell (surface sear vs center temp;
reuse the browning ramp pointed inward). Target from the ticket:
rare/medium/ruined bands. Butter-baste finishes it.
3. **REST** — the nerve test and the named feel of P2. Rest clock:
moisture decays 0.95 → 0.6 over ~20s of real time. Cut at t: juice
flood mass ∝ current moisture 0.6 — cut at 0s and the mess field
drowns the board (the flood MUST be spectacular — it's the lesson);
cut at 20s and it's a whisper. The ticket tempts ("he's waiting,
love"); the judge times you. NO UI timer — the steak visibly relaxes
(subtle slump + steam rate) and that's your only clock.
4. **Cut** (rides the cutting sim): score cut-angle vs `grain.axis`
across = 1.0, along = "chewing gum" penalty; wobble TEARS (uses the
existing wobble accumulator with a meat-specific tear threshold).
5. **Pair**: the mustard family — 4 jars as drawer entries (hot english /
dijon / wholegrain / american), the order names or implies one
(procedural line flavor: "something with a BITE, love"). Wrong mustard
= pairing miss (judged, small weight), not a technique fail.
- Judge groups: **THE SEASON / THE COOK / THE REST / THE CUT / THE
PAIRING** + The Bench. 8+ lines each, house voice. The rest-flood line
bank deserves love ("You cut it EARLY. It wept. We both watched.").
- Harness: `t.steak({saltAt, heat, flipAt, restS, cutAngle, mustard})`
one-call driver + individual verbs; `t.steakStats()` grouped.
- **Exit bar**: doneness ramp measured at 3 cook profiles; rest flood mass
at 0s/8s/20s (monotone, spectacular at 0); across vs along grain scored
1.0 vs ≤0.3; wobbled cut tears; the full matrix day: a perfect steak
scores S, each single sin drops exactly one named row. The FEEL gate
(reviewer judges in browser): you must genuinely want to cut early, and
the flood must teach you never to do it again.
## 6. M19 — THE STEAK DAY + stage close-out
- Day 15: "rib eye, medium, with the right mustard" — full routing: fridge
decision at order start → season → pan → rest (the kitchen keeps moving:
toast still due!) → cut → plate → pass with flakes. The steak resting
WHILE you do other work is the planning pressure — one new pressure,
as always.
- Escalation hooks (data, not systems): later procedural days can ask
rare/well, different mustards, mushrooms-on-the-side (M17 reuse).
- Update §0.5 STATUS in Brief 2 AND §0 here; commit; deploy
(`bash scripts/deploy.sh`); screenshot the steak scorecard for the log.
- **Exit bar**: `t.day(15)` end-to-end S run live in the reviewer's browser
with numbers; the three P2 feels signed off: the foaming window, the
early-cut flood, the across-grain slice.
## 7. Assets — fire this batch FIRST (seeds 180209; style/shot tokens from gen-assets.sh)
Add a `do_fire()` lane to `scripts/gen-assets.sh` (pattern: `do_pantry`):
- 3D: cast-iron skillet w/ pour spout (183), raw rib eye with marbling
(186 — prompt the marbling explicitly), cooked steak rested + sliced
showing pink interior (189), salt cellar with tiny spoon (193), pinch
dish of flaky salt (196), butter block on paper (199), steel tongs (202),
4 squat mustard jars — red/yellow/brown/grain lids (203206), basting
spoon (209).
- 2D: pan-smoke wisps sprite (231), steam curl sprite (234).
- Steak INTERIOR (the doneness gradient) stays procedural — it carries
gameplay. Marbling texture on the raw prop is art; the sear Field is sim.
## 8. Milestone order and the bar
M16 salt → M17 pan → M18 rib eye → M19 steak day. One commit each, message
= what you measured. M16+M17 parallelize as two lanes (dosing/scene vs
pan/scene) with game routing in a third; M18 is single-lane (integrative);
M19 is routing + polish. After M19 ships: STOP, update both STATUS blocks,
deploy, and cut Brief 4 from MASTERPLAN P3 (eggs & air) — re-planned
against what the pan taught you about heat feels. Do not start P3 inside
this brief.
The bet, restated: the pan is the toaster with its dial ripped off and
handed to you mid-cook. If the foaming window, the early-cut flood, and
the across-grain slice all FEEL right, P2 turns the toast shack into a
kitchen — and the judge finally has something worth unsheathing his best
lines for.