diff --git a/tools/site_audit/audit.mjs b/tools/site_audit/audit.mjs index 0b17cd5..de0622c 100644 --- a/tools/site_audit/audit.mjs +++ b/tools/site_audit/audit.mjs @@ -119,7 +119,44 @@ async function verifyPosts(site) { async function loadSite(path) { if (!path) return BACKYARD_01; const j = JSON.parse(await readFile(path, 'utf8')); - // site-as-data shape (Lane A, gate 2). Tolerant: only anchors + bed are needed. + + // A's committed site schema (SPRINT10, world.js loadSite/createWorld) is + // DRESS-SOURCE, not resolved positions, and headless node cannot turn one into + // the other. Two reasons, both load-bearing for an audit that must not lie: + // + // · POSTS are pre-rake. The JSON carries {id,x,z,h}; world.js:361 leans every + // post 8° away from the yard centre before it becomes an anchor. p4's JSON + // spec (-3.2,-1.2) dresses to (-3.72,-1.40) — 0.56 m, the exact error this + // tool's own snapshot shipped with in SPRINT9. Reading x/z raw re-makes it. + // · HOUSE and TREE anchors have NO coordinates at all — just `node`, a name + // baked into E's GLB (fascia_anchor_01, branch_anchor_02…). Their world + // position exists only after dress() reads the empty's matrixWorld, and + // dress() needs GLTFLoader + fetch, neither of which runs in node. + // + // So a headless read of this file gets posts wrong and tree/house anchors not + // at all — and the branch anchors are exactly where the dangerous quads live + // (t2b pulled 10 kN in SPRINT9). Reporting on that silently is the SPRINT6 + // trap in reverse: a site called unriggable because the TOOL couldn't read it. + // + // The correct source of dressed positions is createWorld(site).anchors — which + // is browser-only. Until that path lands (this is raised to A in THREADS), the + // honest move is to refuse this schema loudly, not to guess at it. + const dressSource = Array.isArray(j.posts) || j.house || Array.isArray(j.trees) || Array.isArray(j.structures); + const resolved = Array.isArray(j.anchors) && j.anchors.length + && j.anchors.every((a) => Number.isFinite(a.pos?.x ?? a.x)); + if (dressSource && !resolved) { + throw new Error( + `"${j.name || j.id || path}" is a DRESS-SOURCE site (posts/house/trees), and its anchor\n` + + ` positions cannot be resolved headless: posts are pre-rake (world.js leans them 8°) and\n` + + ` house/tree anchors are GLB node refs that only exist after dress(), which node cannot run.\n` + + ` Audit it in the browser off createWorld(site).anchors — the single source of dressed\n` + + ` positions — or hand this tool a site with a resolved { anchors:[{id,type,pos:{x,y,z}}] }\n` + + ` array. Run with no argument to audit the built-in DRESSED backyard_01 snapshot.\n` + + ` (This limitation and the browser-audit plan are in THREADS for Lane A.)`); + } + + // Resolved-positions shape: a flat anchors[] each carrying real coordinates. + // This is what a dressed export (or a future createWorld dump) would hand us. const anchors = (j.anchors || []).map((a) => ({ id: a.id, type: a.type || 'post', pos: { x: a.pos?.x ?? a.x, y: a.pos?.y ?? a.y, z: a.pos?.z ?? a.z },