PROCITY/tools/flags_check.py
m3ultra 146b5f3d2d Lane F R15 (v3.0): retire the R12 single-venue alias (ledger #2)
The compat alias delegating to the primary venue is gone — every read is now keyed by venueShopId.
B swept the cross-lane readers in R14; this migrates F's own tools + the spec, one atomic commit.

- gig_state.js: delete the alpha-shape alias block + the now-unused primaryId/P; update() just ticks all latches
- flags_check.py + gig_shot.py: .venueShopId->venueShopIds[0], .state->stateOf(id), .cover->coverOf(id), .bandName->bandNameOf(id)
- CITY_SPEC \S v3: delete the two alias sentences, add the omitted nightOf(id) to the accessor list

selfcheck 14264/14264 green (base 0x3fa36874, gig 0x4f4a549d); flags_off + smoke_gigs green, 0 warn.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 11:14:19 +10:00

974 lines
57 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 0x3fa36874, 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
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 = 0x3fa36874 # frozen determinism anchor (Lane A selfcheck) — UNCHANGED by the R7 flip
# 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 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, 'gigs=1')
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 flags_off_regression(p):
head('FLAGS-OFF REGRESSION (prime-law enforcement)')
b, pg, errs = new_page(p)
try:
boot(pg, '')
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)
hash_ok = (f'{GOLDEN_HASH:#010x}'.replace('0x', '0x') in r.stdout) or (hex(GOLDEN_HASH) in r.stdout) \
or ('3fa36874' in r.stdout)
if hash_ok and r.returncode == 0: OK(f'plan fingerprint == golden {hex(GOLDEN_HASH)} (selfcheck green)')
else: FAIL(f'plan fingerprint != golden {hex(GOLDEN_HASH)} — determinism/prime-law broken')
# winmap + dig stay OPT-IN (off by default). The streamed roster is now the shipping DEFAULT
# (R7 flip / prime-law amendment) — so it must be ON, not off, on a no-flag boot.
if state['winmap'] or state['digActive']:
FAIL(f"an opt-in v2 flag is active on default boot (winmap/dig): {state}")
else: OK('opt-in flags off on default boot (winmap/dig)')
if state['streamMode']:
OK('R7 roster flip in effect — streamed roster ACTIVE on default boot')
else: FAIL('roster flip REGRESSED — default boot is not streaming (expected streamMode:true)')
# [R12] gig layer absent flags-off (prime flag law for v3). Any of these leaking means ?gigs=1
# has escaped its flag and the frozen v2 goldens are living on borrowed time.
gig_leaks = [k for k in ('gigState', 'planGigs', 'posters', 'pub', 'venueGeo') if state[k]]
if not gig_leaks:
OK('v3 gig layer wholly absent flags-off (no state machine, no plan.gigs/posters, no pub, no venue geometry)')
else:
FAIL(f"v3 gig layer LEAKED into the default boot: {[(k, state[k]) for k in gig_leaks]}")
if state['draws'] <= STREET_DRAWS_MAX and state['tris'] <= STREET_TRIS_MAX:
OK(f"flags-off street view within budget (draws {state['draws']}{STREET_DRAWS_MAX}, tris {state['tris']}{STREET_TRIS_MAX})")
else: FAIL(f"flags-off 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) on default boot; first: {errs[0][:140]}")
else: OK('0 console errors on default boot')
# 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 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, 'gigs=1')
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, 'gigs=1')
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) };
});
// 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 (1.4 <= f['stature'] <= 2.0) or f['crown'] >= night['H']]
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, 'gigs=1')
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) };
});
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 (1.4 <= f['stature'] <= 2.0)]
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, 'gigs=1')
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, 'gigs=1')
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&gigs=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, 'gigs=1&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()
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")
flags_off_regression(p)
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 — v3.0-alpha gig layer (schedule · band · crowd · budget · cover · noassets)
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()