EVERY CRATE IS DIFFERENT, and F measured it rather than taking G's 15/15 on trust — from the indexes of record, not by entering one lucky shop: 15 keyed crates, 105 pairs, 0 sharing a title; 0 crates overlap the generic v2 pack; 0 ids reused across crates (G's sku fix holds at scale — the R24 vacuity dead at the source, not papered over in F's gate). 4 crates in Red Hill + 11 in Newtown = the manifest's 15 exactly, and the manifest lists nothing not on disk. SOURCING, NOT TIER — C was right and the gate follows C. `tier` is the charter's ladder rung and a mint atlas is ALSO rung 1: still a file, still offline, still deterministic. What differs is where the stock came from. 15/15 indexes declare sourcing, every one agrees with the manifest, a mint crate can never report as real, and the gate PRINTS it per room — provenance a human never reads isn't doing anything (R24's licence lesson, one round on). THE 404 TOLERANCE IS DELETED, NOT TUNED. R24 probed every keyed shop and ate a 404 per atlas-less one; F's smoke could only cope by NAMING them, and a tolerance is a confession. F filed the real fix as a contract ask rather than inventing it; G built the manifest; F now consumes it — the shell asks for nothing unlisted. Zero 404s because nothing absent gets asked for, not because the gate stopped looking. AND DELETING IT IMMEDIATELY CAUGHT A LEFTOVER: F had wired the boot preload but left a second blind probe at the door, 404ing on every keyed-but-unfetchable shop. While the gate said "8 probes, expected", a ninth was invisible too. A named exception starts protecting new bugs the moment the real fix exists. F's beta gate GREEN: distinctness · sourcing (real + mint rooms) · soft fall silent · ZERO 404s · 0 console errors, no attribution exceptions. Selfcheck ALL GREEN 156,212/156,212. TAG HELD — atlas-QA FAIL, 14 errors, and it is not the product. E's SLOT_ID_PREFIX = "sku_" (validate_atlas.py:73) applies to every pack; E reads `sourcing` at :178 but the slot-id check is sourcing-blind. All 14 mint crates fail; the real crate passes. A mint crate HAS no POS sku — it was never in that shop's till — so the rule demands what cannot exist, the same shape as R24's sweep demanding a cafe carry a godverseShopId. F measured the rule's intent and it holds: mint_<dealgod listing id>, non-positional, sorted by it, seeded bake, 0 ids reused across all 15 crates. Only the literal prefix fails. And G refused to go green the easy way for the best reason — F's own R24 collision grown up: a POS sku and a dealgod listing id are different id spaces, and filing one under `sku_` recreates that bug INSIDE the economy, where tier-2's sold-means-gone looks up POS skus. R24's collision entered the wrong shop; the same collision in the money layer sells the wrong record. F did not fix E's one line: E's file, C's contract, C's call as its author (the principle Fable ratified when C overruled `tier`) — and F is the party that benefits, since that line is the only thing between F and the tag. Cost of waiting: one line. Cost of F being quietly wrong: a gate that green-lights id-space collisions in the layer that moves money. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1273 lines
78 KiB
Python
1273 lines
78 KiB
Python
#!/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)
|
||
# [R24] These are TWO assertions and they get two verdicts. This gate used to require
|
||
# `hash_ok and r.returncode == 0` and report BOTH failures with the fingerprint's message — so
|
||
# any unrelated red anywhere in Lane A's selfcheck (R24: 8 godverse-identity checks in a
|
||
# different town) screamed "determinism/prime-law broken", the loudest alarm we own, about a
|
||
# covenant that was provably intact (0x3fa36874, node-side, unmoved). A gate must name what it
|
||
# actually measured; F wrote this one, so F fixed it. The covenant is asserted on its own.
|
||
if hash_ok: OK(f'plan fingerprint == golden {hex(GOLDEN_HASH)} — the classic covenant holds')
|
||
else: FAIL(f'plan fingerprint != golden {hex(GOLDEN_HASH)} — determinism/prime-law broken')
|
||
if r.returncode == 0: OK('Lane A selfcheck green')
|
||
else:
|
||
tail = [l for l in r.stdout.splitlines() if l.strip().startswith('✗')][:3]
|
||
FAIL('Lane A selfcheck RED (separate from the covenant above): '
|
||
+ ('; '.join(t.strip() for t in tail) if tail else 'see selfcheck output'))
|
||
|
||
# winmap flips OFF under ?classic (it's default-on now); dig stays opt-in. The streamed roster IS the
|
||
# v2.0 baseline (R7 flip / prime-law amendment) — so it must be ON under ?classic.
|
||
if state['winmap'] or state['digActive']:
|
||
FAIL(f"a flipped/opt-in flag leaked into ?classic (winmap/dig): {state}")
|
||
else: OK('flipped + opt-in flags off under ?classic (winmap/dig)')
|
||
if state['streamMode']:
|
||
OK('R7 roster flip in effect — streamed roster ACTIVE under ?classic (the v2 baseline)')
|
||
else: FAIL('roster flip REGRESSED — ?classic is not streaming (expected streamMode:true)')
|
||
|
||
# [R12/R16] gig layer wholly absent under ?classic. Any of these present means the gig layer escaped
|
||
# the classic gate and the frozen v2 covenant is broken.
|
||
gig_leaks = [k for k in ('gigState', 'planGigs', 'posters', 'pub', 'venueGeo') if state[k]]
|
||
if not gig_leaks:
|
||
OK('v3 gig layer wholly absent under ?classic (no state machine, no plan.gigs/posters, no pub, no venue geometry)')
|
||
else:
|
||
FAIL(f"v3 gig layer LEAKED into ?classic: {[(k, state[k]) for k in gig_leaks]}")
|
||
|
||
if state['draws'] <= STREET_DRAWS_MAX and state['tris'] <= STREET_TRIS_MAX:
|
||
OK(f"?classic street view within budget (draws {state['draws']}≤{STREET_DRAWS_MAX}, tris {state['tris']}≤{STREET_TRIS_MAX})")
|
||
else: FAIL(f"?classic street view OVER budget: draws {state['draws']}, tris {state['tris']}")
|
||
|
||
# Drift WARN is DRAWS-ONLY — tris is crowd-variable post-flip (guarded by the hard cap above).
|
||
dd = abs(state['draws'] - BASE_DRAWS) / BASE_DRAWS
|
||
if dd <= 0.40:
|
||
OK(f"draws within 40% of re-pinned baseline ({state['draws']} vs {BASE_DRAWS}); tris {state['tris']} (crowd-variable, ≤cap)")
|
||
else:
|
||
WARN(f"draws drifted >40% from baseline ({state['draws']} vs {BASE_DRAWS}) — investigate")
|
||
|
||
if errs: FAIL(f"{len(errs)} console error(s) under ?classic; first: {errs[0][:140]}")
|
||
else: OK('0 console errors under ?classic')
|
||
|
||
# R7 escape hatch: ?roster=v1 must restore the fixed roster (streamMode:false), render, no errors.
|
||
b2, pg2, errs2 = new_page(p)
|
||
try:
|
||
boot(pg2, 'roster=v1')
|
||
esc = pg2.evaluate("() => ({ streamMode: !!(window.PROCITY.citizens||{}).streamMode, mode: window.DBG.info().mode })")
|
||
if not esc['streamMode'] and esc['mode'] == 'street':
|
||
OK('escape hatch ?roster=v1 → fixed roster (streamMode:false), renders')
|
||
else: FAIL(f"escape hatch ?roster=v1 broken: {esc}")
|
||
if errs2: FAIL(f"?roster=v1: {len(errs2)} console error(s); first: {errs2[0][:140]}")
|
||
finally:
|
||
b2.close()
|
||
finally:
|
||
b.close()
|
||
|
||
|
||
def default_boot_gate(p):
|
||
# [R16 — THE FLIP] The enforcement arm of the DEFAULT law: the no-flag boot is now the full experience —
|
||
# all four flags on, the gig layer present, deterministic to the gig golden. This is the number every
|
||
# budget/smoke now means (its inverse is classic_regression above).
|
||
head('DEFAULT-BOOT GATE (the flip → the town shows its best by default)')
|
||
b, pg, errs = new_page(p)
|
||
try:
|
||
boot(pg, '')
|
||
state = pg.evaluate("""() => {
|
||
const P = window.PROCITY, D = window.DBG;
|
||
D.setSegment(5); D.shot('venue_night'); // NIGHT at a venue → the busiest default view (gigs+pools+winmap+tram)
|
||
const i = D.info();
|
||
return { flags: P.flags,
|
||
gigState: !!P.gigs, planGigs: (P.plan.gigs||[]).length, posters: (P.plan.posters||[]).length,
|
||
pub: !!(P.plan.shops||[]).find(s => s.venue), venueGeo: !!P.scene.getObjectByName('venue-presentation'),
|
||
winmapActive: !!(P.winmap && P.winmap.active), tram: !!P.scene.getObjectByName('tram'),
|
||
draws: i.drawCalls, tris: i.tris };
|
||
}""")
|
||
f = state['flags'] or {}
|
||
if f.get('gigs') and f.get('weather') and f.get('winmap') and f.get('tram') and not f.get('classic'):
|
||
OK('default boot flips all four ON (gigs · weather · winmap · tram), classic off')
|
||
else:
|
||
FAIL(f"default boot flag intent wrong: {f}")
|
||
present = [k for k in ('gigState', 'planGigs', 'posters', 'pub', 'venueGeo') if state[k]]
|
||
if len(present) == 5 and state['tram']:
|
||
OK(f"the town is live: gig layer present ({state['planGigs']} gigs, {state['posters']} posters, venue geo) · tram up (winmap is a window shader — its own smoke)")
|
||
else:
|
||
FAIL(f"default boot not fully live: gig={present}, tram={state['tram']}")
|
||
# The default boot's plan IS the gig-augmented plan; selfcheck pins its fingerprint to the gig golden
|
||
# (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()
|
||
|
||
|
||
GODVERSE_TOWN = 'redhill_godverse'
|
||
GODVERSE_SHOP = 3962749 # Monster Robot Party, 147 Musgrave Rd, Red Hill QLD — the one REAL crate
|
||
GODVERSE_BASE = f'assets/stock_godverse/{GODVERSE_SHOP}/'
|
||
STOCK_MANIFEST = 'assets/stock_godverse/index.json'
|
||
|
||
|
||
def smoke_real_crate(p):
|
||
"""R26 — THE BETA GATE: every crate different, sourcing visible, and NOT ONE 404.
|
||
|
||
The lineage of this gate is the epoch's whole argument. R23's #7a would have gone green on "real
|
||
covers render" while the room held the generic v2 pack. R24's brief then said assert id equality —
|
||
which also couldn't discriminate, because both packs numbered items positionally and every atlas id
|
||
was a generic-pack id too. What survives is what a wrong pack cannot fake: TITLES only this crate
|
||
has, and the TEXTURE URL the GPU actually samples.
|
||
|
||
R26 adds the two beta claims. DISTINCTNESS: 15 keyed crates, pairwise title-disjoint — asserted
|
||
across the indexes of record, so it cannot pass by entering one lucky shop. SOURCING (C's §7.2b,
|
||
which correctly overruled the brief's `tier`): every index declares real|mint, a mint crate can
|
||
never report as real, and the gate PRINTS it.
|
||
|
||
And the 404s are gone for the right reason. R24 probed every keyed shop and ate a 404 for each one
|
||
with no atlas; F's smoke could only tolerate that by naming it — a tolerance is a confession. G's
|
||
manifest now says which (shop, type) pairs exist, so the shell asks for nothing else. ZERO console
|
||
errors, NO attribution exceptions. Subject absent ⇒ SKIP with the reason, never a quiet pass."""
|
||
head(f'SMOKE: the beta crate gate (R26 — 15 crates distinct · sourcing · zero 404s · {GODVERSE_TOWN})')
|
||
b, pg, errs = new_page(p)
|
||
four04 = []
|
||
pg.on('response', lambda r: four04.append(r.url) if r.status == 404 else None)
|
||
try:
|
||
pg.goto(f'{HOST}/{STOCK_MANIFEST}')
|
||
if 'godverseShopId' not in pg.content():
|
||
print('· SKIP: no atlas manifest — subject absent, nothing asserted (Lane G)'); return
|
||
boot(pg, f'plansrc=osm&town={GODVERSE_TOWN}&stock=real&dig=1')
|
||
res = pg.evaluate("""async ({manifest, real, base}) => {
|
||
const D = window.DBG, P = window.PROCITY;
|
||
const man = await (await fetch(manifest)).json();
|
||
|
||
// ── DISTINCTNESS across the indexes of record (not just the shop we walk into) ──
|
||
const crates = [];
|
||
for (const sh of (man.shops || [])) for (const t of (sh.types || [])) {
|
||
const u = `assets/stock_godverse/${sh.godverseShopId}/stock_${t}_index.json`;
|
||
const r = await fetch(u);
|
||
if (!r.ok) return { fail: `manifest lists ${u} but it 404s` };
|
||
const idx = await r.json();
|
||
crates.push({ id: sh.godverseShopId, type: t, manSourcing: sh.sourcing,
|
||
sourcing: idx.sourcing || null,
|
||
titles: (idx.items || []).map(i => i.title) });
|
||
}
|
||
const gen = {};
|
||
for (const t of ['record', 'book', 'toy'])
|
||
gen[t] = new Set((((await (await fetch(`assets/models/stock_${t}_index.json`)).json()).items) || [])
|
||
.map(i => i.title));
|
||
let shared = 0, vsGeneric = 0, noSourcing = 0, manMismatch = 0;
|
||
for (let i = 0; i < crates.length; i++) {
|
||
if (!crates[i].sourcing) noSourcing++;
|
||
if (crates[i].sourcing !== crates[i].manSourcing) manMismatch++;
|
||
if (crates[i].titles.some(t => gen[crates[i].type] && gen[crates[i].type].has(t))) vsGeneric++;
|
||
for (let j = i + 1; j < crates.length; j++) {
|
||
const a = new Set(crates[i].titles);
|
||
if (crates[j].titles.some(t => a.has(t))) shared++;
|
||
}
|
||
}
|
||
|
||
// ── the ROOM binds to the right crate: one REAL, one MINT, one soft fall ──
|
||
const byId = new Map((man.shops || []).map(s => [s.godverseShopId, s]));
|
||
const stocked = (P.plan.shops || []).filter(s => { const e = byId.get(s.godverseShopId);
|
||
return e && (e.types || []).includes(s.type); });
|
||
const unfetch = (P.plan.shops || []).find(s => { if (!s.godverseShopId) return false;
|
||
const e = byId.get(s.godverseShopId);
|
||
return !e || !(e.types || []).includes(s.type); });
|
||
const visit = async (shop) => {
|
||
D.exitShop(); await new Promise(r => setTimeout(r, 250));
|
||
D.enterShop('g:' + shop.godverseShopId); await new Promise(r => setTimeout(r, 1200));
|
||
const si = D.stockInfo();
|
||
const c = crates.find(c => c.id === shop.godverseShopId && c.type === shop.type);
|
||
const own = c ? new Set(c.titles) : new Set();
|
||
return { name: si.name, gid: si.godverseShopId, want: shop.godverseShopId, type: si.type,
|
||
base: si.base, packItems: si.packItems,
|
||
sourcing: c ? c.sourcing : null,
|
||
inOwn: si.packTitles.filter(t => own.has(t)).length,
|
||
fromOwnTex: si.texUrls.filter(u => u.includes(`stock_godverse/${shop.godverseShopId}/`)).length,
|
||
fromGenericTex: si.texUrls.filter(u => u.includes('assets/models/stock_')).length };
|
||
};
|
||
const realShop = stocked.find(s => s.godverseShopId === real);
|
||
const mintShop = stocked.find(s => (byId.get(s.godverseShopId) || {}).sourcing === 'mint');
|
||
const out = { crates: crates.length, shared, vsGeneric, noSourcing, manMismatch,
|
||
stocked: stocked.length,
|
||
realRoom: realShop ? await visit(realShop) : null,
|
||
mintRoom: mintShop ? await visit(mintShop) : null };
|
||
if (unfetch) { D.exitShop(); await new Promise(r => setTimeout(r, 250));
|
||
D.enterShop('g:' + unfetch.godverseShopId); await new Promise(r => setTimeout(r, 900));
|
||
const o = D.stockInfo();
|
||
out.softFall = { name: o.name, gid: o.godverseShopId, want: unfetch.godverseShopId,
|
||
packItems: o.packItems }; }
|
||
return out;
|
||
}""", {'manifest': STOCK_MANIFEST, 'real': GODVERSE_SHOP, 'base': GODVERSE_BASE})
|
||
if res.get('fail'):
|
||
FAIL(f"beta crate gate: {res['fail']}"); return
|
||
|
||
# ── distinctness (independently measured; G reported 15/15) ──
|
||
pairs = res['crates'] * (res['crates'] - 1) // 2
|
||
OK(f"DISTINCTNESS: {res['crates']} keyed crates, {pairs} pairs, {res['shared']} sharing a title") \
|
||
if res['shared'] == 0 else FAIL(f"DISTINCTNESS: {res['shared']} crate pair(s) share titles")
|
||
OK(f"DISTINCTNESS: 0/{res['crates']} crates overlap the generic v2 pack") \
|
||
if res['vsGeneric'] == 0 else FAIL(f"{res['vsGeneric']} crate(s) overlap the generic pack")
|
||
# ── sourcing (C §7.2b — an index with no `sourcing` FAILS; the gate PRINTS it) ──
|
||
OK(f"SOURCING: {res['crates']}/{res['crates']} indexes declare sourcing") \
|
||
if res['noSourcing'] == 0 else FAIL(f"SOURCING: {res['noSourcing']} index(es) declare none")
|
||
OK('SOURCING: every index agrees with the manifest') \
|
||
if res['manMismatch'] == 0 else FAIL(f"SOURCING: {res['manMismatch']} index/manifest disagreement(s)")
|
||
|
||
for label, want in (('realRoom', 'real'), ('mintRoom', 'mint')):
|
||
r = res.get(label)
|
||
if not r:
|
||
FAIL(f"{label}: no {want} crate reachable in {GODVERSE_TOWN}"); continue
|
||
if r['gid'] != r['want']:
|
||
FAIL(f"{label}: entered the WRONG shop — wanted {r['want']}, got {r['gid']}"); continue
|
||
print(f" room: {r['name']} (godverse {r['gid']}, {r['type']}) → {r['base']} [sourcing: {r['sourcing']}]")
|
||
OK(f" {want}: sourcing reports '{r['sourcing']}'") if r['sourcing'] == want \
|
||
else FAIL(f" {want} crate reports sourcing '{r['sourcing']}' — a {want} crate must never report otherwise")
|
||
OK(f" {want}: TITLE equality {r['inOwn']}/{r['packItems']} from its OWN crate") \
|
||
if (r['packItems'] > 0 and r['inOwn'] == r['packItems']) \
|
||
else FAIL(f" {want}: only {r['inOwn']}/{r['packItems']} titles are its own")
|
||
OK(f" {want}: TEXTURE provenance {r['fromOwnTex']} own / {r['fromGenericTex']} generic") \
|
||
if (r['fromOwnTex'] > 0 and r['fromGenericTex'] == 0) \
|
||
else FAIL(f" {want}: textures {r['fromOwnTex']} own / {r['fromGenericTex']} generic — wrong file")
|
||
|
||
sf = res.get('softFall')
|
||
if sf:
|
||
if sf['gid'] != sf['want']: FAIL(f"soft fall: entered the wrong shop ({sf['gid']} != {sf['want']})")
|
||
elif sf['packItems'] == 0: OK(f"soft fall: keyed, unfetchable type \"{sf['name']}\" → parody, silently")
|
||
else: FAIL(f"soft fall BROKEN: \"{sf['name']}\" inherited {sf['packItems']} items")
|
||
|
||
# ── ZERO 404s. No exceptions. The manifest is what makes this honest. ──
|
||
OK('ZERO 404s — the manifest means we never ask for an atlas that is not there') \
|
||
if not four04 else FAIL(f"{len(four04)} unexpected 404(s) — the manifest should have prevented every one; first: {four04[0]}")
|
||
OK('0 console errors (no attribution exceptions)') if not errs \
|
||
else FAIL(f'the crate: {len(errs)} console error(s); first: {errs[0][:140]}')
|
||
finally:
|
||
b.close()
|
||
|
||
|
||
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)
|
||
smoke_real_crate(p) # R24 — THE REAL CRATE (per-shop atlas identity · soft fall · no-op off the path)
|
||
finally:
|
||
if srv: srv.terminate()
|
||
|
||
head('VERDICT')
|
||
print(json.dumps({'fails': len(fails), 'warns': len(warns)}, indent=0))
|
||
if fails:
|
||
print(f"\033[31m● flags_check RED\033[0m — {len(fails)} failure(s), {len(warns)} warning(s).")
|
||
for f in fails: print(' FAIL:', f)
|
||
sys.exit(1)
|
||
print(f"\033[32m● flags_check GREEN\033[0m — {len(warns)} warning(s).")
|
||
for w in warns: print(' warn:', w)
|
||
sys.exit(0)
|
||
|
||
if __name__ == '__main__':
|
||
main()
|