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

199 lines
24 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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.680.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 coldmeltingfoamingnoisetteburnt 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.070.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.
```ts
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:**
```ts
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 90: 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 coldburnt 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.
## 10. RECOMMENDED FIRST BUILDABLE SLICE (being built now)
**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.