#!/usr/bin/env python3 """PROCITY Lane F — the v4.0 RELEASE TOUR (round 22 · the epoch close · ledger #4). One deterministic run. The v4 thesis in five frames, into docs/shots/release_v4/ (+ contact.html). The towns ARE the product: • fremantle_hero — the golden-hour hero: a real shopfront corridor with the density crowd on it • fitzroy_mecca — the mecca (160 shops, 2× anything absorbed before this epoch) • toowoomba_thintail — the thin tail (12 shops): proof the floor still reads as a town • synthetic_control — the procedural town, UNCHANGED: the control the whole epoch was measured against • classic_covenant — `?classic=1`: the v2 covenant made visible, byte-identical forever (0x5f76e76) Posing: every real-town frame resolves through Lane B's `DBG` retail-heart anchor / `clusterPose` — the primitive that came out of F's R21 finding (per-EDGE shop counts are ~1 on a real graph, so "densest edge" is the wrong heuristic; you must project the densest SPATIAL cluster onto its front edge and stand on the road). B's R22 bookmark fix resolves through the same anchor, so the tour and the bookmarks agree by construction. The HUD is hidden for every frame (F's R21 ruling corollary — the selector ships, but it doesn't belong in a beauty shot). Run: tools/.venv/bin/python tools/qa/tour_shots.py [--seed N] Needs the Playwright venv + Lane B's ?dbg=1 hook. Writes full-frame 1280x720 PNGs. """ import sys, time, socket, subprocess, pathlib ROOT = pathlib.Path(__file__).resolve().parent.parent.parent PORT = 8130 HOST = f'http://127.0.0.1:{PORT}' SEED = 20261990 OUTDIR = ROOT / 'docs' / 'shots' / 'release_v4' if '--seed' in sys.argv: SEED = int(sys.argv[sys.argv.index('--seed') + 1]) # (file, query, segment, cluster-pose?, caption-prefix). seg 3 = late-afternoon gold; seg 2 = midday. # cluster=True → F's spatial-cluster pose (real towns); cluster=False → the bookmark (synthetic is one dense # cluster already, so street_noon frames it correctly). V4_TOUR = [ ('fremantle_hero', 'plansrc=osm&town=fremantle_real', 3, True, 'the hero — Fremantle, real streets, golden hour'), ('fitzroy_mecca', 'plansrc=osm&town=fitzroy_real', 3, True, 'the mecca — Fitzroy, the densest town in the pack'), ('toowoomba_thintail', 'plansrc=osm&town=toowoomba_real', 2, True, 'the thin tail — Toowoomba: the floor does NOT hold here, and this is the honest frame (12 shops over a 1,459-edge graph = 0.008 shops/edge vs the heroes\' 0.06–0.08; it reads as road, not town). Filed for v4.x'), ('synthetic_control', f'seed={SEED}', 2, False, 'the control — the procedural town, unchanged'), ('classic_covenant', 'classic=1', 2, False, '?classic=1 — the v2 covenant, byte-identical forever'), ] # THE POSING PRIMITIVE (F's R21 finding). On a real graph, per-EDGE shop counts are ~1 — so "the edge with # the most shops" frames an empty arterial (it did, first run: the Fitzroy mecca came out as bare bitumen). # The town's retail heart is a SPATIAL cluster: find the shop lot with the most neighbours inside 60 m, # project it onto its own front edge, and stand ON the road backed off along it, looking down the strip. # Lane B's R22 bookmark fix anchors on the same retail heart — this is that primitive, applied to the tour. SHOT_JS = r""" async ({ seg, cluster }) => { const P = window.PROCITY, D = window.DBG; D.setSegment(seg); let hub = -1; if (!cluster) { D.shot('street_noon'); } else { const plan = P.plan, nById = new Map(plan.streets.nodes.map(n => [n.id, n])); const lots = (plan.lots || []).filter(l => l.use === 'shop' || l.use === 'anchor'); let best = null; for (const l of lots) { let c = 0; for (const o of lots) if (Math.hypot(o.x - l.x, o.z - l.z) < 60) c++; if (c > hub) { hub = c; best = l; } } if (best) { const e = (plan.streets.edges || []).find(x => x.id === best.frontEdge) || plan.streets.edges[0]; const a = nById.get(e.a), b2 = nById.get(e.b); const dx = b2.x - a.x, dz = b2.z - a.z, L = Math.hypot(dx, dz) || 1, ux = dx / L, uz = dz / L; let t = (best.x - a.x) * ux + (best.z - a.z) * uz; t = Math.max(6, Math.min(L - 6, t)); const back = Math.min(26, t); D.teleport(a.x + ux * (t - back), a.z + uz * (t - back), Math.atan2(ux, uz)); } else { D.shot('street_noon'); } } await new Promise(r => setTimeout(r, 1800)); // crowd + chunks + skins + GLBs settle const hud = document.getElementById('pc-hud'); if (hud) hud.style.display = 'none'; // beauty shot for (let i = 0; i < 3; i++) await new Promise(r => requestAnimationFrame(r)); const i = D.info(); return { name: P.plan.name, shops: (P.plan.shops || []).length, hub, edges: (P.plan.streets.edges || []).length, draws: i.drawCalls, tris: i.tris }; } """ def port_up(port): with socket.socket() as s: s.settimeout(0.4) return s.connect_ex(('127.0.0.1', port)) == 0 def ensure_server(): if port_up(PORT): return None proc = subprocess.Popen(['python3', '-m', 'http.server', str(PORT)], cwd=str(ROOT / 'web'), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) for _ in range(50): if port_up(PORT): return proc time.sleep(0.1) proc.terminate(); sys.exit(f'could not start http.server on :{PORT}') def contact_html(shots): cards = ''.join( f'
{s[' f'
{s["name"]}
{s["caption"]}
' for s in shots) return ( 'PROCITY v4.0 — the release tour' '' f'

PROCITY v4.0 — THE REAL MAP · 23 real Australian towns · seed {SEED} · settled

' f'
{cards}
') def main(): try: from playwright.sync_api import sync_playwright except ImportError: sys.exit('playwright not installed — tools/.venv/bin/python -m playwright install chromium') print(f"\033[1mv4.0 RELEASE TOUR\033[0m ({len(V4_TOUR)} frames → docs/shots/release_v4/)") OUTDIR.mkdir(parents=True, exist_ok=True) srv = ensure_server() shots, errs_total = [], 0 try: with sync_playwright() as p: b = p.chromium.launch() for name, query, seg, cluster, cap in V4_TOUR: pg = b.new_page(viewport={'width': 1280, 'height': 720}) errs = [] pg.on('pageerror', lambda e: errs.append(str(e))) pg.goto(f'{HOST}/index.html?{query}&dbg=1') pg.wait_for_function('window.PROCITY && window.PROCITY.plan && window.DBG && window.DBG.ready', timeout=25000) pg.evaluate("() => { const o=document.getElementById('pc-start'); if(o) o.style.display='none'; }") try: pg.wait_for_function('() => !window.PROCITY.fleet || window.PROCITY.fleet.ready', timeout=15000) except Exception: pass r = pg.evaluate(SHOT_JS, {'seg': seg, 'cluster': cluster}) pg.screenshot(path=str(OUTDIR / f'{name}.png')) caption = f"{cap} · {r['name']} · {r['shops']} shops / {r['edges']} edges" + (f" · retail heart: {r['hub']} shops/60m" if r.get('hub', -1) > 0 else '') + f" · {r['draws']} draws · {r['tris']:,} tris" shots.append({'name': name, 'file': f'{name}.png', 'caption': caption}) print(f" \033[32m●\033[0m {name}: {caption}") errs_total += len(errs) if errs: print(f" ! {len(errs)} page error(s): {errs[0][:90]}") pg.close() b.close() finally: if srv: srv.terminate() (OUTDIR / 'contact.html').write_text(contact_html(shots)) print(f"\n\033[1m{len(shots)} frames\033[0m → docs/shots/release_v4/ (+ contact.html) · {errs_total} page errors") sys.exit(1 if errs_total or len(shots) < len(V4_TOUR) else 0) if __name__ == '__main__': main()