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:
parent
8aa49528e3
commit
2a540523ff
@ -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();
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user