From 2a540523ff08a41d69786379703757d955a81161 Mon Sep 17 00:00:00 2001 From: type-two Date: Mon, 20 Jul 2026 17:49:38 +1000 Subject: [PATCH] =?UTF-8?q?Lane=20E=20S15=20gate=201.2:=20RULE=204=20pins?= =?UTF-8?q?=20=E2=80=94=20manifest=3D=3D=3DGLBs=20both=20directions=20(4a)?= =?UTF-8?q?,=20runtime=3D=3D=3Dmanifest=20across=20all=20three=20shipped?= =?UTF-8?q?=20sites=20(4b);=20compared=20against=20the=20manifest,=20never?= =?UTF-8?q?=20a=20literal;=20both=20mutation-checked=20red-then-green=20(p?= =?UTF-8?q?ost=201.0->0.9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 --- web/world/js/tests/e.test.js | 103 +++++++++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) diff --git a/web/world/js/tests/e.test.js b/web/world/js/tests/e.test.js index 4e801cd..1b78c20 100644 --- a/web/world/js/tests/e.test.js +++ b/web/world/js/tests/e.test.js @@ -21,6 +21,8 @@ import * as THREE from '../../vendor/three.module.js'; import { assert } from '../testkit.js'; import { ANCHOR_TYPE } from '../contracts.js'; +import { createWorld, loadSite } from '../world.js'; +import { FACTORY_ANCHOR_RATINGS } from '../anchor_ratings.gen.js'; // GLTFLoader is imported DYNAMICALLY, below, and that is deliberate. // @@ -799,4 +801,105 @@ export default async function run(t) { assert(g.scene.getObjectByName(state), `${state} missing — Lane A toggles these by name`); } }); + + // ------------------------------------------------------------------------- + // SPRINT15 gate 1.2 — RULE 4, the third pin of the S14 family: no silent + // anchors, no unpriced collateral, and now NO BAKED HINT THAT NEVER ARRIVES. + // + // The bug this buries: `adoptAnchor` was the only wire from a baked + // `rating_hint` to a live anchor, and it only runs for anchors that name a + // GLB `node`. Every shipped sail post is a plain JSON entry with no node, so + // the factory said 0.90, `?? 1` said 1.00, and the sim won BY ACCIDENT for + // fourteen sprints. Gate 1.1 ruled the shipped 1.00 canon — which means the + // values now agree BY CONSTRUCTION, so these pins compare runtime against + // the generated MANIFEST (anchor_ratings.gen.js), never against a literal: + // a pin that hard-codes 1.0 would have been green through the entire bug. + // + // Two directions, so nothing can drift silently: + // RULE 4a manifest === GLBs (a re-bake that skips regeneration → red) + // RULE 4b runtime === manifest (the adoptAnchor-shaped gap itself → red) + // ------------------------------------------------------------------------- + + t.test('RULE 4a — the ratings manifest matches the shipped GLBs, both directions', () => { + const types = FACTORY_ANCHOR_RATINGS.types, ambig = FACTORY_ANCHOR_RATINGS.ambiguous; + assert(Object.keys(types).length > 0, + 'vacuous: the manifest rates nothing — regenerate anchor_ratings.gen.js'); + const nodeOf = (src) => { + const [asset, node] = src.split('/'); + return loaded.get(asset)?.scene.getObjectByName(node); + }; + // manifest → GLB: every source it cites exists and carries the same bake. + for (const [ty, entry] of Object.entries(types)) { + assert(entry.sources.length > 0, `manifest type "${ty}" cites no sources`); + for (const src of entry.sources) { + const o = nodeOf(src); + assert(o, `manifest type "${ty}" cites ${src}, which is not in the palette — stale manifest, regenerate`); + assert(o.userData?.rating_hint === entry.rating_hint, + `${src} bakes rating_hint ${o.userData?.rating_hint} but the manifest says ${entry.rating_hint} — ` + + 'the factory and the manifest disagree; regenerate, do not hand-edit'); + assert((o.userData?.collateral ?? null) === (entry.collateral ?? null), + `${src} bakes collateral ${JSON.stringify(o.userData?.collateral)} but the manifest says ` + + `${JSON.stringify(entry.collateral)} — regenerate`); + } + } + // GLB → manifest: every rated, non-denying anchor node is accounted for. + for (const [asset, o] of allNodes()) { + const ty = o.userData?.anchor_type; + if (ty === undefined || o.userData?.tie_off === false) continue; + assert(types[ty] || ambig[ty], + `${asset}/${o.name} is typed "${ty}", which the manifest does not know — regenerate`); + if (types[ty]) { + assert(o.userData?.rating_hint === types[ty].rating_hint, + `${asset}/${o.name} bakes ${o.userData?.rating_hint} but manifest type "${ty}" says ` + + `${types[ty].rating_hint} — either regenerate, or this type just became ambiguous ` + + 'and the manifest must say so'); + } + } + }); + + // RULE 4b needs live worlds; Suite.test() refuses async fns, so the awaits + // happen out here and the assert stays synchronous. + const siteWorlds = []; + for (const name of ['backyard_01', 'site_02_corner_block', 'site_03_swing_lawn']) { + const site = await loadSite(name); + siteWorlds.push({ name, site, world: createWorld(new THREE.Scene(), { site }) }); + } + + t.test('RULE 4b — a node-less JSON anchor of a factory-rated type carries the factory rating at runtime', () => { + const types = FACTORY_ANCHOR_RATINGS.types, ambig = FACTORY_ANCHOR_RATINGS.ambiguous; + let checked = 0; + for (const { name, site, world } of siteWorlds) { + // Every anchor a site DECLARES in JSON, with the same type defaults + // world.js applies at each creation site. + const declared = [ + ...(site.posts ?? []).map((p) => ({ ...p, type: p.type ?? 'post' })), + ...(site.house?.anchors ?? []).map((a) => ({ ...a, type: a.type ?? 'house' })), + ...(site.trees ?? []).flatMap((tr) => (tr.anchors ?? []).map((a) => ({ ...a, type: a.type ?? 'tree' }))), + ...(site.structures ?? []).flatMap((s) => (s.anchors ?? []).map((a) => ({ ...a, type: a.type ?? 'post' }))), + ].filter((a) => !a.node); // node-carrying anchors are adoptAnchor's job at dress + for (const d of declared) { + assert(!ambig[d.type], + `${name}/${d.id}: a node-less anchor of AMBIGUOUS type "${d.type}" has no honest ` + + 'rating — the runtime refuses to guess (ratings.js); declare a `node` instead'); + const entry = types[d.type]; + assert(entry, + `${name}/${d.id}: type "${d.type}" is not factory-rated at all — bake a rating_hint ` + + 'in build_yard_assets.py or this anchor plays at the default hint forever'); + const rt = world.anchor(d.id); + assert(rt, `${name}/${d.id}: declared in JSON but absent from world.anchors`); + assert(rt.ratingHint === entry.rating_hint, + `${name}/${d.id} plays at ratingHint ${rt.ratingHint}; the factory baked ` + + `${entry.rating_hint} — the baked hint never arrived. This is the sail_post ` + + '0.90/1.00 bug shape: factory and sim disagreeing with no error anywhere'); + assert((rt.collateral ?? null) === (entry.collateral ?? null), + `${name}/${d.id} plays at collateral ${JSON.stringify(rt.collateral)}; the factory ` + + `baked ${JSON.stringify(entry.collateral)} — same wire, same bug shape`); + checked += 1; + } + } + assert(checked > 0, + 'vacuous: no node-less declared anchors found across the shipped sites — this pin checked nothing'); + }); + + for (const { world } of siteWorlds) world.dispose(); }