Gate 2's actual deliverable. The node tool cannot dress, so it can only audit
the built-in snapshot or a resolved export and REFUSES A's dress-source site
JSON. This is the honest audit: audit.html builds the site the way the game does
— createWorld(await loadSite(name)) then dress() — and reads world.anchors, the
single source of DRESSED positions (GLB fascia + branch anchors, 8°-raked posts,
all of it), then runs the SAME sweep. It is the only way to audit a site with a
carport (site_02) before it ships.
tools/site_audit/audit.html?site=backyard_01
tools/site_audit/audit.html?site=site_02_corner_block&storm=storm_03_southerly
The winnability math now lives in ONE place, sweep.js, imported by both
front-ends — a tool built to catch reimplemented-formula drift doesn't get to
keep two copies of its own sweep. audit.mjs is refactored onto it (output
byte-identical); the node refusal message now hands you the exact audit.html URL.
Validated in-browser against backyard_01: dressed 12/12 anchors (GLBs loaded),
PASS, best line p1,p2,p3,p4 $65+$15 spare — identical to the node snapshot
verdict and to balance.test, off A's real data with no snapshot. That
cross-agreement also re-proves A's extraction is byte-identical.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Records the venturi gap coordinates for C, the needsLadder mechanism-key for D,
the honest-quad geometry for B's audit, and the one Lane B API the site switch
still needs — rigging.setWorld(world) to re-point the clickable markers, already
called guarded from loadSiteInto.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
SPRINT10 gate 1b. A night is now {storm, site}; the week hands over a yard per
night and night three is the corner block. Nights 1/2/4/5 are the backyard; the
plain-string night form still works and defaults to backyard_01.
site_02_corner_block.json: a smaller 24x16 yard with E's carport as the trap it
was built to be — beam anchors rating_hint 0.22, posts 0.30, both below the
house fascia's 0.35, collateral "carport". The site IS winnable off the honest
anchors (one tree east, four ground posts): I enumerated the honest quads at
authoring time and there are four in-band ones covering the bed, so the carport
is temptation not necessity. Full load audit is Lane B's tools/site_audit.
The carport's beams are keyed work:"bracket" — D's field, the MECHANISM not the
type. A bracket up a bare beam is the one anchor you can't fake from the ground,
so it needs the ladder; keyed on type it would fail open (a carport isn't a
house) and the ladder mechanic would silently vanish, which is exactly the audit
D ran. C's venturi list is in the JSON with two gap coordinates they asked for.
The world is rebuilt on a site change, not re-pointed: createPlayer captures the
world into its ground clamp, collider, ladder and broom (Lane D's modules, no
setters), so a clean rebuild is honest — and a site change only happens at a
forecast, never mid-storm. world.dispose() frees the old yard so nothing leaks.
The re-point of rig + session lives inside loadSiteInto, keyed on the rebuild
itself, after a caller-side version proved fragile.
Verified end to end through real phase transitions: play night 1, hit the
aftermath, and the forecast into night 3 rebuilds into the corner block —
carport present, 10 anchors (cb/cp/q/tr, not h/t/p), smaller bed, player rebuilt,
no scene leak. Then back to the backyard, 12 anchors restored. Selftest 289/0/0.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A's gate-1 merge made createWorld REQUIRE a site (throws without one), which
crashed this tool's post cross-check: it called createWorld(scene, {wind}) with
no site. Fixed by reading the shipped data/sites/backyard_01.json and passing it
(loadSite fetch()es, so it can't run in node — read the file directly). The
cross-check is now BETTER: it verifies the dressed-snapshot posts against the
actual shipped site data, raked in createWorld, rather than against code.
Built-in audit green again; dress-source refusal (exit 2) and resolved-export
(exit 0) paths unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A's committed site schema (SPRINT10 world.js loadSite/createWorld) is
dress-source, and my loadSite assumed resolved positions. Run head-on against
A's real data/sites/backyard_01.json the tool reported:
✗ FAIL — no quad in the band shades the bed at all. The site cannot be rigged.
That is a lie, and the dangerous kind: the site is fine; the TOOL couldn't read
it. It is the SPRINT6 unwinnable-site trap in reverse — a false negative that,
in a "every site runs the audit before it ships" workflow, condemns a good site.
Two things make the schema unreadable headless, both real:
· posts are pre-rake. JSON carries {id,x,z,h}; world.js:361 leans each post 8°
off centre. p4's spec (-3.2,-1.2) dresses to (-3.72,-1.40) — the exact 0.56 m
error this tool's own snapshot shipped in SPRINT9. Reading x/z raw re-makes it.
· house/tree anchors have no coordinates — only `node`, a GLB empty's name.
Their position exists only after dress() reads matrixWorld, and dress() needs
GLTFLoader + fetch, neither of which runs in node. Branch anchors are exactly
where the dangerous quads live (t2b at 10 kN), so dropping them silently is
the worst possible failure.
The correct source of dressed positions is createWorld(site).anchors, which is
browser-only. That path lands once A's data-driven createWorld is in main; until
then loadSite REFUSES dress-source JSON with the reason and the plan, and exits 2
(distinct from the winnability FAIL's exit 1). The built-in DRESSED snapshot and
a resolved { anchors:[{id,type,pos}] } export both still audit. Raised to A in
THREADS: audit in-browser off createWorld(site).anchors, or export resolved anchors.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
C asked in THREADS whether to land a one-liner making gardenHailExposure read
sail.porosity. Answer: it is already wired, in merged main — I did it in SPRINT9
(e576f5c): skyfx.step refreshes sailPorosity from world.sail.porosity, and
gardenHailExposure folds hailBlockFor(size, sailPorosity) into what it returns.
No one-liner needed.
But "already wired" was untested end-to-end. The only test of the hail leak,
weather.selftest's 'fabric choice is real', REIMPLEMENTS the formula inline with
hailBlockFor + hailAt — it proves the primitive and the arithmetic, not the
plumbing. A regression in the plumbing (the :738 refresh dropped, the
wind.def.hail.size lookup resolving wrong, hailBlockFor no longer folded in)
would leave that test green while the game quietly stopped leaking hail. That is
the measure-a-copy pattern this whole browser suite exists to avoid.
New assert 'porous cloth leaks pea hail into the garden, membrane blocks it'
drives the real sky.gardenHailExposure. The isolation is exact: ONE settled,
intact rig, sampled at ONE hail instant, porosity flipped 0.30<->0 between reads.
sky.step(0, t, {sail}) refreshes sailPorosity and rebuilds the shadow grid at the
same instant without advancing physics, so shadow geometry is identical across
the two reads and `block` is the only thing that moved. Measured:
pea (size 0.7): cloth 0.346 vs membrane 0.292 -> porous leaks +18%
ice (size 1.4): cloth 0.458 vs membrane 0.458 -> identical, the no-op
No storm flight, no second rig — deliberately. Two rigs whose corners diverge
share no shadow, and that difference is a cascade not a fabric: membrane cascades
on the ice night, so a naive two-flight version reads a FALSE ice-night
difference. The same-rig swap removes it.
And it can fail: if porosity stops reaching the exposure, both reads collapse to
the membrane value (measured: 0.2917 == 0.2917), cloth > membrane*1.05 is false,
red. The ice check is exact equality — if porous ever starts leaking big ice,
that goes red too.
Selftest 288 passed, 0 failed, 0 skipped.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
SPRINT10 gate 1a, carried since Sprint 8. Everything that was a module constant
— bed rect, house line, tree and post placements, shed, gnome, sun angle, fence
sides — is now data/sites/backyard_01.json, and world.js is a builder.
The extraction is byte-identical, and that's checkable rather than claimable:
E's branch-anchor tripwires pin five positions to 6 dp, a.test's quad-band
asserts pin the whole rigging geometry (>=3 quads in 18-45 m2, full coverage
still >45 m2), D's ladder field and B's balance lines all pass unchanged.
287/0/0.
It's byte-identical by construction, not by luck: the site-derived values keep
the builder's original local names, so ~500 lines of geometry that was already
correct is untouched and only its SOURCE changed.
Two things earned their place:
Anchors carry D's `work: "cloth" | "bracket"` — the MECHANISM, not the type. A
post is tensioned from a cleat at its base and a tree strop is thrown, but a
bracket bolted up a bare wall is the one you cannot fake. Keyed on mechanism
because `type` is a closed enum ('house'|'tree'|'post') that site_02's carport
doesn't fit, and needsLadder keyed on type FAILS OPEN there — the ladder
silently vanishes and you re-rig a 2.6 m bracket standing on the grass.
p4's swept position table moved INTO the JSON's `_why`, beside the coordinate it
justifies. That table is the only thing stopping someone nudging the post toward
the bed and collapsing DESIGN.md's central tension; it belongs with the number,
not in a file the number left.
validateSite() fails loud: sites are content, and a missing bed silently becomes
NaN coverage while a missing anchor list silently becomes an unriggable site.
createWorld now requires a site and says so.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Load goes as wind^2 but the funnel only fires while aligned, so gain 1.35 adds
+40% not +82% — a small quad in the throat peaks 1.99 kN, above a carabiner
(1.2) and under a shackle (3.2). The funnel punishes the cheap corner
specifically without being unwinnable: the corner block teaching itself.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Night 3 = corner block + storm_03b is the right pairing (the funnel teaches the
southerly, 03b is the southerly). Its post-change wind blows toward -1.08 rad,
so that's the venturi axis. Gave A/E the ready template needing only the two gap
coordinates, and flagged B that the venturi is a balance lever their site_audit
must include (gain is mine to tune to their sweep).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The carport anchors have said collateral="carport" since Sprint 9, but the root
carried no collateral_value — so main.js, which reads world.gnome.collateralValue
for the gnome, had no number to reach for. A trap with no price is a warning
label. The root now carries it in the same shape and place, pinned above the
gnome's 25, with the proposed 180 and its reasoning as a named constant so Lane A
can argue with the number rather than the asset.
carport_01_wrecked is the payoff, on the intact one's origin for a mesh-for-mesh
swap: the pads stay (they were never the weak part), the frame racks over like a
parallelogram, the roof peels off downwind in one piece.
Two bugs found by measuring rather than reasoning, both now comments where the
numbers are: the beams were rotated when a beam spans the axis the frame racks
in, so it rides level — tilting it made the wreck taller than the carport; and
the roof sheets dipped below ground, which is what inflated the height. Asserts
now pin the wreck shorter than its twin and sitting on the grass.
291/0/0, Lane E 64 asserts, 40/40 output files byte-identical.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A builds world.js FROM DATA this sprint, and a site's `wind` block (venturi
funnels, later per-site shelter overrides) is hand-authored data like a storm —
so it validates like one. setVenturi already clamps defensively, but a clamp
hides a typo where the repo's ethos is to name it: a gain < 1 (someone reaching
for a shelter), a funnel with no throat coordinates, an axis typed "south". A
calls this at site load next to validateStorm. A site with no wind block, an
empty block, or an empty venturi list all validate — the funnel is opt-in.
Node 56/0/0.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
SPRINT10 gate 1. Adopts SPRINT10's spelling — `work: 'bracket' | 'cloth'` — as
canonical over the boolean I proposed in Sprint 9: naming the mechanism
positively beats a negation, and it's the field A is emitting. Sprint 9's
`worksAtBracket` is still honoured so nothing that already sets it breaks, and
`type === 'house'` remains the fallback, so site_01 is a no-op (verified live:
no anchor there carries either field, and fascia/post/tree verdicts are
unchanged).
Keyed to E's SHIPPED carport_01_v1.glb rather than my invented numbers, and it
confirms the audit exactly — it also carries BOTH mechanisms on one structure,
so a per-structure rule would still have been wrong:
beam_anchor_01/02 y=2.36 anchor_type "carport" rating_hint 0.22
post_anchor_01/02 y=1.75 anchor_type "carport_post" rating_hint 0.30
The beam sits over a 1.72 m person's 2.20 m reach and needs the ladder; the post
doesn't. Neither is typed 'house', so un-declared they BOTH fall back to "ground
work" and canReach waves a 2.36 m bracket through from the grass — the mechanic
deleting itself with no error. That regression is now pinned by a test.
E: the beam is the double trap — your worst rating in the game AND the one that
costs a ladder trip. That reads as a rule, not a bug, which is what §Lane D asks
for. A: the mapping you want is beam→bracket, post→cloth.
63 Lane D asserts headless, 288/0/0 in the browser.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
SPRINT9 §Lane D: "play site_02 cold and log where the verbs break (the ladder
assumes fascia height; the carport may not have one)". site_02 isn't landed yet,
so I audited the verb against it instead of waiting.
The seam is real but it isn't height — it's that `needsLadder` is spelled
`type === 'house'`, and contracts.js types anchors as a closed enum
('house'|'tree'|'post') that E's carport doesn't fit. The failure mode is what
makes it worth fixing now: if needsLadder says false, interact's canReach returns
TRUE UNCONDITIONALLY, so the player re-rigs a 2.6 m bracket standing on the grass
and the mechanic silently stops existing. Same disease as StumbleBack's dead
threshold and the camera-less shadow grid — a rule keyed on the wrong thing,
failing open with no error.
A pure height test is NOT the fix: posts (3.95 m) and tree limbs (5.05 m) are
higher than the fascia and need no ladder, because you work the CLOTH there and
the bracket here. So needsLadder now reads data first —
`anchor.worksAtBracket` — exactly like anchor.ratingHint already does from the
GLB's userData, and falls back to site_01's type. Nothing on site_01 carries the
flag, so it's a no-op there (asserted). Lane A/E: set it in the site JSON and the
ladder follows the site with no code change.
Also made ladder.js headless-importable (lazy GLTFLoader, same as broom.js),
because d.test.js was testing fakeLadder's hand-copied duplicate of needsLadder
rather than the rule itself — a rule the suite re-implements is one it cannot
catch drifting. Verified the ladder's GLB still loads in the browser.
62 Lane D asserts headless, 275/0/0 in the browser.
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>
$80 win re-verified on rebased main (porous + 0.40, hp 52, 1 lost). Not
flipping 0.40 until B's fabric is selectable — the win needs porous cloth.
Handed A the site-JSON venturi shape to wire from their extraction.
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 integrator demoted my settled-at-entry guard to a warning at the Sprint-8
merge and handed B+D the redesign. Investigating it refuted the stated cause and
found a better observable.
The ponding diagnosis does not hold: every storm's rain curve starts at [0,0], so
storm_02.rainMmPerHour(0) is 0.0 mm/h and a held clock rains nothing. The real
confound was that the guard measured in the STORM's wind while the rig settled in
CALM — and that calm->storm step is one the REAL GAME ALSO HAS (wind.use() swaps
on the phase change), so it was never a harness artifact. Their 2 s windows were
also shorter than the cloth's breath: 2 s reads a 32% trend on a settled rig
where 3 s reads 6%.
But fixing the clock isn't enough, because LOAD cannot answer this question at
all. In calm there is no transient to see — the unsettled rig is LIGHTER (0.30 kN
vs 0.47 settled) and the trend test passes at every settle length from 0 to 20 s.
A guard that cannot fail is decoration, which is precisely what StumbleBack and
the fake skips were.
So ask the physical question: has the cloth stopped MOVING. Mean node drift
separates ~6x either side, and cannot be confused by wind or water:
unsettled (0 s) -> 212 mm/s (cover quad) · 208 (miss quad)
settled (8-20 s) -> 35 mm/s worst breath, typically 4-6
Limit 100 mm/s. Proved it FAILS before trusting it to pass: sabotaged the settle
to 0 s and it fired — "cloth still drifting 212 mm/s (limit 100)".
The integrator's 600 N floor was a sound amendment to the LOAD design; it isn't
carried over because drift has no units of force and 212-vs-35 is not a judgement
call. 274/0/0, all five balance asserts green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Both yes, with reasons: the 25%-cover squeak-win is decision 13 paying off
rather than a loophole, and the repair becomes the margin instead of a toll —
SPRINT6's "the line must need the repair" is retired, since enforcing it means
tuning so the best rig always breaks. C gets the go on 0.40, paired with B's
fabric per SPRINT9 decision 2.
Also logs the ninth harness artifact: read 274/0/0 off another session's tree
because my server died on a bound port. The pass count not moving when I added
two asserts was the tell.
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>
Swept all 299 bed-covering quads settled: only p1,p2,p3,p4 is holdable, wins
outright ($90 today). Porous shade cloth + downdraft 0.40 drop p1 under the
carabiner tier → the same win at exactly $80. Both levers needed; neither moves
a tier alone. Not flipping 0.40 unilaterally (B's balance pen, needs fabric
wired first) — offered it. Flagged two design calls (25% thin-cover win, wins
without a repair) for A+B.
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>
Live game and suite now agree (hp 58/57, 2 lost). A's number was right,
their corner count was the miscount, and my phase-contamination
hypothesis was wrong. None of the three candidates flips it because
holding all four corners costs $105 on an $80 budget. Hands A the
pyrrhic-win call with a recommendation, and records why D's settle guard
had to be a trend test (the cloth breathes and never converges).
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>