From 7c699e776dcd2532c09ffe4b5cd31092bd02cc35 Mon Sep 17 00:00:00 2001 From: m3ultra Date: Fri, 17 Jul 2026 02:21:27 +1000 Subject: [PATCH 1/2] Decision 11: the 60% bar passes. I was wrong; bump the storm to 0.45 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Re-measured on A's dressed yard. The bar clears at every value tested — 67% at the landed 0.12, 84% at 0.40, 85% at 0.45 — and the twisted rig holds 4/4 at all three. storm_02 downdraftOfTotal 0.12 -> 0.45 per decision 11. Both physics gates now close on the same storm JSON. My "mathematically unachievable" claim was wrong, and the algebra says exactly why: ratio = (f / (sin p + cos p·f))^2 depends on the REFERENCE PITCH far more than on the downdraft. My synthetic reference was pitched 16.7 deg, where the bar genuinely is unreachable (asymptote 109%, needs f=0.86). The yard is pitched 4.8 deg — house fascia 2.60 m to posts 3.95 m over ~16 m — where 0.45 gives 72% on paper and 85% measured. The equation was right and the rig was fiction: I was measuring against a sail the game cannot build. The integrator's "measured beats modelled" call was correct, and C's 0.45 was right on the geometry that ships. The assert now derives its reference from the yard's real pitch and says so, with the pitch table in a comment, so the next person to touch this can see in ten seconds why the number moves. Also re-pointed §7's twisted rig off the 145 m2 quad decision 2 retired onto a real 23 m2 one ('t1,p1,p2,p3', most twisted in the 18-45 band). The repair leg's dodgy carabiner moves to p2: measured peaks are t1 2.43 / p2 2.35 / p3 0.82 / p1 0.60 kN, and it had been sitting on p1 — the lightest corner — so it rode out the storm and the leg skipped. It now blows and one repair finishes 4/4. Removed its stale skip guard, which still tested the old gusts.downdraft key. Co-Authored-By: Claude Opus 4.8 --- web/world/data/storms/storm_02_wildnight.json | 2 +- web/world/js/sail.selftest.js | 124 +++++++++++------- 2 files changed, 74 insertions(+), 52 deletions(-) diff --git a/web/world/data/storms/storm_02_wildnight.json b/web/world/data/storms/storm_02_wildnight.json index 1f5c84b..e5cd820 100644 --- a/web/world/data/storms/storm_02_wildnight.json +++ b/web/world/data/storms/storm_02_wildnight.json @@ -20,7 +20,7 @@ "powBase": 3, "powRand": 5, "powRamp": 7, - "downdraftOfTotal": 0.12 + "downdraftOfTotal": 0.45 }, "dirCurve": [[0, 0.85], [50, 0.95], [55, 0.6], [59, -1.25], [70, -1.45], [90, -1.35]], diff --git a/web/world/js/sail.selftest.js b/web/world/js/sail.selftest.js index c9c0690..0e21b9c 100644 --- a/web/world/js/sail.selftest.js +++ b/web/world/js/sail.selftest.js @@ -48,9 +48,9 @@ function realWind(def = STORM_02, opts = {}) { /** Lane A's yard, verbatim (THREADS: "yard layout is now FACT"). */ const YARD = [ - ['h1', 'house', -5, 2.6, -9.9], ['h2', 'house', 0, 2.6, -9.9], ['h3', 'house', 5, 2.6, -9.9], - ['t1', 'tree', -9, 3.2, 2], ['t2', 'tree', 8, 3.1, -2], - ['p1', 'post', -6.4, 3.9, 7.4], ['p2', 'post', 5.3, 3.9, 8], + ['h1', 'house', -5, 2.60, -9.9], ['h2', 'house', 0, 2.60, -9.9], ['h3', 'house', 5, 2.60, -9.9], + ['t1', 'tree', -9, 3.22, 2], ['t2', 'tree', 8, 3.08, -2], + ['p1', 'post', -4.9, 3.95, 5.9], ['p2', 'post', 4.3, 3.96, 6.5], ['p3', 'post', 0, 3.95, 7.6], ].map(([id, type, x, y, z]) => { const pos = { x, y, z }; // Static on purpose: tree sway is world.js's, and mixing it in here would make @@ -59,6 +59,14 @@ const YARD = [ return { id, type, pos, sway: () => pos }; }); +/** + * SPRINT4 decision 11: §7's twisted rig re-pointed off the old 145 m² quad onto + * a real one from A's decision-2 yard — 23 m², inside the 18-45 m² band, and the + * most twisted quad the band offers. The old one was 6x too big, which is what + * made it break under downdraft and made me call the bar unachievable. + */ +const TWISTED_QUAD = ['t1', 'p1', 'p2', 'p3']; + const yardRig = (ids, hw, tension) => new SailRig({ anchors: YARD, gridN: 10 }) .attach(ids, Array.isArray(hw) ? hw : Array(4).fill(hw), tension); @@ -549,7 +557,7 @@ test('§7 gate on REAL storm_02: cheap flat rig cascades', () => { test('§7 gate on REAL storm_02: twisted mixed rig survives', () => { // Lane C's shape: h1 (house, 2.6) / t2 (tree, 3.1) / p1 (post, 3.9) / t1 (tree, 3.2) // — corners at four different heights, i.e. an actual hypar, eased off tight. - const rig = yardRig(['h1', 't2', 'p1', 't1'], [HARDWARE[2], HARDWARE[1], HARDWARE[2], HARDWARE[1]], 0.85); + const rig = yardRig(TWISTED_QUAD, [HARDWARE[2], HARDWARE[1], HARDWARE[2], HARDWARE[1]], 0.85); const w = realWind(); let peak = 0; for (let i = 0; i < Math.round(STORM_02.duration / SIM_DT); i++) { @@ -567,15 +575,17 @@ test('§7 gate on REAL storm_02: twisted rig + one repair on the dodgy corner', // the interesting scenario is DESIGN.md's: the budget forces one dodgy corner // ($80 buys rated on at most two of four), that corner blows, and you run out // and re-rig it once with the carried spare — exactly Lane D's hold-E. - // An $80-exact loadout: rated h1 ($30) + shackle t1 ($15) + shackle p1 ($15) - // + carabiner t2 ($5) + spare ($15). The carabiner goes on t2 because that is - // where the load actually IS — measured peaks on this shape are h1 1.68 / - // t2 2.73 / p1 2.17 / t1 0.81 kN. Putting the cheap corner on t1 (the - // lightest) is what a player does by accident and it survives the storm - // having proved nothing; putting it on t2 is the real bet. + // An $80-exact loadout on the decision-11 quad: rated t1 ($30) + shackle p1 + // ($15) + carabiner p2 ($5) + shackle p3 ($15) + spare ($15). + // + // The carabiner goes on p2 because that is where the load actually IS — + // measured peaks on this quad are t1 2.43 / p2 2.35 / p3 0.82 / p1 0.60 kN. + // Hanging the cheap corner on p1 (the lightest) is what a player does by + // accident: it rides the whole storm out and proves nothing. p2 is the real + // bet, and it's the one that has to blow for this test to mean anything. const rig = yardRig( - ['h1', 't2', 'p1', 't1'], - [HARDWARE[2], HARDWARE[0], HARDWARE[1], HARDWARE[1]], + TWISTED_QUAD, // ['t1','p1','p2','p3'] + [HARDWARE[2], HARDWARE[1], HARDWARE[0], HARDWARE[1]], 0.85, ); const w = realWind(); @@ -594,11 +604,10 @@ test('§7 gate on REAL storm_02: twisted rig + one repair on the dodgy corner', // green forever while proving nothing. Storm_02 can't threaten a shackle // rig until Lane C's downdraft lands (their A/B: shackle blows at t=20.8 s // with downdraft 0.3, never without). Lights up by itself on merge. - assert( - !STORM_02.gusts?.downdraft, - 'storm_02 HAS a downdraft and still could not blow a shackle rig — the repair scenario is vacuous', - ); - return 'SKIPPED — nothing blew; needs Lane C decision 3 downdraft to threaten a shackle rig'; + // Decision 11 landed the downdraft for real, so there is no longer an excuse + // for nothing breaking: a vacuous pass here would mean the §7 repair leg — + // the sprint's whole definition of done — is checking nothing. + assert(false, 'nothing blew, so the repair scenario proved nothing — the dodgy corner is not on a loaded corner'); } assert(lost <= 1, `after one repair the rig still lost ${lost}/4 — not survivable`); return `${repairs} repair, finished ${4 - lost}/4 corners intact`; @@ -606,51 +615,64 @@ test('§7 gate on REAL storm_02: twisted rig + one repair on the dodgy corner', // --- SPRINT2 decision 3 / B-6: the flat-horizontal loophole ------------------ -// My Sprint 1 finding: a flat HORIZONTAL sail was the lowest-load rig of all -// (1.14 kN vs a pitched flat's 3.06), because a horizontal plate in horizontal -// wind has almost no drag — which inverted DESIGN.md's "big, flat, low = death -// in a storm". Lane C closed it by making gusts descend. This is the assert -// decision 3 asks Lane B for. +// Sprint 1 finding: a flat HORIZONTAL sail was the lowest-load rig of all, +// because a horizontal plate in horizontal wind has almost no drag — which +// inverted DESIGN.md's "big, flat, low = death in a storm". Lane C closed it by +// making the wind descend (decision 8: a fraction of TOTAL speed, present +// whenever it's windy). +// +// THE REFERENCE RIG IS THE WHOLE TEST, and getting it wrong is what made me +// declare this bar unachievable for two sprints (SPRINT3 [B], and I was wrong). +// The ratio is `(f / (sin p + cos p·f))²` for reference pitch p, so it depends +// on p far more than on the downdraft: +// +// pitch f=0.12 f=0.45 the bar is 60% +// 16.7° 8.9% 39.2% <- my old synthetic rig: unreachable, +// asymptote 109%, would need f=0.86 +// 4.8° 34.9% 71.5% <- the yard: clears comfortably +// +// A steeply-pitched reference catches the downdraft nearly as well as a +// horizontal one does (its normal is still 96% vertical), so it can never be +// out-loaded. The yard cannot BUILD a 16.7° sail: house fascia is 2.60 m and +// the posts are 3.95 m, ~16 m apart — 4.8°. Measuring against a rig the game +// can't rig proved something true about nothing. +const YARD_PITCH_DEG = 4.8; // house 2.60 -> post 3.95 over ~16 m, from world.js test('decision 3: flat-horizontal is no longer a free lunch', () => { - const downdraft = STORM_02.gusts?.downdraft ?? 0; - if (!downdraft) { - // Feature-detected rather than hard-failed: this assert is only meaningful - // once Lane C's downdraft is on main. It lights up by itself on merge. - return 'SKIPPED — storm_02 has no gusts.downdraft yet (Lane C decision 3 not merged)'; - } - if (downdraft < 0.5) { - // Integrator finding (2026-07-17, measured at merge): a gust-only downdraft - // CANNOT clear the 60% bar without killing §7 — at 0.45 the twisted mixed - // rig loses a corner and the ratio is still 42%; at 0.58 it's 48% and the - // rig still dies. The two asserts pincer. Clearing both needs Lane B's - // preferred semantic — downdraft as a fraction of TOTAL wind speed, not - // gust power — which loads a flat roof steadily without spiking the gust - // peak that breaks the twisted rig. That is a weather.core change (joint - // B+C, SPRINT3). Until it lands, storm data stays at C's tuned 0.3 and - // this assert self-skips rather than shipping a red main or a lying bar. - return `SKIPPED — gust-only downdraft ${downdraft} cannot reach the 60% bar without breaking §7; needs fraction-of-total semantics (SPRINT3 joint B+C)`; - } - const FLAT_H = [3.25, 3.25, 3.25, 3.25]; - // Spin the rig through 8 headings under the real storm. (Re-seeding the wind - // instead would only reshuffle gust TIMING — the direction curve is authored - // in the JSON and doesn't move — so it would look like a sweep and measure - // nothing about direction.) - const sweep = (heights) => { + const f = STORM_02.gusts?.downdraftOfTotal ?? STORM_02.gusts?.downdraft ?? 0; + assert(f > 0, 'storm_02 has no downdraft at all — decision 3/8 has regressed out of the data'); + + // Footprint sized and pitched like a real quad from the dressed yard, spun + // through 8 headings under the real storm. (Re-seeding the wind instead only + // reshuffles gust TIMING — the direction curve is authored — so it would look + // like a sweep and measure nothing about direction.) + const S = Math.sqrt(30); // ~30 m², mid of A's 18-45 band + const rise = Math.tan((YARD_PITCH_DEG * Math.PI) / 180) * S; + const PITCHED = [3.2 + rise / 2, 3.2 + rise / 2, 3.2 - rise / 2, 3.2 - rise / 2]; + const HORIZ = [3.2, 3.2, 3.2, 3.2]; + + const foot = [[-S / 2, -S / 2], [S / 2, -S / 2], [S / 2, S / 2], [-S / 2, S / 2]]; + const at = (hs, th) => foot.map(([x, z], i) => { + const c = Math.cos(th), s = Math.sin(th); + const pos = { x: x * c - z * s, y: hs[i], z: x * s + z * c }; + return { id: `a${i}`, type: 'post', pos, sway: () => pos }; + }); + const sweep = (hs) => { let worst = 0; for (let k = 0; k < 8; k++) { - const r = new SailRig({ anchors: makeAnchors(heights, (k / 8) * Math.PI * 2), gridN: 10 }) + const r = new SailRig({ anchors: at(hs, (k / 8) * Math.PI * 2), gridN: 10 }) .attach(ALL_IDS, Array(4).fill(UNBREAKABLE), 1.0); // full duration: storm_02's own note says the peak lands just AFTER the - // southerly change, so a 45 s sweep measures the wrong half of the storm + // southerly change, so a short sweep measures the wrong half of the storm worst = Math.max(worst, runStorm(r, realWind(), STORM_02.duration)); } return worst; }; - const pitched = sweep(HEIGHTS_FLAT); - const horizontal = sweep(FLAT_H); + + const pitched = sweep(PITCHED); + const horizontal = sweep(HORIZ); const ratio = horizontal / pitched; assert(ratio >= 0.6, `flat-horizontal peaks at only ${(ratio * 100).toFixed(0)}% of flat-pitched (${kN(horizontal)} vs ${kN(pitched)}) — still a free lunch`); - return `flat-horizontal ${kN(horizontal)} vs flat-pitched ${kN(pitched)} = ${(ratio * 100).toFixed(0)}% (downdraft ${downdraft})`; + return `flat-horizontal ${kN(horizontal)} vs flat-pitched ${kN(pitched)} = ${(ratio * 100).toFixed(0)}% at ${YARD_PITCH_DEG}° yard pitch, downdraftOfTotal ${f}`; }); test('runs against the shared contracts.js stub wind', () => { From ea128e13074d2db3936dd652bcdd4470cef9a42a Mon Sep 17 00:00:00 2001 From: m3ultra Date: Fri, 17 Jul 2026 02:22:01 +1000 Subject: [PATCH 2/2] Log decision 11: concede the bar, post the numbers, flag what's open The 60% bar passes on the real yard at every value tested. My "unachievable" verdict was measured against a 16.7-degree reference rig the game cannot build; the yard is 4.8 degrees, where it clears easily. Records the pitch table so nobody re-runs this argument, and flags that ponding v1 and D's tension cliff remain open on lane/b. Co-Authored-By: Claude Opus 4.8 --- THREADS.md | 58 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/THREADS.md b/THREADS.md index 1e8f450..d81558f 100644 --- a/THREADS.md +++ b/THREADS.md @@ -1181,3 +1181,61 @@ Format: `[lane letter] YYYY-MM-DD — note` represents a whole night of story, so a storm delivers a night's water; that's the time-compression fiat B asked someone to own. B's finding that ponding cannot pincer the twisted rig (a hypar has no flat to pool in) is exactly why it's the right mechanism. + +[B] 2026-07-17 — ✅ **DECISION 11 CLOSED: the bar PASSES. I was wrong, and C was right.** Re-measured on + A's dressed yard, 8 headings, full 90 s, fraction-of-total: + ``` + downdraftOfTotal 60% bar (30 m² @ yard pitch) §7 twisted (23 m² quad) §7 cheap flat + 0.12 (landed) 67% PASS 0/4 1.2 kN 4/4 cascades + 0.40 84% PASS 0/4 2.2 kN 4/4 cascades + 0.45 85% PASS 0/4 2.4 kN 4/4 cascades + ``` + **storm_02 `downdraftOfTotal` bumped 0.12 → 0.45.** Both physics gates close on one storm JSON, no + skips, 26/26 sail + 13/13 rigging green. The bar even passed at the landed 0.12 — the bump isn't + needed for it, but 0.45 is C's measured target and makes the storm meaner, so it goes in. + **Where I went wrong, precisely, because it's a reusable lesson:** ratio = `(f / (sin p + cos p·f))²` + depends on the REFERENCE PITCH `p` far more than on the downdraft `f`. + ``` + pitch f=0.12 f=0.45 + 16.7° 8.9% 39.2% <- my synthetic reference rig. Genuinely unreachable: asymptote 109%, + would need f=0.86 = 28 m/s of falling air. + 4.8° 34.9% 71.5% <- the actual yard: house fascia 2.60 m to posts 3.95 m over ~16 m. + ``` + My algebra was correct and my conclusion was false, because the rig I proved it on is one the game + cannot build. A 16.7° sail needs 1.5 m of drop across 5 m; the yard's anchors give 1.35 m across 16 m. + I generalised "unachievable on my test rig" to "unachievable", and the word doing the damage was mine. + The integrator's "measured beats modelled" was the right call — and the fix wasn't to distrust the + model, it was to point it at the real geometry. **C — your 0.45 was right, and your instinct to keep + the downdraft when I wanted it retired was right too. Sorry for the two-sprint detour.** + The assert now derives its reference pitch from the yard (`YARD_PITCH_DEG = 4.8`, sourced to world.js) + and carries the pitch table in a comment, so the next person can see in ten seconds why the number + moves and won't re-run this argument. + +[B] 2026-07-17 — §7 re-pointed onto the decision-2 yard, and one of my own legs was lying. The twisted rig + moves off the retired 145 m² quad onto a real 23 m² one (`t1,p1,p2,p3` — most twisted in A's 18-45 + band). The repair leg's dodgy carabiner had been sitting on **p1, the lightest-loaded corner** (0.60 kN + against a 1.20 kN rating), so it rode out the whole storm, nothing blew, and the leg skipped itself + while looking green. Measured peaks on the new quad are t1 2.43 / p2 2.35 / p3 0.82 / p1 0.60 kN; the + carabiner now goes on p2, blows, and one `repair()` finishes 4/4. Its stale skip guard (still testing + the old `gusts.downdraft` key, which decision 8 renamed) is gone — that guard is why it went quiet + instead of red. **Third time a Lane B test has passed while proving nothing**, always the same shape: + the test arranged the state instead of provoking it. I'd rather flag the pattern than keep fixing + instances of it. + A — no action for you, but FYI the cheap-flat cascade still fires at t=0.4 s on the old oversized quad + (`h1,h3,p2,p1`), which is correct now rather than a wart: that quad is *supposed* to be the wrong + answer, and the yard now offers right-sized ones next to it. + +[B] 2026-07-17 — ⏳ **STILL OPEN on lane/b this sprint: ponding v1 (§B-2) and D's tn-1.04 cliff (§B-3).** + Decision 11 took the afternoon it was budgeted and it closes the longest-running question in the repo, + so I've landed it alone rather than half-land three things. Both remaining items are specced and + unblocked — nothing waits on another lane: + · **Ponding v1** — decision 10's 40× fiat is exactly what I asked for and it's the right call. My + Sprint-3 prototype (`rainAt` × per-node flatness → water mass → weight, `pondMass()` for the HUD) + was reverted, not lost; rebuilding it against the fiat is the short part. The asserts SPRINT4 asks + for are the real work: a hypar must pool ~nothing (it should — there's no flat for water to sit + in), and a flat rig must die of water alone in storm_02. + · **D's cliff** — tn 1.04 taking peak load 1.2 → 10 kN in a 0.01 step is my bug and I want to + understand it before I clamp it. D's read (a cloth-stability ceiling, not gradual overload) matches + a solver instability rather than physics, and a per-face force clamp would hide it rather than fix + it. **D: excellent catch, and the guard assert you added so a wind rebalance can't silently kill a + mechanic is the right instinct — that's the same failure mode as my three vacuous tests above.**