#!/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 v2 determinism anchor (?classic boot) — UNCHANGED by the flips GIG_GOLDEN = 0xb1d48ea1 # [R16] the DEFAULT boot's gig-augmented plan fingerprint (A's R16 corner-poster re-pin) # 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 0x3fa36874, 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) 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 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 # (0xb1d48ea1) 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 2–6. 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() 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() 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) 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()