toastsim/OPUS-BUILD-BRIEF-HEAT.md
type-two b76820b88e Heat sources M-H1/M-H2: the knob is not the heat
New bone src/sim/heatsource.ts: a HeatSource sits between the knob and
the browning integrator — the dial sets target, output chases it on a
fuel-specific lag, every station reads output. Five fuels (gas/electric/
induction/charcoal/radiant) + appliance SKUs (oven_gas, oven_fan,
cheap_electric). roasting.ts retrofit is golden-preserving: optional
src?, reads src.output ?? power, byte-identical when absent.

Verified in-browser:
- GOLDEN: no-src oven = mean 0.698 / blister 1.0 / collapse 0 (== HEAD).
- M-H1 chase 3->9: gas 0.433s, electric 6.0s, induction 0.133s (distinct).
- residual after 9->0: gas 0@1s, electric 6.6@6s, charcoal 9@30s.
- M-H2: gas oven stdev 0.1207 vs fan 0.0016 at equal mean 0.6001 (75x).
- day 14 bruschetta now roasts on oven_gas, still grades S (9.62); oven
  HUD reads 'GAS OVEN 8 · blue flame', tomatoes visibly hot-at-centre.

Doctrine: OPUS-BUILD-BRIEF-HEAT.md (the cross-cutting heat & environment
doctrine — 'you never cook the knob, you cook the lag'). Harness:
t.heatChase/residual/ovenStdev/ovenGolden/ovenDemo.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 14:16:54 +10:00

21 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. NEXT: M-H3 (Environment/wind), M-H4 (pan/butter/flare — needs M17), M-H5 (economy SKUs), M-H6 (charcoal zone Field — the showpiece scene). Everything below §2 not checked off here is unbuilt.
  • 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.