diff --git a/tools/site_audit/gardenfly.selftest.js b/tools/site_audit/gardenfly.selftest.js index 5b5b248..92d62cc 100644 --- a/tools/site_audit/gardenfly.selftest.js +++ b/tools/site_audit/gardenfly.selftest.js @@ -112,6 +112,19 @@ export async function buildGardenflyTests() { } catch (err) { sepErr = String((err && err.stack) || err); } } + // SPRINT16 gate 2.3 [B]: every shipped yard has been JUDGED on separation — + // either it pins a block (backyard_01) or it records a measured refusal + // (_separation_finding). Read all three here so the assert below can hold + // the invariant: neither is "unjudged", both at once is a stale finding. + let sepStates = null, sepStatesErr = null; + try { + sepStates = []; + for (const name of ['backyard_01', 'site_02_corner_block', 'site_03_swing_lawn']) { + const sd = await loadSite(name); + sepStates.push({ name, pinned: !!sd.separation, finding: !!sd._separation_finding }); + } + } catch (err) { sepStatesErr = String((err && err.stack) || err); } + let venRun = null, venErr = null; try { const fly = (venturi) => flyGarden({ @@ -184,6 +197,23 @@ export async function buildGardenflyTests() { `the siteDef's venturi is not reaching gardenfly's wind (windForSite must receive the SITE def)`); } }], + ['gate 2.3: every shipped yard is separation-JUDGED — pinned, or measured-and-refused', () => { + // SPRINT16's rule made durable: a yard may pin a separation block or it + // may carry a measured refusal (_separation_finding, the honest "this + // yard cannot separate at $80 under its storm" verdict with receipts) — + // but silence is not an option, and holding both means the finding went + // stale the day the pin landed and must be deleted (its own text says so). + if (sepStatesErr) throw new Error(`site read died: ${sepStatesErr}`); + assert(sepStates.length === 3, `expected 3 shipped yards, read ${sepStates.length}`); + for (const s of sepStates) { + assert(s.pinned || s.finding, + `${s.name} has neither a separation block nor a _separation_finding — the yard is UNJUDGED; ` + + 'run gardenfly, pin honestly or refuse honestly (SPRINT16 gate 2.3)'); + assert(!(s.pinned && s.finding), + `${s.name} carries BOTH a pin and a refusal finding — the finding is stale, delete it in the pinning commit`); + } + return sepStates.map((s) => `${s.name}: ${s.pinned ? 'PINNED' : 'refused w/ finding'}`).join(' · '); + }], ['gardenfly: default fabric is still the pinned 0.30 cloth, and membrane flies heavier', () => { if (fabErr) throw new Error(`fabric flight died: ${fabErr}`); const { byDefault, cloth, membrane } = fabRun; diff --git a/tools/site_audit/scorecard.js b/tools/site_audit/scorecard.js index b1b80b0..156a938 100644 --- a/tools/site_audit/scorecard.js +++ b/tools/site_audit/scorecard.js @@ -243,6 +243,10 @@ export async function scoreSite({ site, stormDef, stormName = null, sepStormDef bestGarden: bestGarden ? { ids: bestGarden.key, ...bestGarden.g } : null, bestMarginal: bestMarginal ? { ids: bestMarginal.key, ...bestMarginal.g } : null, separation, sepStormName, sepUnjudged, + // SPRINT16 gate 2.3 [B]: a site that ATTEMPTED a pin and honestly refused + // one records why in its JSON; the card prints it so NONE PINNED reads as + // "measured and refused, here's the fight" rather than "nobody tried". + separationFinding: site._separation_finding ?? null, collateral, MARGIN: AUDIT.MARGIN, }; diff --git a/web/world/data/sites/site_02_corner_block.json b/web/world/data/sites/site_02_corner_block.json index 615112b..748fd19 100644 --- a/web/world/data/sites/site_02_corner_block.json +++ b/web/world/data/sites/site_02_corner_block.json @@ -9,6 +9,21 @@ "gardenBed": { "x": 0, "z": 1, "w": 5, "d": 3.5 }, + "_separation_finding": [ + "SPRINT16 gate 2.3 [B] — a separation pin was ATTEMPTED and honestly REFUSED. Measured", + "post-ruling (garden_probe, funnel ON, storm_03b_earlybuster, 2026-07-20): bare bed 82.6", + "FULL (hail 10.7 + rain 6.8 HP); best flown line 90.2; best gain +7.6 HP. The block's own", + "semantics need 'bare LOSES the night' (bareMustLoseBelow, win line 50) and a bare bed", + "WINS this night by 32 points — the early buster's pea hail simply cannot hurt an open bed", + "enough. Any pin writable today would be a fudge: either a bareMustLoseBelow above the win", + "line (the field name lying) or thresholds tuned until the slogan lands (the S13 fudge the", + "gate exists to kill). THE FINDING: this yard cannot separate held from bare at ANY price", + "under its shipped storm — that is a balance fact about the NIGHT's hail spectrum, not the", + "rig market. The fight is in THREADS [B] 2026-07-20; candidate levers are C's hail", + "spectrum, the win threshold, or per-night garden stakes — A rules. When a retune lands,", + "re-measure, pin like backyard_01's, and DELETE this block in the same commit." + ], + "_design": [ "DESIGN.md wants the corner block anchor-poor — 'nowhere to tie off'. An empty yard is just a", "smaller yard, so the trap is the interesting build: give it something that LOOKS like four free", diff --git a/web/world/data/sites/site_03_swing_lawn.json b/web/world/data/sites/site_03_swing_lawn.json index e1a6eb1..49b4841 100644 --- a/web/world/data/sites/site_03_swing_lawn.json +++ b/web/world/data/sites/site_03_swing_lawn.json @@ -48,6 +48,16 @@ "The bare bed reads 83.7 FULL, which is the bare-beds-win pool item showing up on a", "third yard - gate-4 material, not this yard's doing and not tuned here." ], + "_separation_finding": [ + "SPRINT16 gate 2.3 [B] — a separation pin was ATTEMPTED and honestly REFUSED, same", + "verdict as site_02's. Measured post-ruling (garden_probe, funnel ON,", + "storm_03_southerly, 2026-07-20): bare bed 83.7 FULL (hail 10.7 + rain 5.6 HP); best", + "flown 89.8; best gain +6.1 HP — D's S14 card numbers reproduced. Bare WINS the", + "southerly by 34 points, so 'bareMustLoseBelow' cannot be written honestly at any", + "threshold. The pea-hail nights cannot hurt an open bed enough for a sail to be the", + "point of the night; that is the NIGHT's balance fact, filed in THREADS [B] 2026-07-20.", + "When the balance ruling lands, re-measure, pin like backyard_01's, delete this block." + ], "yard": { "width": 24, "depth": 16 diff --git a/web/world/js/editor_score.js b/web/world/js/editor_score.js index 9cd19c4..e649e76 100644 --- a/web/world/js/editor_score.js +++ b/web/world/js/editor_score.js @@ -312,6 +312,12 @@ function render(out, s, ms) { } else if (s.sepUnjudged) { const cs = card(out, 'SEPARATION TARGET — NOT JUDGED', 'ed-warn'); cs.append(el('div', 'ed-note', s.sepUnjudged)); + } else if (s.separationFinding) { + // SPRINT16 gate 2.3 [B]: measured, and honestly REFUSED — different fact + // from "nobody tried", and the card must not flatten the two together. + const cs = card(out, 'SEPARATION TARGET — PIN REFUSED (measured)', 'ed-warn'); + cs.append(el('div', 'ed-note', + Array.isArray(s.separationFinding) ? s.separationFinding.join(' ') : String(s.separationFinding))); } else { const cs = card(out, 'SEPARATION TARGET — NONE PINNED', 'ed-warn'); cs.append(el('div', 'ed-note',