#!/usr/bin/env node // PROCITY Lane F — scaffold + determinism + lane-readiness check. // Plain node, zero deps. Run: node tools/qa/scaffold_check.mjs // // What it does, and why F owns it: // 1. Verifies the frozen scaffold everyone builds on (vendored three, core modules, skins). // 2. Locks the PRNG determinism law (CITY_SPEC: same citySeed => byte-identical city) against // golden values — a regression in prng.js is the one thing that silently corrupts every lane. // 3. Prints a LANE READINESS MATRIX: which A–E deliverables have actually landed. This is the // "state of the integration" dashboard fable/Lane F reads before attempting to wire anything. // 4. When Lane A's citygen lands, additionally asserts plan determinism + JSON round-trip. // // Exit code: 0 if everything PRESENT passes. Non-zero if a present artifact fails a check. // Absent lane deliverables are reported as PENDING, not failures — F runs before A–E and grows // teeth as they land. import { readFileSync, existsSync, readdirSync, statSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; import { dirname, join, resolve } from 'node:path'; import { execFileSync } from 'node:child_process'; import { isDeepStrictEqual } from 'node:util'; const HERE = dirname(fileURLToPath(import.meta.url)); const ROOT = resolve(HERE, '..', '..'); // repo root (tools/qa/ -> ../../) const R = (...p) => join(ROOT, ...p); let failures = 0; const fail = (msg) => { failures++; console.log(` \x1b[31m✗ FAIL\x1b[0m ${msg}`); }; const ok = (msg) => console.log(` \x1b[32m✓ ok\x1b[0m ${msg}`); const info = (msg) => console.log(` \x1b[36m·\x1b[0m ${msg}`); const head = (msg) => console.log(`\n\x1b[1m${msg}\x1b[0m`); function assert(cond, okMsg, failMsg) { cond ? ok(okMsg) : fail(failMsg || okMsg); return cond; } // ---------------------------------------------------------------- SCAFFOLD head('SCAFFOLD (frozen — everyone builds on this)'); for (const f of ['web/vendor/three.module.js', 'web/vendor/three.core.js']) { const p = R(f); assert(existsSync(p) && statSync(p).size > 100_000, `vendored ${f} present (${existsSync(p) ? (statSync(p).size / 1024 | 0) + 'k' : 'MISSING'})`, `vendored ${f} missing or truncated`); } for (const f of ['web/js/core/prng.js', 'web/js/core/loaders.js', 'web/js/core/canvas.js']) { assert(existsSync(R(f)), `core module ${f} present`, `core module ${f} MISSING`); } // Syntax-check every hand-written JS file (vendor/ excluded — third party). head('SYNTAX (node --check on all web/js + tools)'); function collectJS(dir) { const out = []; if (!existsSync(dir)) return out; for (const name of readdirSync(dir)) { const p = join(dir, name); if (statSync(p).isDirectory()) { if (name !== 'vendor' && name !== 'node_modules') out.push(...collectJS(p)); } else if (p.endsWith('.js') || p.endsWith('.mjs')) out.push(p); } return out; } const jsFiles = [...collectJS(R('web/js')), ...collectJS(R('tools'))]; let syntaxBad = 0; for (const f of jsFiles) { try { execFileSync(process.execPath, ['--check', f], { stdio: 'pipe' }); } catch (e) { syntaxBad++; fail(`syntax error in ${f.replace(ROOT + '/', '')}: ${String(e.stderr || e).split('\n')[0]}`); } } if (syntaxBad === 0) ok(`${jsFiles.length} JS/MJS files parse clean`); // ---------------------------------------------------------------- DETERMINISM (PRNG law) head('DETERMINISM — PRNG law (CITY_SPEC: same seed ⇒ identical city)'); { const prng = await import(R('web/js/core/prng.js')); const { rng, seedFor, shuffle, pick, irange } = prng; // Golden values — captured from the frozen prng.js. If these drift, every lane's world drifts. const g = { rng_1234_shop_7: [0.8933273730799556, 0.7678728918544948, 0.5031793543603271, 0.10452594514936209, 0.6337878312915564], seedFor_1234_shop_7: 3826086676, seedFor_20261990_district_0: 604920024, shuffle_42: [5, 2, 6, 0, 1, 3, 8, 9, 4, 7], irange_7: [2, 2, 5, 3, 1, 4], }; const eq = (a, b) => JSON.stringify(a) === JSON.stringify(b); const r = rng(1234, 'shop', 7); const first5 = [r(), r(), r(), r(), r()]; assert(eq(first5, g.rng_1234_shop_7), 'rng(1234,"shop",7) stream matches golden', `rng stream DRIFTED: got ${JSON.stringify(first5)}`); assert(seedFor(1234, 'shop', 7) === g.seedFor_1234_shop_7, 'seedFor(1234,"shop",7) matches golden', 'seedFor DRIFTED'); assert(seedFor(20261990, 'district', 0) === g.seedFor_20261990_district_0, 'seedFor(20261990,"district",0) matches golden', 'seedFor DRIFTED'); // Two independent streams from the same key must be byte-identical. const a = rng(777, 'x', 1), b = rng(777, 'x', 1); assert(eq(Array.from({ length: 64 }, () => a()), Array.from({ length: 64 }, () => b())), 'two independent rng(777,"x",1) streams are identical (64 draws)', 'independent streams DIVERGED — determinism broken'); assert(eq(shuffle(rng(42, 'shuffle', 0), [0,1,2,3,4,5,6,7,8,9]), g.shuffle_42), 'shuffle is deterministic + matches golden', 'shuffle DRIFTED'); // Draw six values from ONE stream (a fresh rng() per draw would just repeat the first value). { const ir = rng(7, 'irange', 0); assert(eq(Array.from({ length: 6 }, () => irange(ir, 1, 6)), g.irange_7), 'irange is deterministic + matches golden', 'irange DRIFTED'); } // Math.random must NOT appear in generation code (the house ban). grep web/js excluding core FX. const offenders = grepMathRandom(R('web/js')); if (offenders.length === 0) ok('no Math.random() in web/js (generation-code ban holds)'); else for (const o of offenders) fail(`Math.random() found (banned in gen code): ${o}`); } function grepMathRandom(dir) { // [Lane F R11] The ban protects world-GENERATION determinism (same seed → same town). Runtime FX // PLAYBACK that is transient by nature is exempt — the seeded CHOICE lives upstream in prng-driven // generators, the playback file only renders it. web/js/world/audio.js (Lane B): footstep SFX variant // + gain jitter (audio.js:143,145) are per-step cosmetics; which shop plays which bed is seeded in // interiors.js, not here. (Noted to B — they may instead add a `// cosmetic` marker on those lines.) const RUNTIME_FX_EXEMPT = new Set(['web/js/world/audio.js']); const hits = []; (function walk(d) { if (!existsSync(d)) return; for (const name of readdirSync(d)) { const p = join(d, name); if (statSync(p).isDirectory()) { if (name !== 'vendor') walk(p); continue; } if (!p.endsWith('.js') && !p.endsWith('.mjs')) continue; if (RUNTIME_FX_EXEMPT.has(p.replace(ROOT + '/', ''))) continue; // runtime FX playback, not gen code const txt = readFileSync(p, 'utf8'); txt.split('\n').forEach((ln, i) => { // Exemptions must live inside a // comment — anchor the keywords to the comment, otherwise // a bare "cosmetic"/"banned" anywhere on a real offending line would falsely exempt it. if (/Math\.random\s*\(/.test(ln) && !/\/\/.*(Math\.random|cosmetic|banned)/.test(ln)) hits.push(`${p.replace(ROOT + '/', '')}:${i + 1}`); }); } })(dir); return hits; } // ---------------------------------------------------------------- ASSETS head('ASSETS'); { const gen = R('web/assets/gen'); const jpgs = existsSync(gen) ? readdirSync(gen).filter(f => /\.(jpg|jpeg|png|webp)$/i.test(f)) : []; assert(jpgs.length >= 69, `${jpgs.length} texture skins in web/assets/gen (expect ≥69)`, `only ${jpgs.length} skins in web/assets/gen (expect ≥69)`); const peds = R('web/models/peds'); if (existsSync(peds)) { const glbs = readdirSync(peds).filter(f => f.endsWith('.glb')); info(`web/models/peds staged: ${glbs.length} GLBs (Lane D fleet — code not required for this check)`); } } // ---------------------------------------------------------------- LANE READINESS MATRIX head('LANE READINESS MATRIX (what has actually landed)'); const targets = [ ['A', 'CityPlan generator', 'web/js/citygen/plan.js'], ['A', 'shop-type registry', 'web/js/core/registry.js'], ['A', 'names generator', 'web/js/citygen/names.js'], ['A', 'selfcheck', 'web/js/citygen/selfcheck.js'], ['A', '2D debug map', 'web/map.html'], ['B', 'game shell', 'web/index.html'], ['B', 'chunk streamer', 'web/js/world/chunks.js'], ['B', 'facade kit', 'web/js/world/buildings.js'], ['B', 'fixture plan', 'web/js/world/fixture_plan.js'], ['C', 'interiors API', 'web/js/interiors/interiors.js'], ['C', 'interior test page', 'web/interior_test.html'], ['D', 'citizen rigs', 'web/js/citizens/rigs.js'], ['D', 'citizen sim', 'web/js/citizens/sim.js'], ['D', 'citizens test page', 'web/citizens_test.html'], ['E', 'asset manifest', 'web/assets/manifest.json'], ['E', 'manifest validator', 'pipeline/validate_manifest.py'], ['E', 'audit', 'pipeline/AUDIT.md'], ]; const byLane = {}; for (const [lane, label, path] of targets) { const there = existsSync(R(path)); (byLane[lane] ||= { n: 0, have: 0 }); byLane[lane].n++; if (there) byLane[lane].have++; console.log(` [${lane}] ${there ? '\x1b[32mLANDED \x1b[0m' : '\x1b[33mpending\x1b[0m'} ${label.padEnd(20)} ${path}`); } head('READINESS SUMMARY'); let allLanded = true; for (const lane of ['A', 'B', 'C', 'D', 'E']) { const s = byLane[lane]; const done = s.have === s.n; allLanded &&= done; info(`Lane ${lane}: ${s.have}/${s.n} key deliverables ${done ? '\x1b[32m(complete)\x1b[0m' : ''}`); } info(allLanded ? '\x1b[32mAll lanes landed — Lane F integration + full QA gates can run.\x1b[0m' : '\x1b[33mLanes still pending — F integration is BLOCKED on the above. This check re-runs as they land.\x1b[0m'); // ---------------------------------------------------------------- PLAN DETERMINISM (activates when Lane A lands) if (existsSync(R('web/js/citygen/plan.js'))) { head('PLAN DETERMINISM (Lane A present — running deep checks)'); try { const mod = await import(R('web/js/citygen/plan.js')); if (typeof mod.generatePlan !== 'function') { fail('web/js/citygen/plan.js does not export generatePlan(citySeed)'); } else { const t0 = performance.now(); const p1 = mod.generatePlan(1234); const genMs = performance.now() - t0; const p2 = mod.generatePlan(1234); assert(JSON.stringify(p1) === JSON.stringify(p2), 'generatePlan(1234) is deterministic across two runs', 'generatePlan(1234) NON-DETERMINISTIC — two runs differ'); assert(genMs < 100, `generatePlan under 100ms (${genMs.toFixed(1)}ms)`, `generatePlan too slow: ${genMs.toFixed(1)}ms (budget 100ms)`); // Deep-compare the PARSED round-trip against the LIVE object (normalizing the benign -0→0 // that JSON does losslessly but SameValue treats as distinct). stringify-vs-stringify would // be a tautology — it can't catch a plan carrying Map/Set/undefined/function/Date/NaN. const norm = v => JSON.parse(JSON.stringify(v, (_, x) => Object.is(x, -0) ? 0 : x)); const round = JSON.parse(JSON.stringify(p1)); assert(isDeepStrictEqual(round, norm(p1)), 'plan survives JSON round-trip (no lossy Map/Set/undefined/fn fields)', 'plan carries non-JSON data — Map/Set/undefined/function/Date/NaN lost on round-trip'); if (typeof mod.chunkIndex === 'function') ok('chunkIndex(plan) exported'); else fail('chunkIndex(plan) not exported from plan.js'); info(`plan(1234): ${p1?.shops?.length ?? '?'} shops, ${p1?.lots?.length ?? '?'} lots, ` + `${p1?.streets?.edges?.length ?? '?'} edges, name "${p1?.name ?? '?'}"`); // [F2/R8] plan-source selector — pin goldens keyed by (seed, plansrc, TOWN). // Lane A contract: synthetic 0x5f76e76 (selfcheck) · osm/melbourne 0x9c7e76b3 · osm/katoomba 0xf3aafec8. // Guarded: no-op until A ships generatePlanFor + generatePlanOSM. try { const bar = await import(R('web/js/citygen/index.js')); if (typeof bar.generatePlanFor === 'function' && typeof bar.generatePlanOSM === 'function') { const { xmur3 } = await import(R('web/js/core/prng.js')); const TOWN_GOLDENS = { melbourne: '0x9c7e76b3', katoomba: '0xf3aafec8' }; for (const [town, golden] of Object.entries(TOWN_GOLDENS)) { const a = bar.generatePlanFor(20261990, 'osm', { town }), b2 = bar.generatePlanFor(20261990, 'osm', { town }); assert(JSON.stringify(a) === JSON.stringify(b2), `osm/${town} plan is deterministic across two runs`, `osm/${town} plan NON-DETERMINISTIC`); const h = (xmur3(JSON.stringify(a))() >>> 0).toString(16).padStart(8, '0'); // pad: goldens are 0x + 8 hex assert(`0x${h}` === golden, `osm/${town} fingerprint == golden ${golden} ("${a.name}", ${a.shops?.length} shops)`, `osm/${town} fingerprint DRIFTED: 0x${h} (expect ${golden})`); } assert(bar.generatePlanFor(20261990, 'osm').source === 'osm' && (bar.generatePlanFor(20261990, 'synthetic').source || 'synthetic') === 'synthetic', 'generatePlanFor routes synthetic|osm correctly', 'generatePlanFor selector mis-routes'); } } catch (e) { fail(`osm plan-source check threw: ${e.message}`); } } } catch (e) { fail(`importing/running plan.js threw: ${e.message}`); } } // ---------------------------------------------------------------- VERDICT head('VERDICT'); if (failures === 0) { console.log('\x1b[32m● scaffold_check GREEN\x1b[0m — every present artifact passed.\n'); process.exit(0); } else { console.log(`\x1b[31m● scaffold_check RED\x1b[0m — ${failures} failure(s) above.\n`); process.exit(1); }