Lane E S15 gate 1.2: RULE 4 pins — manifest===GLBs both directions (4a), runtime===manifest across all three shipped sites (4b); compared against the manifest, never a literal; both mutation-checked red-then-green (post 1.0->0.9)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
type-two 2026-07-20 17:49:38 +10:00
parent 8aa49528e3
commit 2a540523ff

View File

@ -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();
}