PROCITY/tools/qa/scaffold_check.mjs
m3ultra b2d29ba112 Lane F R27 (v5.0): the covenant re-pinned, the frame that took five tries, QA GREEN
qa.sh --strict --matrix: 7 passed / 0 failed / 0 warn. selfcheck ALL GREEN 156,352/156,352 ·
MATRIX GREEN 10 towns x 7 gates · scaffold_check GREEN (F's re-pins took) · flags harness green,
carrying the kill-the-server gate and the beta crate gate.

F RE-PINNED ITS OWN FILES per A's handoff (A doesn't edit other lanes' files, correctly):
GOLDEN_HASH 0x3fa36874 -> 0x5f76e76 · GIG_GOLDEN 0xb1d48ea1 -> 0xec7a2d39 · melbourne · katoomba
· the captions. AND A CAUGHT A LIVE HAZARD IN F'S GATE: hash_ok carried `or ('3fa36874' in
r.stdout)` — a bare-string OR-clause, dead the moment the covenant moved and worse than dead,
because it would go green off a SENTENCE ABOUT the old covenant appearing in stdout. Deleted.
The check derives from GOLDEN_HASH or it doesn't exist.

JOHN'S RULING IS THE EPOCH'S BEST LINE: the covenant is anti-drift machinery, not a defect
preservative. 0x3fa36874 did its job for five epochs — it never drifted. It just was never right,
and no hash can catch what was wrong in the first commit. A golden proves NOTHING CHANGED; it
cannot prove anything is CORRECT. Every gate we own was green over a world of back walls, and the
only thing that ever saw it was a human standing in the street looking at a wall.

THE FRONTAGE FRAME TOOK FIVE ATTEMPTS, FOUR OF THEM F'S FAULT:
  1-3. cluster pose (the town's heart isn't this shop) / lot-anchored facing along the edge /
       lot-normal facing — three photographs of empty road. Also never winnable: the shops faced
       backwards the whole time. F was posing a camera at a wall and tuning the geometry.
  4.   B's helper — and F THREW THE RESULT AWAY. poseForShopFront RETURNS a pose; it does not
       apply one. F called it, error-checked it, and never touched the camera, so the frame was
       the spawn paddock — and F's error-check PASSED, because the call succeeded perfectly. The
       vacuous check one last time, in the last frame of the epoch.
  5.   Applied it -> MONSTER ROBOT PARTY. Sign, door, awning, a ped walking in off Musgrave Road.

The measurement that would have found the facade bug in five minutes — compare the two
conventions against each other — is the one nobody ran for nine rounds, F included, right up to
the vacuous 72/72 F nearly filed as "B is wrong".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 16:48:22 +10:00

256 lines
14 KiB
JavaScript
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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 AE 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 AE 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); }