toastsim/OPUS-BUILD-BRIEF-HEAT.md
type-two 8b13ac68a8 M17 pan shipped as a playable day (16); heat brief complete
Routed the pan into a real order: day 16 'pan-sear the mushrooms on the
gas' — butter in, wait for the FOAM not the smoke, land the food, brown
both faces, baste in the noisette. judgePan scores THE SEAR / BOTH SIDES
/ THE BUTTER with a butter-snob line bank. New scorecard group THE PAN;
the cast-iron skillet GLB renders with a fuel-coloured flame.

Balance fix (the floor): food shields the butter once it lands, so a
clean cook keeps the butter at noisette instead of racing to burnt — a
full sear is now winnable. Emergent fuel character kept: induction sears
both sides dead-even (0.016), gas flares the first side so you turn it
early (0.085) — both under the 0.1 bar.

Verified live: day 16 routes to the pan, plays end to end, grades A;
judge nails it ('cooked in different postcodes... foamed not burnt, you
were watching'). Cast-iron pan + butter + searing mushroom on screen.

Heat brief M-H1..M-H6 + M-H4 all shipped and live.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 20:43:30 +10:00

24 KiB
Raw Blame History

TOASTSIM — Build Brief: THE HEAT SOURCE & ENVIRONMENT DOCTRINE

Read this and execute it. It works cold for an Opus 4.8 session with no other context. It is a CROSS-CUTTING doctrine, not a single phase — it is the layer the pan (P2/M17), the pot & fryer (P6), and the torch & grill (P9) all ride. Read MASTERPLAN §1§3 (design law + bones), then KITCHEN-ATLAS §2/§5/§7.5, then OPUS-BUILD-BRIEF-3 §4 (the pan — committed, not yet built), then this file top to bottom. Build the doctrine's core (§2, §6 M-H1/H2) BEFORE or ALONGSIDE M17 so the pan is born fuel-aware; retrofit oven/toaster in the same pass; ship the rest as the phases that need it arrive.

0. STATUS (update this section as you go — a cold session reads here first)

  • M-H1 + M-H2 DONE & LIVE (2026-07-19). src/sim/heatsource.ts (the bone) + the golden-preserving oven retrofit + the gas-oven day are shipped. Verified in-browser: golden — no-source oven reads mean 0.698 / blister 1.0 / collapse 0, byte-identical to HEAD. M-H1 chase 3→9 — gas 0.433s, electric 6.0s, induction 0.133s (distinct, no overlap). Residual after 9→0 — gas 0 @1s, electric 6.6 @6s, charcoal 9 @30s. M-H2 — gas oven stdev 0.1207 vs fan oven 0.0016 at equal mean 0.6001 (75× ratio; bar was 2×). Day 14 bruschetta now roasts on oven_gas and still grades S (9.62), HUD shows "GAS OVEN · blue flame". Harness: t.heatChase, t.residual, t.ovenStdev, t.ovenGolden, t.ovenDemo.
  • M-H3 DONE (2026-07-19). Environment sim (gust walk, flame perturbation, ferrous rejection, indoor smoke→ZAP). Verified: windy gas flame stdev 0.2635 (bar >0.05) and mean drops to 5.66 (wind steals heat); sheltered stdev 0.0065 (bar <0.02, the dodgeable floor); induction-in-wind stdev 0 (no flame); the ZAP fires exactly once indoors, never outdoors. newOutdoorEnv, addSmoke, SMOKE_ZAP. Harness: t.envVar, t.zapTest.
  • M-H5 (noise half) DONE. cheap_electric hob stdev 0.1244 (bar >0.12) vs induction 0.0198 (bar <0.04) vs fan 0.0064. Harness: t.hobNoise. The "both reach 10/10" half needs the M17 pan to cook on — deferred with it.
  • M-H6 DONE & LIVE (2026-07-19) — the charcoal grill, a heat-bed you arrange. src/sim/charcoal.ts (bankCoals paints a 2D heat bed, zoneDecayStep only cools it, food sears by the coals beneath it, a rendering piece over hot coals flares and chars until you move it) + src/scenes/grill.ts (top-down open grill, the bed rendered as an additive glow, food dragged between the sear zone and the cool lee, flames lick up on flare) + the routed day 15 BBQ order with judgeGrill (THE SEAR / THE CHAR / THE GRILL) and the char-snob line bank. Verified: two-zone bed stdev 0.303; bed ceiling monotone-decays 1.68→0.61/90s; flare dodge — left over coals = done 2.08/31 flares (carbon), pulled to the lee = 0/0 clean; the full day plays and grades S (9.4/10) with careful sear-then-pull, F if you let it flare. Retune: RENDER_AT 0.62 (above the good-sear window's floor) so a clean sear is reachable with no flare — the floor holds (active play wins, neglect chars). Harness: t.zoneStdev, t.zoneDecay, t.grillTwoZones, t.grillFlareDodge, t.grillDemo, t.grillDay.
  • M17 + M-H4 DONE & LIVE (2026-07-19) — the pan, born fuel-aware. src/sim/pan.ts (butter state machine cold→melting→foaming→noisette→burnt keyed on pan temp × time; per-side sear Fields; flip; baste paints hot butter up the up-face; food shields the butter once it lands so a clean cook is winnable; rendered fat flares a flame, never induction) + src/scenes/pan.ts (the cast-iron skillet GLB, a fuel-coloured flame, butter that foams and browns, food you flip and baste) + the routed day 16 pan-sear order with judgePan (THE SEAR / BOTH SIDES / THE BUTTER) and the butter-snob line bank. Verified: foaming window shrinks with heat (10.4/6.0/4.6s @4/7/9); well-timed flip → sides within 0.016 on induction, 0.085 on gas (gas flares one side, so you turn it earlier — the fuel character); never flip → 1.2 burnt / 0.23 raw; baste noisette flags the bonus (up 0.07→0.43), burnt-butter baste writes bitterness 0.48; M-H4: noisette on gas 11.0s vs electric 14.4s, induction output pinned at knob 8 with fat searing (never flares). The day plays and grades A; judge lines land ("These two faces were cooked in different postcodes. Good butter, foamed not burnt. You were watching. I noticed."). Harness: t.panButter/panFlip/panNoFlip/panBaste/panFuel/panDay.
  • THE HEAT BRIEF IS COMPLETE: M-H1 through M-H6 + M-H4 all shipped and live. Remaining heat ideas are content/polish: environment bolted onto the grill (wind/windbreak already in heatsource.ts), wok-hei on the pan, the induction "wrong pan" comedy day, charcoal ash-in-the-sear. The bone carries them all.
  • Presupposes the M16 salt doctrine and the M17 pan skeleton from Brief 3; those aren't in yet, so §2 ships as a standalone heatsource.ts (zero dependency on the pan) and M17 will read it from birth.
  • LIVE 🌐 https://partly.party/toastsim/bash scripts/deploy.sh (build --base=/toastsim/ → rsync staging → docker cp into forum-nginx). Curl cache-busted after every deploy.
  • Seed lane for this brief's assets: 680719 (open gap after P12's 630679). A do_heat() lane (cooktops/pans/BBQ/utensils) was already fired at seeds 181219 for the general cast; the 680719 sprites (flames, glows, flare, ZAP) remain.

0b. THE PITCH (one paragraph)

Today every heat surface in TOASTSIM maps its dial straight into the browning integrator — roastStep reads s.power, toastStep reads power, and the number the knob commands is the number the food gets. That is a lie no real cook believes. This doctrine interposes ONE new bone, a HeatSource, between the knob and the integrator: the knob sets a target, an output chases it on a fuel-specific lag, and every station reads output where it read power. Gas is glued to your hand, an electric coil cooks its own future, induction is eerily exact and rejects the wrong pan, charcoal has no knob at all and is a heat-zone Field you arrange. Wrap that in an Environment — indoor is a promise, outdoor is weather that steals heat off the windward edge — and the four axes SOURCE ⟂ MEDIUM ⟂ VESSEL ⟂ ENVIRONMENT become pure data multiplied into the same power → heat → dry → roast pipeline. No new integrator. New primitives are two small structs and a first-order ease; everything else is content.

1. THE HEAT SOURCE & ENVIRONMENT DOCTRINE — the cross-cutting rule

State it as crisply as the salt doctrine ("salt is two objects and a timing question") and the butter doctrine ("cold → foaming → noisette → burnt"):

Every heat surface has a FUEL and lives in an ENVIRONMENT. The knob commands a target; the fuel decides how fast the heat obeys; the environment decides how much of it survives the trip to the food. You never cook the knob — you cook the lag.

Four laws follow, and nothing in any phase may break them:

  1. The knob is not the heat. No station reads its dial into the integrator anymore. It reads hs.output. output chases target on the fuel's riseRate/fallRate. The gap between what you asked for and what you're getting IS the skill (the pan's "the lag IS the skill", generalized to every burner).
  2. No meter. The state reads diegetically. Blue gas cone, orange coil glow, silent induction, coal-bed glow — never a temperature bar. The butter's foam and the food's sear ARE the readout. The dial can lie (electric residual, induction rejection); the food never does.
  3. Floors, not lotteries (WA-4, MASTERPLAN). Hot-spots, wind, flare-ups, empty bottles, altitude — all add NOISE and dodgeable EVENTS to the Field. None may randomly burn a correct cook. Every hazard has a diegetic warning and a physical dodge (turn the pan, raise the windbreak, move the food across the gradient). Careful, lag-aware play reaches 10/10 on the worst gear in the game.
  4. SOURCE ⟂ MEDIUM ⟂ VESSEL ⟂ ENVIRONMENT are orthogonal data axes. A fuel is a profile. A pan is a profile. A wind is a profile. Combinations (gas + wok + windy beach) are content the day table composes, never new code. Adding the fifth fuel or the tenth environment must touch zero integrator lines.

2. THE DATA MODEL — src/sim/heatsource.ts (pure: Field + Rng only)

The SOURCE sits between the knob and the existing integrator. It owns a scalar that chases the setpoint on the SAME first-order shape TempClock.step already uses (climb toward target by rate·dt; if above, fall), retargeted from fridge/bench(0/1) to the knob's setpoint. This is the only new math, and it is not new — it is TempClock's drift and coolStep's decay, generalized.

export type Fuel = 'gas' | 'electric' | 'induction' | 'charcoal' | 'radiant';

export interface HeatSource {
  fuel: Fuel;
  target: number;        // 0..10 — what the knob commands (charcoal: the bed level)
  output: number;        // 0..10 — what actually reaches the vessel; every station reads THIS
  riseRate: number;      // units/sec climbing toward target
  fallRate: number;      // units/sec falling toward target (LOW = residual heat that won't let go)
  maxOut: number;        // powerMax clamp (cheap hobs top out below 10)
  evenNoise: number;     // amplitude blended into the station's heat-map bias (hot-spots)
  hotspot: number;       // radial concentration term (a burner ring, a coal pile)
  flame: boolean;        // has a flame → wind steals from output, fat can flare it
  needsFerrous: boolean; // induction rejects a non-ferrous vessel → output pinned at ambient
  flareGain: number;     // how hard rendered fat spikes output (gas high, induction 0)
  zone?: Field;          // charcoal only: a 2D heat-zone gradient you arrange (itself a Field)
}

setKnob(hs, k)hs.target = clamp(k, 0, hs.maxOut). Charcoal ignores it: target is the fuel-bed ceiling and stoke(hs) bumps it. The step:

export function heatStep(hs, env, dt, fatLoad = 0) {
  const t = hs.flame ? hs.target * (1 - 0.35 * env.gust) : hs.target;   // wind steals flame
  if (hs.needsFerrous && !env.vesselFerrous) { hs.output = env.ambient; return; }  // dead pan
  if (hs.output < t)  hs.output = Math.min(t, hs.output + hs.riseRate * dt);
  else                hs.output = Math.max(t, hs.output - hs.fallRate * dt);        // residual coast
  hs.output = Math.min(hs.maxOut, hs.output + hs.flareGain * fatLoad);              // flare on fat
  if (hs.zone) zoneDecayStep(hs.zone, dt);                                          // coal-bed cools
}

FUELS profiles (tune headless against §6 bars; shipping targets, not gospel):

Fuel rise fall maxOut evenNoise flame needsFerrous flareGain Feel
gas 12 14 10 0.10 3.5 settles <1s, no memory
electric 1.1 0.8 9 0.28 0 610s lag, long residual
induction 20 20 10 0.03 0 instant, flattest, dead to wrong pan
charcoal 0.02 (zone) 0.35 4.0 no knob; a 2D bed, decays
radiant 0.9 0.5 10 0.14 0.5 top-hot; what toaster & electric oven ARE

applyToHeatMap(heat, hs, rng) is the only spatial hook: it scales the station's existing Float32Array and re-blends the bias amplitude to hs.evenNoise plus a hotspot radial term — the amp = 0.12 + 0.42·(1 quality) slot in buildHeatMap, now driven by fuel instead of bread brand. Charcoal writes its zone Field in directly (§3).

Environment — same file:

export interface Environment {
  place: 'indoor' | 'outdoor';
  ambient: number;        // the temp TempClock/coolStep pull toward
  wind: number;           // 0..1 base
  gust: number;           // 0..1 live, walks toward wind on value-noise
  sheltered: boolean;     // lid / windbreak / lee side → gust forced to 0 (dodgeable floor)
  vesselFerrous: boolean; // does the mounted pan take induction
  smoke: number;          // indoor only: accumulates → fires the ZAP once
  rng: Rng;
}

envStep(env, dt) walks gust via value-noise toward wind (0 if sheltered/indoor). Wind does exactly two things, both existing knobs: perturbs flame output in heatStep; accelerates surface cooling (coolStep tau /= (1 + 1.4·gust)). sheltered zeroes gust → wind is NEVER a lottery; it is a floor you dodge. Indoors wind=0 but smoke climbs from burnt butter/food and fires the ZAP once (comedy, no damage).

Exports (documented in-file, the juiceResult() pattern): Fuel, HeatSource, Environment, FUELS, HEATSOURCE_SKUS, newHeatSource(fuelOrSku, seed), setKnob, stoke, heatStep, envStep, applyToHeatMap, bankZone (charcoal).

Retrofit is opt-in and golden-preserving. roasting.ts/toasting.ts gain an optional src?: HeatSource; the step reads src?.output ?? power. With no source passed, every existing oven and toaster judged number is byte-for-byte unchanged (§6 M-H2 golden test). Nothing already shipped moves.

3. THE FEEL per fuel — the must-feel moments

GAS — the live wire. output glued to the knob (rise 12); blue cone visible, maxOut to the clamp. Must-feel — the gas scorch: one nudge to chase a stalling first side and output is instantly there — butter clears foaming → noisette → burnt in one breath. The recovery is the lesson: yank the pan off, heat dies on contact (fall 14, no residual). Punishes and forgives at the same speed.

ELECTRIC COIL — the ring remembers. rise 1.1 / fall 0.8. Turn the dial down and output keeps coasting for ten-plus seconds while the sear climbs past target. No flame: the dial lies, the food is truth. Must-feel — the coil that won't let go: spin the dial to 3, nothing changes for ten seconds; your only save is lifting the pan and riding carryover. Make the turn-down BEFORE you need it.

INDUCTION — eerie precision, dead pans. rise 20 / fall 20, evenNoise 0.03 — the flattest sear in the game. Gated by needsFerrous: wrong pan → output never leaves ambient, readout stone-cold, dial at 8, silence. Must-feel — the induction ZAP: wrong pan, flat sear, beep beep beep; swap and it snaps to setpoint in a heartbeat. Comedy by absence.

CHARCOAL — you build it. No knob. Banking coals paints the 2D zone Field — a screaming-hot side, a cool side to park on. The ceiling decays on zoneDecayStep ("sear early, hold late"). Fat drip spikes a local texel — a flare you dodge by moving the food. The fan (stoke) pumps it back up; the lid raises ambient and traps it.

RADIANT — the element, honest and top-biased. rise 0.9 / fall 0.5, top-hot bias, no flame. This is what the toaster and electric oven ALREADY are — the retrofit names them. Patience: climbs slow, holds.

4. THE ENVIRONMENTS — ambient modifiers + comedy

INDOOR is a promise. ambient constant, wind=0, sources behave as specced — the clean bench to learn each curve. The one hazard is smoke: burnt butter/food accumulates it, past threshold the ZAP fires once (a Mess-style event → result.bench + a judge line; no temp damage).

OUTDOOR is weather. envStep walks gust toward wind; wind steals from flame output on the windward half and accelerates coolStep. All dodgeable via sheltered. Three flagship stages, each ONE new pressure:

  • Backyard BBQ (flagship). Gas bottle with a hidden fuel reserve that reads only diegetically — flame shrinks blue → sad yellow, then floors mid-service. New pressure: fuel is finite. Flare-ups when fat hits flame (local output spike, chars in ~3s, dodge by moving). Wind subtracts from the windward half. The neighbour, a char snob, leaning on the fence.
  • Camp stove. Tiny tippy burner — narrow heatDisc. Altitude lowers maxOut (water never boils; dry gate stalls). One gust and the flame goes out. New pressure: no dial you can trust.
  • Beach / park. Sand tilts the pan so fat pools (heat × dry asymmetry). Constant low breeze. A seagull. New pressure: the tilt.

Charcoal's outdoor gusts blow ash into the sear — a Mess spatter, not a temp hit (WA-4 kept).

5. UTENSIL EXPANSION that pairs with heat (procedural vs generated)

The VESSEL is the third orthogonal axis. Each vessel is a small data profile (heatDisc radius, ferrous flag, tilt susceptibility, wallHeight for boil-over) multiplied into the same map — a heavier pan literally smooths the evenNoise Field (P12), never adds a stat.

  • Pans (cast-iron — M17 hero; carbon-steel; thin cheap). GENERATED props; heatDisc/ferrous/tilt PROCEDURAL.
  • Woks — bowl heatDisc, hot centre / cool rim, the wok-hei window. GENERATED prop; toss + peak-window PROCEDURAL.
  • Spatulas / tongs / basting spoon — GENERATED (tongs seed 217); flip/baste/grip PROCEDURAL (they write to the Field).
  • BBQ tools — long tongs, grill fork, coal fan, chimney, lid. GENERATED; stoke/sheltered/light-time PROCEDURAL.
  • Pots / camp burner / trivet — GENERATED; wallHeight boil-over + coin heatDisc PROCEDURAL (seed P6).

Rule, unchanged: the prop is art, the Field is sim. Judged numbers stay procedural; chrome/marbling/lids are generated.

6. MILESTONES with harness-measurable exit bars (these PROVE the fuels differ)

Add src/dev.ts helpers driving REAL input (tap('Space'), run, point; study t.chop/t.juice/t.roast): t.heatChase(fuel, from, to) → sec, t.residual(fuel) → sec, t.hobNoise(sku) → stdev, t.zoneStdev(), t.envVar(fuel, wind). Verify each fuel first in a dev-stove.ts sandbox like grate. Typecheck + build stay clean; one commit per milestone, message = measured numbers.

  • M-H1 — the core ease. t.heatChase('gas',3,9) < 1.0s; ('electric',3,9) 59s; ('induction',3,9) < 0.15s. Residual after 9→0: gas output<1 within 1s, electric still >6 at 6s, charcoal still >5 at t=30s. Bar: three rise + three residual curves numerically distinct, no overlap.
  • M-H2 — retrofit, golden-preserving. Gas-oven roast stdev > 2× electric-fan roast at equal mean. Golden test: running the existing oven and toaster with src omitted leaves every shipped judged number identical to HEAD — zero drift.
  • M-H3 — environment. Outdoor gas at wind=0.6: output variance >0.05; sheltered:true drops it <0.02 (dodgeable floor proven). Indoor high-smoke fires the ZAP exactly once. Charcoal ash registers as result.bench, not a roast delta.
  • M-H4 — pan / butter / flare. Butter → noisette on gas ~3s faster than electric at the same knob. Electric's post-zero ∫output dt tips butter cold→burnt on coast alone. Induction flareGain·fatLoad == 0. Gas flare spikes one zone, chars in ~3s, DODGEABLE.
  • M-H5 — economy is noise, never tax. Cheap-electric SKU t.hobNoise > 0.12 vs induction < 0.04; BOTH reach 10/10 under lag-aware play. The WA-4 / P12 promise, measured.
  • M-H6 — charcoal is a Field, not a dial. t.zoneStdev() after banking a two-zone bed > 0.25; ceiling monotonically decays; food on the cool zone holds below sear while the hot zone chars — one bed, two outcomes by placement.

7. How it composes with the already-planned systems

  • The pan (P2 / M17). M17 reads hs.output not its knob. Fuel-aware from birth: PanSession holds a HeatSource + the shared Environment. The butter state machine keys on ∫output dt, so gas overshoots to noisette fast and electric's residual keeps butter cooking after the knob is off — the fuel drives the "moment" for free. The ~8s committed lag IS the electric profile.
  • The oven (M14 roasting.ts). Two profiles over the SAME roastStep: gas = radiant + top-weighted gradient (v>0.7) + flame; electric fan = flat, low evenNoise. Pure data on heat[]; golden test proves default untouched.
  • The toaster (toasting.ts). Retrofit as fuel:'radiant': feed hs.evenNoise into buildHeatMap's amp slot, pass hs.output to toastStep. Stripes stay; a cheap toaster is higher evenNoise.
  • Salt & butter (M16). Rendered fat is the fatLoad into heatStep: flame flares, induction never does. Salt's moisture-draw on dry is unchanged.
  • P12 career (buy better hobs). HEATSOURCE_SKUS maps SKU → profile override: cheap thin electric = {maxOut:8.2, evenNoise:0.42, hotspot:0.5, riseRate:0.9} — hot-spot NOISE + slow chase, never a flat tax. Induction is aspirational (flat, silent) with the vessel-match catch. Every SKU: careful play still reaches 10/10.

8. JUDGE LINES (house voice, deadpan — bank 14, expand per day)

  1. "You cooked this on an electric coil. I can taste the patience it required of you."
  2. "There's no char on this. There's a memory of char. A rumour."
  3. "Induction. Even. Correct. Joyless. Like a well-run dentist's office."
  4. "The wind took the left three. You didn't. Notice."
  5. "This has wok-hei. I did not expect wok-hei from a man with a domestic extractor fan."
  6. "Gas. You panicked at the whoomp and never recovered. It's fine. We all hear the whoomp."
  7. "One side is Sunday. The other side is a house fire. Commit to a Tuesday."
  8. "The bottle ran out at minute four. I know because minute five tastes like giving up."
  9. "You seared this over charcoal you arranged with genuine care. I'm almost moved."
  10. "Blistered, not black. You waited through the boring part. That's the whole job."
  11. "Flare-up. You let the fat conduct the orchestra. It chose inferno."
  12. "This never got hot. Altitude, or nerve — either way, it steamed."
  13. "You put cast iron on induction and stood there. It beeped at you. It was right."
  14. "Perfectly even, corner to corner. On a grill. You've smuggled a laboratory outdoors."

(Wire through Criterion{key,label,score,weight,group}; ZAP/empty-bottle events read into result.bench like Mess. Each shipped fuel/environment day deserves 8+ lines in its own group.)

9. ASSET LANES — MODELBEAST (seeds 680719; the 181219 cast is already fired)

The general heat cast (gas/electric/induction hobs, camp stove, cast iron, wok, saucepan, kettle & gas BBQ, spatula, wooden spoon, tongs, ladle) was fired at seeds 181219. The 680719 lane is the remaining SPRITES + specialised props:

  • 3D: kettle grill lid-closed pair (691), chimney starter (693), coal fan/bellows (695), gas bottle full+empty tell pair (697/698), windbreak screen (708), neighbour's fence rail (711).
  • 2D: blue-cone flame frames (713), sad-yellow low-fuel flame (714), coil orange-glow ramp (715), flare-up flash (716), ash-gust spatter (717), smoke-alarm ZAP flash (718), wind streak overlay (719).

Stays PROCEDURAL: the charcoal zone Field, every sear/doneness Field, the flare's local spike, hot-spot noise. Prop is art, Field is sim.

Build src/sim/heatsource.ts + the OVEN retrofit first — NOT the pan-with-fuel from scratch. Ship a "gas oven vs electric fan oven" day.

Justification: the pan (M17) isn't on disk and is P2's biggest integrative build; coupling a new bone to a new station doubles the bug surface. The oven ALREADY ships (roasting.ts, M14) with a power-scaled heat Field and a centre-hot gradient — interposing HeatSource there is a src?.output ?? power change with a golden test proving byte-identical default. "Gas hot-at-top vs electric fan even" is pure data on heat[], proving the doctrine (two fuels, distinct stdev at equal mean) with zero new scene and one order flag. It de-risks M17: the pan then reads a measured, gate-verified heatsource.ts.

Concretely: (1) heatsource.ts with FUELS for radiant/gas-oven, heatStep, applyToHeatMap, no Environment yet. (2) roasting.ts: optional src?; src?.output ?? power; golden test green. (3) dev.ts: t.heatChase, t.residual, gas-vs-fan stdev; hit M-H1 (partial) + M-H2. (4) One day-table entry handing the same tray to gas vs fan oven; judge lines. (5) Deploy, curl, screenshot both scorecards.

Then M17 is born fuel-aware, Environment bolts on for BBQ/camp, and charcoal (M-H6) is the one genuinely new scene — the showpiece, built after the bone is proven.


The bet: the knob was always lying. This doctrine makes the lie a mechanic — gas obeys your hand, electric cooks its own future, induction is exact and unforgiving of the wrong pan, charcoal has no hand to obey. Four fuels, two environments, one first-order ease on a scalar the game already integrates. If the coil that won't let go, the gas scorch, and the induction ZAP all feel like the same lesson taught three ways — you never cook the knob, you cook the lag — then every burner TOASTSIM will ever add is already content.