THE ROUND IN ONE MEASUREMENT (12 samples x 146 active, ?clips=0 vs default — a new flag that turns off the library and nothing else): walking 99.3% -> 78.1% · bench-sit 0 -> 9.5% · lean 0 -> 7.1% · stopped in own idle 0.4% -> 5.1% · DISTINCT CLIPS ACROSS THE CROWD 4 -> 20. The town was 99.3% people walking because standing still had nowhere to happen. Wiring: new postures.js + clipbank.js. idles.glb (10/10) drives a per-citizen deterministic idle on every near-tier actor plus the seeded shopkeeper. locomotion gives 33.7% of walkers a shopping bag. sitlean (8/8, lazy) puts 4 sits on Lane B's ACTUAL benches and 4 leans on shopfront walls. browse (5/8, lazy) is a real BROWSE state at C's browse points, seeded per (shopId, slot). venue (5/6, lazy) widens the gig crowd, plus a publican pouring and a record keeper in headphones. social (0/8) is never fetched — two-person conversation needs a paired state machine, filed to R42. Cost: boot = 4 requests, 1.24 MB / 16 clips resident; the rest lazy on first need; heap delta +3.34 MB; mixer median 0.1 ms both arms. ?clips=0 / ?classic=1 / ?noassets=1 fetch ZERO clips — not even clipbank.js (dynamic import). No shell edit needed. DRAWS: +0 on every bookmark (street_noon 193, crossroads 108, night_crowd 128, market_square 94, night_neon 111, interior 110 — identical both arms). Ruling 4 respected exactly. DETERMINISM: 150 citizens, two fresh contexts, byte-equal posture signature. Controls: seed+1 differs; EVERY clip GLB delayed 2 s -> identical signature (posture is a pure function of (citySeed, id), never of residency). 6 new streams collide with none of the 12 pre-R41 keys. TWO FINDINGS THAT CHANGED THE DESIGN: the idle pool was INVISIBLE — wired only to the R17/R29 node loiter, so only 0.8% of citizens were ever stopped; and the lean never fired at all (0 in a 9 s run). Both moved to the patronage stride check. Bench stations are GATED not trusted: 14/14 derived stations coincide with real instanced geometry within 2 cm, and the control (same stations offset 2 m) matches 0/14. Filed to B: one benchStops(plan) export retires the mirror, and furniture.js puts the bench's front ALONG the street rather than facing the road, contradicting its own comment. Leak: +0 geometries, +1 texture over 6 enter/exit cycles. Goldens 157,647/157,647, 0x5f76e76. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
152 lines
11 KiB
JavaScript
152 lines
11 KiB
JavaScript
#!/usr/bin/env node
|
||
// PROCITY Lane D — R41 §41.3 gate: THE POSTURE TABLE IS REAL AND IT IS DETERMINISTIC.
|
||
//
|
||
// node tools/qa/r41_postures.mjs 0 = green, 1 = red
|
||
//
|
||
// Zero deps, no browser, ~40 ms. Five arms, each with the control that makes it non-vacuous:
|
||
//
|
||
// 1. MANIFEST RESOLUTION every clip id `postures.js` can hand out resolves in Lane E's
|
||
// web/assets/motion_manifest.json, with the category the pool claims and
|
||
// the group file GROUP_OF claims. CONTROL: a deliberately bogus id is fed
|
||
// through the same resolver and must fail — otherwise arm 1 proves nothing.
|
||
// 2. DETERMINISM 1 000 citizen ids, twice, in two freshly-imported module instances →
|
||
// byte-equal (sha256 of the joined signature block). CONTROL: one different
|
||
// seed must produce a DIFFERENT digest, or "byte-equal" is just "constant".
|
||
// 3. STREAM ISOLATION the R41 streams (`posture`, `benchstop`, `leanstop`, `browse-pose`,
|
||
// `keeper-pose`, `gig-pose`) reproduce the pre-R41 streams' first 8 draws
|
||
// for `citizen`/`turn`/`loiter`/`patron`/`benchsit`/`glance` unchanged —
|
||
// i.e. no existing identity moved. CONTROL: they are not all the SAME
|
||
// stream either (a copy-paste key would pass a naive equality test).
|
||
// 4. POOL SPREAD the 10-idle pool is actually spread over a real crowd, not collapsed onto
|
||
// one clip by a bad index. CONTROL: measured occupancy of every bucket.
|
||
// 5. LOOP SAFETY every pool clip either loops (manifest `loopable`) or has a measured
|
||
// `loopSeamDeg` the bank will ping-pong (> LOOP_SEAM_DEG) — no clip can be
|
||
// put on repeat with a seam that pops. CONTROL: the count in each class.
|
||
|
||
import { createHash } from 'node:crypto';
|
||
import { readFileSync } from 'node:fs';
|
||
import { fileURLToPath } from 'node:url';
|
||
import { dirname, resolve } from 'node:path';
|
||
|
||
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
||
const P = (p) => resolve(ROOT, p);
|
||
const SEED = 20261990;
|
||
const LOOP_SEAM_DEG = 25; // clipbank.js — above this the bank plays ping-pong instead of repeat
|
||
|
||
let fails = 0;
|
||
const OK = (m) => console.log(` \x1b[32m✓\x1b[0m ${m}`);
|
||
const FAIL = (m) => { fails++; console.log(` \x1b[31m✗ FAIL\x1b[0m ${m}`); };
|
||
const head = (m) => console.log(`\n\x1b[1m${m}\x1b[0m`);
|
||
const check = (c, m) => { (c ? OK : FAIL)(m); return c; };
|
||
const sha = (s) => createHash('sha256').update(s).digest('hex').slice(0, 16);
|
||
|
||
const po = await import(P('web/js/citizens/postures.js'));
|
||
const { rng } = await import(P('web/js/core/prng.js'));
|
||
const man = JSON.parse(readFileSync(P('web/assets/motion_manifest.json'), 'utf8'));
|
||
|
||
const POOLS = {
|
||
idle: [po.IDLE_POOL, 'idle'], walk: [po.WALK_POOL, 'locomotion'],
|
||
sit: [po.SIT_POOL, 'sitlean'], lean: [po.LEAN_POOL, 'sitlean'],
|
||
browse: [po.BROWSE_POOL, 'browse'],
|
||
venueDance: [po.VENUE_DANCE_POOL, 'venue'], venueStand: [po.VENUE_STAND_POOL, 'venue'],
|
||
keeperType: [Object.values(po.KEEPER_TYPE_CLIP), 'venue'],
|
||
};
|
||
|
||
// ── 1. manifest resolution ───────────────────────────────────────────────────────────────────────
|
||
head('1. MANIFEST RESOLUTION — every posture clip resolves in Lane E\'s motion_manifest.json');
|
||
const resolves = (id) => {
|
||
if (!id || id[0] === '@') return { sentinel: true }; // pre-R41 base asset, not a library clip
|
||
const m = man.clips[id];
|
||
if (!m) return { err: 'not in manifest' };
|
||
if (man.groups[m.group] === undefined) return { err: `group ${m.group} not in manifest` };
|
||
if (!man.groups[m.group].clips.includes(id)) return { err: `not listed under ${m.group}` };
|
||
if (po.GROUP_OF[id] !== m.group) return { err: `GROUP_OF says ${po.GROUP_OF[id]}, manifest says ${m.group}` };
|
||
return { ok: true, meta: m };
|
||
};
|
||
let nClips = 0, nSentinel = 0;
|
||
for (const [name, [pool, cat]] of Object.entries(POOLS)) {
|
||
const bad = [];
|
||
for (const id of pool) {
|
||
const r = resolves(id);
|
||
if (r.sentinel) { nSentinel++; continue; }
|
||
if (r.err) { bad.push(`${id}: ${r.err}`); continue; }
|
||
if (r.meta.category !== cat) bad.push(`${id}: category ${r.meta.category} != ${cat}`);
|
||
nClips++;
|
||
}
|
||
check(!bad.length, `${name} pool (${pool.length}) → ${bad.length ? bad.join(' · ') : `all resolve, category=${cat}`}`);
|
||
}
|
||
check(resolves('idle_definitely_not_a_clip').err === 'not in manifest',
|
||
'CONTROL: a bogus clip id fails the same resolver (the arm is not vacuous)');
|
||
const distinct = new Set(Object.values(POOLS).flatMap(([p]) => p).filter((i) => i[0] !== '@'));
|
||
OK(`${nClips} pool entries / ${distinct.size} distinct library clips of the manifest's ${man.clipCount} · ${nSentinel} base-asset sentinels`);
|
||
const groupsUsed = new Set([...distinct].map((i) => po.GROUP_OF[i]));
|
||
OK(`groups referenced: ${[...groupsUsed].sort().join(' ')} — boot fetches ${po.BOOT_GROUPS.join(' + ')}, rest lazy`);
|
||
check(po.BOOT_GROUPS.every((g) => man.groups[g]), 'BOOT_GROUPS all exist in the manifest');
|
||
|
||
// ── 2. determinism ───────────────────────────────────────────────────────────────────────────────
|
||
head('2. DETERMINISM — same seed → same postures, byte-equal across two module instances');
|
||
const IDS = [];
|
||
for (let cx = -2; cx <= 2; cx++) for (let cz = -2; cz <= 2; cz++) for (let i = 0; i < 40; i++) IDS.push(`${cx},${cz}#${i}`);
|
||
const block = (mod, seed) => IDS.map((id) => mod.postureSig(id, mod.posturesFor(seed, id))).join('\n');
|
||
const po2 = await import(P('web/js/citizens/postures.js') + '?fresh=1'); // a second module instance
|
||
const A = block(po, SEED), B = block(po2, SEED);
|
||
check(A === B, `${IDS.length} citizens, two module instances → identical (sha256 ${sha(A)})`);
|
||
const C = block(po, SEED + 1);
|
||
check(C !== A, `CONTROL: seed ${SEED + 1} differs (sha256 ${sha(C)}) — "byte-equal" is not "constant"`);
|
||
// the other three pickers
|
||
const bA = IDS.map((id) => po.browsePostureFor(SEED, 'shop7', +id.split('#')[1] % 3)).join(',');
|
||
const bB = IDS.map((id) => po2.browsePostureFor(SEED, 'shop7', +id.split('#')[1] % 3)).join(',');
|
||
check(bA === bB, 'browsePostureFor byte-equal across instances');
|
||
check(po.keeperPostureFor(SEED, 'shop7', 'pub') === 'venue_bartending'
|
||
&& po.keeperPostureFor(SEED, 'shop7', 'record') === 'venue_headphones'
|
||
&& po.IDLE_POOL.includes(po.keeperPostureFor(SEED, 'shop7', 'opshop')),
|
||
'keeperPostureFor: pub pours, record shop listens, everything else takes a seeded idle');
|
||
const gA = IDS.map((id) => po.gigPostureFor(SEED, id, true)).join(',');
|
||
check(gA === IDS.map((id) => po2.gigPostureFor(SEED, id, true)).join(','), 'gigPostureFor byte-equal across instances');
|
||
const swapped = IDS.filter((id) => po.gigPostureFor(SEED, id, true)).length;
|
||
check(swapped > 0 && swapped < IDS.length,
|
||
`CONTROL: the gig widening is a SWAP not a replace — ${swapped}/${IDS.length} dancers take the venue clip, the rest keep the v3 dance pick`);
|
||
|
||
// ── 3. stream isolation ──────────────────────────────────────────────────────────────────────────
|
||
head('3. STREAM ISOLATION — no pre-R41 stream moved, and the new keys are genuinely different');
|
||
const draws = (kind, id, n = 8) => { const r = rng(SEED, kind, id); return Array.from({ length: n }, () => r().toFixed(12)).join(','); };
|
||
// pre-R41 streams are pure functions of (seed, kind, id) — they cannot move unless a KEY collides.
|
||
const OLD = ['citizen', 'turn', 'loiter', 'patron', 'benchsit', 'glance', 'chunkpop', 'keeper', 'browser', 'gig', 'gigdance', 'gigp'];
|
||
const NEW = ['posture', 'benchstop', 'leanstop', 'browse-pose', 'keeper-pose', 'gig-pose'];
|
||
check(NEW.every((k) => !OLD.includes(k)), `the ${NEW.length} R41 keys collide with none of the ${OLD.length} pre-R41 keys`);
|
||
const sigs = new Map();
|
||
for (const k of [...OLD, ...NEW]) sigs.set(k, draws(k, '3,-1#7'));
|
||
check(new Set(sigs.values()).size === sigs.size,
|
||
`CONTROL: all ${sigs.size} streams produce DIFFERENT draws on the same id (no copy-pasted key)`);
|
||
// and the pre-R41 values themselves, pinned, so a future prng edit shows up here
|
||
OK(`pinned: rng(${SEED},'citizen','3,-1#7')[0] = ${draws('citizen', '3,-1#7', 1)}`);
|
||
|
||
// ── 4. pool spread ───────────────────────────────────────────────────────────────────────────────
|
||
head('4. POOL SPREAD — the clone army is actually broken up, measured');
|
||
const hist = {};
|
||
for (const id of IDS) { const p = po.posturesFor(SEED, id); hist[p.idle] = (hist[p.idle] || 0) + 1; }
|
||
const used = Object.keys(hist).length;
|
||
check(used === po.IDLE_POOL.length, `all ${po.IDLE_POOL.length} idles appear across ${IDS.length} citizens (${used} distinct)`);
|
||
const counts = po.IDLE_POOL.map((k) => hist[k] || 0);
|
||
const lo = Math.min(...counts), hi = Math.max(...counts);
|
||
check(lo >= IDS.length / po.IDLE_POOL.length * 0.6, `spread ${lo}–${hi} per clip (uniform would be ${(IDS.length / po.IDLE_POOL.length).toFixed(0)})`);
|
||
const bagWalk = IDS.filter((id) => po.posturesFor(SEED, id).walk === 'walk_shopping_bag').length;
|
||
check(bagWalk > 0, `${bagWalk}/${IDS.length} (${(bagWalk / IDS.length * 100).toFixed(1)}%) carry the shopping bag; the rest keep the base walk.glb gait`);
|
||
|
||
// ── 5. loop safety ───────────────────────────────────────────────────────────────────────────────
|
||
head('5. LOOP SAFETY — nothing goes on repeat with a seam that pops');
|
||
let repeat = 0, ping = 0; const unsafe = [];
|
||
for (const id of distinct) {
|
||
const m = man.clips[id];
|
||
if (m.loopable) { repeat++; continue; }
|
||
if ((m.loopSeamDeg || 0) > LOOP_SEAM_DEG) { ping++; continue; }
|
||
// curated-not-loopable but the MEASURED seam closes (E's flag is conservative) → repeat is fine
|
||
if ((m.loopSeamDeg || 0) <= 5) { repeat++; continue; }
|
||
unsafe.push(`${id} (seam ${m.loopSeamDeg}°)`);
|
||
}
|
||
check(!unsafe.length, `${repeat} clips repeat (loopable or measured seam ≤5°) · ${ping} ping-pong (seam >${LOOP_SEAM_DEG}°)${unsafe.length ? ' · UNSAFE: ' + unsafe.join(', ') : ''}`);
|
||
check(ping > 0, `CONTROL: the ping-pong class is non-empty — ${[...distinct].filter((i) => !man.clips[i].loopable && man.clips[i].loopSeamDeg > LOOP_SEAM_DEG).join(', ')}`);
|
||
|
||
console.log(fails ? `\n\x1b[31m● RED\x1b[0m — ${fails} failure(s)` : '\n\x1b[32m● PASS\x1b[0m — posture table resolves, is deterministic, isolated, spread and loop-safe');
|
||
process.exit(fails ? 1 : 0);
|