PROCITY/tools/qa/interior_scale_check.py
m3ultra b8e1082182 Lane F R36 w2 (v7.0 close): the 2 a.m. assets gated — DJ-body figure-scale leg + smoke_djdance
interior_scale_check.py grows the DJ-body leg (E flag a): the 5 trellis bodies
(b339402) spawned BY NAME through the real browse path — bind skeletons measure
0.984-0.989 UNITS vs the legacy roster's ~173 (cm convention), rigs.js:163
span-normalise absorbs it: statures 1.64/1.71/1.81/1.87/1.89 m, all [1.4,2.0],
feet planted, rig-kind verified. The old sample's (i*7)%19 could never reach
fleet indices 17-21 — the subject is now provably exercised.

flags_check.py grows STRICT smoke_djdance (E flag b: gate on binds + NaN, never
node counts): 4 Rokoko clips bind 64/65 tracks (sole unbound mixamorigReference
— E's census reproduced at runtime); gig dancers clip-driven (identity by
duration, mixer advancing, position.y FLAT); ?classic ZERO dance-clip fetches
(positive control: default fetched 4); ?noassets procedural sway ALIVE (amp
0.067 m vs clip-driven 0.00 — the inverse pair proves the branch).

ONE WARN, deliberate (the held finding, machine-recorded): ?classic fetches the
5 DJ body GLBs — unconditional PED_NAMES.normal members (D's file), which also
widened classic's crowd pool 17->22 ungated. Held for Fable per the five-holds
law; F did not improvise in D's file during the close.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-25 23:09:48 +10:00

213 lines
12 KiB
Python
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 python3
"""PROCITY Lane D — interior figure-scale gate (Round 10 regression guard).
Guards the R9 blocker: interior rig figures came out ~2x too tall (keeper head at 3.83 m in a
3.4 m room) because rigs.js `buildFigure` normalised by the head bone's world Y in BIND POSE —
which is head-above-hips (the skeleton origin), ~half the standing height — instead of the
feet->crown span. Fixed in R10 (normalise by `headY - minY`).
This gate boots the real game, enters a sample of open shops, spawns browser rigs at every browse
point (occupancy is 0 right after boot, so we inject it to exercise the browse spawn path
deterministically), and asserts for EVERY spawned interior figure:
1.4 m <= STATURE (crown - foot) <= 2.0 m (human-sized — platform-independent)
crown(head-bone world Y) < room dims.H (never clips the ceiling — its own line)
|feet(min-bone world Y)| < 0.05 m (planted on the floor)
Exits non-zero if any figure fails, so tools/qa.sh --strict fails a giant before it can ship.
[Lane F R13 — debt #6] The human-size check measures STATURE (feet→crown span), not the raw world
crown, so it stays correct the day a figure stands on a platform (a keeper on a 0.3 m riser is not a
giant). This is the same measure smoke_gigs uses for the band on Lane C's stage deck, now ported into
the shared gate; the world-crown "never clips the ceiling" assert is kept as its own separate line
(that is the real R10 failure mode). For floor figures foot≈0 ⇒ stature≈crown ⇒ fully backward-compatible.
Run: tools/.venv/bin/python tools/qa/interior_scale_check.py [--seed N]
Setup: the same Playwright venv as tools/soak.py / tools/flags_check.py.
Needs Lane B's window.DBG hook (?dbg=1) + window.PROCITY.interiorMode (Lane C/F).
"""
import sys, os, time, socket, subprocess, pathlib
ROOT = pathlib.Path(__file__).resolve().parent.parent.parent
PORT = int(os.environ.get('PROCITY_QA_PORT', '8130')) # [R31] port-isolated suite runs (see flags_check.py)
HOST = f'http://127.0.0.1:{PORT}'
SEED = 20261990
if '--seed' in sys.argv:
SEED = int(sys.argv[sys.argv.index('--seed') + 1])
LO, HI = 1.4, 2.0 # human crown band (Round-10 gate)
SEAT_LO, SEAT_HI = 0.9, 1.5 # [R16 ledger #3] seated figs (the drummer): a seated human's feet→crown span, still < ceiling, feet planted
FOOT_EPS = 0.05 # feet must plant at floor y=0
SAMPLE_TYPES = ['opshop', 'toy', 'video', 'record', 'book', 'milkbar', 'clothes', 'hardware']
# [R36 w2 — E's flag (a)] the five trellis DJ bodies (b339402, swept R36 w1). Their bind skeletons
# measure ~0.950.99 UNITS tall vs the legacy roster's ~173180 (the cm convention) — absorbed by
# design at rigs.js:163 (span-normalise at spawn), but they joined PED_NAMES ungated and the browse
# sample above only ever draws pedIndex (i*7)%19, which can never reach fleet indices 1721. This leg
# spawns each of the five BY NAME and asserts the spawned stature lands human — the number E flagged,
# proven at the spawn path, not inferred from the file.
DJ_BODIES = ['woman_dj_01', 'woman_dj_02', 'woman_dj_03', 'dj_techno_01', 'dj_phrtt_01']
fails = []
def FAIL(m): fails.append(m); print(f" \033[31m✗ FAIL\033[0m {m}")
def OK(m): print(f" \033[32m✓\033[0m {m}")
def port_up(port):
with socket.socket() as s:
s.settimeout(0.4)
return s.connect_ex(('127.0.0.1', port)) == 0
def ensure_server():
if port_up(PORT):
return None
proc = subprocess.Popen(['python3', '-m', 'http.server', str(PORT)],
cwd=str(ROOT / 'web'), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
for _ in range(50):
if port_up(PORT): return proc
time.sleep(0.1)
proc.terminate(); sys.exit(f"could not start http.server on :{PORT}")
# JS: enter is done via DBG; this measures every interior fig after injecting browsers at browse points.
MEASURE_JS = r"""
(shopId) => {
const P = window.PROCITY, THREE = P.THREE, im = P.interiorMode, v = new THREE.Vector3();
const cur = im.current;
if (!cur || P.mode !== 'interior') return { err: 'not in interior' };
// occupancy is 0 right after boot; inject a browser rig at each browse point to exercise the
// browse spawn path (same spawnRig->buildFigure the shell uses when patronage fills a shop).
const bps = cur.browsePoints || [];
bps.slice(0, 3).forEach((pt, i) => {
try { im.keepers.spawn(cur.group, { x: pt.x, z: pt.z, ry: pt.ry, shopId: shopId, browse: true,
seedKey: 'qa#' + i, pedIndex: (i * 7) % 19 }); } catch (e) {}
});
const H = +cur.dims.H;
const figs = im.keepers.keepers.map(k => {
const a = k.actor; let crown = 0, foot = Infinity;
a.fig.updateWorldMatrix(true, true);
(a.inner || a.fig).traverse(o => { if (o.isBone) { o.getWorldPosition(v);
if (/head/i.test(o.name)) crown = Math.max(crown, v.y); foot = Math.min(foot, v.y); } });
return { kind: k.kind, browse: !!k.browse, crown: +crown.toFixed(3), foot: +foot.toFixed(3),
stature: +(crown - foot).toFixed(3), // [R13 debt #6] feet→crown span (platform-independent)
seated: !!(a.fig && a.fig.userData && a.fig.userData.procitySeated) }; // [R16 ledger #3] seated-fig tag
});
return { H: +H.toFixed(2), figs };
}
"""
def main():
try:
from playwright.sync_api import sync_playwright
except ImportError:
sys.exit("playwright not installed — python3 -m venv tools/.venv && tools/.venv/bin/pip install playwright && tools/.venv/bin/playwright install chromium")
print(f"\033[1mINTERIOR FIGURE-SCALE GATE\033[0m (seed {SEED}, band [{LO},{HI}] m, no giants)")
srv = ensure_server()
total_figs = 0
shops_checked = 0
try:
with sync_playwright() as p:
b = p.chromium.launch()
pg = b.new_page(viewport={'width': 1280, 'height': 720})
pg.goto(f'{HOST}/index.html?seed={SEED}&dbg=1')
pg.wait_for_function("window.DBG && window.DBG.ready === true", timeout=25000)
pg.evaluate("() => { const o=document.getElementById('pc-start'); if(o) o.style.display='none'; }")
try:
pg.wait_for_function("() => window.PROCITY && window.PROCITY.fleet && window.PROCITY.fleet.ready", timeout=15000)
except Exception:
sys.exit("fleet never became ready — cannot test rig scale")
pg.evaluate("() => window.DBG.setSegment(2)") # midday: shops open
for t in SAMPLE_TYPES:
sid = pg.evaluate("""(t) => {
const P=window.PROCITY;
const s=(P.plan.shops||[]).find(x=>x.type===t && P.isOpen(x));
if(!s) return null; window.DBG.enterShop(s.id); return {id:s.id, name:s.name};
}""", t)
if not sid:
continue
pg.wait_for_timeout(400) # interior build + keeper/browser rigs settle
res = pg.evaluate(MEASURE_JS, sid['id'])
if isinstance(res, dict) and res.get('err'):
FAIL(f"{sid['name']} ({t}): {res['err']}")
pg.evaluate("() => window.DBG.exitShop && window.DBG.exitShop()"); pg.wait_for_timeout(150)
continue
H, figs = res['H'], res['figs']
shops_checked += 1
bad = []
for f in figs:
total_figs += 1
lo, hi = (SEAT_LO, SEAT_HI) if f.get('seated') else (LO, HI) # [R16 #3] seated exemption
if not (lo <= f['stature'] <= hi): bad.append(f"stature {f['stature']}m out of [{lo},{hi}]{' seated' if f.get('seated') else ''}")
elif not (f['crown'] < H): bad.append(f"crown {f['crown']}m >= ceiling {H}m")
elif abs(f['foot']) >= FOOT_EPS: bad.append(f"feet at {f['foot']}m (not planted)")
if bad:
FAIL(f"{sid['name']} ({t}, H={H}m): " + "; ".join(bad))
else:
stats = ", ".join(f"{f['stature']:.2f}" for f in figs)
OK(f"{sid['name']} ({t}, H={H}m): {len(figs)} figs statures=[{stats}] all human-sized")
pg.evaluate("() => window.DBG.exitShop && window.DBG.exitShop()")
pg.wait_for_timeout(150)
# ── [R36 w2] the five DJ bodies, spawned by name (E's flag (a)) ──────────
dj = pg.evaluate(r"""(DJ) => {
const P = window.PROCITY, D = window.DBG, THREE = P.THREE, v = new THREE.Vector3();
const s = (P.plan.shops || []).find(x => x.type === 'record' && P.isOpen(x));
if (!s) return { err: 'no open record shop' };
D.enterShop(s.id);
const im = P.interiorMode, cur = im.current;
if (!cur) return { err: 'interior did not build' };
const bps = cur.browsePoints || [];
const H = +cur.dims.H, fleet = P.fleet, figs = [];
DJ.forEach((name, i) => {
const idx = fleet.all.findIndex(r => r.pedName === name);
if (idx < 0) { figs.push({ name, missing: true }); return; }
// native bind span — the number E flagged (~0.950.99 units vs legacy ~173180)
const sc = fleet.all[idx].scene; sc.updateWorldMatrix(true, true);
let bh = 0, bm = Infinity;
sc.traverse(o => { if (o.isBone) { o.getWorldPosition(v);
if (/head/i.test(o.name)) bh = Math.max(bh, v.y); bm = Math.min(bm, v.y); } });
const pt = bps[i % Math.max(1, bps.length)] || { x: 0, z: 0, ry: 0 };
const k = im.keepers.spawn(cur.group, { x: pt.x, z: pt.z, ry: pt.ry, shopId: s.id,
browse: true, seedKey: 'qa-dj#' + i, pedIndex: idx });
const a = k.actor; a.fig.updateWorldMatrix(true, true);
let crown = 0, foot = Infinity;
(a.inner || a.fig).traverse(o => { if (o.isBone) { o.getWorldPosition(v);
if (/head/i.test(o.name)) crown = Math.max(crown, v.y); foot = Math.min(foot, v.y); } });
figs.push({ name, idx, kind: k.kind, usedPed: fleet.all[idx].pedName,
nativeSpan: +(bh - bm).toFixed(3), stature: +(crown - foot).toFixed(3),
foot: +foot.toFixed(3), crown: +crown.toFixed(3) });
im.keepers.remove(k);
});
D.exitShop();
return { shop: s.name, H, figs };
}""", DJ_BODIES)
if dj.get('err'):
FAIL(f"DJ-body leg (R36): {dj['err']} — the five trellis bodies went unmeasured")
else:
bad = []
for f in dj['figs']:
if f.get('missing'): bad.append(f"{f['name']} NOT IN FLEET"); continue
total_figs += 1
if f['kind'] != 'rig' or f['usedPed'] != f['name']:
bad.append(f"{f['name']} spawned as {f['kind']}/{f.get('usedPed')} — subject not exercised")
elif not (LO <= f['stature'] <= HI): bad.append(f"{f['name']} stature {f['stature']}m out of [{LO},{HI}] (bind span {f['nativeSpan']}u)")
elif not (f['crown'] < dj['H']): bad.append(f"{f['name']} crown {f['crown']}m >= ceiling {dj['H']}m")
elif abs(f['foot']) >= FOOT_EPS: bad.append(f"{f['name']} feet at {f['foot']}m (not planted)")
if bad:
FAIL(f"DJ bodies (R36, {dj['shop']}): " + "; ".join(bad))
else:
det = ", ".join(f"{f['name']} {f['nativeSpan']}u→{f['stature']:.2f}m" for f in dj['figs'])
OK(f"DJ bodies (R36, {dj['shop']}): all 5 spawned as rigs, human-sized — rigs.js span-normalise absorbs the ~1-unit bind [{det}]")
b.close()
finally:
if srv: srv.terminate()
print()
if shops_checked == 0:
sys.exit("\033[31m✗ no shops could be entered — gate inconclusive\033[0m")
if fails:
print(f"\033[31m● {len(fails)} FAIL\033[0m — interior giant(s) detected across {shops_checked} shop(s), {total_figs} figs.")
sys.exit(1)
print(f"\033[32m● PASS\033[0m — {total_figs} interior figs across {shops_checked} shops all human-sized [{LO},{HI}] m and under-ceiling.")
sys.exit(0)
if __name__ == '__main__':
main()