From 39701884e3238850f64aa8d0e9b4f68f8b3ce638 Mon Sep 17 00:00:00 2001 From: type-two Date: Tue, 21 Jul 2026 03:53:20 +1000 Subject: [PATCH] =?UTF-8?q?Lane=20D=20S17=20gate=203.2:=20THE=20POOL=20YAR?= =?UTF-8?q?D=20=E2=80=94=20authored=20cold,=20in=20the=20game=20only=20by?= =?UTF-8?q?=20being=20chosen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit site_04_pool_yard: E's ring (fourteen honest noes) east, the bed poolside, ONE corroded post in the venturi channel where the NE corner wants steel. Authored in the editor (palette shapes, iterative SCORE IT x3, canonical export). Gauntlet: WINNABLE at $80 — 13 quads, 6 clean, 1 marginal; cheapest hold $40 (h1,t1b,p2,p3, 92.7 FULL, no rust); every 92%-cover line routes through c1 — clean $65 rated on t1,p1,p2,c1, UNHOLDABLE on the two shapes that pull the eye harder. audit.html over the shipped file reproduces the card to the digit. Separation REFUSED with receipts (bare 83.7 FULL — mild-night canon, A's 0.4). board.js (A's granted edit): pool_yard_southerly POOL entry with the audit receipt. d.test: +5 pins (chosen-never-scripted, ring/rust facts, judged-XOR, wreck apron, exposure $160). gardenfly.selftest (B's file, flagged): judged walk 3 -> 4 in the same commit that ships the yard. Selftest 515/0/0 (510 + 5, the count moved by exactly what was added). Mutations: 1 full-suite (finding stripped -> DOUBLE red: B's walk 'UNJUDGED' + my XOR pin, 513/2), 10 probed (entry deleted/re-paired/receipt stripped/ scripted-slot, gnome unpriced/rust zeroed/fence priced, apron x3) — every one red with its own words, controls green. Co-Authored-By: Claude Fable 5 --- tools/site_audit/gardenfly.selftest.js | 9 +- web/world/data/sites/site_04_pool_yard.json | 237 ++++++++++++++++++++ web/world/js/board.js | 23 ++ web/world/js/tests/d.test.js | 99 ++++++++ 4 files changed, 366 insertions(+), 2 deletions(-) create mode 100644 web/world/data/sites/site_04_pool_yard.json diff --git a/tools/site_audit/gardenfly.selftest.js b/tools/site_audit/gardenfly.selftest.js index 92d62cc..f2b8ff3 100644 --- a/tools/site_audit/gardenfly.selftest.js +++ b/tools/site_audit/gardenfly.selftest.js @@ -119,7 +119,12 @@ export async function buildGardenflyTests() { let sepStates = null, sepStatesErr = null; try { sepStates = []; - for (const name of ['backyard_01', 'site_02_corner_block', 'site_03_swing_lawn']) { + // SPRINT17 gate 3.2 [D]: site_04_pool_yard added to the judged walk in the + // same commit that ships it (B's file, flagged in THREADS, revert-and-tell- + // me standard) — a yard missing from this list is UNJUDGED silently, which + // is the exact silence the gate-2.3 rule forbids. It carries a refusal + // finding (bare 83.7 FULL under its southerly; receipts in the site file). + for (const name of ['backyard_01', 'site_02_corner_block', 'site_03_swing_lawn', 'site_04_pool_yard']) { const sd = await loadSite(name); sepStates.push({ name, pinned: !!sd.separation, finding: !!sd._separation_finding }); } @@ -204,7 +209,7 @@ export async function buildGardenflyTests() { // 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}`); + assert(sepStates.length === 4, `expected 4 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; ` + diff --git a/web/world/data/sites/site_04_pool_yard.json b/web/world/data/sites/site_04_pool_yard.json new file mode 100644 index 0000000..ebc1c40 --- /dev/null +++ b/web/world/data/sites/site_04_pool_yard.json @@ -0,0 +1,237 @@ +{ + "id": "site_04_pool_yard", + "name": "The Pool Yard", + "blurb": "Fourteen perfect fence posts and not one of them is yours to use. The only steel standing where the shade wants a corner went in before the pool did.", + "_design": [ + "THE YARD WHERE EVERYTHING SAYS NO EXCEPT THE THING THAT SHOULD NOT.", + "ROADMAP arc-2 thesis: fence compliance + corroded cheap hardware. The pool ring is", + "FOURTEEN honest noes (every post tie_off:false in the GLB — a certified pool barrier", + "is not an anchor, at any rating, ever); the corroded post c1 is the one thing that", + "DOES adopt on the east side, at 0.55, and it stands exactly where the bed's NE", + "corner wants steel. That is not an accident, it is the whole level:", + " - the bed is poolside; every 92%-cover quad in the band routes through c1.", + " Full shade in this yard has rust in it, or it does not exist (measured, SCORE IT:", + " the three 92% lines are t1,p1,p2,c1 / t1,t1b,p2,c1 / t1b,p1,p2,c1 — no others).", + " - the honest steel (gum t1/t1b, posts p1,p2,p3, the familiar 0.35 fascia) tops out", + " at 58% cover for $40 — the cheap safe rig leaves the bed's pool corner open.", + " - c1 is SHAPE-SENSITIVE, and that is the corroded tier working: the same drooped", + " eye is clean at $65 rated on t1,p1,p2,c1, and UNHOLDABLE AT ANY PRICE on the two", + " quads that pull it harder (t1,t1b,p2,c1 and t1b,p1,p2,c1). You cannot read which", + " off the object — E built the rung so you gamble rather than compute (their 0.55", + " reasoning, THREADS S17), and this yard is that gamble with a fence around it.", + "WIND: the venturi is the channel between the house's east end and the pool enclosure", + "— real geometry, gate-side, running N-S (axis 1.571, gain 1.4, r 5, throat (3.1,-3.5)).", + "It bites c1 and h3: the two lie-tiers are the two corners in the wind. The wreck", + "apron is PAID FOR (E's placement facts): c1 falls +Z ~3.9 m and the shaft lands in", + "the path between bed (x<=2.5) and ring (x>=3.7) — clear of both by ~0.3 m each side.", + "One corroded post ON PURPOSE: two would share the baked collateral key", + "(structFor/wreckStructure match the first entry; C's envelope dedupes keys), so the", + "second failure would bill and wreck the wrong post. Filed in THREADS [D] S17 —", + "a second rust rung in one yard needs its own key plumbing first." + ], + "_why": [ + "Authored COLD in the yard editor, SPRINT17 gate 3.2 — palette shapes, editor", + "placement, iterative SCORE IT (three passes: 2 quads -> tightened to band -> 13).", + "Every number below is the SCORE IT card, storm_03_southerly, FUNNEL ON, 9 dressed", + "anchors, export-clone world, 136.5 s flight. Confirmed by audit.html over the saved", + "file (same engine, second route) before shipping.", + " WINNABLE at $80 — 13 quads in band, 6 clean, 1 marginal.", + " cheapest honest line h1,t1b,p2,p3 — $40 hardware (+$15 spare), 45 m2, 58% cover,", + " garden 92.7 FULL, +9.0 HP over bare. No rust in it.", + " the rust gate full cover exists ONLY through c1 (92% x3, above); cheapest", + " clean rust line t1,p1,p2,c1 $65 — corrosion prices as +$25 of", + " hardware you must rate UP, plus a $45 stake on a 0.55 eye.", + " margin flags c1 4% headroom on h1,h2,p2,c1 (wrecks the corroded post $45);", + " p1 2% / t1b 2% / t1 4% — the 15% rule, marginal is not PASS.", + " collateral exposure gutter $90 (h1-h3) + corroded post $45 (c1) = $135 from", + " anchors; board exposure $160 with the gnome (exposureOf).", + " bare bed 83.7 FULL — the southerly cannot kill an open bed (its pea", + " hail takes 10.7 HP). The sail here is for the STEEL and the", + " collateral, per A's S17 gate-0.4 ruling — which is this", + " yard's thesis said as canon." + ], + "yard": { + "width": 24, + "depth": 16 + }, + "sun": { + "elevationDeg": 55, + "azimuthDeg": -125 + }, + "gardenBed": { + "x": 0, + "z": 1.5, + "w": 5, + "d": 3.5 + }, + "house": { + "model": "house_yardside_v1", + "wreckedModel": "house_yardside_wrecked_v1", + "collateralKey": "gutter", + "collateralValue": 90, + "collateralLabel": "the gutter", + "x": -3, + "z": -6, + "anchors": [ + { + "id": "h1", + "node": "fascia_anchor_01", + "type": "house", + "work": "bracket" + }, + { + "id": "h2", + "node": "fascia_anchor_02", + "type": "house", + "work": "bracket" + }, + { + "id": "h3", + "node": "fascia_anchor_03", + "type": "house", + "work": "bracket" + } + ] + }, + "structures": [ + { + "id": "c1", + "model": "sail_post_corroded_v1", + "wreckedModel": "sail_post_corroded_wrecked_v1", + "x": 3.1, + "z": -2, + "rotYDeg": 0, + "solid": true, + "collateralKey": "corroded_post", + "collateralValue": 45, + "collateralLabel": "the corroded post", + "anchors": [ + { + "id": "c1_a1", + "node": "top_anchor", + "type": "corroded_post", + "work": "cloth" + } + ] + }, + { + "id": "pool", + "model": "pool_kit_01_v1", + "x": 6.5, + "z": 1.5, + "rotYDeg": 0, + "solid": true, + "anchors": [] + } + ], + "trees": [ + { + "id": "t1", + "model": "tree_gum_02_v1", + "x": -6.8, + "z": 2, + "phase": 1.7, + "trunkH": 3.8, + "anchorY": 3.1, + "anchors": [ + { + "id": "t1", + "node": "branch_anchor_01", + "type": "tree", + "work": "cloth" + }, + { + "id": "t1b", + "node": "branch_anchor_02", + "type": "tree", + "work": "cloth" + } + ] + } + ], + "posts": [ + { + "id": "p1", + "x": -3.4, + "z": 4.6, + "h": 4, + "type": "post", + "work": "cloth" + }, + { + "id": "p2", + "x": 3.4, + "z": 4.6, + "h": 4, + "type": "post", + "work": "cloth" + }, + { + "id": "p3", + "x": -5.8, + "z": -1.5, + "h": 4, + "type": "post", + "work": "cloth" + } + ], + "fence": { + "sides": [ + "north", + "south", + "east", + "west" + ] + }, + "shed": { + "model": "shed_01_v1", + "x": -9.5, + "z": -5.5, + "rotYDeg": -90 + }, + "shedTable": { + "model": "shed_table_v1", + "x": -6.5, + "z": 7, + "rotYDeg": -90, + "pickupNode": "pickup_anchor" + }, + "gnome": { + "model": "garden_gnome_01_v1", + "x": 4.6, + "z": 4.4, + "rotYDeg": 0, + "collateralValue": 25 + }, + "wind": { + "_venturi": [ + "The pool-terrace channel: house east end to pool enclosure, gate side, running N-S.", + "Axis 1.571 is the channel's GEOMETRY (a line, mod pi — site_02's reconciled lesson),", + "not a storm heading. Gain 1.4 / r 5 puts c1 (1.4 m off-throat) and h3 (3.7 m) in the", + "wind: the corroded post is the venturi post, which is the temptation priced." + ], + "venturi": [ + { + "x": 3.1, + "z": -3.5, + "axis": 1.571, + "gain": 1.4, + "radius": 5, + "sharp": 3 + } + ] + }, + "_separation_finding": [ + "SPRINT17 gate 3.2 [D] — a separation pin was ATTEMPTED and honestly REFUSED, the", + "third refusal in the family (site_02, site_03, now here) and the first authored", + "AFTER A's gate-0.4 ruling made the pattern canon. Measured (SCORE IT, funnel ON,", + "storm_03_southerly, 2026-07-21): bare bed 83.7 FULL (hail 10.7 + rain 5.6 HP);", + "best flown 92.7 FULL (h1,t1b,p2,p3, $40); best gain +9.0 HP. bareMustLoseBelow", + "cannot be written at any threshold — bare WINS the night by 33.7 over the win", + "line. On this night the sail is for the steel and the collateral (A, S17 0.4,", + "ruled canon, taught by the board's exposure line): what c1 separates is not the", + "bed from the hail but $45 and your margin from your greed. When B's garden-BONUS", + "separation shape lands (A ruled FOR it, deferred to its own gate), re-measure —", + "this yard's +9.0 over bare at $40 is a bonus-stake candidate, not a win/lose one." + ] +} diff --git a/web/world/js/board.js b/web/world/js/board.js index 78661c2..8e2efa2 100644 --- a/web/world/js/board.js +++ b/web/world/js/board.js @@ -240,6 +240,29 @@ export const POOL = [ + 'working, it stops you buying your way out of choosing. The carport at $180 is the ' + 'tension — the rig this cap pushes you toward is the one with the least margin over it.', }, + { + // SPRINT17 gate 3.2 [D] — THE POOL YARD. The first yard whose ONLY route + // into the game is being chosen: it is in no NIGHTS slot, so declining it + // forever is a complete campaign. That is the arc-2 promise made literal, + // and it is why the entry lives here and not in week.js. + id: 'pool_yard_southerly', + storm: 'storm_03_southerly', site: 'site_04_pool_yard', + client: 'the Karalis place', addr: '5 Clearwater Ct — the pool yard', + brief: 'New pool, new fence, certified in March — and the certifier is back Thursday, so ' + + 'nothing touches that fence. Not a rope, not a clamp. The old sail post from before ' + + 'the pool went in is still standing, they say, and they say it like a selling point. ' + + 'It has been breathing pool air for years. Tuesday\'s southerly, the bed\'s by the water.', + _why: 'AUTHORED COLD + AUDITED (D, S17 gate 3.2 — SCORE IT card, funnel ON, confirmed by ' + + 'audit.html over the shipped file): 13 quads in band, 6 clean, 1 marginal; cheapest ' + + 'hold $40 (h1,t1b,p2,p3 → garden 92.7 FULL, 58% cover, no rust in it). The trap is ' + + 'priced, not padded: every 92%-cover line routes through the corroded post c1 (0.55, ' + + '$45 collateral, IN the venturi) — clean at $65 rated on t1,p1,p2,c1, UNHOLDABLE on ' + + 'the two shapes that pull the eye harder. Southerly = the storm nights 2 and 4 teach; ' + + 'the yard is the only new variable (one-variable law). Separation REFUSED with ' + + 'receipts in the site file (bare 83.7 FULL — mild-night canon per A\'s 0.4 ruling); ' + + 'exposure $160 (gutter 90 + corroded post 45 + gnome 25), the fence\'s fourteen ' + + 'posts deliberately absent from that number because none of them can be tied to.', + }, ]; /** diff --git a/web/world/js/tests/d.test.js b/web/world/js/tests/d.test.js index ac55972..bb6af42 100644 --- a/web/world/js/tests/d.test.js +++ b/web/world/js/tests/d.test.js @@ -24,6 +24,13 @@ import { loadStorm, createWind } from '../weather.js'; // file read the ladder. week.js is contracts-only underneath, so this stays // headless-safe. import { NIGHTS, nightAt } from '../week.js'; +// SPRINT17 gate 3.2: the pool-yard pins at the bottom read the board's POOL +// (pure data, zero THREE) and loadSite (fetch + validateSite — the teeth; a +// site file this suite merely fetch()ed raw could rot invalid and stay green). +// loadSite pulls world.js which pulls THREE — browser-only, same as the GLB +// facts everywhere else in this repo; the node runner never imports this file. +import { POOL, exposureOf } from '../board.js'; +import { loadSite } from '../world.js'; const DT = FIXED_DT; @@ -1229,4 +1236,96 @@ export default async function run(t) { "the wildnight sits directly before the icenight — the icenight's brief says 'less wind than " + "last night', and the paper never lies: re-order these and re-word that brief in the same commit"); }); + + // ── SPRINT17 gate 3.2: THE POOL YARD — authored cold, entered through the ── + // board, pinned by its author. These pins carry the yard's three claims: + // it is CHOSEN or absent (never scripted), its fence is fourteen honest noes + // and its rust is one priced yes, and its refusal + wreck apron are facts + // with numbers, not vibes. The site file itself goes through loadSite here, + // so a hand-edit that breaks validateSite goes red in THIS suite too, not + // only at boot. + let poolSite = null, poolSiteErr = null; + try { poolSite = await loadSite('site_04_pool_yard'); } + catch (err) { poolSiteErr = String((err && err.stack) || err); } + + t.test('pool yard (S17 3.2): in the game ONLY through the board — chosen, never scripted', () => { + const entry = POOL.find((p) => p.site === 'site_04_pool_yard'); + assert(entry, 'the pool yard has a POOL entry — its one door into the game'); + assertEq(entry.storm, 'storm_03_southerly', + 'it flies the southerly — the storm nights 2 and 4 teach, so the yard is the only new variable; ' + + 'any other pairing needs its own gauntlet run FIRST (the _why receipt describes this one)'); + assert(entry._why && entry._why.includes('audit'), + 'the entry carries its audit receipt — an unmeasured pairing is a night the board is guessing about'); + const scripted = NIGHTS.map((_, i) => nightAt(i)); + assert(!scripted.some((n) => n.site === 'site_04_pool_yard'), + 'NO scripted night flies the pool yard — "the first yard whose only route in is being chosen" ' + + 'is the arc-2 promise, and a week.js slot would quietly break it'); + }); + + t.test('pool yard (S17 3.2): the ring says no fourteen times, the rust says yes once', () => { + if (poolSiteErr) throw new Error(`site load died: ${poolSiteErr}`); + const ring = poolSite.structures.find((s) => s.model === 'pool_kit_01_v1'); + assert(ring, 'the pool ring is in the yard — it IS the thesis'); + assertEq((ring.anchors ?? []).length, 0, + 'the ring adopts NOTHING in site data — its fourteen noes are baked tie_off:false in the GLB, ' + + 'and a site-level anchor here would override a certified fence into steel'); + assert(ring.collateralValue == null, + 'the ring is unpriced BY RULING (the bike rule) — nothing can bend a pool fence yet'); + const rust = poolSite.structures.find((s) => s.model === 'sail_post_corroded_v1'); + assert(rust, 'the corroded post stands'); + assertEq((rust.anchors ?? []).length, 1, 'exactly one adoptable eye'); + assertEq(rust.anchors[0].type, 'corroded_post', 'typed to E\'s tier so it rates 0.55, not 1.00'); + assertEq(rust.collateralValue, 45, 'the make-safe bill, E\'s number, adopted with its reasoning'); + assert(rust.wreckedModel === 'sail_post_corroded_wrecked_v1', + 'the wreck variant is wired — a trap that vanishes instead of falling is a free failure'); + const rusts = poolSite.structures.filter((s) => s.model === 'sail_post_corroded_v1'); + assertEq(rusts.length, 1, + 'ONE corroded post on purpose: two would share the baked collateral key and the second failure ' + + 'would bill/wreck the wrong one (structFor matches first; filed in THREADS [D] S17)'); + }); + + t.test('pool yard (S17 3.2): separation-JUDGED — refused with receipts, XOR the pin', () => { + if (poolSiteErr) throw new Error(`site load died: ${poolSiteErr}`); + const pinned = !!poolSite.separation, finding = !!poolSite._separation_finding; + assert(pinned || finding, 'the yard is JUDGED — silence is not an option (S16 gate 2.3)'); + assert(!(pinned && finding), 'both at once means the finding went stale in the pinning commit'); + assert(finding, 'today it is a REFUSAL: bare wins the southerly (mild-night canon, A\'s 0.4 ruling)'); + const text = poolSite._separation_finding.join(' '); + assert(text.includes('83.7') && text.includes('92.7'), + 'the refusal carries its measured receipts (bare 83.7 / best flown 92.7) — a finding without ' + + 'numbers is a shrug wearing a ruling\'s clothes'); + }); + + t.test('pool yard (S17 3.2): the wreck apron threads the needle between bed and ring', () => { + if (poolSiteErr) throw new Error(`site load died: ${poolSiteErr}`); + // E's placement facts (THREADS S17 gate 3.1): the corroded post falls +Z + // ~3.9 m, shaft half-width ~0.26 m. The fallen shaft must land in the path + // — clear of the bed's east edge AND the ring's west run — or the wreck + // interpenetrates client property the moment the trap fires. + const rust = poolSite.structures.find((s) => s.model === 'sail_post_corroded_v1'); + const ring = poolSite.structures.find((s) => s.model === 'pool_kit_01_v1'); + const bedEast = poolSite.gardenBed.x + poolSite.gardenBed.w / 2; + const shaftWest = rust.x - 0.26, shaftEast = rust.x + 0.26; + const ringWest = ring.x - 2.8; // ring proper 5.6 wide (E's measured bbox) + assert(bedEast <= shaftWest, + `bed east edge ${bedEast} overlaps the fallen shaft (west ${shaftWest.toFixed(2)}) — move c1 or the bed`); + assert(shaftEast <= ringWest, + `fallen shaft east ${shaftEast.toFixed(2)} reaches the ring (west ${ringWest.toFixed(2)}) — move c1 or the pool`); + const apronTip = rust.z + 3.9; + assert(apronTip <= poolSite.yard.depth / 2, + `the shaft falls past the south fence (tip z ${apronTip}) — it must land IN the yard, that is the point`); + }); + + t.test('pool yard (S17 3.2): the offer card prices the rust and never the fence', () => { + if (poolSiteErr) throw new Error(`site load died: ${poolSiteErr}`); + const { total, items } = exposureOf(poolSite); + assertEq(total, 160, 'exposure $160 = gutter 90 + corroded post 45 + gnome 25'); + const labels = items.map((i) => i.what).sort().join(' · '); + assertEq(labels, 'garden gnome · the corroded post · the gutter', + 'three priced items exactly — the fourteen fence posts are deliberately NOT exposure: ' + + 'nothing can bend a pool fence yet (the bike rule), and a $0 line item would teach "free"'); + const rustItem = items.find((i) => i.what === 'the corroded post'); + assertEq(rustItem.cost, 45, + 'the rust is the cheapest structure you can break — the stake E sized the gamble in'); + }); }