From b7f065cd57157d195e3f891ad5a75f6046d5c3e8 Mon Sep 17 00:00:00 2001 From: type-two Date: Fri, 17 Jul 2026 20:21:40 +1000 Subject: [PATCH] e.test: pin the gutter price band, the DOWN/UP swap truth, and anchor absence in the wreck MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Five new asserts (Lane E 67 -> 72; selftest 315 -> 320): - price exists, sits between gnome and carport, keyed to the anchors' collateral string, wreck carries the same bill and names its twin - the torn gutter is DOWN in the wreck and UP in the intact house — negative control run: rebuilt with the ground run at eave height, selftest went red ('gutter_down tops out at y=2.68', 1 failed / 319 passed) while the Blender verify PASSed the same bad file (plan position unmoved, no box span changed); restored build round-tripped to identical hashes - wreck height equals house height (a house does not fall when its gutter does) - no fascia_anchor_* survives into the wreck - house pair added to the shared ground-plane test Co-Authored-By: Claude Fable 5 --- web/world/js/tests/e.test.js | 93 +++++++++++++++++++++++++++++++++++- 1 file changed, 92 insertions(+), 1 deletion(-) diff --git a/web/world/js/tests/e.test.js b/web/world/js/tests/e.test.js index 39840e8..c2b1692 100644 --- a/web/world/js/tests/e.test.js +++ b/web/world/js/tests/e.test.js @@ -58,6 +58,9 @@ const ASSETS = [ { name: 'house_yardside', h: [2.5, 3.5], nodes: ['wall', 'door', 'window', 'roof', 'fascia', 'gutter', 'fascia_anchor_01', 'fascia_anchor_02', 'fascia_anchor_03'] }, + { name: 'house_yardside_wrecked', h: [2.5, 3.5], + nodes: ['wall', 'door', 'window', 'roof', 'fascia_torn', + 'gutter_torn', 'gutter_down', 'downpipe_loose', 'debris_fascia'] }, { name: 'shed_01', h: [1.9, 2.4], nodes: ['shell', 'roof', 'doors', 'door_anchor'] }, { name: 'shed_table', h: [0.8, 1.0], nodes: ['table_top', 'table_frame', 'pickup_anchor'] }, { name: 'garden_bed', h: [0.5, 1.1], @@ -178,6 +181,93 @@ export default async function run(t) { 'wrecked carport must name its intact twin so A can pair the swap'); }); + // The gutter, priced — the carport ruling's second application (SPRINT12 + // §gate 3.3). backyard_01's fascia anchors have said collateral:"gutter" + // since Sprint 6 and nobody ever priced one, so collateralFor('gutter') + // returns null and the house is a FREE failure. The proposal is 90 and the + // reasoning lives beside GUTTER_COLLATERAL in build_yard_assets.py; what THIS + // pins is the calibration, not the digit: the gutter must cost more than the + // ornament and less than the structure, because the fascia (0.35) is honestly + // better steel than the carport beam (0.22) and the tutorial yard's trap must + // cost you a night, not the week. A rules the number; if it moves, move it in + // the factory and re-export — do not let site JSON and the GLB tell two prices. + t.test('the gutter is priced, inside the band, and keyed to what the anchors say', () => { + const h = loaded.get('house_yardside')?.scene.getObjectByName('house_yardside'); + assert(h, 'house_yardside root missing'); + const cost = h.userData?.collateral_value; + assert(typeof cost === 'number', `house carries no collateral_value (${JSON.stringify(h.userData)})`); + // collateral_key exists because, unlike the carport, the structure and the + // thing you take have different names: the key says WHICH collateral string + // this value prices, and it must be the one the fascia anchors carry or + // Lane A's wiring prices nothing. + assert(h.userData?.collateral_key === 'gutter', + `collateral_key is ${JSON.stringify(h.userData?.collateral_key)} — must name the anchors' collateral string`); + const a1 = loaded.get('house_yardside')?.scene.getObjectByName('fascia_anchor_01'); + assert(a1?.userData?.collateral === h.userData.collateral_key, + `fascia anchor says collateral=${JSON.stringify(a1?.userData?.collateral)} but the price is keyed ${JSON.stringify(h.userData.collateral_key)} — the chain is broken`); + const gnome = loaded.get('garden_gnome_01')?.scene + .getObjectByName('garden_gnome_01')?.userData?.collateral_value; + const carport = loaded.get('carport_01')?.scene + .getObjectByName('carport_01')?.userData?.collateral_value; + assert(cost > gnome, `gutter (${cost}) must cost more than the gnome (${gnome}) — structural collateral beats ornament`); + assert(cost < carport, `gutter (${cost}) must cost less than the carport (${carport}) — the backyard's trap is the gentler one`); + // Wreckage carries the same price, so scoring can read either state. + const w = loaded.get('house_yardside_wrecked')?.scene.getObjectByName('house_yardside_wrecked'); + assert(w?.userData?.collateral_value === cost, + 'the wrecked house must price its gutter the same as the one it used to be'); + assert(w?.userData?.broken_variant_of === 'house_yardside', + 'wrecked house must name its intact twin so A can pair the swap'); + }); + + // The wreck's whole message is vertical: the gutter that was UP at the eave is + // DOWN. Both halves are measured, and the intact half is the negative control + // that proves the measurement measures — if someone rebuilds the wreck with + // the run back at the eave (or flips the export axis), gutter_down's box + // lands ~2.6 m up and this goes red. Verified red once already: built with + // the ground run at eave height as a negative control, this failed at + // "tops out at y=2.68" (1 failed / 319 passed) while the Blender verify said + // [PASS] 9.46 x 2.10 x 2.92 on the same bad file — the run's PLAN position + // didn't move, so no box span changed. A bounding box cannot answer "did it + // fall" unless something pins which height it fell FROM. + t.test('the torn gutter is DOWN in the wreck, UP in the intact house', () => { + const up = loaded.get('house_yardside')?.scene.getObjectByName('gutter'); + assert(up, 'intact house lost its gutter node'); + const upBox = new THREE.Box3().setFromObject(up); + assert(upBox.max.y > 2.5, + `intact gutter tops out at y=${upBox.max.y.toFixed(2)} — the control is broken, nothing below can mean anything`); + + const wreck = loaded.get('house_yardside_wrecked'); + assert(wreck, 'house_yardside_wrecked did not load'); + const down = new THREE.Box3().setFromObject(wreck.scene.getObjectByName('gutter_down')); + assert(down.max.y < 0.5, + `gutter_down tops out at y=${down.max.y.toFixed(2)} — the run is not on the grass`); + const hang = new THREE.Box3().setFromObject(wreck.scene.getObjectByName('gutter_torn')); + assert(hang.max.y > 2.3, + `gutter_torn tops out at y=${hang.max.y.toFixed(2)} — it should still reach the eave it tore from`); + assert(hang.min.y < 1.6, + `gutter_torn bottoms out at y=${hang.min.y.toFixed(2)} — the torn end should sag well below the fascia line`); + + // And the house itself did NOT fall: unlike the carport, this wreck keeps + // its full height, because only the eave line failed. A shorter (or taller) + // wreck here means someone wrecked the building instead of the gutter. + const hi = new THREE.Box3().setFromObject(loaded.get('house_yardside').scene); + const hw = new THREE.Box3().setFromObject(wreck.scene); + const dh = Math.abs((hi.max.y - hi.min.y) - (hw.max.y - hw.min.y)); + assert(dh < 0.05, `wreck height differs from the house by ${dh.toFixed(3)} m — a house does not fall when its gutter does`); + }); + + // You cannot re-tie to a ripped eave. An anchor that outlives its fascia + // would be the free-failure bug back again, wearing a wreck for a costume — + // world.anchors would keep offering the tie-off after the aftermath swap. + t.test('no fascia anchor survives into the wreck', () => { + const w = loaded.get('house_yardside_wrecked'); + assert(w, 'house_yardside_wrecked did not load'); + for (const n of ['fascia_anchor_01', 'fascia_anchor_02', 'fascia_anchor_03']) { + assert(!w.scene.getObjectByName(n), + `${n} survives in the wreck — the eave it anchored is gone, so it must be too`); + } + }); + // A wreck that stands taller than the thing it was is a bug, not a wreck. t.test('the wrecked carport is shorter than the carport', () => { const a = new THREE.Box3().setFromObject(loaded.get('carport_01').scene); @@ -400,7 +490,8 @@ export default async function run(t) { // swap needs a fudge offset per prop and will grow one. t.test('broken variants sit on the same ground plane as their intact twin', () => { for (const [intact, broken] of [['garden_gnome_01', 'garden_gnome_01_broken'], - ['fence_panel', 'fence_panel_snapped']]) { + ['fence_panel', 'fence_panel_snapped'], + ['house_yardside', 'house_yardside_wrecked']]) { for (const n of [intact, broken]) { const box = new THREE.Box3().setFromObject(loaded.get(n).scene); assert(Math.abs(box.min.y) < 0.03,