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>
154 lines
8.4 KiB
Python
154 lines
8.4 KiB
Python
#!/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.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'<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()
|