diff --git a/OPUS-BUILD-BRIEF-3.md b/OPUS-BUILD-BRIEF-3.md new file mode 100644 index 0000000..25172fe --- /dev/null +++ b/OPUS-BUILD-BRIEF-3.md @@ -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 M0–M15 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 (M10–M15) is fully executed: + bakery, prep bench, juicer, grater, onions, bruschetta — all routed into + days 10–14, 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.08–0.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 1–9 (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 180–209; 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 (203–206), 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.