PROCITY/tools/qa/town_matrix.py
m3ultra 201a2bb868 Lane F R21 (v4.0-beta close): the beta gate — tram dimension, the re-measure, the beta shot
All five lanes in (A's venue cluster bias, B's selector-from-index, C's density door-soak, D's relocation
verify PASSES, E's 22-town pack non-gating); F gated and tagged.

- town_matrix.py: a `tram` dimension — B's R20 shop-adjacency ruling verified EXACTLY (newtown runs 16 stops
  after B's phantom-stop fix, castlemaine 10; katoomba/fremantle/bendigo FENCED at 3/2/1 shops fronted, all
  below the 5-shop floor). 10 towns x 7 gates: boot/determ/district/roads/tram/noassets green on all ten.
- index.html: expose window.PROCITY.tram — B built `routeInfo` FOR F's smoke but nothing published it; F owns
  the shell, so F wired the seam.
- RECORDED (ledger #6b — the instruction was "record the new number"): the venue_night re-measure. A's cap
  WORKED (posters 438->111) but E's density widening ate it — katoomba 200,965 (+965, R19) -> 221,935
  (+21,935, +11%); bendigo 201k (+0.5%); fremantle/newtown/castlemaine in budget. Draws fine everywhere
  (151/300 — half budget); tris-only, at the worst night stress view. Filed as R22's first item, where the
  tri-diet backlog (instrument LOD, tram tri diet) already lives. katoomba is the outlier: 966 edges, 72 shops.
- Ruling (ledger #6d): the town selector stays ALWAYS-IN-HUD, not ?dbg-gated — THE REAL MAP's product IS the
  towns. Corollary (F's own gap): money shots hide the HUD; the R19 alpha shot had the selector in-frame
  because F's harness screenshotted the full viewport.
- Beta money shot: Fremantle's real street at golden hour — a corridor of shopfronts, footpaths, a crossing,
  gum trees, and a genuinely busy crowd (the density widening, visible). 126d/167k tris, HUD hidden.
- docs: LANE_F_NOTES §20 (the honest why-R20-had-no-tag record) + §21, README beta paragraph, F-progress.

qa.sh --strict GREEN; selfcheck 154141/154141 (A's re-pinned goldens hold).

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

196 lines
9.9 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
# [R21 ledger #6c] B's R20 tram ruling: on a REAL-ROADS town the tram only runs if its best main chain
# fronts ≥5 shops — otherwise it's "a highway bus" and gets fenced. These two run; the other three are fenced.
TRAM_RUN = {'newtown_real', 'castlemaine_real'}
TRAM_STOPS_SANE = (2, 40) # B fixed the phantom-stop bug en route (Newtown: 149 → 16)
# 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();
const tr = P.tram ? { fenced: !!(P.tram.routeInfo || {}).fenced, stops: P.tram.stops | 0,
shopsFronted: (P.tram.routeInfo || {}).shopsFronted } : null; // [R21] B's fence verdict
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, tram: tr };
}
"""
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")
# [R21 #6c] tram: B's R20 ruling — a real-roads town runs the tram only if its best main chain fronts
# ≥5 shops; otherwise it's FENCED (empty tram group, 0 stops, no errors). Synthetic/fixtures always run.
t, key = d.get('tram'), (label.split('/', 1)[1] if '/' in label else label)
if is_real and key in TRAM_RUN:
row['tram'] = (bool(t) and not t['fenced'] and TRAM_STOPS_SANE[0] <= t['stops'] <= TRAM_STOPS_SANE[1],
f"run {t['stops']}st" if t else 'absent')
elif is_real:
row['tram'] = (bool(t) and t['fenced'] and t['stops'] == 0,
f"fenced/{t.get('shopsFronted', '?')}sh" if t else 'absent')
else:
row['tram'] = (bool(t) and not t['fenced'], f"run {t['stops']}st" if t else 'absent')
except Exception as e:
row.setdefault('name', '?'); row.setdefault('shops', 0)
for g in ('boot', 'determ', 'budget', 'district', 'roads', 'tram'):
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 × 7 gates (roads + tram dimensions) · the v4 real-map scout evidence)")
srv = ensure_server()
GATES = ['boot', 'determ', 'budget', 'district', 'roads', 'tram', '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 7 gates.")
sys.exit(0)
if __name__ == '__main__':
main()