Commit Graph

169 Commits

Author SHA1 Message Date
type-two
19c39acc8d Lane B S14: collateral exposure on the card + the night-5 invoice line (pool item)
COLLATERAL ON THE CARD. 'Winnable at $80' is an incomplete sentence on a yard
where letting a corner go also bills $180 of carport, so the card now prices
it — through world.collateralFor(), the one resolver (A's structKey fix), never
a second copy of that lookup in a panel. Deduped per STRUCTURE the way main.js
bills ('two beams letting go is one carport gone'): site_02's four carport
anchors read $180 total, not $720. The bill also rides on the margin-flag line
itself, because 'knife-edge' and 'knife-edge AND $180' are different authoring
decisions. An unpriced label renders UNPRICED — not scored, in red, never
'free' — which is the gutter bug's shape and the lie the card exists to prevent.

Verified A's structKey fix rather than assuming it: an editor-shaped carport
(id s1 + collateralKey) prices to $180, a second one (s2) also $180, and the
negative control (id s1, no collateralKey) reads null = not scored. My first
test reported a false alarm because I renamed a SHIPPED spec's id without the
palette's collateralKey — A's editor writes that field explicitly, so the
editor path was never broken.

THE NIGHT-5 INVOICE LINE (pool item, offered S13, accepted). Night 5's garden
is beyond saving by design, and the job sheet was the last surface still
dangling 'up to $45 / paid on what's left of the bed at dawn' — a maximum
nobody can earn, phrased as a rigging problem the player could solve.

NO ECONOMY CHANGE, deliberately. week.js's own comment forbids quote and settle
diverging ('the job sheet promises a number the invoice doesn't pay'), and
zeroing the quote would break that in the opposite direction — settle really
does pay the bonus proportionally. So the NUMBER stays and its CONDITION tells
the truth, which is the job sheet's own idiom:
  nights 1-4  'paid on what's left of the bed at dawn'        (unchanged)
  night 5     'scales with the bed at dawn — and tonight the ice takes it
               whatever you rig'
and the ledger names it too, because an unexplained small number next to
'night lost' reads as the player's failure:
  night 4  'garden bonus — 18% of the bed          +$8'
  night 5  'garden bonus — 18% of the bed, beyond saving from the start  +$8'

Verified in the running game on all five nights: flag fires on 5 only, every
quoted and paid figure identical (garden $45, total $136, bonus $8).

Selftest 374 passed / 0 failed / 0 skipped.
2026-07-18 16:29:41 +10:00
type-two
ff7c1f7985 Lane B S14 gate 2.3: pin the editor's scored wind to the game's played wind
Two chains built independently — GAME (loadSite -> createWorld -> dress ->
windForSite) and EDITOR (siteClone-shaped object -> buildScoringWorld ->
windForSite) — asserted EXACTLY equal at t=30, speed and vector, at every
probe on both shipped yards. Exactly, not nearly: a tolerance is where the
funnel-off bug hid, and it moved the S13 headline 91.5 -> 39.8.

The mutation checks caught my own probe TWICE before this went green, which is
the only reason it is worth anything:
  1. I proposed backyard_01 in THREADS. It declares venturi: [] — no funnel to
     lose, so a funnel-off regression could never turn it red.
  2. So I moved to site_02 — and its BED is 6.08 m from a radius-5 throat.
     Starving that yard of its venturi moved the bed probe by 0.000 m/s.
Both versions passed their equality assert and both were worthless.

Measured sensitivity on site_02/earlybuster at t=30 (funnel on/off, trees
present/hidden):
    bed      ( 0, 1)  12.452  dV +0.000  dT +0.000   <- blind, not a pin site
    throat   (-6, 0)  15.910  dV +4.948  dT +0.000   <- funnel tripwire
    near tr1 ( 7,-1)   8.593  dV +0.000  dT -4.891   <- shelter tripwire

Mutation-checked: starving the editor clone of its venturi turns the @throat
equality red (plus both funnel asserts), while bed/tree stay green exactly as
the table predicts.

Also recorded as a pin, because it is load-bearing for anyone reading the
score card: the corner block's funnel does NOT reach its garden bed. It is
aimed at the rigging zone (the site JSON says so: q1 is 3.5 m inside the
radius), so on that yard the funnel is a hardware-load story, not a
garden-exposure one.

Selftest 374 passed / 0 failed / 0 skipped (362 baseline + 12).
2026-07-18 16:16:29 +10:00
type-two
6825794d98 Lane B S14 gate 2.1: SCORE IT — shared scorecard engine + editor panel
scorecard.js: the audit run-logic that audit.html had grown moves into one
shared engine both front-ends import (build the scoring world, sweep, fly the
winners, pick the best line, judge the separation block). Takes a site OBJECT,
not a path — loadSite always returned an object, so the fetch was never the
audit's business. Builds its OWN dressed world on a re-pointable wind proxy so
the editor's calm stub can never reach a score; every m/s still comes from
windForSite() via sweep.js/gardenfly.js.

editor_score.js: front-end only, zero audit logic. Mounts at A's reserved
order 40, renders the card with A's contract classes, says the funnel state
first and loudly, and marks itself stale when the yard changes underneath it.
2026-07-18 15:56:50 +10:00
type-two
edb3377818 Merge remote-tracking branch 'origin/lane/d'
# Conflicts:
#	THREADS.md
2026-07-18 14:26:16 +10:00
type-two
646efa7244 Merge remote-tracking branch 'origin/lane/b'
# Conflicts:
#	THREADS.md
2026-07-18 14:26:16 +10:00
type-two
68edc607e7 Merge remote-tracking branch 'origin/lane/c'
# Conflicts:
#	THREADS.md
2026-07-18 14:26:16 +10:00
type-two
cc9849d538 Sprint 13 gate 1.4: the played run is the ruling's centre — canon written, pin labeled sim-chain, THREADS
- Played night 4 on the pinned line through the real UI (D's protocol):
  hp 55.7 WIN, 2/4 lost, pyrrhic verdict — the ~8 gap to the sim chain is the
  t=38 crate + ponding landing on p2's 4% margin. Ruled deliberate: the wild
  night sells no clean line; the pyrrhic win is the intended reading.
- Site block records _playedReference; a.test pin re-labeled as the
  deterministic sim-chain tripwire whose margin is what keeps the PLAYED
  night over the win line. Band 18-45 ruled an availability floor (B's 1c).
- THREADS: full second ruling, night-5 canon, Sprint-14 notes.
2026-07-18 14:18:37 +10:00
type-two
80b53e9547 Sprint 13 gate 1.4: NIGHT 5 RULED — the garden is beyond saving BY DESIGN, and the verdict teaches it
- week.js night 5 carries gardenBeyondSaving: true with the measured case
  (bare 0.0, best buyable 27.7 vs WIN>=50 — B's game-true table, my probe
  agreeing) and the arc argument; nightAt defaults the flag FALSE so new
  nights can never inherit the excuse. Brief levels with the player up front.
- verdictFor honours it on a LOSS only: no more 'the hail fell where your sail
  wasn't' on a night where no placement wins; a 4/4 hold is credited as the
  whole job, a torn sail is still named and owned. A win ignores the flag (a
  stale flag must never eat a real win). Asserted both ways + mutation notes.
- GARDEN_DRAIN untouched, per the standing ruling; icenight hail budget also
  untouched — the loss is the design, so the surfaces now say so.
2026-07-18 14:11:58 +10:00
type-two
1c0fcc757b Sprint 13 gate 1.4 RE-RULED on B's skew finding: the pin measures the game now, and the target is restated
- a.test's separation flight had the phantom 12 s settle B caught (SailRig
  samples wind at its INTERNAL clock; the storm flew 12 s off the authored
  curve). Settle deleted — commit->attach->storm from t=0, C's corrected bench
  pattern; venturi wired from site data so the flight can never assume a yard.
- Game-true the line reads 63.6, not 68.3 — FULL (>66) was met by the harness,
  not the game, and the re-sweep says nothing buyable reaches 66 (the twin-post
  covering quad tears at 6.9 kN). Target RESTATED to the game's real stakes,
  with headroom: held >60 and 0/4 (win line 50; measured 63.6), bare <50
  (35.7), separation >=25 (27.9). Field names kept — B's gardenfly reads them.
- DESIGN CANON in the site block: the wild night's garden is WON SCARRED,
  never FULL; the week's arc is full/full/full, won-tattered, beyond-saving.
- p5 _why table corrected to game-true numbers (first sweep ran ~4 HP hot).
2026-07-18 14:11:58 +10:00
type-two
e1e14018bf Lane B S13: THE AUDIT REWRITE — the audit predicts the garden, on C's corrected picture
The scoring quantity is the FLOWN garden state now, not static cover%.
New gardenfly.js: windForSite (C's shared builder — venturi + shelters, one
copy in the repo) → real attach (throws on D's skipped-attach trap) → skyfx
exposure → garden.js, per candidate line; cover% demoted to a labelled
geometry diagnostic. audit.html flies every affordable line + bare bed and
judges the site's pinned separation block on the block's own storm; audit.mjs
(node) keeps fast winnability, gains p5 in its verified dump, and points at
the browser for garden truth.

On C's correction, three structural adoptions:
- windForSite everywhere (three harnesses independently mis-built site wind;
  no fourth copy);
- LIVE anchors via the re-pointable wind proxy — the audit's own frozen-sway
  remap was C's landmine 2 wearing my file's name (audit.html:69);
- the MARGIN rule (AUDIT.MARGIN = 0.15, one copy): every row prices twice —
  $hw holds vs $cleanHw with >=15% headroom — and only clean lines are
  winners; verdict code 'marginal-only' when the budget can only buy the
  knife edge (D's 39.8-TATTERED wild night, C's dead 91.9 headline).

Also game-true flight: the phantom 12s calm settle is GONE from the sweep
(commit->attach->storm is one keypress; the settle skewed every storm sample
12 s off the authored curve — SailRig samples wind at its INTERNAL clock).
That skew is not academic: it is a.test's separation-flight harness too, and
gardenfly.selftest now pins BOTH chains (game-true 63.8 tattered vs skewed
68.4 'full' on the pinned p5 line — the disagreement is flagged to A in
THREADS, not overruled here). The pinned recipe also sweeps regardless of the
18-45 band (it is 45.9 m² — the band predates p5; flagged to A).

Selftest 356/0/0 on the scratch merge (b+a+c@45bdc2d). Mutation-checked:
MARGIN=0 reddens the margin test (node); dropping siteDef from gardenfly's
windForSite call reddens the venturi test (browser, exact test named).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 13:50:15 +10:00
type-two
25eefccc69 Lane B S13: kill the marked fake — garden model comes from A's garden.js everywhere
garden_probe.html drops its TEMPORARY COPY of HAIL_WEIGHT/RAIN_WEIGHT/
GARDEN_DRAIN and flies main.js's own createGarden (imported from garden.js),
so the probe's hp/state/damage-split are the model's, not a re-derivation.
balance.test.js's local GARDEN_DRAIN/W_HAIL/W_RAIN copy re-points at the
garden.js exports (A's flag on balance.test.js:38) — a future retune now
moves this suite the day it lands instead of drifting silently.

Selftest 351/0/0 on the scratch merge (b+a+c) — the constants are identical,
so no number moved; the SOURCE moved.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 13:50:15 +10:00
type-two
45bdc2d079 Lane C S13 correction: the bench flew site_02 funnel-OFF and tree-FROZEN; wild-night headline dies
D's live replay caught landmine 1: bench.js read def.wind.venturi off the
STORM def where a venturi never lives (it is SITE data, main.js:453), and the
probes never attempted it — every site_02 bench number was funnel-off, the
third harness to make B's Sprint-11 mistake. Verifying the fix exposed
landmine 2: the tools remapped anchors to a static sway:()=>pos, freezing the
gum tree whose sway is the dynamic load a tr1 rig eats (ablation on the $80
line's q4: 1.02 funnel-off/static, 1.03 funnel-on/static, 1.24 funnel-on/real
anchors). Backyard numbers matched D's UI to the decimal precisely because
p1..p4 are posts — both landmines are no-ops there.

Fix: windForSite(stormDef, siteDef, anchors) in weather.js is the one shared
wind builder (venturi + shelters, exactly main.js's wiring); all garden_bench
tools build wind through it, fly world.anchors themselves via a re-pointable
wind proxy, print the venturi in their headers, and drop the phantom 12 s
settle (commit->attach->storm is one keypress, and the settle skewed storm
sampling via SailRig's internal clock). c.test.js pins the helper with B's
strictly-raises assert against the real shipped funnel (mutation-checked).

Corrected numbers: $80 line wildnight 91.9 with q4 at 1.24-vs-1.2 rating - a
knife edge the real game falls off (D UI: q3+q4 break, 39.8 TATTERED; D's UI
is canonical there, and a corner over its rating is not a safe line either
way). Buster verdict stands (95.2 bench / 97.9 D live). Icenight 9 DEAD 2/4.
Bench still under-reads UI peaks ~5-15% on site_02 - residual in the pool,
stated in THREADS. Separation-target site_02 example withdrawn to A.

Selftest 341/0/0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 13:08:21 +10:00
type-two
482803a480 Sprint 13 gate 1 RULINGS: p5 the clothesline post; the separation target, amended and PINNED
- backyard_01 gains p5 (5.5,-2) h2.6, honest steel NE of the bed: before it the
  yard could not beat tattered on the wild night at any price (covering quads
  need a fascia corner capped at 2.27 kN; the holding line ceilings at 61.3).
  Swept against the $80 wallet; position/height table in the site JSON.
- Separation target adopted AMENDED (ruled on D's funnel-corrected picture):
  wild night, best buyable line FULL (>66) and WIN; bare bed LOSES (<50).
  Pinned as site data (backyard_01.separation names the $75 recipe
  p5=rated + p1/p2/p3=shackle) and FLOWN by a.test through the real chain:
  shop at START_BUDGET -> commit -> attach -> skyfx -> garden.js. Measured
  68.3 FULL / 0 lost vs bare 35.7 (sep 32.6).
- Full cover stays unbuyable (covering quads still pull 6.4-6.9 kN), the
  >45 m2 floor holds, and the ice night still refuses $80 (needs $105 -
  p1/p2 miss their shackles by 0.05 kN) - night 5 stays bank-funded canon.
- New asserts: M reaches the real bus / is only advertised when real;
  anchor census now 13 (5 posts). Selftest 341/0/0.
2026-07-18 12:44:54 +10:00
type-two
7acee5437b Sprint 13: garden model exported as garden.js (B's gate-1.2 blocker) + applyMute(sky,on) as a value
- createGarden / GARDEN_DRAIN / HAIL_WEIGHT / RAIN_WEIGHT move from main.js
  module-locals to garden.js, unchanged — the audit must predict the sim off
  the sim's OWN code, not a drifting copy. GARDEN_DRAIN's comment now carries
  the gate-1.4 ruling: the constant does not move; it owns the bare-bed arc.
- M-mute application extracted to exported applyMute(sky, on): calls C's
  sky.setMute when the tap exists, reports honestly when it does not, so the
  selftest can fail the wiring instead of trusting a ?.() no-op.
2026-07-18 12:44:37 +10:00
type-two
b70699189c Lane C S13 gate 2.3 + D's sliver: ambient leaves from 30 km/h; hail pool hides at zero
Leaves: a pooled handful (cap 7) streaming with the wind from 8.3 m/s — the
same number D keys the player's lean on, so the yard and the body start
telling one story at one speed. Count rides a 2.5 s speed EMA so it doesn't
strobe; own seeded rng (seed ^ 0x1eaf) so the pool never shifts the crate
sequence; bit-identical under identical (dt, t) streams (asserted).
debris.clear() rewinds the layer. dev_skyfx flies the debris layer now and
warms it to the scrub point, so ?t=50 shows the leaves t=50 would have.

D's aftermath sliver, exactly as diagnosed on lane/d: the hail InstancedMesh
at count:0 with frustum culling and depthWrite off still draws instance 0's
identity matrix — a 5 cm always-on-top speck at the origin. mesh.visible now
gates on n > 0, sky.hail joins the public fx object, and a whole-storm assert
demands hidden-at-zero / shown-when-falling in both directions (gated on
floor(1300 x intensity) >= 1: a burst's first frames honestly draw nothing).

THREADS: gate-2 wrap entry — D's judging unblocked, A's M key live.
Selftest 340/0/0 (+2 tests). Mutation-checked: dropped stepLeaves, a 3 m/s
threshold, and an always-visible hail pool all go red.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 11:51:26 +10:00
type-two
c2a19df7d5 Lane C S13 gate 2.2: the front wall grounds on the horizon, ends feathered
Two QA sightings from in-yard eye height, both geometry, both reproduced on
dev_skyfx before touching anything. (1) The band stopped AT the camera's
horizontal plane, but the ground's true horizon sits ~0.6 deg below it (the
dip), so a bright sky sliver showed under the base — the floating slab. The
band now overshoots to ~12 deg below the equator; the ground occludes the
overshoot by depth, and it survives scale.y's 0.3 rise floor. (2) The arc-end
fade (sin^0.75) left the outer tenth at ~0.36 alpha — the hard vertical seam.
Now sin^1.6 with the arc widened 2.2->2.6 rad so the solid core keeps its
span while the ends drop under 0.12.

Verified by eye on dev_skyfx (southerly t=24, front 0.51): base meets the
ground line, no sliver, no seam. Selftest 338/0/0 (+1 test pinning both in
numbers). Mutation-checked: the old thetaLength and the old exponent each red.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 11:30:48 +10:00
type-two
a861151a4a Lane C S13 gate 2.1: the storm grade — the night finally looks like a night
QA played the southerly at 65 km/h under a noon-blue sky with razor midday
shadows. The arithmetic said why: sky, sun and hemi all multiplied the weather
by the author's palette (darkness 0.5 on the southerly), so the dial could
zero the weather's say — 0.69 storminess became a 0.35 nudge. The grade floors
it (storminess x lerp(0.5, 1, darkness)): a full gale now goes at least half
way to slate on any palette while the wild night keeps its authored black.
Plus the two things nothing keyed at all: the sun disc lerps toward slate and
sun.shadow.radius lerps 1->7 on storminess (PCFSoft, so it is real blur).
Deterministic — pure in t, same curve the rain uses, exposed as sky.stormGrade
above the render gate for headless reads. dispose() hands back intensity,
colour and radius exactly.

Verified by eye in dev_skyfx: southerly t=55, 18.5 m/s reads slate weather.
Selftest 337/0/0 (+1 test). Mutation-checked: the old formula and a dropped
radius line both go red.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 11:21:53 +10:00
type-two
8a3dc323bc Lane C S13: sky.setMute(on) — the tap A's M key was waiting on
One flag plus a gain write, but the flag is the point: a mute pressed on the
splash screen, before the first gesture builds the audio graph, is remembered
and honoured by unlock(). main.js already feature-detects sky.setMute, so the
M key lights itself up on this landing. audio.masterGain getter added so the
assert reads the real bus, not the flag back.

Selftest 336/0/0 (was 335, +1 test). Mutation-checked both ways: a no-op
setMute and an unlock that forgets a pre-unlock mute both go red.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 11:11:39 +10:00
type-two
7758b65331 Lane D: the player leans into the wind (gate 2.4 — as a root pose, not a clip)
The QA pass's biggest player finding: 'stands bolt upright and unbothered'
at 65 km/h. E rendered the two Mixamo lean candidates and rejected both on
screen, and proved the deeper reason a clip can't do this: _rotOnly drops
Hips.quaternion from every clip at load, so a baked lean is discarded, and
a clip's lean axis is fixed while the wind's bearing swings. So the lean is
the ROOT, the same machine as the knockdown pitch and climbY lift.

sim (player.sim.js): sim.lean 0..1 + sim.leanDir, eased over leanSecs.
Magnitude off windBase (the ~4s EMA), NOT raw windSpeed — measured: raw
strobes the posture 8-18x/min across the storms, windBase settles it to 0-2
changes per 90s. Sustained wind = posture; the gust is already the
stumble/knock event. Suppressed on your back (pitch owns the body), braced
(TakeCover is its own hunch), and up a ladder. Bands land on the storm
rating ladder: night 1 upright, 2-3 lean, 4-5 in the gale.

view (player.js): a foot-pivot tip toward leanDir, max leanMaxRad (~17deg,
a third of a knockdown's pi/2), composed under the yaw. It's the else branch
of the pitch tip by construction — the sim guarantees lean=0 whenever
pitch>0, so they never fight.

Three asserts: magnitude scales with sustained speed, a lone gust does NOT
snap you over (windBase inertia), and it's downwind + suppressed when
floored/braced/aloft. Mutation-checked: forcing lean to 0 reddens two.
Verified live at 71 km/h — leanDir dot wind = 1.0, the figure visibly
braces. Selftest 340/0/0. This is the root lean E asked me to wire first;
whether their WindPosture additive follows is a look-at-it-together call.
2026-07-18 01:33:47 +10:00
type-two
701403ab1d Sprint 13 gate 2.5: two camera fixes — spawn framing, aftermath cloth-swallow
SPAWN FRAMING. The QA pass: "a pole dead-centre through the player on every
single first impression." Measured, not eyeballed — player spawns at (0,6)
looking up the yard, and post p3 stands at (0,7), one metre behind, dead on the
view line. spawnYawFor() sweeps outward from the ideal frame (camera opposite
the bed, so the player stands in front of what they protect) and takes the
first yaw that clears every obstacle. Per YARD, off site data: site_02 spawns
elsewhere with its posts elsewhere, so a hand-tuned constant would frame the
corner block by luck.

Two numbers here are measured and both were wrong first. Clearance 0.6, not 1.1
— my first pass asked 1.1m and produced a WORSE frame than the bug, swinging
105° to stare at a fence, because the greatest distance any obstacle can have
from the camera->head line is its own distance from the player, and p3 is only
1m away, so 1.1 was unreachable by construction. maxOff 90° — a frame turned
more than a quarter-circle off the bed is no longer a frame of the bed, so the
"roomiest" fallback is bounded to the arc. The assert reproduces the 105° from
the real yard and pins that the cap brings it to 68°.

AFTERMATH CLOTH-SWALLOW. "The dead draped sail can swallow the camera whole."
The camera has collided with the house since Sprint 2; the sail was just never
in the list. While up it hangs above head height and nothing notices — but a
FAILED sail lies in the yard at head height, the one moment you most want to
look at it. refreshCameraSolids() adds the cloth to the set, called from both
rebuilds (loadSiteInto makes new world.solids, rigSail makes a new cloth; either
alone strands the camera on a disposed mesh). Its bounding sphere recomputes
every frame, so the raycast reads live geometry. Verified live: camera solids
9 -> 10 the moment a sail rigs, and the cloth is in the set.

Added camera.solids getter so that was checkable at all — "did the sail join
the set" was otherwise a claim with no seam. And hoisted sailView's declaration
with rig/rigging: loadSiteInto's boot-time call reads it before any cloth
exists, which is a TDZ throw (page goes blank) that the same block already
documents for rig/rigging.

selftest 339/0/0 (was 338).
2026-07-18 01:18:29 +10:00
type-two
05337c2989 Lane B S13: consumable spares — close D's free-infinite-spares hole (count half)
D measured it live: interact.js gated the shed-table spare on !player.carrying
alone, never reading the count, so a player who bought zero walked off with
unlimited free shackles mid-storm — the spare economy the ladder cost is
balanced against, free on the spare side, on the public deploy.

My half of the cross-lane seam: RiggingSession gains sparesRemaining (getter)
and takeSpare(). `spares` is the shed-table count, so takeSpare() decrements it
— which also fixes a second bug for free: main.js:720 refunds
session.spares * SPARE_COST as "a spare you never had to use", so you were being
refunded for spares you'd already spent. Decrementing makes that honest with no
change to main.js.

Wiring stays A's (thread the session into wireYardActions) and the gate+consume
stays D's (two lines, posted in THREADS, D offered). Asserted: take-without-buy
fails, table runs dry, consumed spare no longer refundable.

rigging.selftest 21/21.
2026-07-18 01:09:51 +10:00
type-two
7048ad6482 Lane B S13: gate-3 smalls — rig.t was a determinism bug, + prep readouts
Four of the QA/D "smalls", one of which was not small.

rig.t never reset between nights: main.js builds one SailRig at boot and
re-attach()es it each night while step() runs in every phase, so the clock ran
on and night 2 sampled its storm from wherever night 1's aftermath left off —
30-50% weaker, past the end of the curve, and offset by however long the invoice
was on screen. A determinism break. Fixed in attach() (zeroes t and _acc);
asserted as "two identical nights, one rig -> byte-equal traces", mutation-checked.

standing-kN preview (D's "$60 blind"): the prep panel shows each corner's
still-air load vs its effective rating, and a summary line, so the tension dial's
static cost is visible before you pay. At 1.4, p4 reads 1.62/1.2 kN !! RIPS AT
REST. session.standingLoads() measures on unbreakable hardware (measuring load,
not rehearsing failure). Cross-checks D's in-play 46m2 number (1.41 kN) exactly.

Panel honesty (D): prep list prints effective rating with the hint shown as its
discount (cb1 0.12/0.3 kN×0.22) — the weak-link arrow now explains itself.

setHardware() throws on non-shop hardware (incl. lookalikes — the shop compares
by identity) instead of silently no-opping; player-reachable failures still
return {ok:false}. balance.test documents the old silent-fail bite.

BLOWN label collision (hud.js — A's file, flagged in THREADS): screen-space
de-collision stacks overlapping corner bars. Verified by projecting cb1/cb2's
real positions through a swept camera — 6/72 viewpoints collide (all ~12m, where
the QA played), thresholds catch exactly those.

Gate 1.2 and the fuse-line stretch stay held on C's geometry fix + A's garden
export. Withdrew my night-1 flag (the card says it's the tutorial night by
design); the ice-night win-line question stands.

selftest 339/0/0 (335 + 4 new).
2026-07-18 01:04:02 +10:00
type-two
d7658fb7af Lane D: ladder survives a mid-carry knockdown, and the prompt names the right bracket
SPRINT13 gate 2.6 pool, both found by the integrator's QA and both silent
(no error, no red, just a mechanic that stopped):

- The ladder left the game if the wind took you over while carrying it.
  knockdown() -> drop() nulls player.carrying but nothing told the ladder,
  so state.carried stayed true: the mesh hid (visible = !carried), pos()
  returned null so there was nothing to walk to, and canUse gated on
  !carried so it could never be picked up again. Swept the whole yard post-
  knockdown: no usable ladder offer anywhere, while ladder_place still said
  'it's by the shed'. On the corner block that silently ends the site's
  thesis (cb1/cb2 are bracket work). Fix: reconcile against the player's
  hands every frame in update() and let the ladder fall flat where it was
  lost, findable and re-takeable. Verified in the running game.

- The place label hardcoded 'the fascia' — a lie on a carport beam. Now
  keyed on anchor.type (a checked enum since Sprint 11): house -> the
  fascia, carport -> the carport bracket, unknown -> the bracket (vague but
  never false). And it stops claiming 'by the shed' once the ladder is
  dropped in the yard.

Two new asserts against the REAL createLadder, not the fake: the drop-
reconcile and the label. Mutation-checked — reverting the reconcile turns
both red on the right assertions. Selftest 337/0/0 (335 + 2).
2026-07-18 01:00:01 +10:00
type-two
6e5f4b5222 Sprint 13 gate 3: the front door — splash, pause, touch notice, Enter pinned
The URL is public, so this is the first thing a stranger meets.

ENTER, PINNED. The exploit shipped because the rule lived in a keydown closure
inside boot(), behind a canvas and a WebGL context — nowhere a test could
reach. That's WHY it went out: not carelessness, no seam. `enterCommits(phase,
cardOpen)` is a value now, asserted exhaustively over PHASES so a sixth phase
is covered the day it appears, and the safe default (may not advance) is the
one it gets. Mutation-checked: reverting the guard reddens forecast, storm and
aftermath.

SPLASH. A cold visitor used to land on a job sheet — an invoice-shaped card
from a business they'd never heard of, quoting money for a job nobody had
explained, controls revealed later on prep's help line. It's a good SECOND
card. One more .card, not a menu system: there is nothing to configure. Built
on E's letterhead because the masthead IS the game's name. boot({splash:false})
for benches and D's harness.

P PAUSE — and the test is the story. The pause lives in frame(), frame() is
only called by rAF, and rAF DOES NOT FIRE IN A HIDDEN TAB. My first probe drove
the real loop and reported success: simT advanced 0 while paused... and 0 while
running. It was measuring a frozen tab, and it would have passed no matter what
I wrote. So the rule is a value: accumulate(acc, raw, paused). Paused DRAINS
the accumulator rather than carrying it — a kept `acc` spends time that elapsed
while you were paused on the first frame after resume, which is a free sixtieth
of a second of storm and exactly how two runs of a deterministic sim come to
disagree. Storm-only, and cleared at every phase change: a pause flag left set
where P is inert would be a soft-lock built out of a comfort feature.

M MUTE — wired, honest, and not advertised yet. C's bus has no tap: createAudio
holds `master` inside skyfx's closure and exposes only unlockAudio(). Asked in
THREADS. `sky.setMute?.(on)` behind a key the splash promises would be D's
night-3 soft-lock with a keycap on it — `rigging.setWorld?.(world)` sat in this
same file for a sprint doing nothing, silently, because ?.() on a missing method
looks exactly like a call. So the UI asks before it advertises, and M lights up
by itself the day C lands the tap.

TOUCH NOTICE. The naive check is the bug: `(pointer: coarse)` asks what the
PRIMARY pointer is, so a touchscreen laptop answers coarse and gets locked out
of a game it can play — worse than the dead canvas it replaces. `(any-pointer:
fine)` asks whether a mouse exists at all. Detection errs toward letting people
in; unknown browsers get the game, not a lecture.

DEV LINE. Out from under B's anchor panel (both were top-left, typesetting on
each other), and off unless localhost or ?dev=1 — a stranger on partly.party was
reading "60 fps · storm 12.3s · debris 4" on the front door.

selftest 338/0/0 (was 335).
2026-07-17 23:59:27 +10:00
type-two
5859bc83fa QA pass: fix Enter storm-skip (deployed), log findings; Sprint 13 plan + prompts 2026-07-17 23:26:47 +10:00
type-two
fee687d550 Retitle the game page: HARD YARDS (release name; SHADES stays the repo name) 2026-07-17 21:37:23 +10:00
type-two
688e821fcb Sprint 12 integration: merge all lanes (335/0/0), skyfx effective-rating creak, [I] entry 2026-07-17 21:17:45 +10:00
type-two
28ef4ef138 Merge remote-tracking branch 'origin/lane/b'
# Conflicts:
#	THREADS.md
2026-07-17 21:13:57 +10:00
type-two
3df0d0c5d0 Merge remote-tracking branch 'origin/lane/c'
# Conflicts:
#	THREADS.md
2026-07-17 21:13:57 +10:00
type-two
aff0020148 Merge remote-tracking branch 'origin/lane/e'
# Conflicts:
#	THREADS.md
2026-07-17 21:13:57 +10:00
type-two
38b6fe9c87 Rule the gutter $90 (E's proposal adopted); teach collateralFor/wreckStructure the house; action B's effective-rating flag
Gate 3.3: collateralFor('gutter') returns {90, 'the gutter'} — canonical in
backyard_01.json beside the reasoning, GLB extras as fallback, per E's
collateralKey chain. House wreck wired on the carport's swap contract
(no-op until lane/e's GLB merges; window_glow state copied). site_02 stays
null: no house, no gutter. hud.js bars + main.js verdict now read
rating × ratingHint, matching sail.js's failure line (B's flag).
selftest 319/0/0.
2026-07-17 20:45:49 +10:00
type-two
807de80969 Wire the kid's bike: site key + loader, gnome pattern; decline the $60
Gate 3.2: bike_kid_01_v1 on backyard_01's south fence, rotYDeg 180 so the
baked -Z lean lands ON the fence — bars measured to the rail (0.286 m
reach, z 9.69 -> grip at 9.98), tyres 0.31 m clear, between posts. Unpriced
by ruling: the sim can't knock a bike over, and billing an unseen event is
the lie the invoice kills; the windward-fence pose is the honest one.
a.test pins lean direction, standoff band, and the ruling. 318/0/0.
2026-07-17 20:34:47 +10:00
type-two
3d89ce8fe8 Paste E's letterhead/invoice kit into hud.js; rule HARD YARDS + sequential ABN
Gate 3.1: the CUT block from tools/jobsheet verbatim — letterhead, billto,
brief, sched, .void, AMOUNT DUE, terms. Docket numbers shared across the
pair (JOB/INV 1040+night); week.js nights carry an addr. Cards scroll when
taller than the window (safe center + overflow). Both rulings adopted and
pinned by DOM asserts; selftest 317/0/0.
2026-07-17 20:27:18 +10:00
type-two
e5c0e3ffee skyfx: the change announces itself — a front wall in the sky (gate 3.4)
Every windchange event gets an in-world tell: a dark bank standing on the
horizon in the quarter the new wind comes FROM, rising across the 12 s
before the change and clearing after the swing, plus a low distant rumble
layer in the audio. Soft-edged via vertex alpha (the hard band read as a
floating rectangle on the bench). Deterministic off (dt, t): progress is a
pure function of t, azimuth comes from dirAt at a fixed post-change
instant. Night 2 teaches the tell at ~18 s; night 3 shows the same wall at
t=6, which is 'looks like night 2 and isn't' made visible.

dev_skyfx.html: the skyfx bench — one storm, bare ground, scrubbable
clock, so the tell can be judged by eye without playing to night 3.
Two new asserts in c.test.js (window, monotonic rise, direction, mesh
orientation, changeless storm stays clear, bit-exact determinism).
Selftest 322/0/0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 20:24:51 +10:00
type-two
b7f065cd57 e.test: pin the gutter price band, the DOWN/UP swap truth, and anchor absence in the wreck
Five new asserts (Lane E 67 -> 72; selftest 315 -> 320):
- price exists, sits between gnome and carport, keyed to the anchors'
  collateral string, wreck carries the same bill and names its twin
- the torn gutter is DOWN in the wreck and UP in the intact house — negative
  control run: rebuilt with the ground run at eave height, selftest went red
  ('gutter_down tops out at y=2.68', 1 failed / 319 passed) while the Blender
  verify PASSed the same bad file (plan position unmoved, no box span changed);
  restored build round-tripped to identical hashes
- wreck height equals house height (a house does not fall when its gutter does)
- no fascia_anchor_* survives into the wreck
- house pair added to the shared ground-plane test

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 20:21:40 +10:00
type-two
4d1a2d7aa9 Gutter priced and wrecked: GUTTER_COLLATERAL=90 baked with reasoning; torn-gutter house wreck, carport pattern
The fascia anchors have said collateral:"gutter" since Sprint 6 and nobody
priced one — collateralFor('gutter') returns null, backyard_01's house is a
free failure. Proposal rides in the asset like the carport's did: 90, between
the gnome (25) and the carport (180), with the band reasoning next to the
constant. A rules the number.

house_yardside_wrecked_v1: same origin/footprint/facade (shared _house_facade
helper — intact house BIN chunk byte-identical to the committed GLB, only the
three new price extras differ), fascia torn through the anchored span, left
run hanging by its last bracket, right run kinked ON the grass, downpipe out
of plumb, offcuts flat on the lawn. No fascia_anchor_* empties survive — you
cannot re-tie to a ripped eave.

collateral_key is new and explicit: unlike the carport, the structure and the
thing you take have different names, so the key names which collateral string
the value prices.

All 33 GLBs byte-identical across two full factory runs; the house pair also
byte-identical between --only and full runs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 20:21:24 +10:00
type-two
9700b40a30 Wire ratingHint into the failure threshold: the ratings are real (A's ruling)
sail.js _checkFailure now fails a corner on load > hw.rating * anchor.ratingHint
(read live from the anchor — dress() mutates in place). Consequence asserted,
not the formula: on site_02's carport line (uniform rated shackles, funnel on,
12 s calm settle) a cb corner blows before any q corner — at DEFAULT tension
(t~29 s into the storm, honest posts hold) and at max. In-suite control pins
the attribution: the same rig with every hint forced to 1 holds 4/4, so the
break is the hint, not the load. Mutation-checked: unwiring the line fails both.

rigging.js weak-link (D's #7): summary.weakest reduces on rating * ratingHint
via _effRating, not bare hw.rating — uniform hardware now flags the genuinely
worst steel instead of the first click. Mutation-checked.

site_audit prices the anchor, not just the steel: tierFor(peak, hint) needs
h.rating * hint >= peak, both front-ends carry ratingHint through (browser off
world.anchors, node dump extended with the live-dumped hints), rows print the
hint. sweep.selftest asserts hints reprice without touching peaks and land
over-ceiling corners in unholdable. Mutation-checked.

Selftest 319/0/0 (315 + 4).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 20:19:01 +10:00
type-two
8208c80d78 storm_envelope: the wind side of the failure envelope, per anchor (gate 2.4)
Second harness on B's post-ratingHint audit: measures what the STORM
delivers at every dressed anchor — peak speedAt, tPeak, dynamic pressure
(downdraft folded back in), dose, and Pa/hint (the wind-side failure-order
prediction under load > rating x ratingHint). No cloth, no rig, no tension:
independent by construction.

Browser front-end dresses the yard the way the game does (createWorld +
dress) and wires venturi + tree shelters exactly as main.js:446-447 does.
envelope.selftest.js wired into c.test.js, every assert written to fail if
a wiring line is deleted (sweep.selftest's pattern). Selftest 320/0/0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 20:10:17 +10:00
type-two
e99f029206 Sprint 11 integration: merge all lanes, wire leadFor + setBudget, untrack launch.json; Sprint 12 plan + prompts 2026-07-17 18:08:32 +10:00
type-two
d194594817 Merge remote-tracking branch 'origin/lane/b'
# Conflicts:
#	THREADS.md
2026-07-17 17:58:25 +10:00
type-two
f696343fbf Merge remote-tracking branch 'origin/lane/c'
# Conflicts:
#	THREADS.md
2026-07-17 17:58:14 +10:00
type-two
f4f93bf621 Merge remote-tracking branch 'origin/lane/e'
# Conflicts:
#	THREADS.md
2026-07-17 17:58:03 +10:00
type-two
a74075cda5 Sprint 11 shepherd: C's venturi correction taken, D's two landmines defused
C AND D BOTH LANDED WHILE I WAS IN GATE 2, AND BOTH CORRECT ME.

VENTURI — reverted to axis 2.1 / gain 1.5. C reached the same physics I did,
independently (|dot| means an axis is a line, mod PI), and then went one step
further and caught my reasoning: -1.08 was never a proposed AXIS. It's
dirAt(40), the southerly's HEADING once the change settles. 2.1 is the GAP'S
ORIENTATION — site geometry, which doesn't move when the wind does. They're
different quantities that sit a hair over PI apart only because the gap was
BUILT along the southerly.

So "measured beats authored" was the wrong rule for this question: I'd have
swapped a geometry fact for a weather fact because they nearly coincide, and
the site's axis would silently mean the wrong thing the day someone authors a
storm from another quarter. "A number gathered from the wrong harness" caught
both of us on the same question from opposite sides in one sprint.

Gain reverted too, and that one I simply jumped: it's a balance lever, B's audit
calls it, and C's standing offer is to drop to 1.35 only if no $80 line survives.
B hasn't run. D then measured the site winnable off honest anchors at 98% — so
the funnel isn't killing the line and the gain stays.

D's LANDMINES, both mine, both in gate 2's path:
 · CALM_STORM loaded only because night one HAPPENED to be storm_01_gentle.
   Gate 2 rewrote every night entry. Drop gentle and windTime() throws every
   non-storm frame on "Cannot read properties of undefined (reading 'duration')"
   — naming neither storms, nor calm, nor the week. Now explicit in the preload,
   and the assert calls stormsToPreload() with a gentle-free ladder, because
   asserting it against the shipped NIGHTS passes either way. That's how it hid.
 · boot({site}) was a documented lie that failed INVERSELY — markers on the
   requested yard, world on the week's. Honoured through the week now (seek to
   that site's night), because a site without its night is half a game: no
   client, no brief, no storm, no bank. Plus opts.bank, so D can boot the corner
   block at the $237 they measured instead of night one's $80 and OFF THE JOB.

RATINGHINT — D is right that nothing reads it, and the naive fix is worse than
the bug: honest posts are built from site JSON, never touch adoptAnchor, so
their ratingHint was undefined — and `load > hw.rating * undefined` is
`load > NaN`, always false. The strongest anchors would have become UNBREAKABLE
while the carport became the only thing that can fail: the trap would look like
it works, for the wrong reason, and read as a successful playtest. Every anchor
carries a real number now (DEFAULT_RATING_HINT), pinned, so the wire is safe
before it's made. The wire itself is sail.js — B's — and the ruling is in THREADS.

selftest 306/0/0 (was 304).
2026-07-17 17:51:30 +10:00
type-two
4095df8b10 Lane B S11: audit the venturi, land setWorld/setBudget, revive the harness
site_audit's headless false-negative is fixed by the browser front-end
(A's loadSite/createWorld landed in main — that was all it waited on), and
fixing it surfaced a worse bug underneath: the sweep built its wind from the
STORM def alone and never called setVenturi, which main.js:424 does at every
site load. The venturi is SITE data, so every site_02 audit ever run — including
the numbers quoted at C in SPRINT10 — was of a corner block with no gap. The
SPRINT6 p1=7.4kN failure inverted: there the tool called a good site unriggable,
here it called a mean site cheap.

- sweep.js: setVenturi on both winds (the gap doesn't switch off for the settle);
  both front-ends pass the site's funnel and print it, so a funnelled run is
  legible as one.
- sweep.selftest.js (new, wired into b.test.js): the auditor gets an auditor.
  Funnel must strictly raise every corner; a funnel-less site must be
  byte-identical. Verified it fails when the setVenturi calls are deleted.
- rigging.js: setWorld(world) + session.setBudget(n)/setAnchors() — A's two asks.
  Markers are rebuilt (the anchor set changes across sites) and disposed; stale
  picks die with the old yard. Deletes A's "one private touch" into this module.
- dev_rigging.html: dead since SPRINT10 (called createWorld without a site, which
  now throws) and nothing noticed, because a dev page has no selftest to go red.
  Fixed, dressed, and given the site switch it should have had (N = next yard).

Verdict for C/A, in THREADS: site_02 PASSES with the funnel on — 64 of 66 lines
affordable, cheapest $20. The funnel kills nothing, so the gain does NOT need
dropping and E's tree stays put. It's a reach problem, not a gain problem: the
throat's radius is 5 m and the bed centre is 6.08 m away, so the gap screams over
the carport at +50% and reaches the garden at +0.0%.

selftest 300/0/0 (296 + 4 new).
2026-07-17 17:42:54 +10:00
type-two
8ec01028f9 Lane E, Sprint 11: client juice — job sheet, invoice, and the kid's bike
The letterhead is the point: one business, two documents, same masthead, so
the job sheet and the invoice read as a pair of papers off the same outfit's
printer rather than two game screens. DESIGN.md has opened with "you run a
small landscaping outfit" since day one and it has never been on the glass.

tools/jobsheet/ — paste-ready for Lane A between CUT markers, purely additive
(.card/.row/.ledger untouched), rendered to four PNGs before handover, each
state addressable as ?v=. The ledger already WAS an invoice; it just never
said so. The line worth keeping is .void: a bonus that was on the job sheet
and didn't survive the night, struck through in the same column as the money
that did — a bonus you never watch fail is one you never learn to protect.

Two design calls left as A's with the reasoning beside them, same as the
carport's $180: the business name (proposal: HARD YARDS) and the ABN
(sequential digits on purpose — a plausible one could be a real business's).

bike_kid_01.glb — 1.15 x 0.33 x 0.74 m, 700 tris, lean baked in because a
bike doesn't stand up on its own. Two bugs caught by LOOKING at the render,
neither catchable by the existing suite: a comment claiming a step-through
above a diamond frame, and a fork with negative rake that drew head tube and
fork as one pole through the front wheel. Both passed the Blender verify —
a bounding box can't answer "is this the right shape".

The lean is an export-time axis fact (+Y in Blender is -Z in three.js), so
e.test.js now pins it in browser coords. Proved the assert fails: rebuilt at
lean=0 and Blender still said PASS while the selftest went red.

selftest 299/0/0. 32/32 GLBs byte-identical across three full factory runs.
2026-07-17 17:39:58 +10:00
type-two
6bcc2540cf Sprint 11 gate 2: the job sheet — five nights become five jobs
DESIGN.md has opened with this since line 3 — "every callout is a different
site, a different client" — and the game had the site and not the client. The
forecast card is a job sheet now and the aftermath is an invoice.

THE WEEK, once it was asked to name a client, mostly wrote itself: four nights
are the same yard, so they're one client on retainer while they're away (the
Hendersons, in Cairns from Friday — SPRINT11's own line). Night 3 is a
different SITE, so it is necessarily a different CLIENT: a short-notice callout
to a corner block you've never rigged, on the night the change comes early.
That isn't flavour bolted on, it's what `site: site_02_corner_block` already
meant, finally said out loud.

BUDGET Y IS THE FEATURE, not the letterhead. DESIGN.md's brief is "client wants
X, budget Y, forecast Z" and this card had Z alone. The fee has existed since
Sprint 8 and the player has never seen it until the money was already spent —
you chose what to rig without knowing what the job was worth, which is a reveal,
not a decision. The sheet quotes base + garden + clean up front, and a test pins
that a perfect night pays exactly what the sheet advertised: a job sheet that
over-promises is the game lying on paper.

THE CLEAN BONUS is the new money, and it's the one number in week.js that isn't
measured. It gives the invoice three axes that mean three different things —
you turned up, their garden lived, you didn't wreck the place — and it's NOT
gated on winning, because a lost night where you broke nothing of theirs is a
different fact and deserves its own row. It also makes the carport bite twice:
180 + 20 = 200, inside E's own "over ~250 is a silent game over" ceiling.
Measured: it adds ~$100 to a competent week (385 -> 485), which makes
BROKE_BELOW's known runaway worse. I did NOT pay for it by quietly trimming
feeFor or gardenBonusMax — those carry measured evidence, and re-tuning someone
else's evidence to fund my own feature is the move this repo keeps catching. If
gate 3 says the bank runs away, this is the lever to cut first and it's data.

TWO BUGS, BOTH CAUGHT BY LOOKING AT THE CARD, neither by a test:
 · "THE HENDERSONS" over "The Backyard — Your own place". The blurb and the
   letterhead called each other liars on screen. The blurb gave way: "your own
   backyard" is a leftover from when the game had one yard, and DESIGN.md never
   wanted it. The line's job survives — night 1 is the tutorial because you know
   this yard, now from having worked it for years rather than owning it.
 · C's `lead` is a 0..1 haze dial (confidence = 1 − lead), not "nights out". I
   read it as nights, passed 1, and the sheet advertised "forecast confidence
   0%". Now 0.6, because C's own hail rule calls L > 0.55 too distant to promise
   ice, and tomorrow is exactly that. C: the slot is mine, the number is yours.

Lane C's forecastLines(def, lead) finally has a caller above 0 — two sprints
after they built it. Lane E: .letterhead / .brief / .jobsheet / .tomorrow are
structure only and yours to style.

selftest 304/0/0 (was 300).
2026-07-17 17:37:18 +10:00
type-two
4aee055f2d Sprint 11 gate 1: three rulings — enum widened, carport priced, venturi reconciled
The corner block's seams, all of them A's to close.

ENUM (D's flag). Anchor.type was a closed JSDoc enum 'house'|'tree'|'post',
so site_02 typed its carport 'post' to fit — quietly enrolling it in the
sail-post family that C's venturi and B's audit both read. Widened to E's own
asset strings (carport, carport_post) and, more to the point, CHECKED: it was
documentation for ten sprints, and documentation cannot fail. validateSite now
rejects an unknown type by name. The ladder is untouched either way — D keyed
it on `work`, the mechanism, which is exactly why that was right.

CARPORT (E's proposal, adopted). $180, with E's reasoning intact: gnome 25, a
night's budget 80, so 180 is 2.25 nights — a good week turned broke and felt
for the rest of the run, without ending a strong one outright. Under ~120 it's
a shrug, over ~250 a silent game over that teaches nothing.

The number lives in site JSON, not the GLB: sites are data, and what a carport
COSTS is this site's economy, not a property of the mesh. E's baked value stays
as the fallback and the proposal.

And it BILLS now. E shipped the trap and said it plainly — the anchors said
collateral "carport" and nothing said what one cost, so nothing scored it. You
could lose the worst steel in the game and pay for a $15 shackle. One broken
corner is enough (the gnome needs two: it needs the sail to land on it; the
carport doesn't need the sail at all), priced per structure so two beams is one
carport gone, not $360. The wreck swaps on the same event — E built it to the
same origin for exactly this.

VENTURI (C's question). There was no disagreement to settle. weather.core
aligns on |dot(wind, axis)| because a gap funnels either way through it, so an
axis is a LINE: -1.08 + PI = 2.0616, and my shipped 2.1 was the same gap read
from the other end. Took C's number because C MEASURED it off storm_03b and I
eyeballed mine at authoring time; the 2.2 deg of my eyeballing was worth
0.44 m/s. gain left at C's 1.35 — that's a balance lever, theirs with B's audit.

Also wired validateSiteWind at site load, C's ask from Sprint 10.

selftest 300/0/0 (was 296).
2026-07-17 17:20:11 +10:00
type-two
8e23bf4702 Land leadFor(): the job sheet's per-night forecast lead
forecastLines(def, lead) always took the param; what was missing was the
map from "this night is N nights away" to a lead. leadFor(nightsOut,
weekNights) is that map, linear onto forecastFor's documented 0..1 domain
so tonight is exact and the week's far end is lead 1. Tomorrow at a
five-night week reads 0.25 — 75% confidence, sustained band ~±8%.

hud.js is Lane A's; this only exports the argument.

The new assert pins the property that makes a band safe to print: it
RESOLVES. lo rises and hi falls monotonically onto the truth as the night
approaches, because the centre-wander is the same seeded draw at every
lead — so tomorrow's band always contains tonight's and the card never
jumps. Verified against a mutation that reseeds per lead (caught).

Its containment half is honestly decoration today and is labelled as
such in the test: 0.6-of-half-width wander means c±w never crosses v, so
deleting the clamps does not make it fail. Kept as a contract guard for a
future rewrite of band(), not counted as coverage of today's code.

Selftest 298/0/0.
2026-07-17 17:14:53 +10:00
m3ultra
df1fd4dd25 Merge remote-tracking branch 'origin/lane/d'
# Conflicts:
#	THREADS.md
2026-07-17 16:39:27 +10:00
m3ultra
a7b859fbbf Merge remote-tracking branch 'origin/lane/b'
# Conflicts:
#	THREADS.md
2026-07-17 16:39:26 +10:00