PROCITY/tools/qa/town_matrix.py
m3ultra ddd1e6827e Lane F R19 (v4.0-alpha close): the alpha gate + roads dimension + the first real-street money shot
A resolved the R18 findings (fragmentation cull/join, poster clamp, goldens pinned) + D verified; F gated.

- town_matrix.py: a `roads` dimension — a schema-v2 cache (roads[]) must build the REAL street graph
  (norm.mode==='roads', read via the plan_osm opts.report sink); synthetic/fixtures stay marched/synthetic.
  10 towns x 6 gates: real roads green everywhere; classic/default proven untouched (roads ride the cache
  schema, not a flag — 0x3fa36874 frozen).
- docs/shots/v4_alpha/katoomba_mainstreet.png: the first real-street money shot — Katoomba's actual main
  street (asphalt + lane markings, footpaths, a pedestrian crossing, a walking crowd, gig posters, B's town
  selector in-frame), PROCITY's look on real bones, 121k tris midday.
- docs: LANE_F_NOTES §18 (why F correctly HELD in R18 — the gate would have failed by design) + §19 (the
  close), README real-roads note, F-progress.

qa.sh --strict --soak GREEN 7/7. One filed NON-blocking finding: katoomba_real venue_night 200,965 tris
(+965 / 0.5% over the soft ceiling, poster-count-scales-with-real-edges driven; midday play is 121k) — a
v4.0-beta hardening item (A caps posters per real town), on the opt-in --matrix warn-gate, not a play view.

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

179 lines
8.5 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 town-matrix smoke (round 17, v3.2 · the real-map scout gate · ledger #7).
The gate side of the scout: run the core smokes across the FULL town matrix — synthetic hero seeds, the
three checked-in osm fixtures, and Lane E's real AU town caches — and emit one town × gate table. This is
the evidence John charters v4 (THE REAL MAP) on: proof the existing CityPlan contract eats real geography
through the default boot, with no new game systems.
Gates per town (all on the DEFAULT boot — gigs/weather/winmap/tram on, post-R16 flip):
• boot — the plan builds + the shell reaches window.PROCITY.plan, 0 console errors
• determ — two fresh in-page generations are byte-equal (the generator is deterministic)
• budget — the busiest default view (venue block at night) ≤ 300 draws / 200k tris
• district — the gig layer placed on this town: ≥1 venue, a week schedule, posters
• noassets — ?noassets=1 boots silent (0 errors, 0 network); real caches fall back to a fixture (correct)
Run: tools/.venv/bin/python tools/qa/town_matrix.py
Needs the Playwright venv + Lane B's ?dbg=1 hook + Lane F's real-cache shell wiring (R17).
"""
import sys, time, socket, subprocess, pathlib
ROOT = pathlib.Path(__file__).resolve().parent.parent.parent
PORT = 8130
HOST = f'http://127.0.0.1:{PORT}'
STREET_DRAWS_MAX, STREET_TRIS_MAX = 300, 200_000
# the matrix: (row-label, boot-query). Synthetic hero seeds + 3 fixtures + E's 5 real AU caches.
MATRIX = (
[(f'synthetic/{s}', f'seed={s}') for s in (20261990, 1234)]
+ [(f'osm/{t}', f'plansrc=osm&town={t}') for t in ('melbourne', 'katoomba', 'silverton')]
+ [(f'real/{t}', f'plansrc=osm&town={t}')
for t in ('bendigo_real', 'castlemaine_real', 'fremantle_real', 'katoomba_real', 'newtown_real')]
)
# default-boot gates 1-4, in one page. Determinism regenerates the plan twice in-page (the real cache is
# already registered by the shell) and compares — no second full boot needed.
DEFAULT_JS = r"""
async () => {
const P = window.PROCITY, D = window.DBG;
const plan = P.plan;
const params = new URLSearchParams(location.search);
const src = params.get('plansrc') === 'osm' ? 'osm' : 'synthetic';
const town = params.get('town') || undefined;
const cg = await import('./js/citygen/index.js');
const g1 = JSON.stringify(cg.generatePlanFor(plan.citySeed, src, { town, gigs: true }));
const g2 = JSON.stringify(cg.generatePlanFor(plan.citySeed, src, { town, gigs: true }));
const rep = {}; // [R19] report sink → norm.mode (roads | marched)
cg.generatePlanFor(plan.citySeed, src, { town, gigs: true, report: rep });
const venues = (plan.shops || []).filter(s => s.venue).length;
D.shot('venue_night'); // busiest default view: the venue block at night
await new Promise(r => setTimeout(r, 500));
const i = D.info();
return { name: plan.name, shops: (plan.shops || []).length, venues,
gigs: (plan.gigs || []).length, posters: (plan.posters || []).length,
determ: g1 === g2, draws: i.drawCalls, tris: i.tris,
mode: rep.mode || (src === 'synthetic' ? 'synthetic' : 'marched'),
edges: (plan.streets.edges || []).length };
}
"""
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 run_town(new_page, query, label):
"""Returns a dict of gate → (ok, note) for one town."""
row = {}
is_real = label.startswith('real/')
# ── default boot: boot / determ / budget / district ──
pg = new_page()
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)))
try:
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=12000)
except Exception:
pass
d = pg.evaluate(DEFAULT_JS)
row['name'] = d['name']; row['shops'] = d['shops']
row['boot'] = (bool(d['name']) and len(errs) == 0, f"{d['shops']} shops, {len(errs)} err")
row['determ'] = (d['determ'], 'byte-equal' if d['determ'] else 'DIFF')
row['budget'] = (d['draws'] <= STREET_DRAWS_MAX and d['tris'] <= STREET_TRIS_MAX,
f"{d['draws']}d/{d['tris']//1000}k")
row['district'] = (d['venues'] >= 1 and d['gigs'] > 0,
f"{d['venues']}v/{d['gigs']}g/{d['posters']}p")
# [R19] roads dimension: real caches (schema v2 with roads[]) must build the REAL street graph
# (mode='roads'); synthetic/fixture towns are marched/synthetic by design (always ✓).
mode = d.get('mode', '?')
row['roads'] = ((mode == 'roads') if is_real else True, f"{mode},{d.get('edges', 0)}e")
except Exception as e:
row.setdefault('name', '?'); row.setdefault('shops', 0)
for g in ('boot', 'determ', 'budget', 'district', 'roads'):
row.setdefault(g, (False, str(e)[:24]))
finally:
pg.close()
# ── ?noassets=1: silent-and-fine (real caches correctly fall back to a fixture — no network) ──
pg = new_page()
reqs, errs2 = [], []
pg.on('request', lambda r: reqs.append(r.url) if ('/assets/towns/' in r.url or '/assets/audio/' in r.url or r.url.endswith('.glb')) else None)
pg.on('pageerror', lambda e: errs2.append(str(e)))
try:
pg.goto(f'{HOST}/index.html?{query}&noassets=1&dbg=1')
pg.wait_for_function('window.PROCITY && window.PROCITY.plan', timeout=20000)
has_plan = pg.evaluate("() => !!(window.PROCITY.plan && window.PROCITY.plan.shops.length)")
row['noassets'] = (has_plan and len(errs2) == 0 and len(reqs) == 0,
f"{len(reqs)} fetch/{len(errs2)} err")
except Exception as e:
row['noassets'] = (False, str(e)[:24])
finally:
pg.close()
return row
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[1mTOWN-MATRIX SMOKE\033[0m ({len(MATRIX)} towns × 6 gates (incl. the R19 roads dimension) · the v4 real-map scout evidence)")
srv = ensure_server()
GATES = ['boot', 'determ', 'budget', 'district', 'roads', 'noassets']
rows = []
try:
with sync_playwright() as p:
b = p.chromium.launch()
def factory():
return b.new_page(viewport={'width': 1280, 'height': 720})
for label, query in MATRIX:
r = run_town(factory, query, label)
rows.append((label, r))
cells = ' '.join(f"{g}:{'' if r[g][0] else ''}" for g in GATES)
print(f" {label:22} {(r.get('name') or '?'):16} {cells}")
b.close()
finally:
if srv: srv.terminate()
# the table John reads
print(f"\n\033[1mTOWN × GATE\033[0m")
hdr = f"{'town':22} {'name':16} " + " ".join(f"{g:>9}" for g in GATES)
print(hdr); print('' * len(hdr))
fails = 0
for label, r in rows:
line = f"{label:22} {(r.get('name') or '?'):16} "
for g in GATES:
ok, note = r[g]
fails += 0 if ok else 1
line += f"{('' if ok else '') + note:>9} "[:10] + " "
print(line)
# per-gate note detail
print("\nnotes:", "; ".join(f"{label}[{g}={r[g][1]}]" for label, r in rows for g in GATES if not r[g][0]) or "all gates green")
print()
if fails:
print(f"\033[31m● {fails} gate failure(s)\033[0m across {len(rows)} towns — the matrix is not clean.")
sys.exit(1)
print(f"\033[32m● MATRIX GREEN\033[0m — {len(rows)} towns (synthetic + fixtures + real caches) pass all 6 gates.")
sys.exit(0)
if __name__ == '__main__':
main()