diff --git a/web/world/js/ratings.js b/web/world/js/ratings.js new file mode 100644 index 0000000..24007eb --- /dev/null +++ b/web/world/js/ratings.js @@ -0,0 +1,68 @@ +/** + * SHADES / HARD YARDS — Lane E owns this file. + * + * The runtime end of the factory's anchor ratings (SPRINT15 gate 1.2). + * + * THE BUG THIS CLOSES: `adoptAnchor` is the only place a baked `rating_hint` + * ever reached an 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's bake never arrived and `?? 1` won — the factory said 0.90, the sim + * played 1.00, and nothing anywhere could see the disagreement. Gate 1.1 ruled + * the shipped 1.00 canon (every measured number stands on it), and this module + * is the mechanism half of that ruling: the same silent divergence is now + * impossible for the whole extras family (`rating_hint`, `collateral`, and by + * exclusion `tie_off`) on ANY node-less JSON anchor whose type the factory + * rates. + * + * HOW: the factory emits `anchor_ratings.gen.js` from the re-imported GLBs — + * per anchor TYPE, the baked extras, but only where every rated node of that + * type in the palette agrees (`types`). Types whose nodes disagree (tree: + * 1.0 → 0.4 down the trunk) are listed under `ambiguous`, and this module + * REFUSES to guess for them — a node-less anchor of an ambiguous type keeps + * the default hint and warns loudly, and e.test.js pins that no shipped site + * declares one. Three-way agreement is pinned in e.test.js: + * GLB === manifest === runtime, so a re-bake that skips regeneration, a + * hand-edit to the gen file, or a future adoptAnchor-shaped gap all go red. + * + * WHO CALLS IT: world.js, at the JSON-anchor creation sites, as + * `Object.assign(makeStaticAnchor(...), ..., a.node ? null : factoryExtras(type))` + * — node-carrying anchors keep their graybox default and are corrected by + * adoptAnchor at dress time, exactly as before; node-less ones take the + * factory's word at construction, because dress will never reach them. + */ + +import { FACTORY_ANCHOR_RATINGS } from './anchor_ratings.gen.js'; + +/** Warn once per type, not once per anchor — a site with four posts of a + * future ambiguous type should not print four identical warnings. */ +const warned = new Set(); + +/** + * The factory's baked extras for an anchor TYPE, in runtime (camelCase) field + * names, ready to `Object.assign` onto an Anchor. + * + * @param {string} type An ANCHOR_TYPE string (contracts.js). + * @returns {{ratingHint: number, collateral?: string}|null} + * The extras when the factory rates this type unambiguously; null when it + * doesn't rate it at all (caller keeps its default) or rates it ambiguously + * (caller keeps its default AND this is almost certainly a site bug — a + * node-less anchor of an ambiguous type has no honest rating; declare a + * `node` instead. e.test.js refuses it in shipped sites). + */ +export function factoryExtras(type) { + const t = FACTORY_ANCHOR_RATINGS.types[type]; + if (t) { + const out = { ratingHint: t.rating_hint }; + if (t.collateral !== undefined) out.collateral = t.collateral; + return out; + } + if (FACTORY_ANCHOR_RATINGS.ambiguous[type] && !warned.has(type)) { + warned.add(type); + console.warn( + `[ratings] anchor type "${type}" is factory-AMBIGUOUS (its GLB nodes carry ` + + `different rating_hints) — a node-less "${type}" anchor falls back to the ` + + `default hint, which is the exact silent-divergence bug gate 1.2 buried. ` + + `Declare a \`node\` on the anchor so adoptAnchor can carry the real bake.`); + } + return null; +}