Commit Graph

103 Commits

Author SHA1 Message Date
m3ultra
b1acbd6668 Merge Sprint 5 lanes; wire decision 13 garden drain; measure the balance gap
Selftest on merged main: 240 pass / 0 fail.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 08:52:58 +10:00
m3ultra
9010a2c784 Merge remote-tracking branch 'origin/lane/d'
# Conflicts:
#	THREADS.md
2026-07-17 08:44:27 +10:00
m3ultra
1304be76f6 Merge remote-tracking branch 'origin/lane/b'
# Conflicts:
#	THREADS.md
2026-07-17 08:44:27 +10:00
m3ultra
95b0917601 Merge remote-tracking branch 'origin/lane/c'
# Conflicts:
#	THREADS.md
2026-07-17 08:44:27 +10:00
m3ultra
c92a7a1b13 Merge remote-tracking branch 'origin/lane/e' 2026-07-17 08:44:27 +10:00
m3ultra
97ea1b668c Log ponding v1, broom API, cliff resolution, and a yard finding
Freezes the pond API for D (broom) and A (HUD), resolves D's tn-1.04
cliff as real ponding load rather than solver instability, and flags that
flat rigs die to wind before water on the current yard — so ponding is a
twist-tax today unless the yard gets a non-collinear level anchor trio.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 03:32:55 +10:00
m3ultra
670443eea0 Guard pond accessors before attach; caught driving the live game
pondMass/pondCentroid/drainPondAt/dumpPond threw when this.water didn't
exist yet — Lane A's HUD reads pondMass() every frame, including in the
forecast/prep phases before the sail is rigged. Node tests always attach
first so they never hit it; the live game did on the first frame. Guarded
+ a pre-attach assert so it can't regress. Verified in the assembled game:
a flat quad ponds 780 kg (belly sagging below ground), dumps on break,
and the merged browser selftest is 219/0/0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 03:32:02 +10:00
m3ultra
b7f93d6486 Land ponding v1: rainwater pools on flat sails and kills them
The water arc, carried since Sprint 3. Rain (Lane C's rainMmPerHour x the
exported RAIN_TIME_COMPRESSION, never hardcoded) lands on each node's
horizontal projection, runs down its steepest of 8 neighbours, and pools
where it can't get out. A flat sail's belly is a basin water flows into
and can't climb from; a hypar drains along its saddle ridge to the low
corners and off — so ponding cannot pincer §7, and measured on real yard
quads a flat rig holds 12 kg/m² vs a twisted rig's 1.7.

The 8-neighbour graph is load-bearing: a 4-way one can't follow the
saddle's diagonal ridge, so it trapped water in the gravity belly and a
hypar pooled as much as a flat sail. Steepest-GRADIENT descent (not
steepest drop) because a diagonal is √2 farther.

Gate 1, in asserts: a flat carport rig under a capped wind survives dry
(4/4) and dies wet (a corner at t=85s) — the control isolates water from
wind — and the broom saves it. Plus dump-on-break, dump-on-tension-up
(the turnbuckle counter-play), mass conservation, and a belly-tear safety
valve at 4 m of sag.

API for D and A, frozen in contracts.js: pondMass(), pondCentroid(),
drainPondAt(node, dt) -> kg-on-your-head. session.reset() for A's "play
again".

On D's tn-1.04 cliff: investigating it IS what surfaced the ponding load
regime. The 10 kN "spike" is real physics, not solver divergence — a
155 m² flat sail holding 2100 kg of water genuinely pulls ~21 kN, stays
finite, and tracks the water. So no physics-altering clamp (the
displacement clamp I tried moved the thesis 39->34%); instead an opt-in
`watchDivergence` tripwire that throws with a repro above 80 kN, live in
every selftest rig and never false-tripping, and a belly-tear that bounds
the runaway physically. Full writeup for D in THREADS.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 03:25:46 +10:00
m3ultra
7dd77775da Log hail landing, decision-13 proof, and the router forward for A
Hail is the garden-score engine (4.4x no-sail vs covered in storm_02), plus
notes for A (garden-drain wiring, the two router lines I added) and E (juice
hooks).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 03:20:04 +10:00
m3ultra
1f728e4a0f Forward hailAt/hailSize in the wind router (decision 13 plumbing)
A's router tripwire ('wind router forwards EVERYTHING the real wind exposes')
went red the moment weather.js grew hailAt/hailSize — which is the tripwire doing
exactly its job. A's own comment on createWindRouter is the standing instruction:
"Anything new on the wind contract must be added here too." Without this, the
garden score (which hangs entirely off gardenHailExposure → hailAt) would be
inert in the game while every assert passed — the precise silent-swallow this
tripwire exists to prevent, and the second time it would have bitten hail.

Two forwards, matching the existing pattern. Selftest 216/0/0, tripwire green,
and hail now reaches skyfx in-game with no console patch (verified). Flagged to A
in THREADS since it's their file.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 03:18:02 +10:00
m3ultra
f1d51d6b87 Hail — the system that makes the garden score respond to the rig (decision 13)
Rain honestly walks under a sail (a droplet's terminal velocity is ~9 m/s, so a
30 m/s crosswind blows it in at ~73° off vertical), which is why a perfect rig
scored 54% garden vs 48% for no rig at all. Hail is dense: terminal ~22 m/s, and
a dense stone couples weakly to the crosswind, so even a gale leans it ≤20°.
Steep hail is blocked by overhead cloth, so the garden score becomes
rig-responsive without faking the rain physics — and it was always DESIGN.md
canon (hail shreds gardens; drainage answers rain, later).

weather.core: `hail` block in storm JSON — authored bursts (envelopes) plus one
synced to every gust at/above `withGustsAbove`, so the biggest gusts arrive WITH
ice. `hailAt(t)` (max over live bursts), `hailSize`, validator. Gust-synced
bursts key off the deterministic gust timeline and draw ZERO randomness, so
tuning hail can't re-time the storm — asserted, same guarantee as the downdraft.
storm_02 bursts on the southerly change (peak 1.0 at t=56.5, 11.4 hail-seconds);
storm_03 a mild 0.5; storm_01 none.

skyfx: `hailVelocity` (steep, ≤20° lean, terminal-velocity reasoning cited so
nobody re-opens the rain-angle argument), a second RainShadow fed the steep
vector, `gardenHailExposure(bed, t)` in the gardenExposure mold (A wires the
drain), instanced falling stones (hidden under the cloth so you SEE the sail
working), and hail audio: ground clatter that fades as the sail intercepts, plus
the cloth DRUM that rises exactly as the sail catches hail — the "my sail is
earning its money" sound.

Decision-13 gate proven: no-sail garden takes 4.4× the hail of a bed under a
good rig over a full storm_02 (bar is ≥2×), through the real gardenHailExposure
and B's SailRig. Verified live too (router-patched): stones fall visibly steeper
than the rain beside them, and a bed-covering rig cuts hail exposure roughly in
half at the burst.

Selftest 214/0/0 (was 207); node 36/0/0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 03:14:27 +10:00
m3ultra
f2527055a5 Add hail juice, plant shred, and a way to screenshot the game
Hail (SPRINT5 §E-1) ships two ways because Lane C's rain is a BoxGeometry with a
flat material and skyfx loads no external texture at all: hail_stone_01 as
geometry that drops into their existing InstancedMesh pattern, and hail_pips.png
as a 4-cell atlas for impacts. A flat quad can't be round and an impact is round
— that's the only reason the atlas is a texture. Both come with a copy-paste
recipe, because a grep showed exactly one of my five shipped textures is
consumed: sail_weave, the one I wrote a recipe for. grass_atlas has sat
unreferenced for four sprints.

plant_shred (§E-2) is elongated after the first pass came out radial and read as
green potatoes — it's the long axis plus the midrib that says "leaf".

tools/yardshot/ finally lands the DESIGN.md pictures, carried since Sprint 2. It
does NOT touch server.py or main.js: it's a Lane E stdlib server that serves the
repo and takes POST /shot, so the browser posts a Blob and bytes go straight to
disk instead of base64 through a console. do_POST is ~25 lines and Lane A is
welcome to lift it and delete this.

Three traps found on the way, all documented: toBlob is async so the WebGL buffer
is already cleared (toDataURL is sync); a backgrounded tab lays the canvas out at
0x0 and toDataURL then returns "data:,"; rAF is paused there so step() must be
driven by hand.

Selftest 209/0/0, 36 output files byte-identical across two runs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 03:03:54 +10:00
m3ultra
4e8a98fca5 Log the broom landing, the B seam status, greyed prompts for A, and the feel pass
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 03:03:50 +10:00
m3ultra
d67126eacf Lane D: the broom, greyed prompts, and the label pass
The broom (SPRINT5 §Lane D-1, DESIGN.md's funniest correct mechanic): a third
carry type that queues behind the same hands, walk under the sail belly, hold-E
poke (Crank per E's anim_hint) → B's drainPondAt() → the water lands ON YOU,
sized to the joke: splash under 15 kg, stagger over 60, flat on your back over
120. Everything defers to E's baked metadata (carry_type, poke_tip.use on the
bristle end, anim_hint) — read, not invented. Duck-typed against the ponds[] /
drainPondAt seam I posted to B early, so it carries/walks/refuses-thin-air today
and lights up fully the moment B lands ponding.

Self-wires from createPlayer like the ladder; needs the live sail rig, which it
reads off interact.sailRig (published by wireYardActions, which main.js re-calls
through rigSail whenever attach() swaps the rig).

Greyed prompts (§Lane D-2, my offer, A's HUD hook): interact.visible() shows the
nearest UNAVAILABLE action with its reason when nothing's usable, instead of the
prompt vanishing — the confusion I logged last sprint. nearest() (what hold-E
acts on) is unchanged, so display and action stay separate. step() now reports
`usable` so the HUD can grey the radial. Fixed the ladder-place label to read as
a reason too ("the fascia needs the ladder — it's by the shed").

broom.js keeps no top-level THREE import (the vendored addons need index.html's
importmap) so d.test.js stays headless; the view loads via dynamic import that
only fires in a browser. 217/0/0 (was 207), 13 new asserts.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 03:03:02 +10:00
m3ultra
cfe80e2837 Post the router tripwire warning and Lane A's Sprint 5 dependencies
The tripwire is aimed at Lane C's incoming hailAt(): the router swallowed C's
rain API silently once already, and decision 13 hangs the whole garden score off
hail, so the same omission would recreate the exact problem Sprint 5 exists to
fix. Also records that Sprint 4's headline finding was re-measured against a true
control and survived.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 02:56:03 +10:00
m3ultra
eb6986d1a0 Tripwire the wind router; take Lane C's gardenExposure helper
The router is a hand-maintained delegation list and it has already cost us once:
when Lane C added rainMmPerHour/rainDepthMm, it didn't forward them, and nothing
went red — every suite holds a real wind, only the GAME holds the router. Lane B's
ponding would have passed every assert it had and done nothing in the yard; the
integrator caught it by hand at merge.

The class matters more than the instance. Sprint 5's headline system is Lane C's
hailAt(), and decision 13 hangs the whole garden score off it — the same silent
swallow would make rigging look irrelevant to the garden all over again, which is
the exact thing the sprint exists to fix. The assert diffs the router against a
real wind and names what's missing. Verified it fires rather than just passing:
rebuilt Sprint 4's pre-fix router and it reports exactly rainMmPerHour,
rainDepthMm.

Garden drain now goes through sky.gardenExposure() — same arithmetic, but it's
Lane C's term to own, and decision 13 extends precisely this shape, so hail lands
as one added term rather than a rewrite. Numbers unmoved: good rig 53%.

Also re-measured Sprint 4's headline finding against a TRUE control (fresh boot,
nothing ever rigged, shadow over bed 0.000) because my original control left the
previous round's sail in the shadow grid: no-sail is 48.5% vs a good rig's 53%.
The finding stands, and decision 13's premise with it.

Selftest 209/0/0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 02:55:32 +10:00
m3ultra
718f011e36 Post the broom↔pond API ask to Lane B early, per SPRINT5 §Lane B-2
The broom is this sprint's gate and B's spec says the two lanes meet in the
middle. Posting the shape now rather than at the end so B can land it while I
build against it: ponds[] with a live world pos (I have to walk under the
belly), and drainPondAt returning the kg dumped (that number decides splash vs
stagger vs flat on your back).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 02:52:36 +10:00
m3ultra
0813b18f5a Add Sprint 5 plan and lane prompts: hail and the broom
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 02:44:30 +10:00
m3ultra
f7d747a578 Merge Sprint 4 lanes; fix wind router rain passthrough; rule decision 13 (hail)
Selftest on merged main: 207 pass / 0 fail. Gate 1 verified by playing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 02:43:10 +10:00
m3ultra
0b4dd0d0bd Merge remote-tracking branch 'origin/lane/d'
# Conflicts:
#	THREADS.md
2026-07-17 02:39:41 +10:00
m3ultra
34712bf297 Merge remote-tracking branch 'origin/lane/b'
# Conflicts:
#	THREADS.md
2026-07-17 02:39:41 +10:00
m3ultra
da35a954e0 Merge remote-tracking branch 'origin/lane/c'
# Conflicts:
#	THREADS.md
2026-07-17 02:39:40 +10:00
m3ultra
ce8d22cb60 Merge remote-tracking branch 'origin/lane/e' 2026-07-17 02:39:40 +10:00
m3ultra
0ceff91d5a Lane D: the ladder sub-system (decision 12)
New ladder.js. The whole mechanic is 200 mm: the fascia bracket sits at 2.48 m,
a 1.72 m person's hands reach 2.20, and E's ladder tops out at 2.90. The asset
and the yard were already built for each other; this is the verb between them.

Carry-ladder is a second carry type, so the ladder and the spare compete for the
same pair of hands and a fascia repair costs two trips while a post repair costs
one — DESIGN.md's "limited hands" rule doing real work, and the reason the house
is the expensive anchor to depend on (which E's ratingHint 0.35 / collateral
"gutter" was already saying in the data).

Climb height is code-driven with ClimbLadder playing on top — the knockdown
precedent, since _rotOnly strips the root and a clip can no more lift the body
than Falling could lay it down. You can't brace up there (both hands on the
rungs), the wind's bar drops to 0.6x, and being blown off is a fall that feeds
straight into the existing get-up chain.

needsLadder is scoped to the fascia on purpose: a height test would have roped in
the 3.95 m posts and 5.05 m limbs, made every repair a two-trip job, and silently
invalidated the recorded §7 run — and it isn't true to rigging either.

Landed with no change to main.js: createLadder self-wires from createPlayer,
which Lane A already hands the scene, world and interact.

Two bugs found by building on my own API, both now asserted: a canUse that reads
player.state cancels its own hold (starting a hold sets busy) — it ate the climb
AND the reach gate before I keyed both on physical height instead; and onLadder
had to become height-based for the same reason. Documented on register().

Selftest 194/0/0 (was 184). Full loop driven by hand in the real game.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 02:34:49 +10:00
m3ultra
966dda9590 Post Sprint 4 gate 1 and the garden-HP finding
The face's first act was to expose that garden HP barely responds to rigging: a
perfect rig scores 53% on storm_02 and rigging nothing at all scores 48%. Cause
is measured (rain shadow decays 0.38 -> 0.04 as wind builds) and it is not a bug
in anyone's lane — but it means decision 7's headline score is nearly
independent of the game. Flagged for Lane C with the numbers; the design call
about whether garden HP is even the right storm score is left open.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 02:32:44 +10:00
m3ultra
2b66ce4b9a Give the game a face: HUD, mouse prep, forecast and aftermath (gate 1)
SPRINT4 §Lane A 1-5. Everything the game already simulated was invisible to a
stranger; you can now play a whole round with eyes and a mouse and never open the
console.

hud.js only ever READS — it takes no decisions and owns no state worth keeping,
so it can be this chatty without anything drifting. Corner load bars are
world-anchored rather than screen-anchored because a number in the corner of the
screen can't tell you WHICH shackle is about to go, and that is the entire
"...the shackle, I knew about the shackle" moment. They hold constant screen size
with distance: a house corner is 18 m away from the posts, where a world-sized
bar is a few unreadable pixels.

Prep is Lane B's createRiggingUI wired to the phase machine — their picking was
already complete, this is the hands. Forecast reads the three authored storms, so
the difficulty select came free: Sea Breeze / Southerly Buster / Wild Night with
real peak wind, gust character and change time.

Two bugs the wiring surfaced, both found by playing rather than reading:
- The rigging session survived "play again": a second round started at $35 with
  three corners already hung, and the four anchors you clicked were silently
  ignored because the session was full. resetRig() walks it back through the
  session's own public moves, so the economy stays the one source of truth.
- The forecast card's lifetime was owned by its own button, so any other route
  into prep left a card floating over a live game eating input. The phase owns it
  now.

Verified by playing it: good rig on the wild night wins at 53%, the same yard's
cheap drum-tight span loses 4 corners, the gentle night with a good rig ends at
93% "not a leaf out of place". Selftest 184/0/0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 02:32:02 +10:00
m3ultra
acb35752ee Log ponding data, the router gap, decision 7 helper and the night pass
Leads with the router allowlist gap: rig.step() gets the router, which doesn't
forward rainMmPerHour, so B's ponding would pass every assert and do nothing
in-game. Two lines in main.js.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 02:29:17 +10:00
m3ultra
1f99cd9bca Rain gets physical units for ponding; night pass on wildnight
SPRINT4 §Lane C 2/3/4.

PONDING DATA (decision 10). rainAt() was a dimensionless 0..1 — fine for drop
count and opacity, useless for water mass. Lane B would have had to invent the
mm/hr scale, which is exactly the "default-off code tuned by a constant I
invented" they rightly reverted. So the scale is storm data now:
rain.peakMmPerHour (validated 0..300; a rain curve without one is a hard error,
since ponding would silently use the default instead of what the author meant),
plus wind.rainMmPerHour(t) and rainDepthMm(t0,t1). RAIN_TIME_COMPRESSION=40 is
exported from weather.core so B applies it cloth-side rather than either of us
hardcoding 40 twice: how hard it rains is mine, how much water a sail holds is
theirs.

Calibrated to B's own arithmetic, and the numbers land on it:
  storm_02  80 mm/hr severe  -> 50.9 mm = 3.12 kN/corner   (B predicted 3.1)
  storm_03  30 mm/hr moderate -> 8.3 mm = 0.51 kN/corner   (teases a carabiner)
  storm_01   8 mm/hr shower   -> 0.9 mm = 0.06 kN/corner   (harmless, as designed)
against a storm_02 wind load of 0.2-1.1 kN. A flat rig should drown in the wild
night; a hypar pools nothing and won't notice. Asserted with B's arithmetic so
the storms are provably fit for their water before their cloth lands.

DECISION 7 helper for Lane A: sky.gardenExposure(bed, t) = rainAt x (1-shadow),
the whole drain term in one call. Note it moves on its own — the rain shadow
follows the wind, so the southerly change walks the dry patch off the bed and
the drain climbs with no corner having failed.

NIGHT PASS. sky.night is now the author's call (was: inferred from a darkness
threshold), and darkening scene.background did nothing anyway — the cloud dome
covers it at 0.85 opacity, so an overcast-grey texture was what you actually saw.
The dome now tints AND crushes (a lerp alone lands #717273: it runs in linear
space and the texture is baked near-white). Stops at 0.78 because the yard has no
lights and a storm you can't see is a black screen. Lightning now lights the
cloud it's inside (#3e3e3f -> #d4ddf2), and fires on the biggest gusts via
sky.lightningGustPow, not just the three authored strikes — driven off the
telegraph so the flash lands with the gust that earned it.

Also: c.test's storm list was hardcoded to two storms while the node runner globs
the directory, so storm_03 was untested in the browser half. Its own ponding
assert caught it.

Selftest 195/0/0 (was 184). Verified live: night reads as night, flash lights the
cloud, exposure responds.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 02:28:09 +10:00
m3ultra
4bb0efcfd1 Add the pond visual, the broom, and rename the snapped panel
Pond (SPRINT4 §E-1, decision 10): pond_water.png and pond_normal.png, for a
patch Lane B builds from the cloth's own nodes — same ride-the-nodes rule as the
tears, since a rigid disc would sit still while the belly moves under it. Alpha
is a radial feather and RGB darkens toward the centre, both encoded radially so
the shading survives being scaled per pondMass (radius ~ sqrt(mass)). Water is
grey-green, not blue: rain caught in a sail is shallow, murky and mirrors an
overcast sky.

Both at 256², not 512²: smooth low-frequency content, and at 512 they were
256 KB + 320 KB against ~20 KB for every other texture here. The chop is seven
waves at golden-angle directions rather than three crossed sines, which
interfered into a lattice and read as basketweave; the normal map gets six
integer-frequency waves for the same reason, and still asserts its own tiling.

broom_01 (§E-2): poke_tip sits on the bristle end deliberately — a broomstick
jabbed at a loaded sail holes it.

Renames fence_panel_broken to fence_panel_snapped to match SPRINT4 §A-4/§E-3.
Lane A's dress() guards every load, so the mismatch would not have crashed —
the wreckage would silently never have appeared, which is worse.

All 21 assets pass, 33 output files byte-identical across two runs, 186/0/0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 02:27:55 +10:00
m3ultra
ea128e1307 Log decision 11: concede the bar, post the numbers, flag what's open
The 60% bar passes on the real yard at every value tested. My
"unachievable" verdict was measured against a 16.7-degree reference rig
the game cannot build; the yard is 4.8 degrees, where it clears easily.
Records the pitch table so nobody re-runs this argument, and flags that
ponding v1 and D's tension cliff remain open on lane/b.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 02:22:01 +10:00
m3ultra
7c699e776d Decision 11: the 60% bar passes. I was wrong; bump the storm to 0.45
Re-measured on A's dressed yard. The bar clears at every value tested —
67% at the landed 0.12, 84% at 0.40, 85% at 0.45 — and the twisted rig
holds 4/4 at all three. storm_02 downdraftOfTotal 0.12 -> 0.45 per
decision 11. Both physics gates now close on the same storm JSON.

My "mathematically unachievable" claim was wrong, and the algebra says
exactly why: ratio = (f / (sin p + cos p·f))^2 depends on the REFERENCE
PITCH far more than on the downdraft. My synthetic reference was pitched
16.7 deg, where the bar genuinely is unreachable (asymptote 109%, needs
f=0.86). The yard is pitched 4.8 deg — house fascia 2.60 m to posts
3.95 m over ~16 m — where 0.45 gives 72% on paper and 85% measured. The
equation was right and the rig was fiction: I was measuring against a
sail the game cannot build. The integrator's "measured beats modelled"
call was correct, and C's 0.45 was right on the geometry that ships.

The assert now derives its reference from the yard's real pitch and says
so, with the pitch table in a comment, so the next person to touch this
can see in ten seconds why the number moves.

Also re-pointed §7's twisted rig off the 145 m2 quad decision 2 retired
onto a real 23 m2 one ('t1,p1,p2,p3', most twisted in the 18-45 band).
The repair leg's dodgy carabiner moves to p2: measured peaks are t1 2.43
/ p2 2.35 / p3 0.82 / p1 0.60 kN, and it had been sitting on p1 — the
lightest corner — so it rode out the storm and the leg skipped. It now
blows and one repair finishes 4/4. Removed its stale skip guard, which
still tested the old gusts.downdraft key.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 02:21:27 +10:00
m3ultra
5d8264f13f Add Sprint 4 plan and lane prompts: face and water
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 02:03:47 +10:00
m3ultra
89fd4e60b9 Merge Sprint 3 lanes; rule on the downdraft dispute; green-light ponding
Selftest on merged main: 184 pass / 0 fail. Gate 3 met (on-record §7 run).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 02:02:09 +10:00
m3ultra
997ea0bab1 Merge remote-tracking branch 'origin/lane/d'
# Conflicts:
#	THREADS.md
2026-07-17 01:58:30 +10:00
m3ultra
ecb1ee7f43 Merge remote-tracking branch 'origin/lane/b'
# Conflicts:
#	THREADS.md
2026-07-17 01:58:30 +10:00
m3ultra
44a4a3e6ec Merge remote-tracking branch 'origin/lane/c'
# Conflicts:
#	THREADS.md
2026-07-17 01:58:30 +10:00
m3ultra
6880ec7d8e Merge remote-tracking branch 'origin/lane/e' 2026-07-17 01:58:30 +10:00
m3ultra
9b97bcfd00 Log decision-8 landing, the 0.45 target proof, and the B re-point ask
Semantic done + both-gates measurements + the finding that A's anchors alone
don't unblock 0.45 (B's §7 rig is still 141 m²). storm_02 held at 0.12 to keep
main green until B re-points to a small quad.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 01:53:56 +10:00
m3ultra
9a2abad1be Retire weather_demo.html — the game is the bench now
Verified all Sprint 3 weather work through the real game (SHADES.step) and a
node harness, never the demo. The game hosts the full storm, and a second
weather harness only drifts (hardcoded yard, mock sail). Sprint 2 item 6.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 01:48:30 +10:00
m3ultra
576422e1f0 Downdraft as fraction of total wind (decision 8) + storm_03
The gust-only downdraft could not satisfy B's 60% no-free-lunch bar and the §7
twisted-survives gate together: the downdraft peaked at the gust peak, where
the horizontal peaked too, so a flat sail never reached 60% of a pitched one's
load without a spike so violent it also broke the twisted rig. The integrator
measured the pincer (0.58 -> 48% and still breaks twisted).

Fix: the downdraft is now a fraction of the LOCAL total wind speed, not of gust
power (weather.core verticalAt = -frac * localHoriz). It presses a flat roof
steadily across the whole storm — peak total 32.6 m/s dwarfs peak gust power
12.6 — so the ratio clears 60% at a gentle fraction, with no gust-peak spike.
It rides the local speed, so a tree's wind shadow shelters from falling air too.
speedAt() stays horizontal (a wind meter doesn't read falling air).

Field renamed downdraft -> downdraftOfTotal; the validator rejects the old name
rather than silently re-meaning it. The vertical now carries NO rng draws at
all, so the determinism guarantee (tuning can't re-time gusts) is structural,
not just separate-stream.

Measured both gates myself with B's SailRig (8-direction flat-vs-pitched sweep +
§7 legs). Target for storm_02 is 0.45: 69% of-max / 60% worst-heading on the
bar, twisted rated rig survives with ~21% margin. HELD at 0.12 this commit —
the current yard's only twisted quad ('h1,t2,p1,t1', ~190 m2) starts losing a
corner near 0.15 in the exact solver, so 0.45 would red B's §7. 0.12 ~= the old
gust-only 0.3 in peak downdraft (-4.2 vs -4.5 m/s). Bump to 0.45 is a one-number
joint step once A lands decision-2 anchors (18-45 m2 quads) and B re-points §7.

storm_03_southerly: campaign ramp between gentle and wildnight — peak gust 21
m/s, sustained 13, one moderate southerly change. Auto-swept by the suite.

Selftest 170/0/0 (all three §7 legs green). Verified live: downdraft/horizontal
ratio is exactly 0.12 in-game, rain occlusion still covers the bed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 01:48:30 +10:00
m3ultra
135b3dcda5 Log the on-record §7 hand-run, the failure-envelope finding, and the ladder deferral
The §7 loop is closed by hand on the merged game: pick up a spare at the shed,
carry it across the storm, the carabiner weak-link blows on its own, chase the
flogging corner (prompt tracks it live), hold-E re-rig → 4/4, ride out → survives
with full coverage. Definition of done met with a real storm break, not induced.

Also logged for B+C: the sail's failure envelope is a cliff — 4/4 up to tn 1.03
(~1.2kN peaks), instant ~10kN double-cascade at tn 1.04. Good rigs survive, flat
rigs cascade; the thesis holds and the decision-8 downdraft semantic won't move
the cliff (it's a cloth-stability ceiling). Ladder stretch goal deferred to
Sprint 4 with reasoning — it's a second carry type + placement + code-driven
climb height, bigger than what's left of this sprint.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 01:43:49 +10:00
m3ultra
9f83fd4f20 Report that decisions 3 and 8 are unachievable, with the algebra
Implemented and swept fraction-of-total downdraft: it pincers exactly
like gust-only did. The cause is not the semantics, it's the bar. A
pitched sail's normal is still 96% vertical, so a downdraft loads it too,
and on its worst heading pitch and downdraft add: |d| = 0.287 + 0.958f
against the horizontal sail's f. The ratio asymptotes at 109% and needs
f=0.86 (28 m/s of falling air) to reach 60%. No value works under any
semantics.

Recommend retiring it as a wind problem. DESIGN.md's own answer is
ponding, and the arithmetic dwarfs wind (1250 kg vs 8 kg of fabric vs
1 kN of wind) — but it can't bite in 90 s without a ~40x time-compression
fiat, so it's an M4 item with an owner, not a Sprint 3 fix.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 01:30:54 +10:00
m3ultra
a34cbd6d19 Log the decision-2 result and the full-coverage finding in THREADS
Records the measured before/after for Lane B (their pre-tension cascade is gone),
and flags that full bed coverage costing >=59 m2 is deliberate design rather than
a tuning miss — with an assert in both directions so nobody optimises the
tradeoff away by accident.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 01:30:53 +10:00
m3ultra
0cabb19dc7 Anchor rework + house/tree GLBs: give the yard a real choice (decisions 2 & 6)
Posts pulled in to (-4.5,5.5)/(4.0,6.0) with p3 at (0,7), and dress() now swaps
Lane E's house_yardside and both gum trees over the graybox, adopting their baked
anchors rather than my constants (decision 6). E's fascia sits at x=-3..3, not my
guessed -5..5 — narrowing the house span by 4 m is a real part of why small quads
exist at all.

Every anchor now carries E's rating_hint: fascia 0.35 with collateral "gutter"
(they encoded DESIGN.md's "the fascia board is a lie" straight into the asset),
tree branches descending 1.0/0.88/0.76 from fork to thin limb. branch_anchor_01
keeps the t1/t2 ids so nothing referencing them breaks; the rest are added.

The yard went from 7 anchors offering nothing under 110 m² to 11 offering 34
quads in the 18-45 m² band, 8 of which shade a quarter of the bed or more.
Measured through the same storm_02: the big house-to-post span loses its
carabiner at t=3.7 s and cascades to 2/4, while a 37.7 m² tree-to-post rig at
0.85 tension survives all 90 s intact and shades 58% of the bed. That is
DESIGN.md's thesis finally standing up in the yard rather than in a doc.

Lane B's "cascade at t=0.4 s from pre-tension alone" is gone: calm peaks are now
634 N (big) and 200 N (small) against a 1200 N carabiner, and no rig breaks
before the storm starts.

Two asserts pin it, because both halves are easy to lose by accident: at least 3
riggable quads in 18-45 m² must shade the bed, AND full bed coverage must stay
above 45 m² — if a small quad ever covers the whole bed, the rigging puzzle has
no wrong answers left. Selftest 172/0/0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 01:30:17 +10:00
m3ultra
9b8aabe0db Add sail UVs and Lane E's weave texture
E's recipe verbatim: grid (i,j) -> (u,v), repeat 6x6, sRGB. Without the
uv attribute three defaults every vertex to (0,0), the map samples one
texel and the membrane reads as flat colour — which looks like the
texture failing rather than like a bug, so E flagged it ahead of time.

Texture URL resolves against import.meta.url rather than the server root,
same as weather.js's STORM_DIR and the same thing the integrator's
/world/ -> relative pass was fixing. A missing texture warns and falls
back to flat colour instead of throwing: the cloth is the game, the weave
is a finish, and it must not be able to take the sail down.

Added anisotropy 4 — the sail is mostly viewed at a raking angle from
underneath, which is exactly where an unfiltered weave turns to moire.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 01:29:22 +10:00
m3ultra
446ed2f14a Log shed dressing verified in-game
Booted merged main and looked at Lane A's dress(): shed and workbench are
standing, scale reads against the fence, and world.shedTable.pos resolves to my
baked pickup_anchor rather than the fallback. First Lane E GLB in the running
game with the contract intact end-to-end.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 01:28:42 +10:00
m3ultra
5d1d0895f2 Lane D: retune stumble/shove against the REAL storm_02, pin to storm JSON
The pre-merge thresholds were tuned against a mock that injected +18 gusts. In
the actual storm_02, gusts-over-baseline peak at 12.1 m/s — so stumbleGust:17
was above every gust in the game and StumbleBack was unreachable dead code, and
shoveGustMin:8 (a low bar in the prototype's 12-38 units) had become a high bar
in ours and threw away most of the shove.

Retuned against the real storm profile (12 gust events, peaks 12.1..6.1, 4
knockdowns, maxExposure 1.55): stumbleGust 17→9, shoveGustMin 8→4. A wild night
now reads shoved (26s of 90) → stumbling (4) → floored (2); a calm day stays 0/0.

d.test.js now loads the real storm JSON via weather.loadStorm and asserts every
threshold is REACHABLE and correctly ordered — the guard the first tuning lacked.
This matters because decision 8 (B+C) reweights the downdraft this sprint, which
is exactly the kind of change that silently killed StumbleBack the first time.
Also pinned the emergent 'unbrace into a gust → stumble' behaviour.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 01:25:17 +10:00
m3ultra
fccbeb9319 Add aftermath wreckage props and verify glTF extras survive
The aftermath screen needs something to point at (SPRINT3 §Lane E-2).
garden_gnome_01_broken snaps at the ankles with the base left where the player
last saw him, head rolled clear with the beard still on, hat off — recognisably
him rather than a shattered pile, because wreckage nobody can identify is just
gravel. fence_panel_broken loses a few palings and hangs one off a nail; most of
it stays standing, which is what makes the hole read as damage rather than as a
design choice. Both keep their intact twin's origin and ground plane so Lane A
swaps mesh-for-mesh with no offsets, asserted both ways.

Also asserts something claimed since Sprint 1 and never checked: that glTF
extras reach three's userData. They do — the gnome's collateral_value, the
canopy's sway_amp, branch_anchor rating_hint and the bin's mass_hint all arrive
as numbers. Had that silently dropped, Lane A's gnome would score $0 and every
anchor would rate identical, which reads as a gameplay decision rather than a
missing field.

Contact-sheet framing now keys the capsule off height, not max(dims): the broken
gnome is 0.39 m across but stands 0.11, so spread-based framing buried it the
same way it once buried the shackle.

Selftest 175/0/0, 30 output files byte-identical across two runs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 01:24:19 +10:00
m3ultra
823dbb942b Post Sprint 3 gate 1 in THREADS — Lane D unblocked
Also records the KeyboardInput.holding getter trap: assigning to it from a probe
silently no-ops and looks identical to broken interact wiring. Cost me a nearly
false bug report; Lane D needs it for the on-record §7 run.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 01:21:02 +10:00
m3ultra
a5ed4fcdb6 Place the shed and spare table; expose world.shedTable (unblocks Lane D)
SPRINT3 decision 9 / §Lane A.1. Lane D's §7 loop — rig, carry a spare, repair
mid-storm — had nowhere to pick a spare up, so wireYardActions self-skipped the
pickup and nothing in the game could put one in your hands.

world.shedTable.pos is published synchronously from constants even though the
meshes arrive later: createWorld() has to stay sync (a.test.js and the selftest
build a yard with no server), and wireYardActions reads the position at wiring
time. dress() then refines it onto Lane E's baked pickup_anchor — which moved it
5 cm off my guess at where a table top is, so it was worth reading.

dress() is async and separate from createWorld for the same reason: a fetch in
the constructor would make the selftest slow and flaky, or break it. Each load is
guarded individually, so a missing GLB leaves its graybox standing instead of
taking the boot down.

Verified by hand rather than by registration check: walk up, hold E, spare in
hand; a second hold says "hands full"; leaning on the table with E held for 6 s
deals exactly one spare (D's latch works); nothing fires from across the yard.
Selftest 169/0/0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 01:20:35 +10:00