PROCITY/tools/qa/tour_shots.py
m3ultra 8ac3b18b36 Lane F R22 (v4.0 — THE EPOCH CLOSE): the re-measure, the selector, the release tour, the tag
The blocking gate is CLOSED, and the epoch closes green.

- #1b THE BLOCKING RE-MEASURE: F's R21 re-measure made the tri diet blocking; B diagnosed layer-by-layer,
  the driver traced A -> E (A's own commit: "B's numbers say the fix is E's"), E fixed it, F re-measured:
  katoomba venue_night 221,935 (+11%) -> 108,365 (-51%); bendigo 201k -> 124,682; fremantle -> 88,397.
  Worst 124,682 — the 200k stress edge is closed. The measure-first loop worked end to end.
- #3 DBG.enterShop(selector) (Lane G's ask): id · registry type · exact name · name substring, resolved
  most-specific-first so an id-shaped string can't be shadowed. One-liner QA on any of 23 towns.
- #4 The release tour -> docs/shots/release_v4/: the Fremantle hero · the Fitzroy mecca · the Toowoomba thin
  tail · the synthetic control · the ?classic=1 covenant. Every real-town frame poses through F's R21
  cluster primitive; B's R22 bookmarks anchor on the same retail heart, so tour and bookmarks agree by
  construction. HUD hidden throughout (F's R21 ruling corollary).
  The primitive earned itself twice: the first cut posed via street_noon and the MECCA CAME OUT AS BARE
  BITUMEN — the same trap as R21. Re-posed through the cluster, Fitzroy reads.
- HONEST FINDING (filed v4.x): the brief asked for the thin tail as "proof the floor holds". It does not,
  and the frame says so — Toowoomba reads as an empty road. The floor is a RATIO, not a count: 0.008
  shops/edge vs the heroes' 0.06-0.08. Shipped labelled honestly rather than swapped for a flattering town.
- #6 Docs: README v4 section (the towns ARE the product), LANE_F_NOTES §22, F-progress.

qa.sh --strict --matrix GREEN 7/7; MATRIX GREEN — 10 towns × 7 gates (the budget gate green everywhere for
the first time); selfcheck 161,300/161,300 (A's 36 goldens + the 23-town pack).

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

154 lines
8.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""PROCITY Lane F — 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 (0x3fa36874)
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.060.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'<figure><img src="{s["file"]}" alt="{s["name"]}" loading="lazy">'
f'<figcaption><b>{s["name"]}</b><br>{s["caption"]}</figcaption></figure>' for s in shots)
return (
'<!doctype html><meta charset="utf-8"><title>PROCITY v4.0 — the release tour</title>'
'<style>body{background:#14110c;color:#f4efe6;font:14px/1.4 -apple-system,Segoe UI,Roboto,sans-serif;margin:24px}'
'.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(460px,1fr));gap:18px}'
'figure{margin:0;background:#1d1810;border-radius:10px;overflow:hidden}img{width:100%;display:block}'
'figcaption{padding:9px 13px;color:#cdc4b2}b{color:#ffd9a0}</style>'
f'<h1>PROCITY v4.0 — THE REAL MAP <small>· 23 real Australian towns · seed {SEED} · settled</small></h1>'
f'<div class="grid">{cards}</div>')
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()