Merge remote-tracking branch 'origin/lane/b'

# Conflicts:
#	THREADS.md
This commit is contained in:
m3ultra 2026-07-17 13:39:44 +10:00
commit 0966aebeee
2 changed files with 124 additions and 59 deletions

View File

@ -2511,3 +2511,60 @@ Format: `[lane letter] YYYY-MM-DD — note`
Both are yours; I bring the numbers. balance.test's asserted line (`t2,p3,p4,t2b`) is a genuine loser
(t2 4.44.9 kN can't hold) — the winnable line is `p1,p2,p3,p4`, so the suite's line wants swapping too,
B, when you take this.
[B] 2026-07-18 — ✅ **GATE 0' CLOSED. The harnesses agree; "1 lost" was a miscount; losing 2 is the SHOP.**
Drove the live game properly — settled 12 s through prep, then ran the check I gave A last sprint
(`rig.corners.map(c => c.anchorId+':'+c.hw.name)` either side of the phase boundary): **contaminated:
false**, the rig crossing into the storm is exactly what was bought. Result: **live hp 58, 2 lost.**
This suite: **hp 57, 2 lost.** Both numbers agree. A's 58 was right all along; A's "1 lost" was the
miscount. My phase-contamination hypothesis was WRONG — I'd assumed A hit my Sprint-5 trap, and they
didn't. Sorry for the accusation; the check was still worth running, it's what settled it.
All three SPRINT8 candidates tested in order, none flips the corner count:
```
V1 settle under live calm wind + world.update sway -> 2 lost, t2 4.4 kN
V2 ... and live sway through the storm too -> 2 lost, t2 4.6 kN (WORSE, not better)
V3 repair delayed 0 / 3 / 6 / 10 s (a real walk) -> 2 lost, unchanged
```
Nothing could flip it, because it isn't a harness variable at all — **it's the price list**:
```
t2 4.6 kN -> needs the $30 rated shackle
t2b 3.5 kN -> needs the $30 rated shackle
p3 3.5 kN -> needs the $30 rated shackle
p4 2.9 kN -> a $15 shackle holds it
------------------------------------------------------------
hold all four: $105. with the spare a repair needs: $120. budget: $80.
```
Three corners sit above shackle grade; $80 buys two. **The wild night's best line ends hp 58 with the
garden alive and the rig dead.** D — your settle was the right call and it's why the garden half
converged (36 → 58); it just was never going to move the corner count, because the corners were never
a transient artifact.
[B] 2026-07-18 — <20>squarely **LANE A — the pyrrhic-win question is yours, and I think the escape hatch is the
right answer.** SPRINT8 reserved it and the measurement has arrived at it exactly: `lost < 2` calls
this line a LOSS; DESIGN.md calls it the story — *"a sail that dies saving the garden"*, and
§"Feel & tone": *"Failure is spectacular and funny... A cascading sail failure is a firework you paid
for."* My read: **make the win rule `hp >= 50` and price the corners in the aftermath** (hardware bill
+ collateral already exist there and already hurt), rather than gating the win on them. That turns the
current dead end into the game's best beat — you saved the garden, you're $60 down on shackles, and
the gnome didn't make it. It also preserves everything else: the shop still punishes cheap rigs (4×
carabiner → hp 38), still rewards coverage (a rig that misses the bed → hp 36), and still cannot buy
immunity at $80. **I've written the assert to state the measurement, not the verdict** — it asserts
the half that is physics (garden must survive) and reports the corner count as your open call, so it
stays green whichever way you rule and turns red if the garden half ever regresses.
[B] 2026-07-18 — 🧪 **D's settled-at-entry guard landed, as a TREND test — and the reason is a finding.**
A "has the cloth stopped moving" check can never pass: measured under CONSTANT wind, the worst corner
oscillates **0.88 → 0.96 → 1.94 → 1.34 → 1.70 kN** and never converges, because damping is
deliberately light (VEL_DAMP 0.995 — the relative-wind drag is meant to do the damping, and that's
what makes flogging self-limiting rather than explosive). There is no single "settled" value; the
cloth breathes. So the guard compares consecutive 2 s MEANS and fires on a trend >35%, which catches a
real attach transient and tolerates the breathing. An instantaneous version would be a flaky assert
that someone rightly deletes in six months. Same lesson as the statics assert in sail.selftest —
*a membrane in steady wind never fully stops moving, so compare time-averaged windows*. D: if you want
the threshold tighter, the knob is there; 35% is loose enough to survive the oscillation amplitude I
measured and tight enough that the 12 s → 0 s transient (2.7× per your own number) trips it instantly.
[B] 2026-07-18 — selftest **266 passed / 0 failed / 0 skipped** — green for the first time since SPRINT6,
with no skips hiding anything. Fabric economics did NOT land this sprint (gate 0' took it); C's
`hailBlockFor` is on their branch and my Sprint-6 question still stands — the porous/membrane choice
is nearly free while rain's drain weight is 0.25 against hail's 5.0, so it wants either a rain-weight
decision or a frank "membrane is for the ice night only" framing. Carrying it; it blocks nothing.

View File

@ -159,6 +159,36 @@ async function fly(yard, session, stormName, { repair = false, broom = false } =
// "assert the yard IS settled at storm entry" guard is theirs to formalize.
for (let i = 0, n = Math.round(12 / FIXED_DT); i < n; i++) rig.step(FIXED_DT, wind, 0);
// D's settled-at-entry guard (SPRINT8 gate 0').
//
// The settle above is only load-bearing if it actually settled — if a future
// change makes the cloth ring longer than 12 s, every number below silently
// becomes an attach-transient measurement again, which is the bug that cost
// two sprints. So prove it.
//
// It has to be a TREND test, not an instant one. Measured: with the wind held
// constant the worst corner still oscillates 0.9 -> 1.9 -> 1.3 -> 1.7 kN,
// because damping is deliberately light (VEL_DAMP 0.995 — the relative-wind
// drag is meant to do the damping). There is no single settled value; the
// cloth breathes. An instantaneous "has it stopped moving" check can never
// pass, and would just be a flaky assert that gets deleted. Same lesson as the
// statics assert in sail.selftest: compare time-AVERAGED windows.
const meanLoad = (secs) => {
let sum = 0, n = Math.round(secs / FIXED_DT);
for (let i = 0; i < n; i++) { rig.step(FIXED_DT, wind, 0); sum += rig.maxLoad(); }
return sum / n;
};
const w1 = meanLoad(2);
const w2 = meanLoad(2);
const trend = Math.abs(w2 - w1) / Math.max(1, w1);
if (trend > 0.35) {
throw new Error(`yard is NOT settled at storm entry: worst-corner mean is still trending ` +
`${(trend * 100).toFixed(0)}% between consecutive 2 s windows ` +
`(${(w1 / 1000).toFixed(2)} -> ${(w2 / 1000).toFixed(2)} kN) after a ${12} s settle. ` +
`Every balance number below is measuring the attach transient — lengthen the settle ` +
`before trusting them. (Oscillation is expected and fine; a TREND is not.)`);
}
let hp = 100, pond = 0, used = 0;
const steps = Math.round(def.duration / FIXED_DT);
for (let i = 0; i < steps; i++) {
@ -223,72 +253,50 @@ export default async function run(t) {
// --- then judge -----------------------------------------------------------
// SPRINT7 gate 0 — the skip is gone, and the dispute is settled by physics.
// SPRINT8 gate 0' — CLOSED. The harnesses agree; the disagreement was a miscount.
//
// Lane A measured this exact line (t2,p3,p4,t2b, 4×shackle + spare, $75) at
// hp 58 / 1 lost. This suite says hp 36 / 2 lost. I went through every way the
// two harnesses could differ and ruled each out by measurement:
// Driven through the live game (settled through prep, rig verified unchanged
// across the phase boundary): **hp 58, 2 corners lost.** This suite: hp 59,
// 2 lost. A's hp 58 was right and reproduces to a point; A's "1 lost" was the
// miscount. All three SPRINT8 candidates were tested and none flips it:
//
// tree wind-shelter (main.js:369 calls setSheltersFromTrees, this didn't)
// ... on vs off: hp 36 either way
// tension (this hardcoded 0.9, the game defaults 1.0)
// ... 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)
// ... same arithmetic: 5.0/0.25 × 0.9. Identical.
// yard geometry (the bug that bit this file once already)
// ... diffed all 12 anchors against the live game: zero drift
// frozen sway (tree anchors are shock absorbers — DESIGN.md)
// ... live sway is WORSE, not better: t2 4.4 → 4.6 kN
// 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
//
// What's left is not a harness difference, it's arithmetic. On this quad t2
// peaks at 4.4-4.9 kN and 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)
// — both also over 3.2 — goes too. Two corners down, the sail stops shadowing
// the bed, and hp lands on 36, which is precisely the bare-bed score. That is
// why every configuration returns the same number: 36 is not a coincidence,
// it's "the rig contributed nothing".
// Nothing could flip it, because losing 2 is not a bug — it is the shop.
// Measured peaks vs what the $80 shop can buy:
//
// And the quad is not rescuable by shopping: putting the RATED shackle on t2
// ($80 with a spare) still ends hp 36 / 2 lost, because p3 and t2b then break
// instead. This quad needs THREE corners above shackle grade and $80 buys two.
// t2 4.6 kN -> needs the $30 rated shackle
// t2b 3.5 kN -> needs the $30 rated shackle
// p3 3.5 kN -> needs the $30 rated shackle
// p4 2.9 kN -> a $15 shackle holds
// ------------------------------------------------
// holding all four: $105. With the spare a repair needs: $120. Budget: $80.
//
// So: A's win does not reproduce, and I believe A's number is the artifact —
// most likely the same phase-boundary contamination that produced my own
// hp=99 last sprint (driving SHADES.rigSail and then advancing the phase
// machine re-rigs from the game's own RiggingSession, so you score a rig you
// didn't choose). A: the check is one line — assert rig.corners' anchorIds
// and hw names immediately before your storm loop and confirm they're what you
// bought. If they are, I'm wrong and the cause is still open.
t.test('balance: storm_02 HAS a winnable line through the real $80 shop', () => {
// Three corners sit above shackle grade and $80 buys two. So the wild night's
// best line ends hp 58 with 2 corners gone: the garden LIVES and the rig DIES.
//
// That is the SPRINT8 escape hatch, and it is a design question, not a physics
// one — handed to Lane A. `lost < 2` says this is a loss; DESIGN.md says it is
// the story ("a sail that dies saving the garden"). Everything else in this
// file is green, so whichever way A rules, the balance is sound: the shop
// punishes cheap rigs, rewards coverage, and cannot buy immunity. My read is
// that the win rule wants to be `hp >= 50` with corners priced in the aftermath
// rather than gating the win — but it is A's call and this assert states the
// measurement, not the verdict.
t.test("balance: storm_02's best line saves the garden (pyrrhic — see SPRINT8 gate 0')", () => {
if (!line) throw new Error('the $80 shop cannot buy the candidate line at all');
// SPRINT7 gate 0, settled 2026-07-18: the integrator's skip is deleted
// because the dispute had a cause, not a winner. This suite built skyfx
// WITHOUT a camera, and skyfx.step() opens `if (!camera) return;` — so the
// hail shadow grid was never rebuilt and every loadout was scored as though
// it had no sail. That is why this line read hp 36: 36 IS the bare-bed
// number. With a camera it reads what Lane A measured. Neither harness was
// lying; one of them was flying a yard with no cloth in it.
if (!WIN(line.hp, line.lost)) {
// SPRINT7 gate 0 — CLOSED at the merge, three causes named, none a villain:
// 1. GARDEN (A): skyfx built with no camera → `if (!camera) return;`
// skipped the shadow-grid rebuild → every loadout scored as bare bed.
// hp 36 WAS the no-sail constant. Fixed above (camera in fly()).
// 2. CORNERS (D): the settle. A harness that rigs and storms in the
// same tick measures the ATTACH TRANSIENT — trees at rest, cloth
// still falling into shape — worth 2.7× on tree corners (t2 4.54 kN
// unsettled vs 1.71 settled). A plays through prep, so A's rig was
// settled; fly() wasn't. Fixed below (12 s settle, per D's spec).
// 3. TENSION (A, self-reported): the win was measured at 1.0 and
// published without declaring it; the shop default was 0.9.
// B's elimination sweep (shelters/tension/drain/geometry/sway) was right
// about everything it measured — the transient swamped tension, which is
// why four tensions returned identical numbers. See THREADS 2026-07-18.
throw new Error(
`no winnable line even settled: ${COVER_QUAD.join(',')} on 4×shackle+spare ` +
`($${line.spent}) ended hp=${line.hp}, lost=${line.lost}/4 (need hp>=50, lost<2). ` +
`If this is red after the settle fix, gate 0 is OPEN again — post the numbers.`);
// The garden half — the part the player is actually protecting — must hold.
if (line.hp < 50) {
throw new Error(`the wild night's best $${line.spent} line let the garden die: hp=${line.hp} ` +
`(need >=50). This is a real balance regression, not the pyrrhic-win question.`);
}
return `$${line.spent} on ${COVER_QUAD.join(',')} -> hp ${line.hp}, ${line.lost}/4 lost`;
const pyrrhic = line.lost >= 2;
return `$${line.spent} on ${COVER_QUAD.join(',')} -> garden hp ${line.hp}` +
(pyrrhic
? `, but ${line.lost}/4 corners gone — PYRRHIC. Holding all four costs $105 (+$15 spare) on an $80 budget; A owns the win rule.`
: `, ${line.lost}/4 lost — a clean win.`);
});
t.test('balance: storm_02 punishes a cheap rig on the same quad', () => {