SPRINT9 task for B+D. The demoted guard compared the worst corner's mean LOAD
across two windows and called the difference a "trend". The integrator's
diagnosis was that its clock was wrong — held at t=0, storm_02's compressed
rain ponds the cloth during the guard's own windows. That mechanism is real,
but it is not the bug, and neither of the two suggested fixes works. Measured
on the dressed yard, 0 s settle vs 12 s:
probe under CALM, held in prep 13% vs 17%
probe under STORM, held clock 24% vs 104%
probe under STORM, advancing clock 24% vs 104%
probe under STORM, RAINLESS 17% vs 96%
Every variant is either flat — calm cannot excite the cloth, so the guard is
vacuous and passes forever — or INVERTED, firing hardest on the properly
settled rig. Rain is not the culprit: the rainless probe ponds 1 kg and still
inverts. Advancing the clock isn't a fix either, because RAIN_TIME_COMPRESSION
is 40× and 4 s of storm is 160 s of rain regardless.
What a load-trend measures is the rig LOADING UP when the wind changes, and a
taut settled cloth ramps harder than a limp unsettled one (0.63 -> 1.23 kN vs
0.44 -> 0.52). The observable was pointed at the storm's arrival, not at the
cloth. No threshold, clock or rain switch fixes that.
"Settled" is a statement about the cloth, so ask the cloth. sail.js gains
nodeSpeed(): RMS node speed out of verlet. Sampled over 2 s of the same calm
prep the rig already stands in — no wind change to ramp against, no compressed
rain, and nothing left in the rig but two more seconds of prep a player does
anyway. Measured:
settle 0 s 4 s 12 s 30 s
speed 0.19 0.014 0.017 0.007 m/s (pyrrhic quad: 0.23 -> 0.028)
An order of magnitude, in the direction the word means. Absolute check at
0.08 m/s, not a trend: the cloth breathes at 0.02 and the transient sits at
0.19, so there is real room for a line.
Promoted back from warn to a hard failure — and it earns that by being able to
fail. `the settled-at-entry guard can fail` flies the same line with NO settle
and asserts the guard refuses it. A guard nobody has seen fail is
indistinguishable from one that cannot, which is exactly how this one survived
two sprints being both vacuous and inverted. The control is flown up front in
run(), because Suite.test() does not await.
The old guard also left 43 kg of water and 4 s of storm load in the rig before
the storm started; the suite was flying a wet rig into a dry entry. It didn't
move the peaks (the storm's own peak dwarfs it) but it was never true.
D: this is yours to veto. Selftest 277 passed, 0 failed, 0 skipped.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
site_audit answers "is this site winnable?" before it ships — the check that
was missing when SPRINT6 shipped a yard whose bed-covering quad pulled 7.4 kN
against a shop that holds 6.5. It enumerates in-band quads, flies the real
storm headless, maps each corner to the cheapest tier that holds it, and
verdicts against the $80 budget. On backyard_01 it independently reproduces
THE LINE: p1,p2,p3,p4 at $65 + $15 spare, matching balance.test to within 2%.
It also caught three of my own bugs, which is the point of building it:
· it settled on STORM wind frozen at t=0 — the exact anti-pattern
balance.test documents at 1.94 kN vs 0.40 kN. Now settles on the calm
day on a running clock, the way main.js does.
· peakLoad is peak-since-ATTACH, so the settle transient was being
reported as a storm peak. sail.js gains resetPeaks().
· its p4 was typed from world.js's postSpecs, missing that posts are RAKED
8° away from centre — 0.56 m off. The audit now re-derives all four posts
from live world.js on every run and hard-fails on drift.
That last check is deliberately narrow. Node CANNOT dress: dress() needs
GLTFLoader (bare 'three') and fetch()es .glb over file://. createWorld() still
SUCCEEDS in node — it just returns the GRAYBOX yard, house at x=±5, no branch
anchors. So a headless tool that "reads world.js for the real yard" gets the
fictional x=±5 house that cost two sprints. Reading live code is not reading
truth. Verified in-browser: the dump matches the dressed yard on all 12
anchors. Posts are the only thing dress() never touches, so they're the only
thing worth cross-checking — and that check is what caught p4.
C IS RIGHT ABOUT p1 AND I WAS WRONG, for the third time. My comment claimed
C's 1.08 kN didn't reproduce, that p1 peaked at 1.27 and the line won on TIME
rather than headroom. Re-measured on the dressed yard, same flight:
shade cloth p1 0.98 kN 0/4 lost membrane p1 1.23 kN p1 LETS GO
p1 never touches its 1.2 kN rating on cloth — 18% of real headroom. Where 1.27
came from: a loadout where a corner BROKE and dumped its share onto p1 (the
same quad with hardware the budget can't buy reads 2.48). Any p1 number
gathered without checking `lost` measures a cascade, not a fabric.
So the fabric is the decision, not a skin: 0.98 vs 1.23 against a 1.20 rating
is the difference between the $5 carabiner holding the wild night and failing
it. New assert `fabric decides p1` locks that in, so nobody has to trust the
paragraph.
selftest.html rendered ['A'..'E'] while importing BAL too, so the balance
suite ran, counted toward the summary, and had every row silently dropped —
including, had one gone red, the row saying which assert failed. The merge
gate was invisible in its own report. My bug: I added the import, not the
renderer. Now derived from the report.
Selftest: 276 passed, 0 failed, 0 skipped (LANE BAL now visible).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
carport_01 is site_02's dressing and its whole design. DESIGN.md wants the
corner block anchor-poor, and the interesting way to build that isn't to give
the yard nothing — an empty yard is just a smaller yard. It's to give it
something that looks like four free anchors and isn't. Light posts on small
pads, a beam sized for one sheet of roofing: tie 25 m2 to it in a southerly and
you take the carport, not the shackle. Ships with the worst ratings in the game
(beam 0.22 vs the fascia's 0.35) and collateral="carport", pinned in e.test.js
below the fascia with the reason attached, so nobody quietly fixes them upward
and turns the site back into a small yard.
The pyrrhic card is the third outcome on the same camera: dawn, bed green, gnome
standing, sail post down across the yard behind him. Warm light on purpose — a
pyrrhic result is a win, and the wreckage is the price, not the verdict. Copy
goes to Lane A in THREADS; cardpreview renders all three.
277/0/0, Lane E 60 asserts, 39/39 output files byte-identical.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A venturi is a shelter's opposite — a gap between buildings SPEEDS the wind up
when it blows along the gap's axis. Where a tree shadow depends on being downwind
of the tree, a venturi depends on the wind being ALIGNED with a fixed axis the
SITE owns, so it fires only when the storm's direction swings to match: the
corner block is calm until the southerly comes through, then it screams. That
interplay is exactly SPRINT9's brief — funnel geometry is the site's, the wind
that funnels is the storm's.
Extends the existing shelter system rather than bolting on a parallel one:
speedAt/vecAt/verticalAt all route through one localHoriz, which now folds
venturiFactor in alongside spatialFactor and shelterFactor. The downdraft rides
local speed, so a funnelled wind gets a proportionally stronger downdraft for
free — physically right.
`setVenturi(list)` on the wind + router (A's "add it to the router too" rule; the
tripwire is green). Inert on an empty list, so backyard_01 is byte-identical —
asserted. Multiplier is ALWAYS ≥ 1 (a venturi accelerates, never shelters), and
it's C1-continuous through the change, so it can't snap a corner — both asserted
(aligned wind boosts 1.5×, crosswind exactly 1.0×, max frame jump 0.50 m/s).
Data lives in site JSON (A's schema, arriving with the extraction) — this is the
physics + the setter, ready to wire. Proposed shape flagged for A in THREADS.
Also fixed a stale comment: storm_02's _gusts_comment still said "HELD at 0.12
for now" from Sprint 3 while the value has been 0.45 since Sprint 4 — a lying
comment in a repo that reads comments as canon. Now records the real history and
the pending paired-0.40 flip.
Selftest 278/0/0; node 55/0/0.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Selftest 275/0/0. balance.test: **$80 · shade cloth · p1,p2,p3,p4 ->
hp 63, 0/4 lost — CLEAN WIN.** First time the wild night has been
winnable without sacrificing the rig. C found the line and the $10 gap;
this spends both levers together, as they only work together.
Fabric is priced at $0 BOTH WAYS, and that is a finding, not laziness.
SPRINT6 asked me to price the difference; I measured it and there is no
price at which membrane is a sane buy — it is dominated:
buys: +26% hail, but only on the PEA-hail nights (hailBlockFor(0.7,
0.3)=0.74), which are the easy ones — on storm_02 (1.3) and the
ice night (1.4) a knit already blocks 100%, because a 2 cm stone
can't pass a 2 mm weave (C's ruling). Plus rain, at 0.25 of the
drain vs hail's 5.0 — ~5% of the damage.
costs: double the wind load, and it PONDS.
Charge for that and you've shipped a trap. Free, it's DESIGN.md's actual
words — "fabric choice is a forecast bet" — and the bet is real: cloth on
the windy nights, membrane when the hail is small and the wind is mild.
If A ever raises rain's weight, membrane earns a price.
Wired C's hailBlockFor into skyfx.gardenHailExposure, which nothing
consumed — without it the fabric choice would have been cosmetic, which
is the bug class this repo keeps finding.
I did NOT reproduce C's numbers, and the comment says so rather than
repeating them. C reported p1 dropping to 1.08 kN (under a carabiner's
1.2) with cloth + 0.40. I measure 1.27, and p1 never crosses under 1.2 at
any combo. The line still wins on a $5 carabiner — but because 1.27 vs
1.2 is a 6% overshoot and breaking needs 0.4 s SUSTAINED, not because the
corner got under its rating. The margin is TIME, not headroom, and it's
thinner than the table implies: anything that lengthens storm_02's gust
HOLDS could take it. Flagged for C in THREADS.
The old t2 quad stays as the sacrifice-play control (hp 56, 2/4 lost) —
now a worse bet the shop will happily sell you, rather than the wild
night's only outcome.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The garden is the client's. The sail is yours. Saving the bed with a rig that
tore itself apart is the job done expensively — not a failure. DESIGN.md doesn't
call a cascade a loss, it calls it "a firework you paid for", and *paid for* is
the point: the aftermath already bills the broken hardware, the collateral, and a
week's fee you didn't earn cleanly. Gating the win on corners bills you twice for
one night and then lies about it — the same species as the verdict that used to
tell a 4/4 hold they'd skimped.
B and D arrived here independently with the numbers, and B checked what it does
NOT break: cheap rigs still lose (4x carabiner -> hp 38), rigs that miss the bed
still lose (hp 36), $80 still cannot buy immunity. C's exhaustive sweep found the
only winnable $80 line on the wild night lands exactly here — hp 52, 1 lost.
The pyrrhic ending gets its own verdict, and it had to be checked FIRST: `lost >= 2`
was unconditionally a cascade-and-a-loss, and under the new rule it can be the
best night the game has. Two asserts pin both halves — garden saved + sail
destroyed is a win that still names the weakest link; the same wreck WITHOUT the
garden is still a plain cascade, because the garden is the whole job.
Selftest 276 pass / 0 fail (Lane A 32 -> 34).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Ran every remaining suite-vs-game difference one at a time against the real
storm. None of them moves the corner count:
live calm settle (running clock + calmWind) entry 1.94 -> 0.40 kN, 2 lost
live tree sway t2 4.33 -> 4.55 kN, 2 lost
walked repair timing (~4 s) 2 lost
all three together hp 58, 2 lost
Then checked the structural candidates: anchors are bit-identical between
harnesses (all four, 0.0000 m), ring order is identical (p4,t2,t2b,p3), and
wind.sample() at t2 matches to 2 dp (36.49 m/s @ t=75.9) across suite, suite
with shelters, and the live router.
So I re-ran the live game itself, fresh page, A's exact line, tension 1.0:
live -> hp 58, 2 lost [t2,t2b], t2 peak 4.55 kN
suite -> hp 58, 2 lost [t2,t2b], t2 peak 4.54 kN
They agree. The suite was right.
RETRACTED: my Sprint-7 "12 s settle flips 2 lost -> 1". It does not reproduce —
0 s and 12 s both end 2 lost, because the storm's own peak (4.54 kN at t=75.9)
dwarfs the attach transient. The settle is a real fidelity fix and NOT the
corner-count cause. B's arithmetic was right from their first post: t2 pulls
~4.5 kN, a shackle is rated 3.2, and the spare cannot save it (measured: t2b
blew @9 s, repaired, blew again).
Landed anyway, because it is still true and still worth guarding: fly() now
settles the way main.js drives prep (calmWind on a running clock, not the storm
frozen at t=0), and a new "the yard is SETTLED when the storm starts" assert
holds every run under 1.0 kN at entry. 266 pass / 1 red — the red is the
pyrrhic-win design call, not a bug, and it now says so.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
grass_atlas.png binned (six sprints, zero refs, Lane E's own recommendation).
Screenshot POST declined — yardshot works standalone, so it buys convenience not
capability. Saying no rather than carrying either an eighth sprint.
Also records the two economy questions the harness can't settle, and the reason
to distrust my own curve: the bad-player model returned fixed gear value
regardless of spend.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
SPRINT8 gate 1, carried since Sprint 6. week.js is a thin wrapper over the phase
machine, not a new system: it answers three questions between rounds — which
storm is tonight, how much money is there, and is it over. Zero THREE, pure data,
so the ladder and the money are testable at no cost.
The forecast card stops being a difficulty picker and becomes NIGHT n OF 5 with
the ladder as pips, tonight's storm, and the bank. Aftermath itemises the
settlement (fee / bonus / gear recovered / collateral / spent → bank), and E's
dawn tint comes up on their 2.2s ease BEFORE the scoreboard — their note is right
that you sell the survival first. E's diptych and copy are wired verbatim.
Two economy decisions, both made because measuring the week exposed them:
A lost night pays a FRACTION of the fee. Zero is unrecoverable and reads as
punishment; full pay means failure only costs the broken shackle and the bank
only ever climbs. Gear comes back at HALF — a full refund makes hardware free, so
the only sink is breakage and "broke = game over" becomes unreachable. Half is
also honest: a shackle that rode out a gale has been fatigued, which DESIGN.md
already says.
And the one that matters: REACHING night five is not SURVIVING it. My first draft
said it was, and playing the week found it — a $20-carabiner player loses four
gardens, never dips under the broke line because a cheap rig barely costs
anything, and got handed "THE WEEK HELD — everything's still where you put it"
over four dead gardens. The end card is the last thing this game says; it doesn't
get to lie the way the aftermath verdict used to. Words now scale with gardens
held: clean (5), scraped (3-4, E's own alternate copy), solvent (0-2, mine —
"STILL TRADING · The books balanced. The garden didn't." E never wrote words for
that ending because nobody knew the game could produce it).
Two economy questions I could NOT settle from a harness and have documented
rather than quietly tuned: a competent week runs the bank 80 → 350, so night five
stops being a decision; and a floor-scraping week may never actually go broke.
My bad-player model handed back a fixed $30 of gear whatever it spent, which
flattered the bottom of the curve — the same class of mistake this repo keeps
catching me in. Both want a human playing five nights.
Selftest 270 pass; the single red is gate 0's balance line, which is B+D's this
sprint and was already red on main.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The model landed in Sprint 6 and nothing ever called it, which is why this kept
getting carried: `forecastFor` existed while the card went on estimating inline.
So this is the wiring, not more API.
`forecastLines(def, lead)` turns a storm into the card's two stat lines, already
worded in A's voice. lead 0 = tonight = exact numbers (reads exactly as the card
always has); further out it hedges into ranges and prints a confidence line. The
bands always contain the truth — a forecast may be vague, never wrong, or a
player who rigs for the top of the stated range gets ambushed.
Two things the card gains beyond the bands:
- The numbers are now MEASURED. The inline estimate (`baseCurve peak + powBase
+ powRamp`) read 30 m/s for storm_02; it really gusts to 32.3, because gust
power is drawn per gust and rides a ramp. The wild night now says 116 km/h
because that is what hits you.
- The card finally MENTIONS HAIL. Hail has been the garden score since decision
13 and the forecast never said so — you were being scored on something the
card didn't tell you was coming. Now: "hail likely" on the wild night, silent
on the gentle one, "possible" when it's too far out to promise.
hud.js is Lane A's file and A is meant to build the week uninterrupted, so this
is deliberately a 3-line swap that keeps their markup, classes and wording — the
week's card rewrite can carry it or drop `forecastLines` and call `forecastFor`
directly. `lead` defaults to 0, so nothing changes until the week passes one.
Verified live at both ends: tonight renders exact; five nights receding into the
week render 0%-confidence hedges ("16–28 m/s · gusts ~72–144 km/h · hail
possible"). Selftest 265/0/0 + gate 0's 2 skips.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
step() opened `if (!camera) return`, which read as an innocent null-guard and
was in fact a physics gate: a harness with no camera skipped the shadow-grid
rebuild, so gardenExposure/gardenHailExposure reported full exposure no matter
what the cloth was doing, and every rig scored as if the sail did not exist.
hp 36 was the bare-bed constant all along. It cost gate 0 two sprints of four
lanes' time before A found it, and the fix belongs in my file, not in each
harness that trips over it.
A camera means "there is a view to place things in", not "the weather is real".
So the grids — what the sail actually DOES — now rebuild above the render gate,
with the yard centre as the wind-sample fallback; only drops, dome, lights and
audio sit below it. Headless callers get correct numbers and skip the 3k drop
matrices they were never going to draw.
Asserted both halves so it cannot come back: a camera-less skyfx shelters the
bed (hail shadow > 0.9, exposure < 0.1 under a panel), and the camera does not
change the physics — headless and viewed shadows agree to 1e-9. If anyone
re-adds an early return, that test goes red.
Selftest 264/0/0 + the 2 gate-0 skips.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Drove the live game — settled through prep, rig verified unchanged across
the phase boundary (contaminated: false) — and it returns hp 58, 2 lost.
This suite returns hp 57, 2 lost. A's hp 58 was right and reproduces to a
point; A's "1 lost" was the miscount. The two harnesses now agree on both
numbers and the dispute is over.
All three SPRINT8 candidates tested; none flips the corner count:
V1 settle under live calm wind + world.update sway -> 2 lost, t2 4.4
V2 ... and live sway through the storm too -> 2 lost, t2 4.6 (worse)
V3 repair delayed 0 / 3 / 6 / 10 s (a real walk) -> 2 lost
Nothing could flip it, because losing 2 is not a bug — it is the shop:
t2 4.6 kN, t2b 3.5, p3 3.5 all need the $30 rated shackle; p4 2.9 takes
a $15 shackle. Holding all four = $105, $120 with the spare a repair
needs. Budget is $80. Three corners sit above shackle grade and $80 buys
two, so the wild night's best line ends hp 58 with the garden alive and
the rig dead.
That is SPRINT8's escape hatch, and it's a design question handed to A:
`lost < 2` calls this a loss, DESIGN.md calls it the story. The assert now
asserts the half that IS physics (the garden must survive, hp >= 50) and
reports the corner count as the open question, so the suite states the
measurement rather than pre-empting A's ruling.
Landed D's settled-at-entry guard, as a windowed TREND test rather than an
instantaneous one: measured, the worst corner oscillates 0.9 -> 1.9 -> 1.3
-> 1.7 kN under constant wind because damping is deliberately light, so
there is no single settled value and a "has it stopped moving" check can
never pass — it would just be a flaky assert someone deletes. Comparing
consecutive 2 s means catches a real transient and tolerates the breathing.
Same lesson as the statics assert in sail.selftest.
Selftest: 266 passed, 0 failed, 0 skipped — green for the first time since
SPRINT6.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
B asked for the hail ruling before coding fabric choice; this is it as code.
The physics: porosity is about AIR (blows through → less wind load) and WATER
(drains → no ponding), NOT ice. A knitted shade cloth's gaps are ~1-3 mm; a
damaging hailstone is 6-45 mm, so it can't pass a mesh an order of magnitude
finer than itself — porous and membrane block the big stones identically. The
ONE true difference is the finest pea hail, which IS small enough to rattle
through an open weave. So: membrane stops everything; porous stops everything
except the smallest stones.
Verified across the storm sizes: shade cloth (0.3) fully blocks the wild-night
1.3/1.4 stones and leaks 26% of storm_03's 0.7 pea hail; an open 0.5 weave
leaks 90% of pea hail but still catches the big ice. That makes the fabric
choice cost you exactly on the mild-hail nights and stay honest on the ice
nights — a real tradeoff without a physics lie.
The assert also proves the integration end to end so nobody wires it blind: a
membrane-covered bed takes ~0 hail on storm_03, a porous-covered one takes 0.62
— the choice is genuinely non-trivial. Formula for whoever wires the garden
drain: coveredHail = hailShadowOver(bed) * hailBlockFor(hailSize, sail.porosity).
Pure helper (no THREE), exported through weather.js alongside stormStats/
forecastFor; NOT on the wind contract, so no router change. Selftest 263/0/0.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Each limb was one straight uniform tube — a peg poking out of a pole, and the
most visible art problem in docs/yard_day.jpg. They now leave the trunk thick,
sweep up on a bezier and taper to nothing, which is what a gum does. +96 tris on
gum_01, silhouette bounds unchanged.
Nothing physical moved. The tip is a bezier endpoint and the four rng draws per
branch are the same four in the same order, so every branch_anchor_* is
bit-for-bit where it was — verified before and after, all five identical to 6dp.
Lane A's winning line rigs off t2 and is untouched; no balance number can shift.
e.test.js now hard-codes those five world positions as a tripwire, so no future
art change of mine can quietly invalidate a sprint of someone else's physics. If
it goes red, the art moved an anchor — fix the art, not the numbers.
Card text and the dawn-tint treatment go to Lane A in THREADS. The tint is a CSS
snippet rather than a ninth texture: only two of my eight are consumed, and a
full-screen gradient needs no asset at all.
Selftest 262/0/0, 38/38 output files byte-identical.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
THE CAUSE, and it is not a winner: balance.test.js built skyfx with no camera,
and skyfx.step() opens `if (!camera) return;` — so the hail shadow grid was
never rebuilt across all 5400 calls and every loadout was scored as though it
had no sail. hp 36 IS the bare-bed number. Measured with the camera as the only
variable: hailShadowOver(bed) peaks 0.000 vs 1.000, hp 36 vs 69. Neither harness
was lying; one was flying a yard with no cloth in it. Camera + wind shelters
added — the suite must drive what the game drives.
MY ERROR, named: the hp-58 win was measured at tension 1.0 and I never declared
it. At the shop's default 0.9 my own end-to-end also loses 2 corners.
STILL OPEN: this suite says 2 corners lost, my end-to-end says 1 at tension 1.0.
Ruled out by measurement, each worth <=0.02 kN: shelters, frozen vs live tree
sway, the scripted repair. Next suspects named in the skip: session.commit() vs
rigSail(), and main.js passing debris as rig.step's 4th arg (debris ADDS load,
so it should break MORE here, not less — that inversion is the thread).
And the find that explains how this survived a merge: Suite.test() ignored its
return value, so `return "SKIPPED — ..."` was recorded as a PASS. The
integrator's storm_02 skip was a fake pass; so was mine for storm_01; `skip: 0`
in every report we ever printed was the tell and nobody read it, me included — I
misread one of those fakes as my win reproducing. test() now honours the string,
and rejects async fns outright (they hand back a Promise that cannot throw
synchronously, passing forever while proving nothing).
That guard immediately caught its own author: BOTH my wind-router tripwire tests
were async and have asserted nothing for two sprints — including the sprint where
I reported the tripwire "caught its first real omission". It had, in a hand-run
probe, never in the suite. Awaits hoisted into run(); they asserted for the first
time today, and pass.
Selftest 261 pass / 0 fail / 2 honest skips.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A measured t2,p3,p4,t2b on 4x shackle + spare at hp 58 / 1 lost. This
suite says hp 36 / 2 lost. Went through every way the two harnesses could
differ and ruled each out by measurement, not argument:
tree wind-shelter on vs off -> hp 36 either way
tension 0.6/0.75/0.9/1.0 -> hp 36, 2 lost, all four
drain wiring garden.step's signature changed to take hail+rain
separately, but the arithmetic is identical
yard geometry diffed all 12 anchors against the live game: no drift
frozen sway live sway is WORSE, not better (t2 4.4 -> 4.6 kN)
What's left isn't a harness difference, it's arithmetic. t2 peaks
4.4-4.9 kN on this quad; a shackle is rated 3.2. t2 cannot hold under any
variation above. Losing it spends the only spare, then p3 (3.7) or t2b
(3.4) — also over 3.2 — goes too. Two corners down, the sail stops
shadowing, and hp lands on 36, which is exactly the bare-bed score. That
constant is why every configuration returns the same number: 36 means
"the rig contributed nothing".
Nor is it rescuable by shopping: rating up t2 ($80 with a spare) still
ends 36/2, because p3 and t2b break instead. The quad needs three corners
above shackle grade and $80 buys two.
So the skip is deleted and the assert is RED again, which is the true
state. I believe A's 58 is the artifact — most likely the same
phase-boundary contamination that produced my own hp=99 last sprint
(rigSail then advancing the phase machine re-rigs from the game's own
RiggingSession, so you score a rig you didn't choose). A: assert your
rig.corners' anchorIds and hw names immediately before your storm loop.
If they match what you bought, I'm wrong and the cause is still open.
Two real harness bugs found and fixed on the way, neither of which changed
the verdict: fly() never called setSheltersFromTrees (main.js:369 does),
and shop() hardcoded tension 0.9 while the dial's neutral is 1.0. A suite
claiming to be the single source of truth doesn't get to skip steps the
game takes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Measured against A's gate-1 winning line (t2+p3+p4+t2b, 4x shackle + spare,
tension 1.0, one repair) under an identical harness: the first draft's 26 s
hold gave 32 hail-seconds and landed ~39 HP below storm_02. That is not a
harder night — no amount of play recovers 39 HP when you are already flying the
best rig the shop sells. Hold 26 -> 12 s puts it at 21.1 hail-seconds (1.6x
storm_02, was 2.8x) and 13 HP below it, which excellent rigging + the repair +
the broom can answer — and night 5 arrives with four nights of banked money,
which is the campaign's own answer to escalation.
Week now: gust 11.3 / 21.4 / 21.2 / 32.3 / 28.3, hail-seconds 0 / 2.4 / 2.4 /
11.4 / 21.1. Force ramps then plateaus while the question changes underneath.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
hide_render does not survive glTF export — verified in three r175, every node
arrives visible:true. So garden_bed has been drawing plants_full, tattered AND
dead superimposed since Sprint 1, and my THREADS note told Lane A the opposite.
The asserts missed it by checking the nodes existed rather than that only one
was on. The flag now rides in extras, which does export, and consumers apply it
in one line; e.test.js pins the flag instead of the node.
Night dressing: house_yardside gains window_glow, an unlit emissive pane marked
hidden_by_default, plus window_light_anchor. A warm window means someone is
inside, which is why the player is out in the dark keeping the sail on. Plus a
moon sprite whose halo, not its disc, is the part that survives cloud.
End cards are rendered from the game's own props as a diptych: identical camera
and yard, only the night's outcome differs. The left third is left empty for
Lane A's text.
Cards are opt-in behind --cards because EEVEE's shadow pass is not
byte-reproducible across processes in 5.1 — proven with a minimal repro, not
PYTHONHASHSEED, and present even with a hard sun. The dawn shadow is the art, so
the cards become committed art rather than build output. Everything the game
loads stays byte-identical: 38/38 verified.
Selftest 241/0/0.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The gate asks the only question no per-lane suite can: can the night be
won through the real $80 shop. It buys loadouts via RiggingSession, flies
the real storm JSON over the real yard, integrates skyfx's real hail/rain
exposure into main.js's real garden drain, and judges with main.js's own
win rule.
Status: storm_01 warm-up PASS, cheap-rig-punished PASS, decision-13
miss-the-bed control PASS, and **storm_02's winnable line FAILS** at
hp=36, 2/4 lost. That reproduces the integrator's finding exactly and is
the gate doing its job — SPRINT6 says everything waits on gate 1.
Two things I got wrong on the way, both worth the comments they now
carry:
- The first draft hardcoded the anchor table from a THREADS entry and
was badly wrong (the dressed yard has the house at x=±3, not ±5). It
flew a fictional yard. It now reads the yard from world.js and freezes
only the sway — the yard IS the balance, so it cannot be a copy. Same
failure as Sprint 3's 16.7° reference rig.
- The asserts were async. testkit's Suite.test() calls fn() WITHOUT
awaiting, so every one would have passed forever while proving nothing
— the exact vacuous-test pattern I've flagged three times. Storms are
now flown up front in run() (which runAll does await) and the asserts
are synchronous judgements over the results.
Registers as a sixth 'BAL' entry in selftest.html. A: your "nobody
touches this file" rule guards against five lanes conflicting here; one
jointly-owned entry is the case it makes room for. Revert if you disagree.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Variants for A's five-night week. Each gets ONE new trick, and neither is
"the same storm but harder":
- storm_03b_earlybuster (night 3): same force as storm_03 (21 m/s gust, 13
sustained) but the change lands at t=18 instead of t=30. You rig for a hot
NW'er and get a southerly a third of the way in. Tests timing, not budget.
- storm_02b_icenight (night 5): LESS wind than the wild night (28 vs 32 gust,
19 vs 20 sustained) and 2.8× the hail — 32 hail-seconds vs 11.4, held 26 s
straight across the change, size 1.4 stones, ice on every gust ≥9. Hail is
the score (decision 13), so this asks the one question storm_02 only asks
for nine seconds: is the bed actually covered, and does the cover hold? A rig
that survives storm_02 by hiding in a small quad off the bed loses here.
Week escalation now reads 11.3 / 21.4 / 21.2 / 32.3 / 28.3 m/s peak gust with
0 / 2.4 / 2.4 / 11.4 / 32.0 hail-seconds — force ramps then plateaus while the
question changes underneath it.
Forecast uncertainty (DESIGN.md partial-information canon): `forecastFor(def,
lead)` + `stormStats(def)`, pure functions on a def rather than methods on the
wind — the card holds defs, doesn't want a field, and this keeps forecasting
off the wind contract and out of A's router. Bands widen with lead and resolve
to the exact truth at lead 0; deterministic per storm, so re-reading the card
can't reroll it.
The invariant, asserted across every storm at five leads: a band ALWAYS
contains the truth. That's the line between partial information and a lie — a
player who rigs for the top of the stated range must never be ambushed.
stormStats also measures what the card was estimating: storm_02 gusts to 32.3
m/s, not the 30 that `baseCurve peak + powBase + powRamp` implied.
Selftest 253/0/0; node 49/0/0.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Rebuilt the broom against B's FROZEN contract, which is not the shape I asked
for and is better: pondCentroid() (one pond, not a list — B's belly pools into a
single heaviest place) and drainPondAt(node, dt, radius) draining PER FRAME and
returning the kg shed that call. Added interact `onHold` for actions that do work
over the hold rather than at the end of it; the total is only knowable by
accumulating. Falls out nicely: an interrupted poke sheds only what it got
through, which is the honest rule.
Calibration, measured on B's live sim rather than modelled (flat rig, storm_02,
pond mass at poke → kg shed → outcome):
8s · 85 kg → 76 shed → wet (caught it before the ticker)
12s · 152 kg → 104 shed → stagger (you answered the warning)
20s · 281 kg → 182 shed → knocked (you ignored it)
Bands sit under those so all three are REACHABLE — my Sprint-5 guesses (60/120)
would have flattened the player on every poke, and my first pass at real numbers
(150/350) put the knockdown above anything this rig can shed, i.e. dead code.
That is the StumbleBack lesson exactly, so d.test.js guards reachability with the
measured numbers and will fail loudly when gate 1 moves pond masses.
Bug the live sim found that no stub could: a loaded belly SAGS toward you — real
ponds ride from y=1.2 down through y=-1.0 — and my reach gate rejected `up < 0`,
so the broom answered "nothing pooling here" with 281 kg hanging over the garden.
Sagging toward you is the mechanic working. Gate is now a physical band between
sagFloor and reach, with the floor set between two measured cases: a live 281 kg
belly at y=-0.8 (pokeable) and post-tear wreckage at y=-4.5 (nothing to poke).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
SPRINT6 gate 1's anchor lever, placed by measurement rather than by guess.
The wild night had no winnable line for a geometric reason: every anchor near
the bed (p1/p2/p3) is SOUTH of it, and nothing stands north short of the house
10 m away. So covering rigs had to span the yard and died, while rigs small
enough to survive sat beside the bed rather than over it. p4 supplies the
missing north-west corner.
Its position is swept, not chosen. Against the smallest quad covering 90% of the
bed: (-2.2,-1.2) → 44.4 m², (-3.2,-1.2) → 48.6, (-3.2,-2.0) → 51.7, beyond
z=-4 → no effect at all. Pulling it inward to 44.4 would put full coverage
INSIDE the survivable band and collapse DESIGN.md's central tension — covering
the bed has to cost risk. a.test.js's >45 m² floor is what shouts if anyone
moves it in. With the 8° rake the top anchor lands at (-3.72, -1.40), and full
coverage costs 51.6 m² against 63.5 before: a real new option, 19% cheaper,
still bigger than the 23-38 m² rigs that survive unaided.
Measured through the real $80 shop: t2+p3+p4+t2b, four shackles and a spare
($75), ends hp 58 with 1 corner lost — a WIN, where before every covering rig
lost 2-3 corners and ended at 36%. Hail damage falls 51 → 34 HP versus an
uncovered rig, so decision 13 is visibly doing its job.
Selftest 244/0/0; both yard tensions still green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The aftermath was lying. Any run under 50 hp read "the rain found what you
skimped on" — including Lane B's twisted quad that holds 4/4 corners and skimps
on nothing. The verdict is the game's entire feedback channel: DESIGN.md wants
every disaster to replay as "…the shackle, I knew about the shackle", and
blaming the wrong thing teaches the opposite of the lesson the storm just spent
90 seconds giving.
The garden now keeps its damage split by cause, because the caller was pre-summing
hail and rain into one number and that sum is exactly the information the verdict
needs and can never recover afterwards. A garden that only knows it went 100 → 39
cannot tell a player whether their hardware let go or their perfectly-held sail
simply wasn't over the bed — opposite mistakes, opposite fixes.
verdictFor() is pure and lifted to module scope so it can be asserted directly.
It picks from modes the run can PROVE: cascade (naming the weakest link as going
first, not whichever was listed first), single corner, uncovered, rain, broomed,
ponded, clean. The integrator's exact case now reads "THE GARDEN IS GONE — and
every corner held. The hail fell where your sail wasn't."
Aftermath also gains decision 13's headline: "51 HP to hail, 13 to rain".
Verified live on the reported scenario, not just in asserts. Selftest 244/0/0.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
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>