From 1c0fcc757b85979e04588b860d2e37ec6607a345 Mon Sep 17 00:00:00 2001 From: type-two Date: Sat, 18 Jul 2026 14:11:58 +1000 Subject: [PATCH] Sprint 13 gate 1.4 RE-RULED on B's skew finding: the pin measures the game now, and the target is restated MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - a.test's separation flight had the phantom 12 s settle B caught (SailRig samples wind at its INTERNAL clock; the storm flew 12 s off the authored curve). Settle deleted — commit->attach->storm from t=0, C's corrected bench pattern; venturi wired from site data so the flight can never assume a yard. - Game-true the line reads 63.6, not 68.3 — FULL (>66) was met by the harness, not the game, and the re-sweep says nothing buyable reaches 66 (the twin-post covering quad tears at 6.9 kN). Target RESTATED to the game's real stakes, with headroom: held >60 and 0/4 (win line 50; measured 63.6), bare <50 (35.7), separation >=25 (27.9). Field names kept — B's gardenfly reads them. - DESIGN CANON in the site block: the wild night's garden is WON SCARRED, never FULL; the week's arc is full/full/full, won-tattered, beyond-saving. - p5 _why table corrected to game-true numbers (first sweep ran ~4 HP hot). --- web/world/data/sites/backyard_01.json | 63 +++++++++------- web/world/js/tests/a.test.js | 101 +++++++++++++++++++------- 2 files changed, 110 insertions(+), 54 deletions(-) diff --git a/web/world/data/sites/backyard_01.json b/web/world/data/sites/backyard_01.json index 87c7fe5..4fb4b16 100644 --- a/web/world/data/sites/backyard_01.json +++ b/web/world/data/sites/backyard_01.json @@ -115,38 +115,46 @@ "", "WHY (5.5, -2) at h 2.6 and not somewhere kinder: the LOW top is the point - it twists the", "p1+p2+p3+p5 quad so it spills the gusts (small+twisted survives, DESIGN.md), and the", - "position is swept against the $80 wallet:", + "position is swept against the $80 wallet. NUMBERS CORRECTED to the game-true chain after", + "B's 12s-skew finding (no phantom settle; the first sweep's hp column ran ~4 HP hot):", " (4.5,-2) h2.6 -> p1 3.35 kN, needs RATED, line costs $90 DO NOT", " (5,-2.5) h2.8 -> p5 6.20, p1 3.75 -> $90 DO NOT", - " (5.5,-2) h2.8 -> p2 3.08, p1 3.06 (4% margin on shackle) - legal but knife-edged", - " (5.5,-2) h2.6 -> p5 4.88, p2 3.14, p1 2.91, p3 1.82 -> $75, hp 68.3 FULL <- here", - " (6,-2) h2.6 -> shadow drops, hp 65.8 tattered DO NOT", - "The separation block below pins the recipe; a.test flies it. Full cover STAYS unbuyable:", - "the covering quads (p1+p2+p4+p5 and up) still pull 6.4-6.9 kN and still tear - the", - "smallest full-cover quad stays >45 m2 (a.test's floor holds), so the tradeoff decision 2", - "protects is intact. And the ICE night still refuses the $80 wallet: same line needs $105", - "there (p1 3.25, p2 3.22 vs a shackle's 3.20 - the same few-hundredths miss as C's q3),", - "so night 5 stays the night the bank pays for, exactly as ruled in SPRINT9." + " (5.5,-2) h2.6 -> p5 5.10, p2 3.08, p1 2.96, p3 1.81 -> $75, hp 63.6 WIN <- here", + " (6,-2) h2.6 -> shadow drops DO NOT", + "A second, LOWER-still or wider post cannot buy FULL either: the twin-post covering quad", + "(p6 at (-3,-2) h2.6 + p5 + p1 + p2, 100% cover) pulls 6.9/6.7/6.2 kN and tears - full", + "cover honestly exceeds the wild night at ANY buyable price, which is decision 2's tension", + "doing its job. The separation block below pins the recipe; a.test flies it game-true.", + "The smallest full-cover quad stays >45 m2 (a.test's floor holds). And the ICE night still", + "refuses the $80 wallet: same line needs $105 there (p1 3.25, p2 3.22 vs a shackle's 3.20", + "- the same few-hundredths miss as C's q3), so night 5 stays the night the bank pays for." ] } ], "separation": { "_why": [ - "SPRINT13 gate 1.4 — A's ruling on C's proposed target, AMENDED to the numbers the real game", - "measures (ruled on D's funnel-corrected picture, not C's original headline). C proposed:", - "'on a wild night the best buyable line reads FULL (>66) and a bare bed reads DEAD (<33)'.", - "Adopted in spirit, amended in both halves:", - " - The bare half becomes 'a bare bed LOSES the night (hp < 50, the win line)'. Nowhere in", - " the shipped game does a bare bed read DEAD on the wild night (35.7, tattered) — D's", - " check — and tuning storm_02's damage budget until a slogan lands is exactly the fudge", - " gate 1 exists to kill. The felt boundary is the WIN line, and 35.7 fails it honestly.", - " - The FULL half binds THIS yard, because the shipped week's only wild night is here", - " (week.js night 4). C's passing example was site_02, whose wild night the corner block", - " never plays — and with its venturi live the example dies anyway (D's landmine).", - "This block is the pin: the recipe below is the measured best $80 line, a.test flies it", - "through the real chain (session -> commit -> attach -> skyfx) every run, and B's audit can", - "read the same recipe instead of inventing one. A target nothing checks is decoration —", - "the repo's oldest lesson, and the reason this is data and not a THREADS sentence." + "SPRINT13 gate 1.4 — A's ruling on C's proposed target, RULED TWICE and honest the second", + "time. C proposed: 'on a wild night the best buyable line reads FULL (>66) and a bare bed", + "reads DEAD (<33)'. First ruling amended the bare half (nowhere does a bare bed read DEAD;", + "35.7 is tattered, and tuning storm_02 until a slogan lands is the fudge gate 1 exists to", + "kill) and kept FULL — then B's audit rewrite caught that FULL was only ever met through the", + "assert's own 12 s phantom settle (SailRig samples wind at its INTERNAL clock: game-true", + "63.8/63.6, skewed 68.4/68.3). The geometry was re-swept game-true: nothing buyable reaches", + "66 (the twin-post covering quad tears at 6.9 kN; every FULL-flavoured candidate is also", + "knife-edged under B's 15% margin rule). So the target is RESTATED to the boundaries the", + "game actually stakes, with real headroom, instead of pinning another knife edge:", + " - the best buyable line HOLDS (0/4) and WINS COMFORTABLY: hp > 60 (win line is 50;", + " game-true 63.6 - a red means the wild night's best line regressed, not noise),", + " - a bare bed LOSES: hp < 50 (game-true 35.7),", + " - the spread reads as most of a plant state: >= 25 HP (game-true 27.9).", + "DESIGN CANON, written down on purpose: the wild night's garden is WON SCARRED, never FULL.", + "The week's garden arc is full/full/full on nights 1-3, won-but-tattered on night 4, and", + "beyond saving on night 5 (see week.js night 5) - the state bands are the yard's face, but", + "the stakes the player plays for are the WIN line and the invoice, and those are what pin.", + "This block is the pin: a.test flies the recipe through the real chain (session -> commit ->", + "attach -> skyfx, NO settle, from t=0) every run, and B's audit reads the same recipe", + "(heldMustExceed/bareMustLoseBelow are B's field names too - renaming them breaks gardenfly).", + "A target nothing checks is decoration - the repo's oldest lesson." ], "stormKey": "storm_02_wildnight", "tension": 1.0, @@ -156,8 +164,9 @@ { "anchor": "p2", "hw": "shackle" }, { "anchor": "p3", "hw": "shackle" } ], - "heldMustExceed": 66, - "bareMustLoseBelow": 50 + "heldMustExceed": 60, + "bareMustLoseBelow": 50, + "separationAtLeast": 25 }, "shed": { "model": "shed_01_v1", "x": 11.8, "z": 6.2, "rotYDeg": -90 }, diff --git a/web/world/js/tests/a.test.js b/web/world/js/tests/a.test.js index c123f20..4506b4c 100644 --- a/web/world/js/tests/a.test.js +++ b/web/world/js/tests/a.test.js @@ -221,6 +221,10 @@ export default async function run(t) { assertEq(nightAt(4).storm, 'storm_02b_icenight', 'night five is the ice night'); assertEq(nightAt(2).site, 'site_02_corner_block', 'night three moves to the corner block'); assertEq(nightAt(0).site, 'backyard_01', 'the rest are the backyard'); + // SPRINT13 gate 1.4: the unsavable-garden flag is night 5's alone, and a + // night without it is presumed savable — new nights can't inherit the excuse. + assertEq(nightAt(4).gardenBeyondSaving, true, 'night 5 is beyond saving BY DESIGN — canon, ruled'); + for (let i = 0; i < 4; i++) assertEq(nightAt(i).gardenBeyondSaving, false, `night ${i + 1} is savable`); const w = createWeek(); assertEq(w.night, 1); assertEq(w.bank, 80); assertEq(w.site, 'backyard_01', 'week.site tracks the ladder'); @@ -797,6 +801,39 @@ export default async function run(t) { assert(hailed.verdict !== rained.verdict, 'hail and rain deaths must not read identically'); }); + t.test("night 5's loss is TAUGHT, not blamed: beyondSaving rewrites the garden verdicts only", () => { + // SPRINT13 gate 1.4, the night-5 ruling. Measured game-true (B's pay table, + // my probe agreeing): the icenight garden cannot reach the win line at any + // price — bare 0.0, best buyable 27.7. So week.js flags the night, and the + // verdict must stop implying a better rig existed. The worst liar was + // 'uncovered' ("the hail fell where your sail wasn't") on a night where no + // WHERE saves it. + const dmg = { hail: 90, rain: 5 }; + const held = verdictFor({ hp: 25, lost: [], win: false, dmg, pondPeak: 0, pondDumped: 0, beyondSaving: true }); + assertEq(held.mode, 'beyond-saving'); + assert(/BEYOND SAVING/.test(held.verdict), `says so plainly: "${held.verdict}"`); + assert(/held|did it/i.test(held.verdict), 'and a 4/4 hold is CREDITED — keeping the steel was the job'); + assert(!/wasn't|skimp/i.test(held.verdict), 'no placement-blame, no skimp-blame'); + + // Losing your sail on the unsavable night is still yours to own. + const shackle = { name: 'shackle', rating: 3200 }; + const torn = verdictFor({ hp: 5, + lost: [{ hw: shackle, anchorId: 'p1', anchor: { ratingHint: 1 } }], + win: false, dmg, pondPeak: 0, pondDumped: 0, beyondSaving: true }); + assertEq(torn.mode, 'beyond-saving'); + assert(/shackle at P1/.test(torn.verdict), `still names the steel that went: "${torn.verdict}"`); + + // The flag defaults OFF and changes nothing anywhere else: the two garden + // deaths keep their opposite sentences (mutation check — delete the branch + // and the first assert above reds; pass the flag everywhere and THIS one does). + const uncov = verdictFor({ hp: 20, lost: [], win: false, dmg: { hail: 70, rain: 10 }, pondPeak: 0, pondDumped: 0 }); + assertEq(uncov.mode, 'uncovered', 'a savable night keeps its honest placement lesson'); + // And a WIN on a flagged night ignores the flag — week.js says the flag + // would be stale, and a stale excuse must never eat a real win. + const win = verdictFor({ hp: 90, lost: [], win: true, dmg, pondPeak: 0, pondDumped: 0, beyondSaving: true }); + assertEq(win.mode, 'clean'); + }); + t.test('a clean hold reads as a clean hold, and the broom gets its credit', () => { const clean = verdictFor({ hp: 96, lost: [], win: true, dmg: { hail: 2, rain: 2 }, pondPeak: 0, pondDumped: 0 }); assertEq(clean.mode, 'clean'); @@ -1016,19 +1053,33 @@ export default async function run(t) { // --- SPRINT13 gate 1.4: THE PINNED SEPARATION TARGET ---------------------- // - // The ruling (THREADS [A], on D's funnel-corrected picture): on the shipped - // week's wild night — night 4, THIS yard — the best line START_BUDGET can buy - // must read FULL (>66) and WIN, and a bare bed must LOSE (<50). The recipe is - // DATA (site.separation) so B's audit reads the same one, and this flight is - // the pin: real shop at the real budget, real commit()->attach() (D's - // skipped-attach landmine — session.commit(rig) IS the attach path, verified - // by C's probe2 agreement), real skyfx exposure, the real garden model out of - // garden.js. Flown OUTSIDE t.test() because Suite.test cannot await — the - // two async-test ghosts above are the precedent. + // The ruling, RESTATED after B's skew finding (THREADS [A], twice — read the + // second entry): on the shipped week's wild night — night 4, THIS yard — the + // best line START_BUDGET can buy must HOLD and WIN comfortably (>60), a bare + // bed must LOSE (<50), and the spread must read as most of a plant state + // (≥25 HP), because the win line and the invoice are the game's real stakes. + // FULL (>66) was the first ruling and it died honestly: it was only ever met + // through this flight's own 12 s phantom settle — SailRig samples wind at its + // INTERNAL clock, so a settled rig flies the storm 12 s off the authored + // curve while the sky flies t=0..90 (B's three-way probe: game-true 63.8 / + // skewed 68.4 / shape-only 64.3). The settle is GONE: in the real game the + // rig does not exist before commit, and commit→attach→storm is one keypress + // (C's bench correction, same day, same bug). + // + // The recipe is DATA (site.separation) so B's audit reads the same one, and + // this flight is the pin: real shop at the real budget, real + // commit()->attach() (D's skipped-attach landmine — session.commit(rig) IS + // the attach path), real skyfx exposure, the real garden model out of + // garden.js. Wind is wired the way main.js wires a site (venturi from site + // data — backyard's list is empty, but the wiring must not ASSUME that — plus + // tree shelters); switch to C's windForSite at integration, it is the same + // wiring behind one door. Flown OUTSIDE t.test() because Suite.test cannot + // await — the two async-test ghosts above are the precedent. // // Sway is frozen (balance.test's reasoning): the recipe is all posts, whose // real sway is constant anyway, so frozen here means "identical to the game", - // not "conveniently calm". + // not "conveniently calm". (C's landmine 2 — a frozen TREE under-reads — is + // exactly why the pinned recipe staying post-only matters to this harness.) const sep = site.separation; let sepRun = null; if (sep && typeof document !== 'undefined') { @@ -1039,7 +1090,6 @@ export default async function run(t) { }); const trees = frozen.filter((a) => a.type === 'tree'); const stormDef = await loadStorm(sep.stormKey); - const calmDef = await loadStorm(CALM_STORM); const session = new RiggingSession({ anchors: frozen }); // the REAL budget const shopLog = []; @@ -1056,16 +1106,12 @@ export default async function run(t) { session.setTension(sep.tension); const spent = START_BUDGET - session.budget; const rig = session.commit(new SailRig({ anchors: frozen, gridN: 10 })); - - const calmWind = createWind(calmDef); - calmWind.setSheltersFromTrees(trees); - let prepT = 0; - for (let i = 0; i < Math.round(12 / FIXED_DT); i++) { - rig.step(FIXED_DT, calmWind, prepT % 3); prepT += FIXED_DT; - } + // NO settle here, deliberately — see the header. The rig flies the storm + // from t=0 on a fresh internal clock, exactly like a committed rig does. const flyNight = (sail) => { const wind = createWind(stormDef); + wind.setVenturi(site.wind?.venturi ?? []); // main.js:‘the funnel is SITE data’ wind.setSheltersFromTrees(trees); const camera = new THREE.PerspectiveCamera(60, 1.6, 0.1, 400); camera.position.set(0, 2, 8); @@ -1092,7 +1138,7 @@ export default async function run(t) { } } - t.test('PINNED: the wild night separates — best buyable line FULL and winning, bare bed losing', () => { + t.test('PINNED: the wild night separates — best buyable line holds and wins, bare bed loses', () => { if (!sep) throw new Error('backyard_01 lost its separation block — the target is decoration again'); if (typeof document === 'undefined') return t.skip('needs DOM (skyfx)'); if (sepRun.err) throw new Error(`separation flight died: ${sepRun.err}`); @@ -1100,16 +1146,17 @@ export default async function run(t) { assert(sepRun.spent <= START_BUDGET, `recipe costs $${sepRun.spent} — must be buyable night 1`); assertEq(sepRun.lost, 0, 'the pinned line HOLDS the wild night — 0/4 lost'); assert(sepRun.held.hp > sep.heldMustExceed, - `held bed reads ${sepRun.held.hp.toFixed(1)} — the target is FULL (>${sep.heldMustExceed}). ` + - `Measured 68.3 at landing; a red here means a retune moved the wild night and the ` + - `separation ruling needs re-litigating in THREADS, not a quiet threshold bump.`); - assertEq(sepRun.held.state, 'full', 'and the plants SAY full — the state the player sees'); + `held bed reads ${sepRun.held.hp.toFixed(1)} — the target is comfortably WON ` + + `(>${sep.heldMustExceed}; the win line is 50). Game-true 63.6 at landing — FULL (>66) was the ` + + `first ruling and it was only ever met through this flight's own 12 s clock skew (B's finding). ` + + `A red here means a retune moved the wild night: re-litigate in THREADS, no quiet threshold bump.`); + assert(sepRun.held.state !== 'dead', 'the held bed is ALIVE — tattered is the wild night speaking'); assertLess(sepRun.bare.hp, sep.bareMustLoseBelow, - `bare bed must LOSE the night (<${sep.bareMustLoseBelow}) — measured 35.7 at landing`); + `bare bed must LOSE the night (<${sep.bareMustLoseBelow}) — game-true 35.7 at landing`); // The felt spread, stated so a red prints the whole story: - assert(sepRun.held.hp - sepRun.bare.hp > 25, - `separation ${(sepRun.held.hp - sepRun.bare.hp).toFixed(1)} — rigging must matter by a full state, ` + - `not a garden-bonus margin (measured 32.6 at landing)`); + assert(sepRun.held.hp - sepRun.bare.hp >= (sep.separationAtLeast ?? 25), + `separation ${(sepRun.held.hp - sepRun.bare.hp).toFixed(1)} — rigging must matter by most of a ` + + `plant state, not a garden-bonus margin (game-true 27.9 at landing)`); }); t.test('sway() returns an absolute position, not an offset', () => {