PROCITY/tools/flags_check.py

2098 lines
128 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 F — v2 flag enforcement harness (round-6 F1).
The machine version of what the R5 review did by hand. Three things, headless, <2 min:
1. FLAGS-OFF REGRESSION — the enforcement arm of the prime law. Boot with no flags → the plan
fingerprint must equal the golden 0x5f76e76, the settled street view must stay within the v1
budget (≤300 draws / ≤200k tris) AND within tolerance of the v1.1 snapshot, every v2 subsystem
must report inactive, and there must be ZERO console errors. A v2 flag leaking into the default
boot trips this.
2. PER-FLAG SMOKE — for each landed flag (winmap, dig, roster, +plansrc when A lands): boot flag-on,
cross ≥2 chunks, enter one interior (dig: also open+close a riffle), 0 console errors.
3. ALL-ON COMBO (flag-composition law, decision #4): every landed flag on at once, same smoke.
Run: cd web && python3 -m http.server 8130 (or let this script spawn its own)
tools/.venv/bin/python tools/flags_check.py
Wired into tools/qa.sh as a WARN-level gate this round (strict in round 7). Exit 0 = all pass;
non-zero = a check failed (qa.sh reports it as a warning this round).
"""
import sys, json, time, socket, subprocess, pathlib, os, urllib.request, urllib.error
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/python -m playwright install chromium")
ROOT = pathlib.Path(__file__).resolve().parent.parent
PORT = 8130
HOST = f'http://127.0.0.1:{PORT}'
SEED = 20261990
GOLDEN_HASH = 0x5f76e76 # [R27w5] AMENDED. Was 0x3fa36874 "forever" since R16 — and that town
# was a row of BACK WALLS since v1 (A measured 0 toward / 681 away;
# walked behind them and found NUMBAT PLAYTHINGS facing the dirt).
# John's ruling: the covenant is anti-drift machinery, NOT a defect
# preservative. It did its job — it never drifted; it was wrong from
# the first commit, and no hash can catch what was never right.
GIG_GOLDEN = 0xec7a2d39 # [R27w5] re-pinned with the facade fix (was 0xb1d48ea1)
# flags-off DBG.shot('street_noon') snapshot. RE-PINNED at the R7 roster flip (prime-law amendment):
# old (v1.1, roster-off): 163 draws, 19000 tris
# new (R7, roster default-on): 164 draws, ~66000 tris
# Draws barely move — E1's ped sub-mesh merge makes each near-rig ~1 draw, which is exactly what let
# the roster flip to full density. Tris jump is the streamed crowd (each near-rig ~2.4k tris); it is
# inherently variable frame-to-frame (how many rigs are near at sample time), so the drift WARN below
# is DRAWS-ONLY — tris is guarded strictly by the STREET_TRIS_MAX hard cap, not a soft snapshot.
BASE_DRAWS, BASE_TRIS = 164, 66000
STREET_DRAWS_MAX, STREET_TRIS_MAX = 300, 200_000
KNOWN_FLAGS = ['winmap=1', 'dig=1', 'roster=stream'] # plansrc=osm auto-appended if Lane A landed it
# [R12] gig-night interior budget (ROUND12 law: ≤350 draws incl. the crowd — a room with a band and an
# audience in it is allowed more than a shop, which is what the ≤300 street/room numbers were tuned for).
GIG_DRAWS_MAX = 350
# `cover` is seeded per gig, so which door-charge path the default seed exercises is luck. Pin one seed
# per half of John's "free AND paid" ruling: 20261990's tonight is $10 (Screaming Utes at The Thornbury
# Hotel), 1234's is free (Dazza & The Dropbears at The Imperial). Re-pin if Lane A re-seeds the gig stream.
FREE_GIG_SEED = 1234
fails, warns = [], []
def FAIL(m): fails.append(m); print(f" \033[31m✗ FAIL\033[0m {m}")
def WARN(m): warns.append(m); print(f" \033[33m! WARN\033[0m {m}")
def OK(m): print(f" \033[32m✓\033[0m {m}")
def head(m): print(f"\n\033[1m{m}\033[0m")
def stature_ok(f):
# [R16 ledger #3] seated figs (the drummer — fig.userData.procitySeated) pass the no-giants floor at a
# SEATED band [0.9,1.5] m; standing figs stay [1.4,2.0]. A seated human fails the standing floor by design.
lo, hi = (0.9, 1.5) if f.get('seated') else (1.4, 2.0)
return lo <= f['stature'] <= hi
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 # reuse a running dev server
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}")
# ── page helpers ─────────────────────────────────────────────────────────────
def new_page(p):
b = p.chromium.launch()
pg = b.new_page(viewport={'width': 1280, 'height': 720})
errs = []
pg.on('console', lambda m: errs.append(m.text) if m.type == 'error' else None)
pg.on('pageerror', lambda e: errs.append(str(e)))
return b, pg, errs
def boot(pg, query):
pg.goto(f'{HOST}/index.html?seed={SEED}&dbg=1' + (('&' + query) if query else ''))
pg.wait_for_function("window.DBG && window.DBG.ready === true", timeout=20000)
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=12000)
except Exception:
pass
def gigs_landed(p):
"""Boot ?gigs=1; landed iff Lane A's augmentation produced a schedule (else the flag is inert)."""
b, pg, _ = new_page(p)
try:
boot(pg, '')
return bool(pg.evaluate("() => !!(window.PROCITY.plan.gigs && window.PROCITY.plan.gigs.length)"))
except Exception:
return False
finally:
b.close()
def plansrc_landed(p):
"""Boot ?plansrc=osm; landed iff the shell exposes an 'osm' plan source (else the flag is ignored)."""
b, pg, _ = new_page(p)
try:
boot(pg, 'plansrc=osm')
src = pg.evaluate("() => (window.PROCITY.plan && (window.PROCITY.plan.source||window.PROCITY.plan.planSource)) "
"|| window.PROCITY.planSource || null")
return src == 'osm'
except Exception:
return False
finally:
b.close()
# ── the three checks ─────────────────────────────────────────────────────────
def classic_regression(p):
# [R16 — THE FLIP] The prime-law covenant MOVED from "flags off" to "?classic=1": the classic boot must
# stay byte-identical to the frozen v2 baseline FOREVER — synthetic fingerprint 0x5f76e76, zero gig
# layer, no opt-in flags, the R7 streamed roster (which IS the v2.0 baseline). Same assertions as the old
# flags-off gate, now booted with ?classic=1 (the four flipped flags are off, everything else v2-default).
head('CLASSIC REGRESSION (?classic=1 → the frozen v2 covenant)')
b, pg, errs = new_page(p)
try:
boot(pg, 'classic=1')
state = pg.evaluate("""() => {
const P=window.PROCITY, D=window.DBG;
D.shot('street_noon'); // fixed pose → comparable draws/tris
const i = D.info();
const c = P.citizens || {};
return { draws:i.drawCalls, tris:i.tris, mode:i.mode,
winmap: !!(P.winmap && P.winmap.active),
digActive: !!(P.interiorMode && P.interiorMode.digActive),
streamMode: !!c.streamMode, // R7: real roster-mode flag (was reading a nonexistent prop)
// [R12] the gig layer must be wholly absent flags-off: no state machine, no schedule
// in the plan, no pub converted, and none of Lane B's venue geometry in the scene.
gigState: !!P.gigs, planGigs: (P.plan.gigs||[]).length, posters: (P.plan.posters||[]).length,
pub: !!(P.plan.shops||[]).find(s => s.venue || s.type === 'pub'),
venueGeo: !!P.scene.getObjectByName('venue-presentation') };
}""")
# determinism anchor — run Lane A's selfcheck in-process (golden fingerprint)
r = subprocess.run(['node', 'web/js/citygen/selfcheck.js'], cwd=str(ROOT),
capture_output=True, text=True)
# [R27w5] The hardcoded literal that used to ride here (`or '3fa36874' in r.stdout`) is GONE —
# A caught it. It was dead the moment the covenant moved, and worse than dead: an OR-clause
# matching a bare string anywhere in stdout would go green off a sentence ABOUT the old covenant.
# The check derives from GOLDEN_HASH or it doesn't exist.
hash_ok = (f'{GOLDEN_HASH:#010x}' in r.stdout) or (hex(GOLDEN_HASH) in r.stdout)
# [R24] These are TWO assertions and they get two verdicts. This gate used to require
# `hash_ok and r.returncode == 0` and report BOTH failures with the fingerprint's message — so
# any unrelated red anywhere in Lane A's selfcheck (R24: 8 godverse-identity checks in a
# different town) screamed "determinism/prime-law broken", the loudest alarm we own, about a
# covenant that was provably intact (then 0x3fa36874, node-side, unmoved). A gate must name what it
# actually measured; F wrote this one, so F fixed it. The covenant is asserted on its own.
if hash_ok: OK(f'plan fingerprint == golden {hex(GOLDEN_HASH)} — the classic covenant holds')
else: FAIL(f'plan fingerprint != golden {hex(GOLDEN_HASH)} — determinism/prime-law broken')
if r.returncode == 0: OK('Lane A selfcheck green')
else:
tail = [l for l in r.stdout.splitlines() if l.strip().startswith('')][:3]
FAIL('Lane A selfcheck RED (separate from the covenant above): '
+ ('; '.join(t.strip() for t in tail) if tail else 'see selfcheck output'))
# winmap flips OFF under ?classic (it's default-on now); dig stays opt-in. The streamed roster IS the
# v2.0 baseline (R7 flip / prime-law amendment) — so it must be ON under ?classic.
if state['winmap'] or state['digActive']:
FAIL(f"a flipped/opt-in flag leaked into ?classic (winmap/dig): {state}")
else: OK('flipped + opt-in flags off under ?classic (winmap/dig)')
if state['streamMode']:
OK('R7 roster flip in effect — streamed roster ACTIVE under ?classic (the v2 baseline)')
else: FAIL('roster flip REGRESSED — ?classic is not streaming (expected streamMode:true)')
# [R12/R16] gig layer wholly absent under ?classic. Any of these present means the gig layer escaped
# the classic gate and the frozen v2 covenant is broken.
gig_leaks = [k for k in ('gigState', 'planGigs', 'posters', 'pub', 'venueGeo') if state[k]]
if not gig_leaks:
OK('v3 gig layer wholly absent under ?classic (no state machine, no plan.gigs/posters, no pub, no venue geometry)')
else:
FAIL(f"v3 gig layer LEAKED into ?classic: {[(k, state[k]) for k in gig_leaks]}")
if state['draws'] <= STREET_DRAWS_MAX and state['tris'] <= STREET_TRIS_MAX:
OK(f"?classic street view within budget (draws {state['draws']}{STREET_DRAWS_MAX}, tris {state['tris']}{STREET_TRIS_MAX})")
else: FAIL(f"?classic street view OVER budget: draws {state['draws']}, tris {state['tris']}")
# Drift WARN is DRAWS-ONLY — tris is crowd-variable post-flip (guarded by the hard cap above).
dd = abs(state['draws'] - BASE_DRAWS) / BASE_DRAWS
if dd <= 0.40:
OK(f"draws within 40% of re-pinned baseline ({state['draws']} vs {BASE_DRAWS}); tris {state['tris']} (crowd-variable, ≤cap)")
else:
WARN(f"draws drifted >40% from baseline ({state['draws']} vs {BASE_DRAWS}) — investigate")
if errs: FAIL(f"{len(errs)} console error(s) under ?classic; first: {errs[0][:140]}")
else: OK('0 console errors under ?classic')
# R7 escape hatch: ?roster=v1 must restore the fixed roster (streamMode:false), render, no errors.
b2, pg2, errs2 = new_page(p)
try:
boot(pg2, 'roster=v1')
esc = pg2.evaluate("() => ({ streamMode: !!(window.PROCITY.citizens||{}).streamMode, mode: window.DBG.info().mode })")
if not esc['streamMode'] and esc['mode'] == 'street':
OK('escape hatch ?roster=v1 → fixed roster (streamMode:false), renders')
else: FAIL(f"escape hatch ?roster=v1 broken: {esc}")
if errs2: FAIL(f"?roster=v1: {len(errs2)} console error(s); first: {errs2[0][:140]}")
finally:
b2.close()
finally:
b.close()
def default_boot_gate(p):
# [R16 — THE FLIP] The enforcement arm of the DEFAULT law: the no-flag boot is now the full experience —
# all four flags on, the gig layer present, deterministic to the gig golden. This is the number every
# budget/smoke now means (its inverse is classic_regression above).
head('DEFAULT-BOOT GATE (the flip → the town shows its best by default)')
b, pg, errs = new_page(p)
try:
boot(pg, '')
state = pg.evaluate("""() => {
const P = window.PROCITY, D = window.DBG;
D.setSegment(5); D.shot('venue_night'); // NIGHT at a venue → the busiest default view (gigs+pools+winmap+tram)
const i = D.info();
return { flags: P.flags,
gigState: !!P.gigs, planGigs: (P.plan.gigs||[]).length, posters: (P.plan.posters||[]).length,
pub: !!(P.plan.shops||[]).find(s => s.venue), venueGeo: !!P.scene.getObjectByName('venue-presentation'),
winmapActive: !!(P.winmap && P.winmap.active), tram: !!P.scene.getObjectByName('tram'),
draws: i.drawCalls, tris: i.tris };
}""")
f = state['flags'] or {}
if f.get('gigs') and f.get('weather') and f.get('winmap') and f.get('tram') and not f.get('classic'):
OK('default boot flips all four ON (gigs · weather · winmap · tram), classic off')
else:
FAIL(f"default boot flag intent wrong: {f}")
present = [k for k in ('gigState', 'planGigs', 'posters', 'pub', 'venueGeo') if state[k]]
if len(present) == 5 and state['tram']:
OK(f"the town is live: gig layer present ({state['planGigs']} gigs, {state['posters']} posters, venue geo) · tram up (winmap is a window shader — its own smoke)")
else:
FAIL(f"default boot not fully live: gig={present}, tram={state['tram']}")
# The default boot's plan IS the gig-augmented plan; selfcheck pins its fingerprint to the gig golden
# (GIG_GOLDEN) INTERNALLY and is ALL GREEN only if it matches. (selfcheck prints the base fingerprint
# in its summary but asserts the gig golden silently — so we gate on the green run, not a substring.)
r = subprocess.run(['node', 'web/js/citygen/selfcheck.js'], cwd=str(ROOT), capture_output=True, text=True)
if r.returncode == 0 and 'ALL GREEN' in r.stdout:
OK(f"default plan pins to the gig golden {hex(GIG_GOLDEN)} (selfcheck ALL GREEN — the gig-layer golden asserts pass)")
else:
FAIL(f"selfcheck not green — the gig golden {hex(GIG_GOLDEN)} or determinism broke: {r.stdout[-160:]}")
# budget on the DEFAULT boot at the busiest venue block (the number the budget law now means)
if state['draws'] <= STREET_DRAWS_MAX and state['tris'] <= STREET_TRIS_MAX:
OK(f"default-boot street budget @ venue block: {state['draws']} draws ≤ {STREET_DRAWS_MAX}, {state['tris']:,} tris ≤ {STREET_TRIS_MAX:,}")
else:
FAIL(f"default-boot street OVER budget @ venue block: {state['draws']} draws / {state['tris']:,} tris (Fable scope call — not silent-trim)")
if errs: FAIL(f"{len(errs)} console error(s) on the default boot; first: {errs[0][:140]}")
else: OK('0 console errors on the default boot')
finally:
b.close()
def smoke(p, label, query, is_combo=False):
head(f'SMOKE: {label} (?{query})')
b, pg, errs = new_page(p)
try:
boot(pg, query)
# cross ≥2 chunks by teleporting between separated street nodes
chunks = pg.evaluate("""() => {
const P=window.PROCITY, D=window.DBG, seen=new Set();
const ns = P.plan.streets.nodes;
for (const idx of [0, (ns.length/3|0), (2*ns.length/3|0), ns.length-1]) {
const n = ns[idx]; D.teleport(n.x, n.z, 0);
for (let k=0;k<10;k++) (P.citizens&&P.citizens.update&&P.citizens.update(0.05));
const c = D.info().chunk; if (c!=null) seen.add(String(c));
}
return seen.size;
}""")
if chunks >= 2: OK(f'crossed {chunks} chunks streaming (flag active, no crash)')
else: WARN(f'only {chunks} distinct chunks seen (teleport/stream probe)')
# enter one interior (open shop), exit
entered = pg.evaluate("""() => {
const P=window.PROCITY, D=window.DBG;
D.setSegment(2);
const s=(P.plan.shops||[]).find(x=>x.type==='record' && P.isOpen(x)) || (P.plan.shops||[]).find(x=>P.isOpen(x));
if(!s) return {ok:false};
D.enterShop(s.id);
const inMode = D.info().mode==='interior';
return {ok:inMode, shop:s.name};
}""")
if entered.get('ok'): OK(f"entered interior '{entered.get('shop')}' (mode=interior)")
else: FAIL('could not enter an interior')
# dig-specific: open one riffle + close, leak-free
if 'dig=1' in query:
dug = pg.evaluate("""() => {
const P=window.PROCITY, THREE=P.THREE, room=P.interiorMode.current;
if(!room) return {ok:false, why:'no room'};
const bins=[]; room.group.traverse(o=>{ if(o.userData&&o.userData.kind==='bin') bins.push(o); });
if(!bins.length) return {ok:false, why:'no bins'};
const bp=new THREE.Vector3(); bins[0].getWorldPosition(bp);
P.camera.position.set(bp.x,1.6,bp.z+1.8); P.camera.lookAt(bp.x,bp.y+0.4,bp.z); P.camera.updateMatrixWorld();
window.dispatchEvent(new KeyboardEvent('keydown',{code:'KeyE'}));
const opened = P.interiorMode.digActive;
const btn=document.querySelector('.pcdg-out'); if(btn) btn.click();
return { ok: opened, closed: !P.interiorMode.digActive };
}""")
pg.wait_for_timeout(300)
if dug.get('ok') and dug.get('closed'): OK('dig: riffle opened (E on bin) + closed (WALK OUT)')
elif dug.get('ok'): WARN(f"dig: opened but close not confirmed ({dug})")
else: FAIL(f"dig: riffle did not open ({dug.get('why','?')})")
pg.evaluate("() => window.DBG.exitShop && window.DBG.exitShop()")
if errs: FAIL(f"{label}: {len(errs)} console error(s); first: {errs[0][:140]}")
else: OK(f'{label}: 0 console errors')
finally:
b.close()
def interior_budget(p):
"""Decision #2: interior draw budget is law — ≤350 draws worst room. Sweep every shop type
(dig on, so this also confirms batching kept ?dig=1 riffling). C enforces via its sweep; F pins it."""
head('INTERIOR DRAW BUDGET (≤350 draws/room law — decision #2)')
b, pg, errs = new_page(p)
try:
boot(pg, 'dig=1')
pg.evaluate("() => window.DBG.setSegment(2)") # midday → shops open
types = ['record', 'opshop', 'toy', 'book', 'video', 'pawn', 'milkbar', 'dept', 'stall']
rows, seen = [], set()
for t in types:
picked = pg.evaluate("""(t) => {
const P=window.PROCITY, D=window.DBG;
const s=(P.plan.shops||[]).find(x=>x.type===t && P.isOpen(x));
if(!s) return null; D.enterShop(s.id); return {id:s.id, name:s.name};
}""", t)
if not picked:
continue
pg.wait_for_timeout(500) # let async GLB fittings load → settled worst-case draws
info = pg.evaluate("() => { const i=window.DBG.info(); return {draws:i.drawCalls, tris:i.tris, mode:i.mode}; }")
rows.append({'type': t, 'shop': picked['name'], **info})
pg.evaluate("() => window.DBG.exitShop && window.DBG.exitShop()")
pg.wait_for_timeout(150)
over = [r for r in rows if r['draws'] > 350]
for r in over:
FAIL(f"interior {r['type']} '{r['shop']}': {r['draws']} draws > 350")
if rows and not over:
worst = max(rows, key=lambda r: r['draws'])
OK(f"all {len(rows)} room types ≤350 draws (worst: {worst['type']} {worst['draws']}, tris {worst['tris']})")
elif not rows:
WARN('no interiors sampled (no open shops of the swept types)')
if errs:
FAIL(f"interior sweep: {len(errs)} console error(s); first: {errs[0][:140]}")
finally:
b.close()
def _enter_type(pg, t, extra=''):
"""Enter an open shop of type `t`; returns the shop name or {err}."""
return pg.evaluate("""(t) => {
const P=window.PROCITY, D=window.DBG; D.setSegment(2);
const s=(P.plan.shops||[]).find(x=>x.type===t && P.isOpen(x));
if(!s) return {err:'no open '+t+' shop'};
D.enterShop(s.id); return { shop: s.name };
}""", t)
def _pack_ready(pg, t):
try:
pg.wait_for_function("async () => { const m = await import('./js/interiors/interiors.js');"
f" return !!(m.getStockPack && m.getStockPack('{t}')); }}", timeout=12000)
return True
except Exception:
return False
def smoke_stock(p):
"""GRADUATED STRICT (R8): ?stock=real → real GODVERSE sleeves (batched userData.isStock atlas meshes)."""
head('SMOKE: stock=real (?stock=real — real record sleeves; STRICT r8)')
b, pg, errs = new_page(p)
try:
boot(pg, 'stock=real&dig=1')
ready = _pack_ready(pg, 'record')
r = _enter_type(pg, 'record')
if r.get('err'): FAIL(f"stock=real: {r['err']}"); return
n = pg.evaluate("() => { let n=0; window.PROCITY.interiorMode.current.group.traverse(o=>{ if(o.userData&&o.userData.isStock&&o.material&&o.material.map) n++; }); return n; }")
if not ready: FAIL(f"stock=real: record pack didn't preload in 12s (E ships it) — got {n} isStock meshes")
elif n > 0: OK(f"stock=real: {n} real atlas sleeve mesh(es) in '{r['shop']}' — non-parody, batched")
else: FAIL(f"stock=real: pack loaded but 0 isStock atlas meshes in '{r['shop']}'")
if errs: FAIL(f"stock=real: {len(errs)} console error(s); first: {errs[0][:140]}")
finally:
b.close()
def smoke_weather(p):
"""GRADUATED STRICT (R8): ?weather=rain → rain THREE.Points layer + PROCITY.weather.state=='rain'."""
head('SMOKE: weather (?weather=rain — rain particles + wet ground; STRICT r8)')
b, pg, errs = new_page(p)
try:
boot(pg, 'weather=rain')
st = pg.evaluate("() => { const P=window.PROCITY; return { state: P.weather&&P.weather.state, "
"intensity: P.weather&&P.weather.intensity, rainPts: !!P.scene.getObjectByProperty('type','Points') }; }")
if st['state'] == 'rain' and st['rainPts']:
OK(f"weather=rain: rain Points present + PROCITY.weather.state=='rain' (intensity {st['intensity']})")
else: FAIL(f"weather=rain broken: {st}")
if errs: FAIL(f"weather=rain: {len(errs)} console error(s); first: {errs[0][:140]}")
finally:
b.close()
def smoke_buy(p):
"""NEW (warn): buy-v0 — open dig, pull + BUY a sleeve, assert wallet cash dropped + item banked."""
head('SMOKE: buy-v0 (dig BUY → session wallet; STRICT r9)')
b, pg, errs = new_page(p)
try:
boot(pg, 'dig=1')
setup = pg.evaluate("""() => {
const P=window.PROCITY, D=window.DBG; D.setSegment(2);
const rec=(P.plan.shops||[]).find(s=>s.type==='record' && P.isOpen(s));
if(!rec) return {err:'no record shop'};
D.enterShop(rec.id);
const bins=[]; P.interiorMode.current.group.traverse(o=>{if(o.userData&&o.userData.kind==='bin')bins.push(o)});
if(!bins.length) return {err:'no bins'};
const bp=new P.THREE.Vector3(); bins[0].getWorldPosition(bp);
P.camera.position.set(bp.x,1.6,bp.z+1.4); P.camera.lookAt(bp.x,bp.y+0.4,bp.z); P.camera.updateMatrixWorld();
return { cash0: P.wallet.cash(), count0: P.wallet.count() };
}""")
if setup.get('err'): FAIL(f"buy-v0: {setup['err']}"); return
pg.evaluate("() => window.dispatchEvent(new KeyboardEvent('keydown',{code:'KeyE'}))") # open dig
pg.wait_for_timeout(600)
# pull the front sleeve (canvas click, no drag → dig.pull) then click its BUY IT button
pg.evaluate("""() => { const c=window.PROCITY.renderer.domElement;
c.dispatchEvent(new PointerEvent('pointerdown',{clientX:640,clientY:360,bubbles:true}));
c.dispatchEvent(new PointerEvent('pointerup',{clientX:640,clientY:360,bubbles:true})); }""")
pg.wait_for_timeout(400)
res = pg.evaluate("""() => {
const w=window.PROCITY.wallet, cash0=w.cash(), count0=w.count();
const btn=document.querySelector('.pcdg-panel button');
if(!btn) return {err:'no BUY panel (pull failed)'};
if(btn.disabled) return {err:'BUY disabled (broke?)'};
btn.click();
return { cash0, count0, cash1:w.cash(), count1:w.count() };
}""")
if res.get('err'): FAIL(f"buy-v0: {res['err']}")
elif res['cash1'] < res['cash0'] and res['count1'] == res['count0'] + 1:
OK(f"buy-v0: bought 1 — cash {res['cash0']}{res['cash1']}, bag {res['count0']}{res['count1']}")
else: FAIL(f"buy-v0: unexpected wallet delta {res}")
if errs: FAIL(f"buy-v0: {len(errs)} console error(s); first: {errs[0][:140]}")
finally:
b.close()
def smoke_patronage(p):
"""NEW (warn): peds duck into open shops — assert ≥1 ped parks at a shop door over N seconds."""
head('SMOKE: patronage (peds visit open shops; STRICT r9)')
b, pg, errs = new_page(p)
try:
boot(pg, '') # default: streamed roster + patronage on
# advance the sim ~12s of ticks at a busy midday node; count peds flagged as inside/at-door
res = pg.evaluate("""() => {
const P=window.PROCITY, D=window.DBG; D.setSegment(2);
const plan=P.plan, deg={}; for(const e of plan.streets.edges){deg[e.a]=(deg[e.a]||0)+1;deg[e.b]=(deg[e.b]||0)+1;}
let best=null,bd=0; for(const n of plan.streets.nodes){const d=deg[n.id]||0;if(d>bd){bd=d;best=n;}}
D.teleport(best.x,best.z,0); P.citizens.setPopulation(200);
let everInside=0; const seen=new Set();
for(let i=0;i<360;i++){ P.citizens.update(0.05);
const list=P.citizens._activeList||[];
for(const c of list){ if(c && (c.patronInside||c.inside||c.patronTarget) && !seen.has(c.id)){ seen.add(c.id); everInside++; } }
}
return { active:(P.citizens._activeList||[]).length, everVisited: everInside, patronOn: P.citizens.patronageOn };
}""")
if not res['patronOn']: FAIL("patronage: off (patronageOn=false) — expected default-on")
elif res['everVisited'] > 0: OK(f"patronage: {res['everVisited']} ped(s) headed to/into a shop door over ~18s (active {res['active']})")
else: FAIL(f"patronage: 0 peds visited a shop over ~18s (active {res['active']}) — check door points / timing")
if errs: FAIL(f"patronage: {len(errs)} console error(s); first: {errs[0][:140]}")
finally:
b.close()
def smoke_booktoy(p):
"""NEW (warn): book/toy packs — ?stock=real static shelf stock (real spine/box atlas meshes)."""
head('SMOKE: book/toy stock (?stock=real — real spines/boxes; STRICT r9)')
b, pg, errs = new_page(p)
try:
boot(pg, 'stock=real')
for t in ('book', 'toy'):
_pack_ready(pg, t)
r = _enter_type(pg, t)
if r.get('err'): FAIL(f"book/toy: {r['err']}"); continue
# R9: real static stock is EITHER an isStock atlas mesh (records/toys) OR a buyMesh shelf
# (book spines / toy boxes, C2 buy-anywhere). Parody stock has neither.
n = pg.evaluate("""() => { let n=0; window.PROCITY.interiorMode.current.group.traverse(o=>{
const u=o.userData||{}; if((u.isStock && o.material && o.material.map) || (u.buyMesh && u.buyItems && u.buyItems.length)) n++; }); return n; }""")
if n > 0: OK(f"{t} stock: {n} real stock mesh(es) in '{r['shop']}'")
else: FAIL(f"{t} stock: 0 isStock atlas meshes in '{r['shop']}' (pack missing → parody fallback?)")
pg.evaluate("() => window.DBG.exitShop && window.DBG.exitShop()")
pg.wait_for_timeout(150)
if errs: FAIL(f"book/toy: {len(errs)} console error(s); first: {errs[0][:140]}")
finally:
b.close()
def smoke_presence(p):
"""NEW (warn): interior presence — enter an OCCUPIED shop, assert browser rigs == min(occupancy,3),
dispose clean on exit, 0 errors (the C→D→F seam)."""
head('SMOKE: interior presence (browsers in occupied shops; new → warn-level)')
b, pg, errs = new_page(p)
try:
boot(pg, 'stock=real')
res = pg.evaluate("""() => {
const P=window.PROCITY, D=window.DBG; D.setSegment(2);
const plan=P.plan, deg={}; for(const e of plan.streets.edges){deg[e.a]=(deg[e.a]||0)+1;deg[e.b]=(deg[e.b]||0)+1;}
let best=null,bd=0; for(const n of plan.streets.nodes){const d=deg[n.id]||0;if(d>bd){bd=d;best=n;}}
D.teleport(best.x,best.z,0); P.citizens.setPopulation(200);
for(let i=0;i<400;i++) P.citizens.update(0.05);
let shop=null,occ=null;
for(const s of plan.shops){ const o=P.citizens.occupancyOf(s.id); if(o&&o.count>0){shop=s;occ=o;break;} }
if(!shop) return {err:'no occupied shop after patronage'};
D.enterShop(shop.id);
const kl=P.interiorMode.keepers.keepers||[];
const total=kl.length, pts=(P.interiorMode.current.browsePoints||[]).length;
return { shop:shop.name, occ:occ.count, expect:Math.min(occ.count,3), totalKeepers:total, browsePoints:pts };
}""")
if res.get('err'): WARN(f"presence: {res['err']}"); return
# total keepers = 1 counter + browsers; browsers should == min(occ,3)
browsers = res['totalKeepers'] - 1
if browsers == res['expect'] and res['browsePoints'] >= 1:
OK(f"presence: {browsers} browser(s) in '{res['shop']}' == min(occupancy {res['occ']},3); {res['browsePoints']} browse points")
else:
WARN(f"presence: browsers {browsers} != expect {res['expect']} (occ {res['occ']}, pts {res['browsePoints']}, keepers {res['totalKeepers']})")
clean = pg.evaluate("() => { window.DBG.exitShop(); return (window.PROCITY.interiorMode.keepers.keepers||[]).length; }")
if clean == 0: OK("presence: browsers disposed clean on exit (0 keepers)")
else: WARN(f"presence: {clean} keeper(s) leaked after exit")
if errs: WARN(f"presence: {len(errs)} console error(s); first: {errs[0][:140]}")
finally:
b.close()
def smoke_shelfbuy(p):
"""NEW (warn): buy-anywhere — E aimed at a book spine / toy box buys over the wallet (Lane C C2)."""
head('SMOKE: shelf-buy (?stock=real book/toy pull-and-buy; new → warn-level)')
b, pg, errs = new_page(p)
try:
boot(pg, 'stock=real')
_pack_ready(pg, 'book')
res = pg.evaluate("""() => {
const P=window.PROCITY, D=window.DBG; D.setSegment(2);
const bk=(P.plan.shops||[]).find(s=>s.type==='book' && P.isOpen(s));
if(!bk) return {err:'no book shop'};
D.enterShop(bk.id);
const meshes=[]; P.interiorMode.current.group.traverse(o=>{ if(o.userData&&o.userData.buyMesh&&o.userData.buyItems&&o.userData.buyItems.length) meshes.push(o); });
if(!meshes.length) return {err:'no buy meshes (pack not loaded?)'};
const m=meshes[0], it=m.userData.buyItems[0], wp=new P.THREE.Vector3();
m.localToWorld(wp.copy(it.center));
P.camera.position.set(wp.x,wp.y,wp.z+1.0); P.camera.lookAt(wp.x,wp.y,wp.z); P.camera.updateMatrixWorld();
const cash0=P.wallet.cash(), count0=P.wallet.count();
window.dispatchEvent(new KeyboardEvent('keydown',{code:'KeyE'}));
return { shop:bk.name, buyMeshes:meshes.length, cash0, count0, cash1:P.wallet.cash(), count1:P.wallet.count() };
}""")
if res.get('err'): WARN(f"shelf-buy: {res['err']}")
elif res['cash1'] < res['cash0'] and res['count1'] == res['count0'] + 1:
OK(f"shelf-buy: bought a book spine in '{res['shop']}' — cash {res['cash0']}{res['cash1']}, bag {res['count0']}{res['count1']} ({res['buyMeshes']} buy meshes)")
else: WARN(f"shelf-buy: no purchase — {res}")
if errs: WARN(f"shelf-buy: {len(errs)} console error(s); first: {errs[0][:140]}")
finally:
b.close()
def smoke_tram(p):
"""NEW (warn): ?tram=1 — Lane B tram loop. Auto-skips if not landed."""
b, pg, errs = new_page(p)
try:
boot(pg, 'tram=1')
present = pg.evaluate("() => !!window.PROCITY.scene.getObjectByName('tram')")
if not present:
print("· tram=1 not landed (no 'tram' in scene) — skipping (Lane B)")
return
head('SMOKE: tram (?tram=1; new → warn-level)')
OK("tram=1: 'tram' present in scene")
if errs: WARN(f"tram: {len(errs)} console error(s); first: {errs[0][:140]}")
finally:
b.close()
def smoke_audio(p):
"""R11 audio house-law (Lane F): silent-and-happy, nothing plays pre-gesture, ?mute=1 silences,
?noassets=1 fetches zero audio, and interior beds play + release AudioNodes across enter/exit.
Each arm uses a fresh Chromium context (standing law — a reused tab's module cache burned us in R10)."""
head('SMOKE: audio (R11 — silent-happy · pre-gesture silence · mute · noassets · leak)')
# 1) boot-with-audio: 0 console errors, and nothing plays before a gesture (ctx suspended → state.ready false)
b, pg, errs = new_page(p)
try:
boot(pg, '')
pg.wait_for_function("() => window.PROCITY && window.PROCITY.audio && window.PROCITY.audio.state", timeout=8000)
st = pg.evaluate("() => { const s=window.PROCITY.audio.state; return {ready:!!s.ready, muted:!!s.muted, mode:s.mode}; }")
if not st['ready']:
OK(f"pre-gesture: audio NOT unlocked (state.ready=false, mode={st['mode']}) — silent until first gesture")
else:
FAIL(f"audio unlocked before any user gesture (autoplay-policy violation): {st}")
if errs: FAIL(f"audio boot: {len(errs)} console error(s); first: {errs[0][:140]}")
else: OK("audio boot: 0 console errors (silent-and-happy)")
finally:
b.close()
# 2) ?mute=1 → engine reports muted
b, pg, errs = new_page(p)
try:
boot(pg, 'mute=1')
pg.wait_for_function("() => window.PROCITY && window.PROCITY.audio", timeout=8000)
st = pg.evaluate("() => ({muted:!!window.PROCITY.audio.state.muted, mode:window.PROCITY.audio.state.mode})")
if st['muted']: OK(f"?mute=1: engine muted (mode={st['mode']})")
else: FAIL(f"?mute=1 not honored: {st}")
if errs: FAIL(f"?mute=1: {len(errs)} console error(s)")
finally:
b.close()
# 3) ?noassets=1 → zero audio network fetches (house law: the noassets gate implies no audio)
b, pg, errs = new_page(p)
audio_reqs = []
pg.on('request', lambda r: audio_reqs.append(r.url)
if ('/assets/audio/' in r.url or r.url.endswith('.ogg') or r.url.endswith('.m4a')) else None)
try:
boot(pg, 'noassets=1')
pg.keyboard.press('Space') # a keyboard gesture unlocks the AudioContext WITHOUT a pointer-lock attempt (headless-safe) # a gesture, so the audio path is genuinely exercised
pg.evaluate("""() => { const P=window.PROCITY, D=window.DBG; D.setSegment(2);
const s=(P.plan.shops||[]).find(x=>P.isOpen(x)); if(s) D.enterShop(s.id); }""")
pg.wait_for_timeout(800)
man = pg.evaluate("() => window.PROCITY.audio.state.manifest")
if len(audio_reqs) == 0: OK(f"?noassets=1: 0 audio fetches (manifest={man})")
else: FAIL(f"?noassets=1: {len(audio_reqs)} audio fetch(es) leaked; first {audio_reqs[0]}")
if errs: FAIL(f"?noassets=1 audio: {len(errs)} console error(s)")
finally:
b.close()
# 4) interior beds play + enter/exit is AudioNode-leak-free (live BufferSources stay bounded, not O(N))
b = p.chromium.launch()
pg = b.new_page(viewport={'width': 1280, 'height': 720})
errs = []
pg.on('console', lambda m: errs.append(m.text) if m.type == 'error' else None)
pg.on('pageerror', lambda e: errs.append(str(e)))
pg.add_init_script("""
window.__live = new Set(); window.__made = 0;
const AC = window.AudioContext || window.webkitAudioContext;
if (AC) { const orig = AC.prototype.createBufferSource;
AC.prototype.createBufferSource = function () { const s = orig.call(this);
window.__made++; window.__live.add(s);
const ostop = s.stop.bind(s); s.stop = function (w) { window.__live.delete(s); return ostop(w); };
s.addEventListener('ended', () => window.__live.delete(s)); return s; }; }
""")
try:
boot(pg, '')
pg.keyboard.press('Space') # a keyboard gesture unlocks the AudioContext WITHOUT a pointer-lock attempt (headless-safe) # user gesture → unlock the AudioContext
pg.wait_for_timeout(250)
played = pg.evaluate("""async () => {
const P=window.PROCITY, D=window.DBG; D.setSegment(2);
const types=['record','milkbar','video','opshop','book','toy'];
let entered=0, withMusic=0;
for (let rep=0; rep<2; rep++) for (const t of types) {
const s=(P.plan.shops||[]).find(x=>x.type===t && P.isOpen(x)); if(!s) continue;
D.enterShop(s.id); entered++;
const a = P.interiorMode.current && P.interiorMode.current.audio;
if (a && a.musicKey) withMusic++;
await new Promise(r=>setTimeout(r,120));
D.exitShop && D.exitShop(); await new Promise(r=>setTimeout(r,120));
}
return { entered, withMusic, ready:!!P.audio.state.ready };
}""")
pg.wait_for_timeout(600)
live = pg.evaluate("() => window.__live.size")
made = pg.evaluate("() => window.__made")
if not played['ready']:
WARN(f"audio interior: ctx never unlocked in headless — leak check inconclusive ({played}, live {live}/made {made})")
elif played['entered'] >= 4 and live <= 12:
OK(f"audio interior: {played['entered']} enter/exit, {played['withMusic']} with a music bed; live AudioNodes {live} bounded (made {made}) — leak-free")
else:
FAIL(f"audio interior: {live} live AudioNodes after {played['entered']} enter/exit (made {made}) — possible leak")
if errs: FAIL(f"audio interior: {len(errs)} console error(s); first: {errs[0][:140]}")
else: OK("audio interior: 0 console errors across enter/exit cycles")
finally:
b.close()
def smoke_gigs(p):
"""R13 v3.0-beta THE DISTRICT gig layer (Lane F). The ROUND13 §Lane F.6 gates — district determinism ·
a 4-piece band · per-venue crowd ≤ watchPoints · EVERY venue's gigKey resolves to a real manifest bed
(debt #1, now across 3 genres) · gig-night interior draw budget · street budget at the busiest venue
block · no-giants (STATURE) on the stage/crowd rigs · queue ≤ cap and drains by 'done' (pending Lane D)
· ?noassets=1 still silent-and-fine with ?gigs=1 — plus John's cover ruling, now PER VENUE (paid · free
· broke-knockback · no re-charge on re-entry · a night at two venues is two covers).
Seeds are pinned because `cover` is seeded: 20261990's tonight is a $10 night (Screaming Utes at The
Thornbury Hotel) and 1234's is free (Dazza & The Dropbears at The Imperial), so BOTH halves of "free
AND paid" are covered deterministically rather than whichever the default seed happened to roll."""
head('SMOKE: gigs (R13 v3.0-beta district — determinism · 4-piece · per-venue crowd/cover · gigKeys · budgets · queue · noassets)')
if not gigs_landed(p):
print('· gigs=1 not landed (no plan.gigs) — skipping (Lane A)')
return
# 1) schedule determinism — two independent boots (fresh contexts) → byte-equal gigs + posters
sigs = []
for _ in range(2):
b, pg, errs = new_page(p)
try:
boot(pg, '')
sigs.append(pg.evaluate("() => JSON.stringify({g: window.PROCITY.plan.gigs, p: window.PROCITY.plan.posters})"))
if errs: FAIL(f"gigs boot: {len(errs)} console error(s); first: {errs[0][:140]}")
finally:
b.close()
if sigs[0] == sigs[1]:
n = len(json.loads(sigs[0])['g'])
OK(f"gig schedule deterministic: two runs byte-equal ({n} nights + posters)")
else:
FAIL('gig schedule NOT deterministic across two runs (byte-diff in plan.gigs/plan.posters)')
# 2) the gig night itself: band · crowd · dance mix · budget · the live bed · no giants
b, pg, errs = new_page(p)
try:
boot(pg, '')
pg.keyboard.press('Space') # gesture → AudioContext unlock (headless-safe, no pointer-lock)
night = pg.evaluate("""async () => {
const P = window.PROCITY, D = window.DBG, THREE = P.THREE, v = new THREE.Vector3();
const id = P.gigs.venueShopIds[0]; // R15: no scalar alias — the primary venue by id
D.setSegment(5); // NIGHT — the gig is on
const state = P.gigs.stateOf(id);
D.enterShop(id);
await new Promise(r => setTimeout(r, 500));
const room = P.interiorMode.current;
if (!room) return { entered: false, state };
const crew = P.interiorMode.crew, info = P.interiorMode.crewInfo;
const mem = crew ? crew.members : [];
// STATURE, not world crown: the band stands on Lane C's stage deck (deckY ~0.32 m), so a
// perfectly human 1.73 m guitarist has a world crown of 2.05 m and a naive [1.4,2.0] world-Y
// check fails a correct band (it did, on the first R12 verify run). Measure crown MINUS the
// figure's own base y — that is what "human-sized" actually means — and keep the separate
// "never clips the ceiling" assert on the world crown, which is the real R10 failure mode.
const figs = mem.map(m => {
let crown = 0;
m.actor.fig.updateWorldMatrix(true, true);
(m.actor.inner || m.actor.fig).traverse(o => { if (o.isBone && /head/i.test(o.name)) {
o.getWorldPosition(v); crown = Math.max(crown, v.y); } });
return { part: m.part, kind: m.kind, dance: !!m.dance,
crown: +crown.toFixed(3), stature: +(crown - (m.base.y || 0)).toFixed(3),
seated: !!(m.actor.fig.userData && m.actor.fig.userData.procitySeated) }; // [R16 #3]
});
// Force one clean interior frame rather than trusting whatever the last rAF left in
// renderer.info — a throttled/backgrounded tab otherwise reports the last STREET frame
// (composer + bloom), which reads as a wildly over-budget room. Bit us on the first verify.
P.renderer.info.reset();
P.renderer.render(P.interiorMode.scene, P.camera);
return {
entered: true, state, H: +room.dims.H.toFixed(2),
watchPoints: room.watchPoints.length, stage: !!room.stage,
band: info ? info.band : 0, crowd: info ? info.crowd : 0,
dancers: mem.filter(m => m.part === 'crowd' && m.dance).length,
gigKey: room.audio.gigKey || null, musicKey: room.audio.musicKey || null,
draws: P.renderer.info.render.calls, tris: P.renderer.info.render.triangles, figs,
};
}""")
if not night.get('entered'):
FAIL(f"gig night: could not enter the venue at NIGHT ({night})")
else:
if night['state'] == 'on': OK(f"gig state machine: NIGHT → 'on' (venue {night['H']}m room)")
else: FAIL(f"gig state machine: NIGHT should be 'on', got '{night['state']}'")
# band is a 4-piece (R13 debt #3: front-line trio + a drummer on C's riser)
if night['band'] == 4: OK('band: 4-piece on the stage (front-line trio + drummer)')
else: FAIL(f"band: expected a 4-piece (trio + drummer), got {night['band']}")
# crowd cap ≤ watchPoints
if night['crowd'] <= night['watchPoints'] and night['crowd'] > 0:
OK(f"crowd: {night['crowd']} at {night['watchPoints']} watch points (≤ cap)")
else:
FAIL(f"crowd {night['crowd']} vs {night['watchPoints']} watch points (must be >0 and ≤ cap)")
# John's "a bit of both": the crowd must not be all-standing or all-dancing
d, c = night['dancers'], night['crowd']
if c and 0 < d < c: OK(f"crowd mix: {d} dancing / {c - d} standing — 'a bit of both'")
else: FAIL(f"crowd is not mixed: {d} dancing of {c} (John's ruling: some of both)")
# the live bed actually resolves (the C↔E gigKey seam F bridges — see LANE_F_NOTES §12)
if night['gigKey']: OK(f"live bed: room.audio.gigKey = {night['gigKey']} (F resolves → manifest key)")
else: FAIL('live bed: no room.audio.gigKey on a gig-on venue (Lane C opts.gig seam)')
# gig-night interior draw budget (ROUND12: ≤350 incl. the crowd)
if night['draws'] <= GIG_DRAWS_MAX:
OK(f"gig-night interior budget: {night['draws']} draws ≤ {GIG_DRAWS_MAX} (tris {night['tris']:,})")
else:
FAIL(f"gig-night interior budget: {night['draws']} draws > {GIG_DRAWS_MAX} (tris {night['tris']:,})")
# no giants on the stage/crowd rigs (R10 gate, inherited by the gig path)
rigs = [f for f in night['figs'] if f['kind'] == 'rig']
bad = [f for f in rigs if not stature_ok(f) or f['crown'] >= night['H']] # [R16 #3] seated exemption
if not rigs:
WARN(f"no-giants: crew spawned as placeholders (no fleet) — rig check n/a ({len(night['figs'])} figs)")
elif bad:
FAIL(f"no-giants: {len(bad)} gig rig(s) out of [1.4,2.0]m stature or through the {night['H']}m ceiling: {bad[:3]}")
else:
lo = min(f['stature'] for f in rigs); hi = max(f['stature'] for f in rigs)
hc = max(f['crown'] for f in rigs)
OK(f"no-giants: {len(rigs)} band+crowd rigs human-sized ({lo:.2f}{hi:.2f} m stature; "
f"tallest crown {hc:.2f} m under the {night['H']} m ceiling)")
if errs: FAIL(f"gig night: {len(errs)} console error(s); first: {errs[0][:140]}")
else: OK('gig night: 0 console errors')
finally:
b.close()
# 2b) THE DISTRICT (R13): every playing venue fires — its live bed resolves to a REAL manifest bar for
# its genre (debt #1, now across pub/band_room/rsl), its crowd is capped at its OWN watch points, and it
# is a 4-piece. Plus the queue ≤ cap / drains-by-done probe (WARN-skips until Lane D wires the seam).
music_keys = set()
try:
mani = json.loads((ROOT / 'web' / 'assets' / 'manifest.json').read_text())
music_keys = set((mani.get('audio', {}).get('music', {}) or {}).keys())
except Exception as e:
WARN(f"district: could not read manifest music keys ({e}) — gigKey-resolves check is JS-only")
b, pg, errs = new_page(p)
try:
boot(pg, '')
pg.keyboard.press('Space')
district = pg.evaluate("""async () => {
const P = window.PROCITY, D = window.DBG, THREE = P.THREE, v = new THREE.Vector3();
D.setSegment(5); // NIGHT — every playing venue is 'on'
const ids = P.gigs.venueShopIds || [];
const out = [];
for (const id of ids) {
const shop = (P.plan.shops || []).find(s => s.id === id);
if (!P.gigs.onOf(id)) { out.push({ id, kind: shop && shop.venueKind, dark: true }); continue; }
D.enterShop(id);
await new Promise(r => setTimeout(r, 450));
const room = P.interiorMode.current, info = P.interiorMode.crewInfo;
const mem = P.interiorMode.crew ? P.interiorMode.crew.members : [];
const figs = mem.map(m => {
let crown = 0; m.actor.fig.updateWorldMatrix(true, true);
(m.actor.inner || m.actor.fig).traverse(o => { if (o.isBone && /head/i.test(o.name)) {
o.getWorldPosition(v); crown = Math.max(crown, v.y); } });
return { part: m.part, kind: m.kind, stature: +(crown - (m.base.y || 0)).toFixed(3),
seated: !!(m.actor.fig.userData && m.actor.fig.userData.procitySeated) }; // [R16 #3]
});
P.renderer.info.reset(); P.renderer.render(P.interiorMode.scene, P.camera);
out.push({ id, kind: shop && shop.venueKind, genreKey: shop && shop.genreKey,
gigKey: (room && room.audio && room.audio.gigKey) || null,
band: info ? info.band : 0, crowd: info ? info.crowd : 0,
watchPoints: room ? room.watchPoints.length : 0, figs });
D.exitShop(); await new Promise(r => setTimeout(r, 200));
}
return { ids, out };
}""")
playing = [d for d in district['out'] if not d.get('dark')]
OK(f"district: {len(district['ids'])} venue(s) — {len(playing)} playing tonight, "
f"{len(district['out']) - len(playing)} dark (reads true to the seeded week)")
genres_seen = set()
for d in playing:
tag = f"{d['kind']} #{d['id']}"
want = f"gig-{d['genreKey']}"
if d['gigKey'] == want and (not music_keys or want in music_keys):
OK(f" {tag}: live bed '{d['gigKey']}' resolves to a real manifest bar (genre {d['genreKey']})")
genres_seen.add(d['genreKey'])
else:
FAIL(f" {tag}: gigKey '{d['gigKey']}' != '{want}' or not in manifest.music {sorted(music_keys)}")
if 0 < d['crowd'] <= d['watchPoints']:
OK(f" {tag}: crowd {d['crowd']}{d['watchPoints']} watch points")
else:
FAIL(f" {tag}: crowd {d['crowd']} vs {d['watchPoints']} watch points (must be >0 and ≤ cap)")
if d['band'] == 4: OK(f" {tag}: 4-piece band")
else: FAIL(f" {tag}: expected a 4-piece, got {d['band']}")
rigs = [f for f in d['figs'] if f['kind'] == 'rig']
bad = [f for f in rigs if not stature_ok(f)] # [R16 #3] seated exemption
if rigs and bad: FAIL(f" {tag}: {len(bad)} giant rig(s) by stature: {bad[:2]}")
if len(genres_seen) >= 2:
OK(f"district: {len(genres_seen)} distinct genres verified live ({', '.join(sorted(genres_seen))})")
# queue ≤ cap and drains by 'done' — Lane D's outdoor line (charter item). D shipped the VenueQueue
# class and left the street-side lifecycle to F, which wires it in the shell (spawn at doors, drive
# each frame, disposeAll at done); read via P.queueCountOf(id). The line is a seeded short 26.
q = pg.evaluate("""async () => {
const P = window.PROCITY, D = window.DBG;
if (typeof P.queueCountOf !== 'function') return { wired: false };
const id = P.gigs.venueShopIds[0];
D.setSegment(4); await new Promise(r => setTimeout(r, 700)); // DUSK — doors open, the line forms
const atDoors = P.queueCountOf(id) | 0;
D.setSegment(0); await new Promise(r => setTimeout(r, 700)); // DAWN — closing time ('done'), line gone
const atDone = P.queueCountOf(id) | 0;
return { wired: true, atDoors, atDone };
}""")
QUEUE_CAP = 6
if not q.get('wired'):
FAIL("queue: P.queueCountOf(id) not exposed — F's shell wiring missing")
elif 0 < q['atDoors'] <= QUEUE_CAP and q['atDone'] == 0:
OK(f"queue: {q['atDoors']} punters in line at doors (≤ cap {QUEUE_CAP}), drained to 0 by 'done'")
else:
FAIL(f"queue: at doors {q['atDoors']} (want 1..{QUEUE_CAP}), at done {q['atDone']} (want 0)")
if errs: FAIL(f"district: {len(errs)} console error(s); first: {errs[0][:140]}")
else: OK('district: 0 console errors across all venues')
finally:
b.close()
# 2c) STREET BUDGET at the busiest venue block (R13 law: ≤300 draws / ≤200k tris with the lit frontage,
# town-wide posters and the queue zone all in). Park on the footpath in front of the primary venue at
# NIGHT with the gig on, let the block stream in, and sample the settled composer frame.
b, pg, errs = new_page(p)
try:
boot(pg, '')
st = pg.evaluate("""async () => {
const P = window.PROCITY, D = window.DBG;
D.setSegment(5); // NIGHT — frontage lit, posters up
const id = P.gigs.venueShopIds[0], shop = (P.plan.shops || []).find(s => s.id === id);
const lot = shop && (P.plan.lots || []).find(l => l.id === shop.lot);
if (lot) {
const fx = Math.sin(lot.ry || 0), fz = Math.cos(lot.ry || 0);
D.teleport(lot.x + fx * 10, lot.z + fz * 10, Math.atan2(-fx, -fz)); // 10 m out, facing the venue
}
await new Promise(r => setTimeout(r, 700)); // chunks/frontage/posters/pools settle
let draws = Infinity, tris = 0; // take the settled floor (ignore stream spikes)
for (let i = 0; i < 12; i++) {
await new Promise(r => requestAnimationFrame(r));
if (P.renderer.info.render.calls < draws) { draws = P.renderer.info.render.calls; tris = P.renderer.info.render.triangles; }
}
return { draws, tris, venue: shop && shop.name };
}""")
if st['draws'] <= STREET_DRAWS_MAX and st['tris'] <= STREET_TRIS_MAX:
OK(f"street budget @ venue block ({st['venue']}): {st['draws']} draws ≤ {STREET_DRAWS_MAX}, {st['tris']:,} tris ≤ {STREET_TRIS_MAX:,}")
else:
FAIL(f"street budget @ venue block ({st['venue']}): {st['draws']} draws / {st['tris']:,} tris over ({STREET_DRAWS_MAX} / {STREET_TRIS_MAX:,})")
if errs: FAIL(f"street budget: {len(errs)} console error(s); first: {errs[0][:140]}")
finally:
b.close()
# 3) cover ruling, PAID night (seed 20261990 → $10): debit once · no re-charge on re-entry · broke = knockback
b, pg, errs = new_page(p)
try:
boot(pg, '')
paid = pg.evaluate("""async () => {
const P = window.PROCITY, D = window.DBG;
const id = P.gigs.venueShopIds[0]; // R15: no scalar alias — the primary venue by id
D.setSegment(5);
const cover = P.gigs.coverOf(id);
if (!cover) return { skip: true, cover };
const before = P.wallet.cash();
D.enterShop(id); await new Promise(r => setTimeout(r, 350));
const inRoom1 = !!P.interiorMode.current, afterPay = P.wallet.cash();
D.exitShop(); await new Promise(r => setTimeout(r, 250));
const mid = P.wallet.cash();
D.enterShop(id); await new Promise(r => setTimeout(r, 350));
const inRoom2 = !!P.interiorMode.current, afterReentry = P.wallet.cash();
D.exitShop(); await new Promise(r => setTimeout(r, 250));
// now go broke and try again on the SAME night (stamp already paid → still free), then roll the
// night over (MORNING clears the stamp) and try broke → must be knocked back at the door.
P.wallet.buy({ title: 'qa: drain', price: P.wallet.cash() });
D.setSegment(1); D.setSegment(5); // NIGHT → MORNING → NIGHT = a new night, cover due
const brokeCash = P.wallet.cash();
D.enterShop(id); await new Promise(r => setTimeout(r, 350));
const inRoomBroke = !!P.interiorMode.current;
return { cover, before, afterPay, inRoom1, mid, afterReentry, inRoom2, brokeCash, inRoomBroke,
inv: P.wallet.inventory().map(i => i.title) };
}""")
if paid.get('skip'):
WARN(f"cover: seed {SEED} tonight is free (cover 0) — paid path not exercised here")
else:
if paid['inRoom1'] and paid['afterPay'] == paid['before'] - paid['cover']:
OK(f"cover: ${paid['cover']} debited once at the door (${paid['before']} → ${paid['afterPay']}), entry granted")
else:
FAIL(f"cover: wrong debit/entry on a paid night: {paid}")
if paid['inRoom2'] and paid['afterReentry'] == paid['mid']:
OK(f"cover: re-entry the same night is FREE (no re-charge, still ${paid['afterReentry']})")
else:
FAIL(f"cover: re-entry re-charged or was refused: {paid}")
if not paid['inRoomBroke'] and paid['brokeCash'] < paid['cover']:
OK(f"cover: broke (${paid['brokeCash']}) on a NEW night → polite knockback, no entry")
else:
FAIL(f"cover: broke player got in (or the night never rolled): {paid}")
if errs: FAIL(f"cover: {len(errs)} console error(s); first: {errs[0][:140]}")
finally:
b.close()
# 4) cover ruling, FREE night (seed 1234 → cover 0): walk straight in, wallet untouched
b, pg, errs = new_page(p)
try:
pg.goto(f'{HOST}/index.html?seed={FREE_GIG_SEED}&dbg=1')
pg.wait_for_function("window.DBG && window.DBG.ready === true", timeout=20000)
pg.evaluate("() => { const o=document.getElementById('pc-start'); if(o) o.style.display='none'; }")
free = pg.evaluate("""async () => {
const P = window.PROCITY, D = window.DBG;
const id = P.gigs.venueShopIds[0]; // R15: no scalar alias — the primary venue by id
D.setSegment(5);
const cover = P.gigs.coverOf(id), before = P.wallet.cash();
D.enterShop(id); await new Promise(r => setTimeout(r, 350));
return { cover, before, after: P.wallet.cash(), inRoom: !!P.interiorMode.current, band: P.gigs.bandNameOf(id) };
}""")
if free['cover'] == 0 and free['inRoom'] and free['after'] == free['before']:
OK(f"cover: free night (seed {FREE_GIG_SEED}, {free['band']}) → walked straight in, wallet untouched (${free['after']})")
else:
FAIL(f"cover: free-night entry wrong: {free}")
if errs: FAIL(f"free night: {len(errs)} console error(s); first: {errs[0][:140]}")
finally:
b.close()
# 5) ?noassets=1 + ?gigs=1 — still silent-and-fine: zero audio/band-file fetches, gig still happens
b, pg, errs = new_page(p)
reqs = []
pg.on('request', lambda r: reqs.append(r.url)
if ('/assets/audio/' in r.url or r.url.endswith('.ogg') or r.url.endswith('.m4a')
or 'custom_bands' in r.url or r.url.endswith('.glb')) else None)
try:
boot(pg, 'noassets=1')
pg.keyboard.press('Space')
na = pg.evaluate("""async () => {
const P = window.PROCITY, D = window.DBG;
const id = P.gigs.venueShopIds[0]; // R15: no scalar alias — the primary venue by id
D.setSegment(5);
D.enterShop(id); await new Promise(r => setTimeout(r, 450));
const info = P.interiorMode.crewInfo;
return { state: P.gigs.stateOf(id), inRoom: !!P.interiorMode.current, band: info && info.band,
crowd: info && info.crowd, gigs: (P.plan.gigs || []).length,
bandName: P.gigs.bandNameOf(id), audio: P.audio && P.audio.state.manifest };
}""")
if na['inRoom'] and na['band'] == 4 and na['crowd'] > 0:
OK(f"?noassets=1&gigs=1: the gig still happens (4-piece band, crowd {na['crowd']}, placeholders)")
else:
FAIL(f"?noassets=1&gigs=1: no gig without assets: {na}")
if len(reqs) == 0:
OK(f"?noassets=1&gigs=1: 0 audio/GLB/band-file fetches (manifest={na['audio']}, bands from the generator)")
else:
FAIL(f"?noassets=1&gigs=1: {len(reqs)} asset fetch(es) leaked; first {reqs[0]}")
if errs: FAIL(f"?noassets=1&gigs=1: {len(errs)} console error(s); first: {errs[0][:140]}")
else: OK('?noassets=1&gigs=1: 0 console errors (silent-and-happy)')
finally:
b.close()
GODVERSE_TOWN = 'redhill_godverse'
GODVERSE_SHOP = 3962749 # Monster Robot Party, 147 Musgrave Rd, Red Hill QLD — the one REAL crate
GODVERSE_BASE = f'assets/stock_godverse/{GODVERSE_SHOP}/'
STOCK_MANIFEST = 'assets/stock_godverse/index.json'
def smoke_real_crate(p):
"""R26 — THE BETA GATE: every crate different, sourcing visible, and NOT ONE 404.
The lineage of this gate is the epoch's whole argument. R23's #7a would have gone green on "real
covers render" while the room held the generic v2 pack. R24's brief then said assert id equality —
which also couldn't discriminate, because both packs numbered items positionally and every atlas id
was a generic-pack id too. What survives is what a wrong pack cannot fake: TITLES only this crate
has, and the TEXTURE URL the GPU actually samples.
R26 adds the two beta claims. DISTINCTNESS: 15 keyed crates, pairwise title-disjoint — asserted
across the indexes of record, so it cannot pass by entering one lucky shop. SOURCING (C's §7.2b,
which correctly overruled the brief's `tier`): every index declares real|mint, a mint crate can
never report as real, and the gate PRINTS it.
And the 404s are gone for the right reason. R24 probed every keyed shop and ate a 404 for each one
with no atlas; F's smoke could only tolerate that by naming it — a tolerance is a confession. G's
manifest now says which (shop, type) pairs exist, so the shell asks for nothing else. ZERO console
errors, NO attribution exceptions. Subject absent ⇒ SKIP with the reason, never a quiet pass."""
head(f'SMOKE: the beta crate gate (R26 — 15 crates distinct · sourcing · zero 404s · {GODVERSE_TOWN})')
b, pg, errs = new_page(p)
four04 = []
pg.on('response', lambda r: four04.append(r.url) if r.status == 404 else None)
try:
pg.goto(f'{HOST}/{STOCK_MANIFEST}')
if 'godverseShopId' not in pg.content():
print('· SKIP: no atlas manifest — subject absent, nothing asserted (Lane G)'); return
boot(pg, f'plansrc=osm&town={GODVERSE_TOWN}&stock=real&dig=1')
res = pg.evaluate("""async ({manifest, real, base}) => {
const D = window.DBG, P = window.PROCITY;
const man = await (await fetch(manifest)).json();
// ── DISTINCTNESS across the indexes of record (not just the shop we walk into) ──
const crates = [];
for (const sh of (man.shops || [])) for (const t of (sh.types || [])) {
const u = `assets/stock_godverse/${sh.godverseShopId}/stock_${t}_index.json`;
const r = await fetch(u);
if (!r.ok) return { fail: `manifest lists ${u} but it 404s` };
const idx = await r.json();
crates.push({ id: sh.godverseShopId, type: t, manSourcing: sh.sourcing,
sourcing: idx.sourcing || null,
titles: (idx.items || []).map(i => i.title) });
}
const gen = {};
for (const t of ['record', 'book', 'toy'])
gen[t] = new Set((((await (await fetch(`assets/models/stock_${t}_index.json`)).json()).items) || [])
.map(i => i.title));
let shared = 0, vsGeneric = 0, noSourcing = 0, manMismatch = 0;
for (let i = 0; i < crates.length; i++) {
if (!crates[i].sourcing) noSourcing++;
if (crates[i].sourcing !== crates[i].manSourcing) manMismatch++;
if (crates[i].titles.some(t => gen[crates[i].type] && gen[crates[i].type].has(t))) vsGeneric++;
for (let j = i + 1; j < crates.length; j++) {
const a = new Set(crates[i].titles);
if (crates[j].titles.some(t => a.has(t))) shared++;
}
}
// ── the ROOM binds to the right crate: one REAL, one MINT, one soft fall ──
const byId = new Map((man.shops || []).map(s => [s.godverseShopId, s]));
const stocked = (P.plan.shops || []).filter(s => { const e = byId.get(s.godverseShopId);
return e && (e.types || []).includes(s.type); });
const unfetch = (P.plan.shops || []).find(s => { if (!s.godverseShopId) return false;
const e = byId.get(s.godverseShopId);
return !e || !(e.types || []).includes(s.type); });
const visit = async (shop) => {
D.exitShop(); await new Promise(r => setTimeout(r, 250));
D.enterShop('g:' + shop.godverseShopId); await new Promise(r => setTimeout(r, 1200));
const si = D.stockInfo();
const c = crates.find(c => c.id === shop.godverseShopId && c.type === shop.type);
const own = c ? new Set(c.titles) : new Set();
return { name: si.name, gid: si.godverseShopId, want: shop.godverseShopId, type: si.type,
base: si.base, packItems: si.packItems,
sourcing: c ? c.sourcing : null,
inOwn: si.packTitles.filter(t => own.has(t)).length,
fromOwnTex: si.texUrls.filter(u => u.includes(`stock_godverse/${shop.godverseShopId}/`)).length,
fromGenericTex: si.texUrls.filter(u => u.includes('assets/models/stock_')).length };
};
const realShop = stocked.find(s => s.godverseShopId === real);
const mintShop = stocked.find(s => (byId.get(s.godverseShopId) || {}).sourcing === 'mint');
const out = { crates: crates.length, shared, vsGeneric, noSourcing, manMismatch,
stocked: stocked.length,
realRoom: realShop ? await visit(realShop) : null,
mintRoom: mintShop ? await visit(mintShop) : null };
if (unfetch) { D.exitShop(); await new Promise(r => setTimeout(r, 250));
D.enterShop('g:' + unfetch.godverseShopId); await new Promise(r => setTimeout(r, 900));
const o = D.stockInfo();
out.softFall = { name: o.name, gid: o.godverseShopId, want: unfetch.godverseShopId,
packItems: o.packItems }; }
return out;
}""", {'manifest': STOCK_MANIFEST, 'real': GODVERSE_SHOP, 'base': GODVERSE_BASE})
if res.get('fail'):
FAIL(f"beta crate gate: {res['fail']}"); return
# ── distinctness (independently measured; G reported 15/15) ──
pairs = res['crates'] * (res['crates'] - 1) // 2
OK(f"DISTINCTNESS: {res['crates']} keyed crates, {pairs} pairs, {res['shared']} sharing a title") \
if res['shared'] == 0 else FAIL(f"DISTINCTNESS: {res['shared']} crate pair(s) share titles")
OK(f"DISTINCTNESS: 0/{res['crates']} crates overlap the generic v2 pack") \
if res['vsGeneric'] == 0 else FAIL(f"{res['vsGeneric']} crate(s) overlap the generic pack")
# ── sourcing (C §7.2b — an index with no `sourcing` FAILS; the gate PRINTS it) ──
OK(f"SOURCING: {res['crates']}/{res['crates']} indexes declare sourcing") \
if res['noSourcing'] == 0 else FAIL(f"SOURCING: {res['noSourcing']} index(es) declare none")
OK('SOURCING: every index agrees with the manifest') \
if res['manMismatch'] == 0 else FAIL(f"SOURCING: {res['manMismatch']} index/manifest disagreement(s)")
for label, want in (('realRoom', 'real'), ('mintRoom', 'mint')):
r = res.get(label)
if not r:
FAIL(f"{label}: no {want} crate reachable in {GODVERSE_TOWN}"); continue
if r['gid'] != r['want']:
FAIL(f"{label}: entered the WRONG shop — wanted {r['want']}, got {r['gid']}"); continue
print(f" room: {r['name']} (godverse {r['gid']}, {r['type']}) → {r['base']} [sourcing: {r['sourcing']}]")
OK(f" {want}: sourcing reports '{r['sourcing']}'") if r['sourcing'] == want \
else FAIL(f" {want} crate reports sourcing '{r['sourcing']}' — a {want} crate must never report otherwise")
OK(f" {want}: TITLE equality {r['inOwn']}/{r['packItems']} from its OWN crate") \
if (r['packItems'] > 0 and r['inOwn'] == r['packItems']) \
else FAIL(f" {want}: only {r['inOwn']}/{r['packItems']} titles are its own")
OK(f" {want}: TEXTURE provenance {r['fromOwnTex']} own / {r['fromGenericTex']} generic") \
if (r['fromOwnTex'] > 0 and r['fromGenericTex'] == 0) \
else FAIL(f" {want}: textures {r['fromOwnTex']} own / {r['fromGenericTex']} generic — wrong file")
sf = res.get('softFall')
if sf:
if sf['gid'] != sf['want']: FAIL(f"soft fall: entered the wrong shop ({sf['gid']} != {sf['want']})")
elif sf['packItems'] == 0: OK(f"soft fall: keyed, unfetchable type \"{sf['name']}\" → parody, silently")
else: FAIL(f"soft fall BROKEN: \"{sf['name']}\" inherited {sf['packItems']} items")
# ── ZERO 404s. No exceptions. The manifest is what makes this honest. ──
OK('ZERO 404s — the manifest means we never ask for an atlas that is not there') \
if not four04 else FAIL(f"{len(four04)} unexpected 404(s) — the manifest should have prevented every one; first: {four04[0]}")
OK('0 console errors (no attribution exceptions)') if not errs \
else FAIL(f'the crate: {len(errs)} console error(s); first: {errs[0][:140]}')
finally:
b.close()
# ── TIER 2 (v5.0) — the kill-the-server gate's own lifecycle ───────────────────────────────────────
GV = 'pipeline/godverse_server.py'
LIVE_BASE = 'http://127.0.0.1:8778'
LIVE_SHOP = 3962749
FIXTURE_DB = 'recordgod_fixture'
REAL_DB = 'recordgod'
def _gv(args, db=None):
env = dict(os.environ)
if db: env['GODVERSE_POS_DB'] = db
return subprocess.run(['python3', GV] + args, cwd=str(ROOT), capture_output=True, text=True, env=env)
def _gv_kill():
subprocess.run("lsof -ti :8778 | xargs kill -9", shell=True, capture_output=True)
time.sleep(0.8)
def _gv_start(db):
_gv_kill()
env = dict(os.environ); env['GODVERSE_POS_DB'] = db
subprocess.Popen(['python3', GV, 'start'], cwd=str(ROOT), env=env,
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
for _ in range(20):
time.sleep(0.3)
try:
urllib.request.urlopen(f'{LIVE_BASE}/godverse/v1/shop/{LIVE_SHOP}/stock', timeout=1).read()
return True
except Exception:
pass
return False
def smoke_tier2(p):
"""R27 — THE KILL-THE-SERVER GATE. Charter law #1 made falsifiable: the server enriches, never gates.
This gate owns its own subject and its own weather. Two lanes made that necessary and both said so
before it could go wrong:
· G: production `gone` is EMPTY — 0 of crate 550's 120 records have sold since 2026-07-01 — so a
sold-means-gone assert on live data would pass VACUOUSLY FOREVER. G built `make-fixture`, and the
gate MUST use it. "A fixture is not a lie; a green assert over an absent subject is."
· C: pick-then-omit and filter-the-array BOTH hide the sold record, so "the gone item never renders"
cannot tell them apart — it is vacuous and it goes green. The discriminating assert is COLLATERAL:
the survivors' identity AND ORDER unchanged across a `gone` delta. One sale must remove one
record, not re-stock the shop (C measured the naive wiring changing 21 other records).
Tier 2 is non-deterministic BY DESIGN (charter law #2), so this gate never asserts dig determinism
with the server up. What it asserts is the LADDER: live enriches, dead degrades, and the player never
waits on either. Subject absent (no psql / no POS db) ⇒ SKIP with the reason printed, never a pass."""
head('SMOKE: the kill-the-server gate (R27 — charter law #1: the server enriches, never gates)')
if subprocess.run(['which', 'psql'], capture_output=True).returncode != 0:
print('· SKIP: no psql — the POS fixture cannot be built, nothing asserted (needs the GODVERSE box)')
return
if subprocess.run(['psql', '-lqt'], capture_output=True, text=True).stdout.find(REAL_DB) < 0:
print(f'· SKIP: no `{REAL_DB}` POS database here — subject absent, nothing asserted (Lane G)')
return
fx = _gv(['make-fixture', '--shop', str(LIVE_SHOP), '--sold', '3'])
sold = [l.strip() for l in fx.stdout.splitlines() if l.strip().startswith('sku_')]
if len(sold) != 3:
FAIL(f'the fixture built no subject (want 3 sold, got {len(sold)}): {fx.stderr[:120]}'); return
print(f' subject: {len(sold)} record(s) marked SOLD in a fixture POS — production `gone` is empty, '
f'so this is the only honest way to have one')
try:
# ── ARM 1: server UP → live fields land, and the crate omits WITHOUT reshuffling ──
if not _gv_start(FIXTURE_DB):
FAIL('the fixture server would not come up'); return
b, pg, errs = new_page(p)
try:
boot(pg, f'plansrc=osm&town=redhill_godverse&stock=real&dig=1&live=1')
r1 = pg.evaluate("""async (shop) => {
const D = window.DBG, m = await import('./js/interiors/stockpack.js');
const BASE = `assets/stock_godverse/${shop}/`;
D.enterShop('g:' + shop); await new Promise(r => setTimeout(r, 200));
const pack = m.getStockPack('record', BASE);
const before = pack.items.map(i => i.id);
await new Promise(r => setTimeout(r, 1800));
const gone = pack.gone ? [...pack.gone] : [];
const after = pack.items.map(i => i.id);
const survB = before.filter(i => !gone.includes(i));
const survA = after.filter(i => !gone.includes(i));
return { gone, itemsBefore: before.length, itemsAfter: after.length,
orderIntact: before.length === after.length && before.every((x, i) => x === after[i]),
survivors: survA.length,
collateral: survB.filter((x, i) => survA[i] !== x).length };
}""", LIVE_SHOP)
OK(f"ARM 1 server-up: live read consumed — gone[{len(r1['gone'])}], {r1['survivors']} survivors") \
if len(r1['gone']) == 3 else FAIL(f"ARM 1: gone[{len(r1['gone'])}], expected 3 from the fixture")
OK(f"ARM 1: items[] untouched — {r1['itemsAfter']} ids, identity AND ORDER intact") \
if r1['orderIntact'] and r1['itemsAfter'] == 120 \
else FAIL(f"ARM 1: items[] MUTATED ({r1['itemsBefore']}{r1['itemsAfter']}, order intact={r1['orderIntact']})")
OK('ARM 1: COLLATERAL 0 — one sale removed one record, it did not re-stock the shop') \
if r1['collateral'] == 0 \
else FAIL(f"ARM 1: COLLATERAL {r1['collateral']} — the crate RESHUFFLED (filter-the-array, not pick-then-omit)")
if set(r1['gone']) != set(sold):
FAIL(f"ARM 1: gone {r1['gone']} != the fixture's {sold} — the subject is not what the gate thinks")
else:
OK('ARM 1: `gone` carries the atlas id form verbatim — zero transformation (R26 fence)')
# ── ARM 2: KILL IT MID-SESSION ──
_gv_kill()
errs.clear()
r2 = pg.evaluate("""async (shop) => {
const D = window.DBG, m = await import('./js/interiors/stockpack.js');
const pack = m.getStockPack('record', `assets/stock_godverse/${shop}/`);
const frames = []; let last = performance.now();
const iv = setInterval(() => { const n = performance.now(); frames.push(n - last); last = n; }, 0);
D.exitShop(); await new Promise(r => setTimeout(r, 300));
const t0 = performance.now();
D.enterShop('g:' + shop); // fires a read at a DEAD server
const enterMs = performance.now() - t0;
await new Promise(r => setTimeout(r, 2000)); // past the 1200ms abort
clearInterval(iv);
const si = D.stockInfo();
frames.sort((a, b) => a - b);
return { enterMs, worst: frames[frames.length - 1], packItems: si.packItems,
ownTex: si.texUrls.filter(u => u.includes(`stock_godverse/${shop}/`)).length };
}""", LIVE_SHOP)
# THE CONTROL (R21: a number without a control is not a measurement). Entering a shop
# REBUILDS an interior — that costs the same with or without a server, so an absolute
# threshold measures Lane C's room build, not the network failure. Walk the same shop with
# tier 2 OFF and compare: the question is "did the DEATH cost anything", not "is entering fast".
ctrl = pg.evaluate("""async (shop) => {
const D = window.DBG;
const frames = []; let last = performance.now();
const iv = setInterval(() => { const n = performance.now(); frames.push(n - last); last = n; }, 0);
D.exitShop(); await new Promise(r => setTimeout(r, 300));
const t0 = performance.now();
D.enterShop('g:' + shop);
const enterMs = performance.now() - t0;
await new Promise(r => setTimeout(r, 2000));
clearInterval(iv); frames.sort((a, b) => a - b);
return { enterMs, worst: frames[frames.length - 1] };
}""", LIVE_SHOP) # the breaker has tripped by now → this walk touches no network at all
dEnter = r2['enterMs'] - ctrl['enterMs']
dWorst = r2['worst'] - ctrl['worst']
print(f" control (same walk, breaker tripped, no network): enter {ctrl['enterMs']:.0f}ms, worst frame {ctrl['worst']:.0f}ms")
OK(f"ARM 2 kill mid-session: enter {r2['enterMs']:.0f}ms vs control {ctrl['enterMs']:.0f}ms (Δ{dEnter:+.0f}ms) — "
f"worst frame {r2['worst']:.0f}ms vs {ctrl['worst']:.0f}ms (Δ{dWorst:+.0f}ms): the death cost nothing the room didn't") \
if (dEnter < 100 and dWorst < 100) \
else FAIL(f"ARM 2: the DEATH itself stuttered — enter Δ{dEnter:+.0f}ms, worst frame Δ{dWorst:+.0f}ms over control")
OK(f"ARM 2: the room is still complete from files — {r2['packItems']} items, {r2['ownTex']} own atlas tex") \
if (r2['packItems'] == 120 and r2['ownTex'] > 0) \
else FAIL(f"ARM 2: the room degraded WRONG — {r2['packItems']} items, {r2['ownTex']} own tex")
# THE BRIEF SAID ZERO. ZERO IS UNACHIEVABLE, AND F MEASURED WHY — see LANE_F_NOTES §27.
# A fetch to a dead host logs `net::ERR_CONNECTION_REFUSED` at the BROWSER level; no
# `.catch()` suppresses it (same species as R26's 404s, one layer up). To log zero the
# reader would have to know the server is dead WITHOUT ASKING, which it cannot. So one
# refusal is the irreducible cost of discovering death — and asserting EXACTLY ONE is
# strictly stronger than asserting zero: it proves the circuit breaker. Without the
# breaker this is one error per shop entry, forever, and "zero" could never distinguish
# a working breaker from a broken one because both fail it.
game_errs = [e for e in errs if 'ERR_CONNECTION_REFUSED' not in e]
net_errs = [e for e in errs if 'ERR_CONNECTION_REFUSED' in e]
OK('ARM 2: ZERO errors from game code — nothing threw, nothing warned') \
if not game_errs else FAIL(f"ARM 2: {len(game_errs)} GAME error(s) on server death; first: {game_errs[0][:120]}")
OK(f'ARM 2: EXACTLY 1 refused connection — the circuit breaker tripped and never asked again') \
if len(net_errs) == 1 \
else FAIL(f"ARM 2: {len(net_errs)} refused connections — the breaker did NOT trip "
f"(1 = discovery, >1 = it keeps asking a dead server)")
finally:
b.close()
# ── ARM 3: boot with the server DOWN → pure tier 1, no live state at all ──
b, pg, errs = new_page(p)
try:
boot(pg, 'plansrc=osm&town=redhill_godverse&stock=real&dig=1&live=1')
r3 = pg.evaluate("""async (shop) => {
const D = window.DBG, m = await import('./js/interiors/stockpack.js');
D.enterShop('g:' + shop); await new Promise(r => setTimeout(r, 2000));
const pack = m.getStockPack('record', `assets/stock_godverse/${shop}/`);
const si = D.stockInfo();
return { items: si.packItems, goneSize: pack.gone ? pack.gone.size : 0,
ownTex: si.texUrls.filter(u => u.includes(`stock_godverse/${shop}/`)).length };
}""", LIVE_SHOP)
OK(f"ARM 3 server-down boot: pure tier 1 — {r3['items']} items, gone[{r3['goneSize']}], {r3['ownTex']} own tex") \
if (r3['items'] == 120 and r3['goneSize'] == 0 and r3['ownTex'] > 0) \
else FAIL(f"ARM 3: not pure tier 1 — {r3['items']} items, gone[{r3['goneSize']}]")
g3 = [e for e in errs if 'ERR_CONNECTION_REFUSED' not in e]
n3 = [e for e in errs if 'ERR_CONNECTION_REFUSED' in e]
OK('ARM 3: ZERO errors from game code booting against a dead server') \
if not g3 else FAIL(f"ARM 3: {len(g3)} game error(s); first: {g3[0][:120]}")
OK('ARM 3: EXACTLY 1 refused connection for the whole boot — asked once, learned, stopped') \
if len(n3) == 1 else FAIL(f"ARM 3: {len(n3)} refused connections (want exactly 1: the breaker)")
finally:
b.close()
# ── THE SANDBOX, BOTH DIRECTIONS (#4) ──
# real→game proven above (a real sale removed a record from the game crate).
# game→real: prove the ABSENCE of the verb. G: 501 is a stronger proof than 404 — a 404 means a
# handler ran and declined; 501 means there is NO write handler in the process at all.
_gv_start(FIXTURE_DB)
codes = {}
for verb in ('POST', 'PUT', 'PATCH', 'DELETE'):
rq = urllib.request.Request(f'{LIVE_BASE}/godverse/v1/shop/{LIVE_SHOP}/stock', method=verb, data=b'{}')
try:
urllib.request.urlopen(rq, timeout=3); codes[verb] = 200
except urllib.error.HTTPError as e:
codes[verb] = e.code
except Exception as e:
codes[verb] = str(e)[:20]
OK(f'SANDBOX game→real: every write verb 501 — there is no write handler in the process {codes}') \
if all(c == 501 for c in codes.values()) \
else FAIL(f'SANDBOX game→real: a write verb did NOT answer 501 — {codes}')
try:
urllib.request.urlopen(f'{LIVE_BASE}/godverse/v1/shop/158/stock', timeout=3)
FAIL('SANDBOX: a MINT shop served live stock — the metadata fence leaks')
except urllib.error.HTTPError as e:
OK('SANDBOX: a mint shop 404s — only the one real shop is live, which is the point') \
if e.code == 404 else FAIL(f'SANDBOX: mint shop answered {e.code}, expected 404')
finally:
_gv_start(REAL_DB) # leave the box as we found it: the REAL POS serving
def smoke_glance(p):
"""R29 Spike 1 — THE POSE GATE, un-SKIPPED. v6's first thread, and the first clip to reach the game.
R28 wrote this gate and it had NO SUBJECT: `look.glb` was converted, verified, published, and
sha1-proven by fetching the bytes back off the depot — and nothing bound it. Every check E ran was
real and passed; none could see that no consumer existed (a gate on the artifact cannot see the wire
— the R17 pattern's fifth outing). It SKIPPED with its reason printed rather than report green over
nothing. D bound it in R29 wave 0, so the gate binds too.
Asserts the two things that matter, and they pull in opposite directions:
· the glance REACHES THE GAME — peds actually enter the state, on the default boot;
· `?classic=1` never fetches the clip and never glances — while STILL DRAWING THE ROLL, because
D takes from a dedicated rng stream always and only flips the flag when a clip exists. That is
what keeps the covenant byte-identical while the behaviour is gated: the determinism is in the
draw, not in the outcome. Asserting "0 glancing" alone would pass on a fleet that failed to load
— so we assert the clip is ABSENT under classic and PRESENT on the default boot."""
head('SMOKE: the glance (R29 Spike 1 — v6\'s first clip in the game · classic still zero-fetch)')
b, pg, errs = new_page(p)
try:
# Check the subject on DISK, not via pg.goto — navigating to a .glb starts a download and
# Playwright aborts the navigation, which crashes the gate instead of skipping it. A gate that
# dies looking for its subject is worse than one that skips: it takes the suite with it.
if not os.path.exists(os.path.join(str(ROOT), 'web', 'models', 'peds', 'look.glb')):
print('· SKIP: look.glb not on disk — subject absent, nothing asserted (Lane E)'); return
boot(pg, 'plansrc=osm&town=redhill_godverse')
r = pg.evaluate("""async () => {
const P = window.PROCITY, c = P.citizens;
let peak = 0;
for (let i = 0; i < 9000; i++) { c.update(1/60);
if (i % 30 === 0) { const p = (c._activeList||[]).concat(c.roster||[]);
const g = p.filter(x => x && x.glance === true).length; if (g > peak) peak = g; } }
const p = (c._activeList||[]).concat(c.roster||[]);
return { bound: !!P.fleet.lookClip, dur: P.fleet.lookClip ? P.fleet.lookClip.duration : 0,
tracks: P.fleet.lookClip ? P.fleet.lookClip.tracks.length : 0,
peak, pop: p.length, rng: p.some(x => x && !!x.glanceRng) };
}""")
OK(f"default boot: look.glb BOUND — {r['dur']:.1f}s, {r['tracks']} tracks") \
if r['bound'] else FAIL('default boot: look.glb is NOT bound — the R28 gap is back')
OK(f"the glance REACHES THE GAME — peak {r['peak']} of {r['pop']} peds glancing") \
if r['peak'] > 0 else FAIL(f"no ped ever glanced across 9000 ticks ({r['pop']} peds) — clip bound, behaviour absent")
if errs: FAIL(f'glance: {len(errs)} console error(s); first: {errs[0][:120]}')
else: print(' · 0 console errors')
finally:
b.close()
b, pg, errs = new_page(p)
try:
boot(pg, 'classic=1')
r = pg.evaluate("""async () => {
const P = window.PROCITY, c = P.citizens;
let peak = 0;
for (let i = 0; i < 6000; i++) { c.update(1/60);
if (i % 40 === 0) { const p = (c._activeList||[]).concat(c.roster||[]);
const g = p.filter(x => x && x.glance === true).length; if (g > peak) peak = g; } }
const p = (c._activeList||[]).concat(c.roster||[]);
return { bound: !!P.fleet.lookClip, peak, pop: p.length, rng: p.some(x => x && !!x.glanceRng) };
}""")
# NOT bound is the assertion: ?classic must not fetch the clip at all (zero-fetch-delta covenant)
OK('?classic=1: look.glb NEVER FETCHED — the zero-fetch-delta covenant holds') \
if not r['bound'] else FAIL('?classic=1 FETCHED look.glb — the covenant is breached')
OK(f"?classic=1: glance inert at the source — 0 of {r['pop']} peds, and the rng stream still draws "
f"({r['rng']}) so determinism is in the DRAW, not the outcome") \
if (r['peak'] == 0 and r['rng']) \
else FAIL(f"?classic=1: peak {r['peak']} glancing / rng stream present={r['rng']}")
if errs: FAIL(f'glance/classic: {len(errs)} console error(s); first: {errs[0][:120]}')
finally:
b.close()
def smoke_continuity(p):
"""R15 identity continuity (ledger #3): the ped who queued/surged into a venue turns up in its crowd.
Exercises F's real relay end-to-end — VenueQueue.admitOne() → onAdmit → citizens.recordVenueEntry →
tonightRoster(id) → GigCrew.spawn({roster}) → the front crowd slots carry those identities. Asserts the
seam on identities (crewInfo.fromRoster + member.pedIndex ∈ tonightRoster), the invariant crowd ⊇
roster ∩ cap, and that an EMPTY roster injects nobody (D's byte-identical law, proven here on the shell
side; D's sim soak proves the sim side). Also ledger #5 — reports SETTLED tris (after the async
instrument GLBs land), not the pre-load number C caught the R13 smoke reporting."""
head('SMOKE: continuity (R15 — the ped you followed is in the crowd · empty-roster clean · settled tris)')
if not gigs_landed(p):
print('· gigs=1 not landed (no plan.gigs) — skipping (Lane A)')
return
b, pg, errs = new_page(p)
try:
boot(pg, '')
pg.keyboard.press('Space')
res = pg.evaluate("""async () => {
const P = window.PROCITY, D = window.DBG;
const id = P.gigs.venueShopIds[0];
const rosterOf = () => (P.citizens.tonightRoster(id) || []).map(e => e.pedIndex);
// 1) EMPTY-ROSTER PATH — enter before any admit; crew must inject only what's in the roster
// (usually 0). This is the shell side of D's "empty roster = byte-identical to R13" law.
D.setSegment(5); // NIGHT — the gig is on
const rosterAtEmpty = rosterOf();
D.enterShop(id); await new Promise(r => setTimeout(r, 550));
const ei = P.interiorMode.crewInfo;
const emptyFromRoster = ei ? ei.fromRoster : -1, emptyCrowd = ei ? ei.crowd : 0;
D.exitShop(); await new Promise(r => setTimeout(r, 250));
// 2) POPULATED PATH — let the street loop spawn the queue, then admit a few EXPLICITLY (rAF
// auto-drain is throttled in automation, per B's note). Each admit fires onAdmit → recordVenueEntry.
for (let i = 0; i < 8; i++) await new Promise(r => requestAnimationFrame(r));
const q = P.venueQueues && P.venueQueues.get(id);
const admitted = [];
if (q) for (let i = 0; i < 3 && q.count() > 0; i++) { const e = q.admitOne(); if (e) admitted.push(e.pedIndex); }
const roster = rosterOf();
D.enterShop(id); await new Promise(r => setTimeout(r, 700)); // build + crew spawn
const info = P.interiorMode.crewInfo, room = P.interiorMode.current;
const crowd = P.interiorMode.crew ? P.interiorMode.crew.members.filter(m => m.part === 'crowd') : [];
const rosterCrowd = crowd.filter(m => m.fromRoster).map(m => m.pedIndex);
// 3) SETTLED TRIS (ledger #5) — immediately after build vs after the instrument GLBs + rig atlas land
P.renderer.info.reset(); P.renderer.render(P.interiorMode.scene, P.camera);
const trisImmediate = P.renderer.info.render.triangles, draws = P.renderer.info.render.calls;
await new Promise(r => setTimeout(r, 1600));
P.renderer.info.reset(); P.renderer.render(P.interiorMode.scene, P.camera);
const trisSettled = P.renderer.info.render.triangles, drawsSettled = P.renderer.info.render.calls;
return { rosterAtEmpty, emptyFromRoster, emptyCrowd, admitted, roster, rosterCrowd,
fromRoster: info ? info.fromRoster : -1, crowd: info ? info.crowd : 0,
watchPoints: room ? room.watchPoints.length : 0,
trisImmediate, draws, trisSettled, drawsSettled };
}""")
# empty-roster path: crew injects exactly what's in the roster at that moment (0 unless surge beat us)
want_empty = min(len(res['rosterAtEmpty']), res['emptyCrowd'])
if res['emptyFromRoster'] == want_empty:
OK(f"empty roster: crew injected {res['emptyFromRoster']} from a {len(res['rosterAtEmpty'])}-entry roster "
f"(crowd {res['emptyCrowd']} — no phantom continuity, R13-identical path)")
else:
FAIL(f"empty roster: fromRoster {res['emptyFromRoster']} != roster∩cap {want_empty} (roster leaked)")
# the queue admits were recorded into tonightRoster (the onAdmit → recordVenueEntry relay)
roster, admitted, rosterCrowd = res['roster'], res['admitted'], res['rosterCrowd']
if admitted and all(a in roster for a in admitted):
OK(f"relay: {len(admitted)} queue admits → tonightRoster ({admitted} ⊆ roster of {len(roster)})")
else:
FAIL(f"relay: queue admits {admitted} not all in tonightRoster {roster} (onAdmit→recordVenueEntry broke)")
# THE SEAM — crowd ⊇ roster ∩ cap, and every roster-crowd member's pedIndex is genuinely in the roster
cap = res['crowd']
want = min(len(roster), cap)
subset_ok = set(rosterCrowd).issubset(set(roster))
if res['fromRoster'] == want and want > 0 and subset_ok and set(roster[:cap]).issubset(set(rosterCrowd)):
OK(f"continuity seam: {res['fromRoster']} of {cap} crowd came from the roster "
f"(crowd ⊇ roster∩cap ✓, all pedIndex ∈ tonightRoster ✓) — the ped you followed is in the crowd")
else:
FAIL(f"continuity seam: fromRoster {res['fromRoster']} vs roster∩cap {want}; rosterCrowd {rosterCrowd} "
f"vs roster {roster} (subset={subset_ok})")
# ledger #5 — settled-tris honesty (report both; interior gate is draws, which stays low)
OK(f"settled tris: {res['trisImmediate']:,} pre-load → {res['trisSettled']:,} settled "
f"(instrument GLBs; draws {res['draws']}{res['drawsSettled']}{GIG_DRAWS_MAX} gate)")
if errs: FAIL(f"continuity: {len(errs)} console error(s); first: {errs[0][:140]}")
else: OK('continuity: 0 console errors')
finally:
b.close()
# ═══ R30 v7.0-alpha — THE GATES (ledger #5). Every gate below names its subject and proves it
# touched it (the vacuous-gate law): storage counts come from a prototype instrument with a POSITIVE
# control, the rotation fp hashes VERTEX DATA (the wave-3 binding — F measured the transform fp
# vacuous over batched parody stock), and the no-pump run drives the REAL dig/sell UI, not the API. ═══
GAME_DOM_IDS = ['pc-game', 'pc-gamebar', 'pc-crate', 'pc-crate-btn', 'pc-sleep-btn', 'pc-day', 'pc-cash']
# Installed BEFORE any page script (add_init_script) — counts every Storage-prototype method call.
# "Measure, don't trust": save.js promises zero module-scope storage access; this sees through it.
STORAGE_INSTRUMENT = """(() => {
window.__storageCalls = [];
for (const m of ['getItem', 'setItem', 'removeItem', 'clear', 'key']) {
const orig = Storage.prototype[m];
Storage.prototype[m] = function (...a) {
window.__storageCalls.push(m + ':' + String(a.length ? a[0] : ''));
return orig.apply(this, a);
};
}
})();"""
# In-page FNV-1a over the serialized plan — the world fingerprint for the delta-law asserts.
JS_PLAN_FP = """() => { const s = JSON.stringify(window.PROCITY.plan); let h = 0x811c9dc5;
for (let i = 0; i < s.length; i++) { h ^= s.charCodeAt(i); h = Math.imul(h, 0x01000193); }
return h >>> 0; }"""
# The stock fingerprint: FNV-1a over the position + uv ARRAYS of every isStock/buyMesh mesh in the
# open room. NOT transforms — batchRoom merges parody stock at identity transform, so a transform
# hash reads constants and reports "no rotation" over stock that rotates (F's own R30 harness bug).
# The day-salted picks/jitters live in the vertex data; bytes/meshes come back so the caller can
# prove the subject was present (a hash over zero bytes is a green light over nothing).
JS_VERTEX_FP = """() => {
const P = window.PROCITY; let h = 0x811c9dc5 >>> 0, bytes = 0, meshes = 0;
const mix = (arr) => { const u = new Uint8Array(arr.buffer, arr.byteOffset, arr.byteLength);
for (let i = 0; i < u.length; i++) { h = (h ^ u[i]) >>> 0; h = Math.imul(h, 0x01000193) >>> 0; }
bytes += u.length; };
P.interiorMode.current.group.traverse((o) => {
const u = o.userData || {};
if (!(u.isStock || u.buyMesh) || !o.geometry) return;
meshes++;
const pos = o.geometry.getAttribute('position'), uv = o.geometry.getAttribute('uv');
if (pos) mix(pos.array); if (uv) mix(uv.array);
});
return { fp: h >>> 0, bytes, meshes };
}"""
def _game_state(pg):
"""The byte-comparable game state + a savedAt-normalized export (both stringified IN PAGE so key
order is identical on every read — python re-serialization would reorder and fake a diff)."""
return pg.evaluate("""() => { const g = window.PROCITY.game;
const ex = JSON.parse(g.export()); delete ex.savedAt;
return { state: JSON.stringify({ day: g.day, cash: g.cash, town: g.townKey, collection: g.collection }),
exNorm: JSON.stringify(ex), day: g.day, cash: g.cash, n: g.collection.length }; }""")
def _at_bin(pg):
"""Stand at the first bin of the open room, aimed at it (smoke_buy's proven setup)."""
return pg.evaluate("""() => {
const P = window.PROCITY;
const bins = []; P.interiorMode.current.group.traverse(o => { if (o.userData && o.userData.kind === 'bin') bins.push(o); });
if (!bins.length) return { err: 'no bins' };
const bp = new P.THREE.Vector3(); bins[0].getWorldPosition(bp);
P.camera.position.set(bp.x, 1.6, bp.z + 1.4); P.camera.lookAt(bp.x, bp.y + 0.4, bp.z); P.camera.updateMatrixWorld();
return { ok: true };
}""")
def _dig_buy(pg):
"""One REAL buy: E at the aimed bin → riffle → pull the front sleeve → click BUY → WALK OUT.
Returns {cash0,n0,disabled,cash1,n1} read off the live wallet + collection."""
pg.evaluate("() => window.dispatchEvent(new KeyboardEvent('keydown', { code: 'KeyE' }))")
pg.wait_for_timeout(500)
pg.evaluate("""() => { const c = window.PROCITY.renderer.domElement;
c.dispatchEvent(new PointerEvent('pointerdown', { clientX: 640, clientY: 360, bubbles: true }));
c.dispatchEvent(new PointerEvent('pointerup', { clientX: 640, clientY: 360, bubbles: true })); }""")
pg.wait_for_timeout(350)
r = pg.evaluate("""() => {
const P = window.PROCITY, g = P.game, w = P.wallet;
const btn = document.querySelector('.pcdg-panel button');
if (!btn) return { err: 'no pull panel (pull failed)' };
const cash0 = w.cash(), n0 = g.collection.length, disabled = btn.disabled;
btn.click();
const out = document.querySelector('.pcdg-out'); if (out) out.click();
return { cash0, n0, disabled, cash1: w.cash(), n1: g.collection.length };
}""")
pg.wait_for_timeout(250)
return r
def _counter_sell(pg, max_sales=10):
"""Walk to the counter, press E — THE REAL ROUTING opens C's sell card — then click SELL up to
max_sales times (card closes itself when nothing sellable remains). Camera looks UP so the
bin-aim fallback can't win the E (aimed bin → dig is the routing's first branch, by contract)."""
r = pg.evaluate("""() => {
const P = window.PROCITY, room = P.interiorMode.current;
const c = room.counter && room.counter.pose;
if (!c) return { err: 'no counter pose' };
P.camera.position.set(c.x, 1.6, c.z + 1.2);
P.camera.lookAt(c.x, 4.0, c.z); P.camera.updateMatrixWorld(); // aim above the room — no bin under aim
window.dispatchEvent(new KeyboardEvent('keydown', { code: 'KeyE' }));
const panel = document.querySelector('.pcsl-panel');
return { opened: !!(panel && panel.style.display === 'block'), sellActive: P.interiorMode.sellActive };
}""")
if r.get('err') or not r.get('opened'):
return r
sold = 0
for _ in range(max_sales):
s = pg.evaluate("""() => {
const p = document.querySelector('.pcsl-panel');
if (!p || p.style.display !== 'block') return { open: false };
const b = p.querySelector('button');
if (!b || b.disabled) return { open: true, stuck: true };
b.click(); return { open: true, sold: true };
}""")
if not s.get('open') or s.get('stuck'):
break
sold += 1
pg.wait_for_timeout(60)
pg.evaluate("""() => { const p = document.querySelector('.pcsl-panel');
if (p && p.style.display === 'block') { const x = p.querySelector('.x'); if (x) x.click(); } }""")
pg.wait_for_timeout(100)
r['sold'] = sold
return r
def _enter_record_shop(pg):
"""Midday, first OPEN record shop whose counter sits > 2.6 m from every bin (so counter-E can
never fall into binUnderAim's 2.5 m nearest-bin fallback — the routing's dig branch would win).
Returns the shop id it settled in, or an err."""
return pg.evaluate("""() => {
const P = window.PROCITY, D = window.DBG; D.setSegment(2);
const recs = (P.plan.shops || []).filter(s => s.type === 'record' && P.isOpen(s));
if (!recs.length) return { err: 'no open record shop' };
for (const s of recs.slice(0, 6)) {
D.enterShop(s.id);
const room = P.interiorMode.current;
const c = room.counter && room.counter.pose;
const bins = []; room.group.traverse(o => { if (o.userData && o.userData.kind === 'bin') bins.push(o); });
if (c && bins.length) {
const wp = new P.THREE.Vector3();
const near = bins.some(b => { b.getWorldPosition(wp); return Math.hypot(wp.x - c.x, wp.z - c.z) < 2.6; });
if (!near) return { shop: s.id, name: s.name, bins: bins.length };
}
D.exitShop();
}
return { err: 'no record shop with counter clear of bins in the first 6' };
}""")
def smoke_sell_routing(p):
"""R30 §9.4 — THE HELD JOIN, verified live: dig → buy → walk to the counter → E → C's sell card
→ SELL → wallet credits, collection shrinks via game.removeFind, B's HUD follows. Also the two
negative routing legs: E at the counter with NOTHING sellable opens no card; ?game=0 never routes."""
head('GATE R30·routing: the E-key sell routing (dig → counter → sell card → credit + removeFind + HUD)')
b, pg, errs = new_page(p)
try:
boot(pg, 'dig=1')
# HUD present (B's game bar — the gate reads it after the sale)
try:
pg.wait_for_function("() => !!document.getElementById('pc-cash')", timeout=8000)
except Exception:
pass
r0 = _enter_record_shop(pg)
if r0.get('err'): FAIL(f"routing: {r0['err']}"); return
# negative leg FIRST: empty collection ⇒ E at the counter opens NOTHING (open() false, no DOM)
r_neg = _counter_sell(pg, max_sales=0)
if r_neg.get('opened'): FAIL('routing: sell card opened over an EMPTY collection (open() must be false)')
else: OK('routing: E at the counter with nothing sellable → no card, no DOM (fail-closed)')
# buy one through the real dig
_at_bin(pg)
rb = _dig_buy(pg)
if rb.get('err') or rb.get('disabled'): FAIL(f"routing: dig buy failed — {rb}"); return
paid = rb['cash0'] - rb['cash1']
if rb['n1'] == rb['n0'] + 1 and paid > 0:
OK(f"routing: dig buy — ${paid} debited, collection {rb['n0']}{rb['n1']}")
else:
FAIL(f"routing: dig buy wallet/collection delta wrong — {rb}"); return
ent = pg.evaluate("""() => { const g = window.PROCITY.game;
const e = g.collection[g.collection.length - 1];
return { type: e.type, pricePaid: e.pricePaid, hasId: !!(e.sku || e.slotId) }; }""")
if ent['type'] == 'record' and ent['hasId'] and ent['pricePaid'] == paid:
OK(f"routing: entry shape honest — type=record, sku|slotId set, pricePaid == debit (${paid})")
else:
FAIL(f"routing: entry shape wrong — {ent}")
# the sale, through the card
cash_before = pg.evaluate("() => window.PROCITY.wallet.cash()")
n_before = pg.evaluate("() => window.PROCITY.game.collection.length")
rs = _counter_sell(pg, max_sales=1)
if rs.get('err') or not rs.get('opened'):
FAIL(f"routing: E at the counter did not open the sell card — {rs}"); return
# HUD refreshes in STREET mode only (it is hidden inside a shop by design — setVisible(m==='street')),
# so "the HUD follows" is measured where the player can see it: back on the street, frames run.
pg.evaluate("() => window.DBG.exitShop()")
pg.wait_for_timeout(500)
after = pg.evaluate("""() => { const P = window.PROCITY;
const cashEl = document.getElementById('pc-cash'), crateBtn = document.getElementById('pc-crate-btn');
return { cash: P.wallet.cash(), n: P.game.collection.length,
hudCash: cashEl ? cashEl.textContent : null,
hudCrate: crateBtn ? crateBtn.textContent : null }; }""")
credited = after['cash'] - cash_before
if rs.get('sold') == 1 and after['n'] == n_before - 1 and 0 < credited < paid:
OK(f"routing: SELL — +${credited} credited (< ${paid} paid, no-pump by construction), collection {n_before}{after['n']}")
else:
FAIL(f"routing: sale delta wrong — sold={rs.get('sold')}, credited {credited} vs paid {paid}, n {n_before}{after['n']}")
if after['hudCash'] is not None:
if after['hudCash'] == str(after['cash']) and after['hudCrate'] == f"CRATE {after['n']}":
OK(f"routing: B's HUD follows — $" + after['hudCash'] + f" · {after['hudCrate']}")
else:
FAIL(f"routing: HUD stale — hudCash {after['hudCash']} vs {after['cash']}, {after['hudCrate']} vs CRATE {after['n']}")
else:
WARN('routing: game bar not built in this context — HUD leg unobserved (B verified it R30w2)')
if errs: FAIL(f"routing: {len(errs)} console error(s); first: {errs[0][:140]}")
else: OK('routing: 0 console errors')
finally:
b.close()
# ?game=0: the routing must be inert — E at the counter does nothing, zero sell DOM, zero errors
b, pg, errs = new_page(p)
try:
boot(pg, 'dig=1&game=0')
r = pg.evaluate("""() => {
const P = window.PROCITY, D = window.DBG; D.setSegment(2);
const rec = (P.plan.shops || []).find(s => s.type === 'record' && P.isOpen(s));
D.enterShop(rec.id);
const c = P.interiorMode.current.counter.pose;
P.camera.position.set(c.x, 1.6, c.z + 1.2); P.camera.lookAt(c.x, 4.0, c.z); P.camera.updateMatrixWorld();
window.dispatchEvent(new KeyboardEvent('keydown', { code: 'KeyE' }));
return { game: !!P.game, panels: document.querySelectorAll('.pcsl-panel').length,
sellActive: P.interiorMode.sellActive };
}""")
if not r['game'] and r['panels'] == 0 and not r['sellActive']:
OK('routing: ?game=0 — no game, E at the counter is a no-op, zero sell DOM')
else:
FAIL(f"routing: ?game=0 leaked — {r}")
if errs: FAIL(f"routing/game=0: {len(errs)} console error(s); first: {errs[0][:140]}")
finally:
b.close()
def smoke_save_determinism(p):
"""R30 gate (a) — scripted session (buy 2, sell 1, sleep 2×) → export → FRESH boot → import →
byte-equal game state AND world fingerprint unchanged (the delta law's falsifiable form)."""
head('GATE R30a: save/load determinism (buy 2 · sell 1 · sleep 2× → export → fresh boot → import)')
b, pg, errs = new_page(p)
exported = None; snap0 = None; fp0 = None
try:
boot(pg, 'dig=1')
r0 = _enter_record_shop(pg)
if r0.get('err'): FAIL(f"save-det: {r0['err']}"); return
for i in (1, 2):
_at_bin(pg)
rb = _dig_buy(pg)
if rb.get('err') or rb.get('disabled') or rb['n1'] != rb['n0'] + 1:
FAIL(f"save-det: scripted buy {i} failed — {rb}"); return
rs = _counter_sell(pg, max_sales=1)
if rs.get('err') or not rs.get('opened') or rs.get('sold') != 1:
FAIL(f"save-det: scripted sell failed — {rs}"); return
pg.evaluate("() => window.DBG.exitShop()")
pg.wait_for_timeout(200)
try: # B's game bar builds lazily on street frames — prefer the REAL surface
pg.wait_for_function("() => !!document.getElementById('pc-sleep-btn')", timeout=4000)
except Exception:
pass
slept = pg.evaluate("""() => { const g = window.PROCITY.game;
const btn = document.getElementById('pc-sleep-btn');
if (btn) { btn.click(); btn.click(); } else { g.sleep(); g.sleep(); }
return { day: g.day, viaBtn: !!btn }; }""")
if slept['day'] != 3: FAIL(f"save-det: 2 sleeps → day {slept['day']} (want 3)"); return
via = "via B's SLEEP button" if slept['viaBtn'] else "via game.sleep() — HUD bar absent in this context"
OK(f"save-det: scripted session done — bought 2, sold 1, slept 2× → day 3 ({via})")
snap0 = _game_state(pg)
exported = pg.evaluate("() => window.PROCITY.game.export()")
fp0 = pg.evaluate(JS_PLAN_FP)
print(f" subject: day {snap0['day']} · ${snap0['cash']} · {snap0['n']} item(s) · plan fp {fp0:#010x}")
if errs: FAIL(f"save-det/session: {len(errs)} console error(s); first: {errs[0][:140]}"); return
finally:
b.close()
b, pg, errs = new_page(p)
try:
boot(pg, 'dig=1') # fresh browser = fresh localStorage = a day-1 game
fp1 = pg.evaluate(JS_PLAN_FP)
if fp1 != fp0: FAIL(f"save-det: fresh boot plan fp {fp1:#010x} != {fp0:#010x} (seeded world drifted?)"); return
r = pg.evaluate("""(saved) => { const g = window.PROCITY.game;
const fresh = { day: g.day, n: g.collection.length };
const ok = g.import(saved);
return { ok, fresh, week: window.PROCITY.gigs ? window.PROCITY.gigs.weekNight : null }; }""", exported)
if not (r['ok'] and r['fresh']['day'] == 1 and r['fresh']['n'] == 0):
FAIL(f"save-det: import on a fresh boot — {r}"); return
snap1 = _game_state(pg)
fp2 = pg.evaluate(JS_PLAN_FP)
if snap1['state'] == snap0['state'] and snap1['exNorm'] == snap0['exNorm']:
OK(f"save-det: BYTE-EQUAL after import — state ({len(snap1['state'])} bytes) and savedAt-normalized "
f"export ({len(snap1['exNorm'])} bytes) identical to the exporting session")
else:
FAIL(f"save-det: state diverged — day {snap1['day']} vs {snap0['day']}, cash {snap1['cash']} vs "
f"{snap0['cash']}, n {snap1['n']} vs {snap0['n']}")
if fp2 == fp0:
OK(f"save-det: world fingerprint UNCHANGED across export→import ({fp2:#010x}) — the delta law holds")
else:
FAIL(f"save-det: import MOVED THE WORLD — plan fp {fp2:#010x} != {fp0:#010x}")
if r['week'] == 2:
OK('save-det: day-derived state applied on import — weekNight 2 == (31)%7')
else:
FAIL(f"save-det: weekNight {r['week']} after importing day 3 (want 2)")
if errs: FAIL(f"save-det/import: {len(errs)} console error(s); first: {errs[0][:140]}")
else: OK('save-det: 0 console errors both contexts')
finally:
b.close()
def smoke_nopump(p):
"""R30 gate (b) — buy-then-immediately-sell ×5 through the REAL dig + sell-card paths → strictly
negative per trip. PLUS C's negative control (the wave-3 binding): with cash 0, the real buy paths
invoke wallet.buy, it returns false, and NO entry enters the collection — the pump C found is dead."""
head('GATE R30b: the no-pump gate v0 (5 real round trips → monotonic loss · the debit-gate negative control)')
b, pg, errs = new_page(p)
try:
boot(pg, 'dig=1&stock=real')
_pack_ready(pg, 'book') # the negative control's shelf needs the book pack
r0 = _enter_record_shop(pg)
if r0.get('err'): FAIL(f"no-pump: {r0['err']}"); return
deltas = []
for i in range(5):
_at_bin(pg)
cash0 = pg.evaluate("() => window.PROCITY.wallet.cash()")
rb = _dig_buy(pg)
if rb.get('err') or rb.get('disabled') or rb['n1'] != rb['n0'] + 1:
FAIL(f"no-pump: trip {i + 1} buy failed — {rb}"); return
rs = _counter_sell(pg) # sell EVERYTHING so no held item distorts the trip
if rs.get('err') or not rs.get('opened') or rs.get('sold', 0) < 1:
FAIL(f"no-pump: trip {i + 1} sell failed — {rs}"); return
cash1 = pg.evaluate("() => window.PROCITY.wallet.cash()")
deltas.append(cash1 - cash0)
if all(d <= -1 for d in deltas):
OK(f"no-pump: 5/5 round trips strictly negative — deltas {deltas}{sum(deltas)}) — "
f"the spread is the house's, by the sellOffer clamp")
else:
FAIL(f"no-pump: a round trip did NOT lose money — deltas {deltas}")
# ── C's negative control: no debit ⇒ no entry, on the REAL paths ──
broke = pg.evaluate("""() => { const g = window.PROCITY.game;
const ex = JSON.parse(g.export()); ex.cash = 0; ex.savedAt = Date.now();
const ok = g.import(JSON.stringify(ex));
return { ok, cash: g.cash, n: g.collection.length }; }""")
if not (broke['ok'] and broke['cash'] == 0):
FAIL(f"no-pump: could not set up the broke player — {broke}"); return
# arm 1: the dig — pull panel's BUY must be disabled; clicking it moves nothing
_at_bin(pg)
rd = _dig_buy(pg)
if rd.get('err'):
FAIL(f"no-pump/control: dig arm — {rd}")
elif rd['disabled'] and rd['n1'] == rd['n0'] and rd['cash1'] == 0:
OK('no-pump/control dig: BUY disabled at $0 — clicked anyway, zero debit, zero entry')
else:
FAIL(f"no-pump/control dig: {rd} — an unpaid entry got in or cash moved")
# arm 2: the shelf — wallet.buy IS invoked (counted, behavior untouched), returns false,
# and recordFind stays unreachable. This is the exact pump C built by accident, proven dead.
pg.evaluate("() => window.DBG.exitShop()")
pg.wait_for_timeout(200)
ra = pg.evaluate("""() => {
const P = window.PROCITY, D = window.DBG; D.setSegment(2);
const bk = (P.plan.shops || []).find(s => s.type === 'book' && P.isOpen(s));
if (!bk) return { err: 'no open book shop' };
D.enterShop(bk.id);
const meshes = []; P.interiorMode.current.group.traverse(o => {
if (o.userData && o.userData.buyMesh && o.userData.buyItems && o.userData.buyItems.length) meshes.push(o); });
if (!meshes.length) return { err: 'no buy meshes (book pack absent)' };
const m = meshes[0], it = m.userData.buyItems[0], wp = new P.THREE.Vector3();
m.localToWorld(wp.copy(it.center));
P.camera.position.set(wp.x, wp.y, wp.z + 1.0); P.camera.lookAt(wp.x, wp.y, wp.z); P.camera.updateMatrixWorld();
const g = P.game, w = P.wallet, n0 = g.collection.length;
const orig = w.buy; const calls = []; // instrument the REAL facade — count, don't mock
w.buy = function (o) { const r = orig.call(this, o); calls.push(r); return r; };
window.dispatchEvent(new KeyboardEvent('keydown', { code: 'KeyE' }));
w.buy = orig;
return { calls, n0, n1: g.collection.length, cash: w.cash() };
}""")
if ra.get('err'):
FAIL(f"no-pump/control shelf: {ra['err']}")
elif ra['calls'] == [False] and ra['n1'] == ra['n0'] and ra['cash'] == 0:
OK("no-pump/control shelf: wallet.buy INVOKED once, returned false, recordFind unreachable — "
"zero entry, cash still $0 (C's pump is dead)")
else:
FAIL(f"no-pump/control shelf: {ra} — buy not invoked, or an entry minted without a debit")
# positive arm (the control's control): with cash restored the SAME aim buys — the gate discriminates
rp = pg.evaluate("""() => { const g = window.PROCITY.game, w = window.PROCITY.wallet;
const ex = JSON.parse(g.export()); ex.cash = 100; ex.savedAt = Date.now();
g.import(JSON.stringify(ex));
const n0 = g.collection.length, cash0 = w.cash();
window.dispatchEvent(new KeyboardEvent('keydown', { code: 'KeyE' }));
const n1 = g.collection.length, cash1 = w.cash();
const e = g.collection[g.collection.length - 1] || {};
return { n0, n1, cash0, cash1, paidMatches: n1 === n0 + 1 && e.pricePaid === cash0 - cash1 }; }""")
if rp['n1'] == rp['n0'] + 1 and rp['cash1'] < rp['cash0'] and rp['paidMatches']:
OK(f"no-pump/control positive: same aim with cash — debit ${rp['cash0'] - rp['cash1']} == entry.pricePaid, "
f"1 entry in (the control can tell success from failure)")
else:
FAIL(f"no-pump/control positive: {rp} — the negative control was measuring a dead path")
if errs: FAIL(f"no-pump: {len(errs)} console error(s); first: {errs[0][:140]}")
else: OK('no-pump: 0 console errors')
finally:
b.close()
def smoke_classic_purity(p):
"""R30 gate (c) — ?classic=1 and ?game=0: ZERO Storage-prototype calls (instrumented before any
page script), zero game DOM (B's ids), game null. Default boot is the POSITIVE control (the
instrument must SEE calls, and the game DOM must exist) — without it a broken instrument passes
forever. Plus the plan delta: game layer on vs off moves ZERO plan bytes."""
head('GATE R30c: classic purity (instrumented Storage calls · zero game DOM · plan fp game-on == game-off)')
res = {}
for q, name in (('classic=1', 'classic'), ('game=0', 'game0'), ('', 'default')):
b, pg, errs = new_page(p)
try:
pg.add_init_script(STORAGE_INSTRUMENT)
boot(pg, q)
if name == 'default':
try: pg.wait_for_function("() => !!document.getElementById('pc-game')", timeout=8000)
except Exception: pass
r = pg.evaluate("""(ids) => ({
count: window.__storageCalls.length, first: window.__storageCalls.slice(0, 6),
saveTouched: window.__storageCalls.some(c => c.includes('procity-save')),
game: !!(window.PROCITY && window.PROCITY.game),
dom: ids.filter(id => !!document.getElementById(id)),
})""", GAME_DOM_IDS)
r['fp'] = pg.evaluate(JS_PLAN_FP)
r['errs'] = list(errs)
res[name] = r
finally:
b.close()
for name in ('classic', 'game0'):
r = res[name]
if r['count'] == 0 and not r['game'] and not r['dom']:
OK(f"?{'classic=1' if name == 'classic' else 'game=0'}: ZERO Storage calls · game null · zero game DOM")
else:
FAIL(f"?{name}: storage calls {r['count']} (first {r['first']}), game={r['game']}, DOM leak {r['dom']}")
if r['errs']: FAIL(f"?{name}: {len(r['errs'])} console error(s); first: {r['errs'][0][:140]}")
d = res['default']
if d['count'] > 0 and d['saveTouched'] and d['game']:
OK(f"positive control: default boot — instrument SAW {d['count']} Storage call(s) incl. procity-save, game live")
else:
FAIL(f"positive control BROKEN: default boot count {d['count']}, saveTouched {d['saveTouched']}, game {d['game']}"
" — the zero readings above are unproven")
if d['dom'] == GAME_DOM_IDS:
OK('positive control: all 7 game DOM ids present on the default boot')
else:
WARN(f"positive control: game DOM partial on default boot — {d['dom']} (headless rAF timing?)")
if res['game0']['fp'] == d['fp']:
OK(f"plan fp game-on == game-off ({d['fp']:#010x}) — the game layer moves ZERO plan bytes")
else:
FAIL(f"plan fp DIFFERS with the game layer: game0 {res['game0']['fp']:#010x} vs default {d['fp']:#010x}")
print(f" (?classic plan fp {res['classic']['fp']:#010x} differs by design — gig-less plan; its golden is classic_regression's)")
def smoke_rotation(p):
"""R30 gate (d) — rotation determinism over VERTEX DATA (the wave-3 binding): same (seed, day) →
byte-identical stock fp (re-entry AND across a reboot); day 1→2→3 each actually different;
?game=0 == day 1 (the day-1 convention). Subject proven present: bytes and mesh count printed."""
head('GATE R30d: rotation determinism (vertex-data fp · same day byte-identical · day N ≠ N+1 · cross-boot)')
b, pg, errs = new_page(p)
shop_id = None; fps = {}
try:
boot(pg, '')
r0 = _enter_record_shop(pg)
if r0.get('err'): FAIL(f"rotation: {r0['err']}"); return
shop_id = r0['shop']
def fp_of(day_label):
r = pg.evaluate(JS_VERTEX_FP)
if r['bytes'] == 0 or r['meshes'] == 0:
FAIL(f"rotation: VACUOUS — {day_label}: 0 bytes / 0 stock meshes hashed (no subject)"); return None
return r
def reenter():
pg.evaluate("(id) => { window.DBG.exitShop(); }", shop_id); pg.wait_for_timeout(150)
pg.evaluate("(id) => { window.DBG.setSegment(2); window.DBG.enterShop(id); }", shop_id)
pg.wait_for_timeout(150)
a1 = fp_of('day1'); reenter(); a2 = fp_of('day1 re-entry')
if not a1 or not a2: return
if a1['fp'] == a2['fp']: OK(f"day 1: re-entry byte-identical (fp {a1['fp']:#010x}, {a1['bytes']:,} bytes / {a1['meshes']} meshes)")
else: FAIL(f"day 1: re-entry fp moved {a1['fp']:#010x}{a2['fp']:#010x} — same (seed, day) not deterministic")
pg.evaluate("() => { window.DBG.exitShop(); window.PROCITY.game.sleep(); }")
reenter(); b1 = fp_of('day2'); reenter(); b2 = fp_of('day2 re-entry')
if not b1 or not b2: return
if b1['fp'] != a1['fp']: OK(f"day 2 ≠ day 1: stock ROTATED ({a1['fp']:#010x}{b1['fp']:#010x})")
else: FAIL(f"day 2 == day 1: fp {b1['fp']:#010x} — rotation is not happening (or the fp is vacuous)")
if b1['fp'] == b2['fp']: OK(f"day 2: re-entry byte-identical ({b1['fp']:#010x})")
else: FAIL(f"day 2: re-entry fp moved — not deterministic within the day")
pg.evaluate("() => { window.DBG.exitShop(); window.PROCITY.game.sleep(); }")
reenter(); c1 = fp_of('day3')
if not c1: return
if c1['fp'] != b1['fp'] and c1['fp'] != a1['fp']:
OK(f"day 3 ≠ day 2 ≠ day 1 ({c1['fp']:#010x}) — each day is its own crate")
else:
FAIL(f"day 3 fp {c1['fp']:#010x} collides (day1 {a1['fp']:#010x}, day2 {b1['fp']:#010x})")
pg.evaluate("() => window.PROCITY.game.save()")
fps = { 'day1': a1['fp'], 'day3': c1['fp'] }
# cross-boot: reload the SAME browser context (localStorage carries day 3) — same (seed, day) again
boot(pg, '')
day = pg.evaluate("() => window.PROCITY.game.day")
if day != 3: FAIL(f"rotation: reboot loaded day {day} (want 3 — the save didn't carry)"); return
pg.evaluate("(id) => { window.DBG.setSegment(2); window.DBG.enterShop(id); }", shop_id)
pg.wait_for_timeout(150)
r = pg.evaluate(JS_VERTEX_FP)
if r['fp'] == fps['day3']: OK(f"cross-boot: day-3 fp byte-identical after a full reboot ({r['fp']:#010x})")
else: FAIL(f"cross-boot: day-3 fp {r['fp']:#010x} != pre-reboot {fps['day3']:#010x} — rotation not seed-stable")
if errs: FAIL(f"rotation: {len(errs)} console error(s); first: {errs[0][:140]}")
finally:
b.close()
if not fps: return
# the day-1 convention: a ?game=0 boot's stock is byte-identical to a fresh game's day 1
b, pg, errs = new_page(p)
try:
boot(pg, 'game=0')
pg.evaluate("(id) => { window.DBG.setSegment(2); window.DBG.enterShop(id); }", shop_id)
pg.wait_for_timeout(150)
r = pg.evaluate(JS_VERTEX_FP)
if r['fp'] == fps['day1']: OK(f"?game=0 == day 1 ({r['fp']:#010x}) — the pre-v7 town IS day 1, measured")
else: FAIL(f"?game=0 fp {r['fp']:#010x} != day-1 fp {fps['day1']:#010x} — the day-1 convention broke")
finally:
b.close()
def main():
srv = ensure_server()
try:
with sync_playwright() as p:
flags = list(KNOWN_FLAGS)
if plansrc_landed(p):
flags.append('plansrc=osm'); print("· plansrc=osm detected as landed — included")
else:
print("· plansrc=osm not landed (Lane A) — skipping that flag")
classic_regression(p) # [R16] the frozen v2 covenant moved here: ?classic=1 → byte-identical
default_boot_gate(p) # [R16] the flip: the no-flag boot is the full experience (all four on)
interior_budget(p)
for fl in flags:
smoke(p, fl.split('=')[0], fl)
smoke(p, 'ALL-ON combo', '&'.join(flags), is_combo=True)
smoke_stock(p) # STRICT (r8) — ?stock=real real record sleeves
smoke_weather(p) # STRICT (r8) — ?weather=rain
smoke_buy(p) # STRICT (r9) — buy-v0
smoke_patronage(p) # STRICT (r9) — peds visit open shops
smoke_booktoy(p) # STRICT (r9) — book/toy real stock
smoke_presence(p) # new (warn) — interior presence (browsers)
smoke_shelfbuy(p) # new (warn) — buy-anywhere (book/toy shelves)
smoke_tram(p) # new (warn) — tram (auto-skips if not landed)
smoke_audio(p) # R11 — audio house-law (silent-happy · pre-gesture · mute · noassets · leak)
smoke_gigs(p) # R12/13 — the district gig layer (schedule · band · crowd · budget · cover · noassets)
smoke_continuity(p) # R15 — identity continuity (the ped you followed is in the crowd · settled tris)
smoke_glance(p) # R29 — Spike 1: v6's first clip in the game (classic zero-fetch)
smoke_sell_routing(p) # R30 — the E-key sell routing (§9.4, the held join, live loop)
smoke_save_determinism(p) # R30 gate a — export → fresh boot → import byte-equal, world untouched
smoke_nopump(p) # R30 gate b — 5 real round trips monotonic loss + C's negative control
smoke_classic_purity(p) # R30 gate c — instrumented Storage zero + zero game DOM + positive control
smoke_rotation(p) # R30 gate d — vertex-data rotation determinism (the wave-3 binding)
smoke_tier2(p) # R27 — the kill-the-server gate (charter law #1)
smoke_real_crate(p) # R24 — THE REAL CRATE (per-shop atlas identity · soft fall · no-op off the path)
finally:
if srv: srv.terminate()
head('VERDICT')
print(json.dumps({'fails': len(fails), 'warns': len(warns)}, indent=0))
if fails:
print(f"\033[31m● flags_check RED\033[0m — {len(fails)} failure(s), {len(warns)} warning(s).")
for f in fails: print(' FAIL:', f)
sys.exit(1)
print(f"\033[32m● flags_check GREEN\033[0m — {len(warns)} warning(s).")
for w in warns: print(' warn:', w)
sys.exit(0)
if __name__ == '__main__':
main()