Merge remote-tracking branch 'origin/lane/b'
# Conflicts: # THREADS.md
This commit is contained in:
commit
34712bf297
57
THREADS.md
57
THREADS.md
@ -1385,3 +1385,60 @@ Format: `[lane letter] YYYY-MM-DD — note`
|
|||||||
`data/storms/`, so **storm_03 was never loaded in the browser half** — it only surfaced when my new
|
`data/storms/`, so **storm_03 was never loaded in the browser half** — it only surfaced when my new
|
||||||
ponding case reached for `storms.storm_03_southerly` and got `undefined`. Fixed, with a note on the
|
ponding case reached for `storms.storm_03_southerly` and got `undefined`. Fixed, with a note on the
|
||||||
list. If you add a storm, that list is the thing to update.
|
list. If you add a storm, that list is the thing to update.
|
||||||
|
[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.**
|
||||||
|
|||||||
@ -20,7 +20,7 @@
|
|||||||
"powBase": 3,
|
"powBase": 3,
|
||||||
"powRand": 5,
|
"powRand": 5,
|
||||||
"powRamp": 7,
|
"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]],
|
"dirCurve": [[0, 0.85], [50, 0.95], [55, 0.6], [59, -1.25], [70, -1.45], [90, -1.35]],
|
||||||
|
|||||||
@ -48,9 +48,9 @@ function realWind(def = STORM_02, opts = {}) {
|
|||||||
|
|
||||||
/** Lane A's yard, verbatim (THREADS: "yard layout is now FACT"). */
|
/** Lane A's yard, verbatim (THREADS: "yard layout is now FACT"). */
|
||||||
const YARD = [
|
const YARD = [
|
||||||
['h1', 'house', -5, 2.6, -9.9], ['h2', 'house', 0, 2.6, -9.9], ['h3', 'house', 5, 2.6, -9.9],
|
['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.2, 2], ['t2', 'tree', 8, 3.1, -2],
|
['t1', 'tree', -9, 3.22, 2], ['t2', 'tree', 8, 3.08, -2],
|
||||||
['p1', 'post', -6.4, 3.9, 7.4], ['p2', 'post', 5.3, 3.9, 8],
|
['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]) => {
|
].map(([id, type, x, y, z]) => {
|
||||||
const pos = { x, y, z };
|
const pos = { x, y, z };
|
||||||
// Static on purpose: tree sway is world.js's, and mixing it in here would make
|
// 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 };
|
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) =>
|
const yardRig = (ids, hw, tension) =>
|
||||||
new SailRig({ anchors: YARD, gridN: 10 })
|
new SailRig({ anchors: YARD, gridN: 10 })
|
||||||
.attach(ids, Array.isArray(hw) ? hw : Array(4).fill(hw), tension);
|
.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', () => {
|
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)
|
// 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.
|
// — 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();
|
const w = realWind();
|
||||||
let peak = 0;
|
let peak = 0;
|
||||||
for (let i = 0; i < Math.round(STORM_02.duration / SIM_DT); i++) {
|
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
|
// 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
|
// ($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.
|
// 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)
|
// An $80-exact loadout on the decision-11 quad: rated t1 ($30) + shackle p1
|
||||||
// + carabiner t2 ($5) + spare ($15). The carabiner goes on t2 because that is
|
// ($15) + carabiner p2 ($5) + shackle p3 ($15) + spare ($15).
|
||||||
// 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
|
// The carabiner goes on p2 because that is where the load actually IS —
|
||||||
// lightest) is what a player does by accident and it survives the storm
|
// measured peaks on this quad are t1 2.43 / p2 2.35 / p3 0.82 / p1 0.60 kN.
|
||||||
// having proved nothing; putting it on t2 is the real bet.
|
// 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(
|
const rig = yardRig(
|
||||||
['h1', 't2', 'p1', 't1'],
|
TWISTED_QUAD, // ['t1','p1','p2','p3']
|
||||||
[HARDWARE[2], HARDWARE[0], HARDWARE[1], HARDWARE[1]],
|
[HARDWARE[2], HARDWARE[1], HARDWARE[0], HARDWARE[1]],
|
||||||
0.85,
|
0.85,
|
||||||
);
|
);
|
||||||
const w = realWind();
|
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
|
// 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
|
// 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.
|
// with downdraft 0.3, never without). Lights up by itself on merge.
|
||||||
assert(
|
// Decision 11 landed the downdraft for real, so there is no longer an excuse
|
||||||
!STORM_02.gusts?.downdraft,
|
// for nothing breaking: a vacuous pass here would mean the §7 repair leg —
|
||||||
'storm_02 HAS a downdraft and still could not blow a shackle rig — the repair scenario is vacuous',
|
// 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');
|
||||||
return 'SKIPPED — nothing blew; needs Lane C decision 3 downdraft to threaten a shackle rig';
|
|
||||||
}
|
}
|
||||||
assert(lost <= 1, `after one repair the rig still lost ${lost}/4 — not survivable`);
|
assert(lost <= 1, `after one repair the rig still lost ${lost}/4 — not survivable`);
|
||||||
return `${repairs} repair, finished ${4 - lost}/4 corners intact`;
|
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 ------------------
|
// --- SPRINT2 decision 3 / B-6: the flat-horizontal loophole ------------------
|
||||||
|
|
||||||
// My Sprint 1 finding: a flat HORIZONTAL sail was the lowest-load rig of all
|
// 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
|
// because a horizontal plate in horizontal wind has almost no drag — which
|
||||||
// wind has almost no drag — which inverted DESIGN.md's "big, flat, low = death
|
// inverted DESIGN.md's "big, flat, low = death in a storm". Lane C closed it by
|
||||||
// in a storm". Lane C closed it by making gusts descend. This is the assert
|
// making the wind descend (decision 8: a fraction of TOTAL speed, present
|
||||||
// decision 3 asks Lane B for.
|
// 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', () => {
|
test('decision 3: flat-horizontal is no longer a free lunch', () => {
|
||||||
const downdraft = STORM_02.gusts?.downdraft ?? 0;
|
const f = STORM_02.gusts?.downdraftOfTotal ?? STORM_02.gusts?.downdraft ?? 0;
|
||||||
if (!downdraft) {
|
assert(f > 0, 'storm_02 has no downdraft at all — decision 3/8 has regressed out of the data');
|
||||||
// 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.
|
// Footprint sized and pitched like a real quad from the dressed yard, spun
|
||||||
return 'SKIPPED — storm_02 has no gusts.downdraft yet (Lane C decision 3 not merged)';
|
// 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
|
||||||
if (downdraft < 0.5) {
|
// like a sweep and measure nothing about direction.)
|
||||||
// Integrator finding (2026-07-17, measured at merge): a gust-only downdraft
|
const S = Math.sqrt(30); // ~30 m², mid of A's 18-45 band
|
||||||
// CANNOT clear the 60% bar without killing §7 — at 0.45 the twisted mixed
|
const rise = Math.tan((YARD_PITCH_DEG * Math.PI) / 180) * S;
|
||||||
// rig loses a corner and the ratio is still 42%; at 0.58 it's 48% and the
|
const PITCHED = [3.2 + rise / 2, 3.2 + rise / 2, 3.2 - rise / 2, 3.2 - rise / 2];
|
||||||
// rig still dies. The two asserts pincer. Clearing both needs Lane B's
|
const HORIZ = [3.2, 3.2, 3.2, 3.2];
|
||||||
// preferred semantic — downdraft as a fraction of TOTAL wind speed, not
|
|
||||||
// gust power — which loads a flat roof steadily without spiking the gust
|
const foot = [[-S / 2, -S / 2], [S / 2, -S / 2], [S / 2, S / 2], [-S / 2, S / 2]];
|
||||||
// peak that breaks the twisted rig. That is a weather.core change (joint
|
const at = (hs, th) => foot.map(([x, z], i) => {
|
||||||
// B+C, SPRINT3). Until it lands, storm data stays at C's tuned 0.3 and
|
const c = Math.cos(th), s = Math.sin(th);
|
||||||
// this assert self-skips rather than shipping a red main or a lying bar.
|
const pos = { x: x * c - z * s, y: hs[i], z: x * s + z * c };
|
||||||
return `SKIPPED — gust-only downdraft ${downdraft} cannot reach the 60% bar without breaking §7; needs fraction-of-total semantics (SPRINT3 joint B+C)`;
|
return { id: `a${i}`, type: 'post', pos, sway: () => pos };
|
||||||
}
|
});
|
||||||
const FLAT_H = [3.25, 3.25, 3.25, 3.25];
|
const sweep = (hs) => {
|
||||||
// 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) => {
|
|
||||||
let worst = 0;
|
let worst = 0;
|
||||||
for (let k = 0; k < 8; k++) {
|
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);
|
.attach(ALL_IDS, Array(4).fill(UNBREAKABLE), 1.0);
|
||||||
// full duration: storm_02's own note says the peak lands just AFTER the
|
// 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));
|
worst = Math.max(worst, runStorm(r, realWind(), STORM_02.duration));
|
||||||
}
|
}
|
||||||
return worst;
|
return worst;
|
||||||
};
|
};
|
||||||
const pitched = sweep(HEIGHTS_FLAT);
|
|
||||||
const horizontal = sweep(FLAT_H);
|
const pitched = sweep(PITCHED);
|
||||||
|
const horizontal = sweep(HORIZ);
|
||||||
const ratio = horizontal / pitched;
|
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`);
|
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', () => {
|
test('runs against the shared contracts.js stub wind', () => {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user