diff --git a/F-progress.md b/F-progress.md index 0ad91b2..1a0bdfa 100644 --- a/F-progress.md +++ b/F-progress.md @@ -4,6 +4,23 @@ --- +## Round 22 (v4.0 — THE EPOCH CLOSE) — F closed it + +- **The blocking gate CLOSED.** F's R21 re-measure made the tri diet blocking; B diagnosed, the driver + traced A→**E**, E fixed it, F re-measured: katoomba `venue_night` **221,935 → 108,365 (−51%)**, worst town + 124,682 — the 200k stress edge is closed. Selfcheck **161,300/161,300** (A's 36 goldens + 23 towns). +- **`DBG.enterShop(selector)`** (Lane G's ask) — id · type · exact name · substring, most-specific-first. +- **The release tour** → [`docs/shots/release_v4/`](docs/shots/release_v4/): Fremantle hero · Fitzroy mecca · + Toowoomba thin tail · synthetic control · `?classic=1` covenant. All posed through F's R21 cluster + primitive — which earned itself twice: the first cut posed via `street_noon` and **the mecca came out as + bare bitumen**; re-posed through the cluster, Fitzroy reads. +- **⚠ Honest finding (filed v4.x):** the thin tail does **not** prove the floor holds — 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. The frame + ships labelled honestly rather than swapped for a flattering town. +- Docs (README v4 "the towns are the product", notes §22), `qa.sh --strict --matrix` green, tag **`v4.0`**. + +--- + ## Round 20–21 (v4.0-beta THE FIVE TOWNS) — F never ran R20; closed R21 **R20: F never ran** (no gate, no tag) — the honest record is in LANE_F_NOTES §20. Unlike R18 (where F ran diff --git a/README.md b/README.md index 9ded626..c2ad2bc 100644 --- a/README.md +++ b/README.md @@ -76,15 +76,27 @@ master off: | `?noassets=1` · `?plansrc=osm` · `?mute=1` · `?dbg=1` | off | asset-free (zero network) · OSM plan source · silence · the `window.DBG` capture/soak harness | | `?plansrc=osm&town=` | — | boot a **real Australian town**: fixtures `melbourne`/`katoomba`/`silverton`, or a real OSM cache `bendigo_real`/`castlemaine_real`/`fremantle_real`/`katoomba_real`/`newtown_real` (ODbL, `web/assets/towns/SOURCES.md`). **REAL ROADS:** a cache with `roads[]` (schema v2) builds the CityPlan street graph from the town's **actual OSM streets**; without `roads[]` a cache marches onto synthetic avenues. Or just **pick a town from the HUD selector** — no URL needed | -### v4.0-beta — the five towns, at density +## v4.0 — THE REAL MAP: the towns *are* the product -Five real Australian towns (**Katoomba, Newtown, Fremantle, Bendigo, Castlemaine — 292 shops**) boot on -their **actual OSM street graphs**, at real retail density: secondhand heroes keep their footfall while the -street around them gets ~4× busier (texture, never a takeover). Venues bias toward the retail cluster, the -tram runs only where it fronts a real high street (Newtown/Castlemaine — elsewhere it was "a highway bus", -so it's fenced), and the in-HUD **town selector** reads the towns index, so you can just pick one. Every -town: deterministic, districted, silent under `?noassets`, `?classic=1` still byte-identical to v2. -See [`docs/shots/v4_beta/`](docs/shots/v4_beta/) and the charter, [`docs/V4_REAL_MAP.md`](docs/V4_REAL_MAP.md). +**23 real Australian towns — 1,210 real shops, every state and territory — on their actual streets.** Not +"inspired by": Fitzroy's shops sit on Fitzroy's roads, at their real positions, because the pipeline eats +OpenStreetMap and marches it through the same CityPlan contract the procedural town uses. **Pick one from +the in-HUD selector and walk it.** Everything built across v1–v3 comes along unchanged — the venues and their +seeded week of gigs, the 4-piece band with the seated drummer, the crowd that follows you into the pub, the +interiors behind every door, weather, the tram — because they all hang off the plan contract and never had +to know the geography got real. + +The towns run from **meccas** (Fitzroy, 160 shops) to the **thin tail** (12), at real retail density: the +secondhand heroes keep their footfall while the street around them gets ~4× busier — texture, never a +takeover. Venues bias to the retail heart. The tram only runs where it fronts a real high street (elsewhere +it was honestly "a highway bus", so it's fenced). Everything stays **deterministic from the seed**, silent +under `?noassets=1`, and **`?classic=1` still boots the exact v2 town, byte-identical forever** (`0x3fa36874`). + +**The tour:** [`docs/shots/release_v4/contact.html`](docs/shots/release_v4/) — the Fremantle hero, the +Fitzroy mecca, the thin tail (labelled honestly: the floor is shops-per-street-metre, and Toowoomba is +below it), the synthetic control, and the `?classic=1` covenant made visible. Charter: +[`docs/V4_REAL_MAP.md`](docs/V4_REAL_MAP.md) · data + licences (ODbL): +[`web/assets/towns/SOURCES.md`](web/assets/towns/SOURCES.md). | `?seed=N` | `20261990` | town seed | **Release tour:** [`docs/shots/release_v3_1/contact.html`](docs/shots/release_v3_1/) — the money shots on diff --git a/docs/LANES/LANE_F_NOTES.md b/docs/LANES/LANE_F_NOTES.md index 6a554f7..0497de5 100644 --- a/docs/LANES/LANE_F_NOTES.md +++ b/docs/LANES/LANE_F_NOTES.md @@ -791,3 +791,52 @@ streetlamps, and a genuinely busy crowd (the density widening, visible). 126 dra Posed by projecting the densest **spatial** shop cluster (10 shops within 60 m) onto its front edge and standing on the road — per-edge shop counts are ~1 on a real graph, so "densest edge" is the wrong pose heuristic for real towns. + +--- + +## 22. Round 22 — **v4.0: THE EPOCH CLOSE.** F: the re-measure, the selector, the tour, the tag. + +### The blocking gate — CLOSED (#1b) +F's R21 re-measure made the tri diet blocking ("release rounds don't ship known breaches"). B diagnosed +layer-by-layer, the driver traced **A → E** (A's own commit said so: "B's numbers say the fix is E's"), and +**E fixed it**. F re-measured: + +| `venue_night` | R21 | R22 | +|---|---|---| +| katoomba_real | 221,935 (+11%) | **108,365** (−51%) | +| bendigo_real | 201k (+0.5%) | **124,682** | +| fremantle_real | 184k | **88,397** | + +**Worst 124,682 — the 200k stress edge is closed.** Selfcheck **161,300/161,300** (A's 36 goldens + the +23-town pack). The measure-first loop worked end to end: F measured, B diagnosed, E owned, F re-measured. + +### `DBG.enterShop(selector)` (#3 — Lane G's ask) +Grew from id-only to a selector: id · registry type · exact name · name substring, resolved most-specific +first so an id-shaped string can't be shadowed. `DBG.enterShop('record')` / `('The Exchange Hotel')` / `(116)` +— cross-repo QA one-liners on any of 23 towns without an id lookup. + +### The release tour (#4) — `docs/shots/release_v4/` +Five frames: the Fremantle golden-hour 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** +(project the densest SPATIAL cluster onto its front edge, stand on the road); B's R22 bookmark fix anchors +on the same retail heart, so tour and bookmarks agree by construction. HUD hidden throughout. + +**The primitive earned itself twice.** First cut of the tour posed via `DBG.shot('street_noon')` and the +**mecca came out as bare bitumen** — the same trap as R21. Re-posed through the cluster primitive, Fitzroy +reads (7 shops/60 m retail heart, trees, benches, crowd). *Per-edge is always the wrong heuristic on a real +graph.* + +### ⚠ The honest thin-tail finding (filed for v4.x) +The brief asked for a thin-tail frame as **"proof the floor holds."** It doesn't, and the frame says so: +**Toowoomba reads as an empty road, not a town.** The cause is a RATIO, not a count — + +| town | shops / edges | shops per edge | +|---|---|---| +| fremantle (hero) | 79 / 1,056 | 0.075 | +| fitzroy (mecca) | 139 / 2,408 | 0.058 | +| **toowoomba (thin tail)** | **12 / 1,459** | **0.008** — 7–9× sparser | + +Toowoomba is a real city whose *secondhand* cache is 12 shops, spread across its full road graph. The floor +isn't "≥N shops" — it's **shops-per-street-metre**. v4.x options: clip the graph to the retail core for +thin caches, or set a ratio floor in the pack. The frame ships **labelled honestly** rather than swapped for +a flattering town — same discipline as the R21 re-measure that got worse and said so. diff --git a/docs/shots/release_v4/classic_covenant.png b/docs/shots/release_v4/classic_covenant.png new file mode 100644 index 0000000..ee9294a Binary files /dev/null and b/docs/shots/release_v4/classic_covenant.png differ diff --git a/docs/shots/release_v4/contact.html b/docs/shots/release_v4/contact.html new file mode 100644 index 0000000..e077e7f --- /dev/null +++ b/docs/shots/release_v4/contact.html @@ -0,0 +1 @@ +PROCITY v4.0 — the release tour

PROCITY v4.0 — THE REAL MAP · 23 real Australian towns · seed 20261990 · settled

fremantle_hero
fremantle_hero
the hero — Fremantle, real streets, golden hour · Fremantle · 79 shops / 1056 edges · retail heart: 10 shops/60m · 126 draws · 77,322 tris
fitzroy_mecca
fitzroy_mecca
the mecca — Fitzroy, the densest town in the pack · Fitzroy · 139 shops / 2408 edges · retail heart: 7 shops/60m · 166 draws · 95,824 tris
toowoomba_thintail
toowoomba_thintail
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 · Toowoomba · 12 shops / 1459 edges · retail heart: 1 shops/60m · 78 draws · 78,496 tris
synthetic_control
synthetic_control
the control — the procedural town, unchanged · Boolarra Heads · 493 shops / 22 edges · 180 draws · 29,158 tris
classic_covenant
classic_covenant
?classic=1 — the v2 covenant, byte-identical forever · Boolarra Heads · 493 shops / 22 edges · 164 draws · 26,472 tris
\ No newline at end of file diff --git a/docs/shots/release_v4/fitzroy_mecca.png b/docs/shots/release_v4/fitzroy_mecca.png new file mode 100644 index 0000000..ef426a4 Binary files /dev/null and b/docs/shots/release_v4/fitzroy_mecca.png differ diff --git a/docs/shots/release_v4/fremantle_hero.png b/docs/shots/release_v4/fremantle_hero.png new file mode 100644 index 0000000..e4db6df Binary files /dev/null and b/docs/shots/release_v4/fremantle_hero.png differ diff --git a/docs/shots/release_v4/synthetic_control.png b/docs/shots/release_v4/synthetic_control.png new file mode 100644 index 0000000..84c5fbd Binary files /dev/null and b/docs/shots/release_v4/synthetic_control.png differ diff --git a/docs/shots/release_v4/toowoomba_thintail.png b/docs/shots/release_v4/toowoomba_thintail.png new file mode 100644 index 0000000..9b4311b Binary files /dev/null and b/docs/shots/release_v4/toowoomba_thintail.png differ diff --git a/tools/qa/tour_shots.py b/tools/qa/tour_shots.py index 4ca5f41..8f9a884 100644 --- a/tools/qa/tour_shots.py +++ b/tools/qa/tour_shots.py @@ -1,22 +1,23 @@ #!/usr/bin/env python3 -"""PROCITY Lane F — the v3.0 release money-shot tour (round 15 · ?gigs=1). One deterministic run. +"""PROCITY Lane F — the v4.0 RELEASE TOUR (round 22 · the epoch close · ledger #4). One deterministic run. -Captures the release set into docs/shots/release_v3/ (+ a contact.html), grouped by the flags each shot -needs, all from seed 20261990 (deterministic, reproducible — never hand-cropped): - • venue_night — lit pub frontage + streetlamp pools + posters at night (?gigs=1) - • district_posters — the spine at night: poster poles + lit frontages receding (?gigs=1) - • queue_night — JOHN'S HERO: close on the pub door at 'on', queue + marquee + A's now-visible poster - • gig_interior — the 4-piece with real instruments + a roster crowd (continuity) (?gigs=1) - • rain_verandah — a wet night outside the lit video-shop window (?weather=rain&winmap=1) - • tram_stop — the bus shelter the tram pauses at (?tram=1) +The v4 thesis in five frames, into docs/shots/release_v4/ (+ contact.html). The towns ARE the product: -Discipline (ROUND15 ledger #5 + B's note): every shot is measured SETTLED — after the async instrument -GLBs / poster textures / chunk stream land (shoot-twice), from a clean per-group load (no orphan-chunk -inflation); the queue is driven LIVE (rAF is throttled in automation, so we run the animate loop + admit -explicitly rather than trust auto-drain). + • 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 (incl. queue_night). Writes full-frame 1280x720 PNGs. +Needs the Playwright venv + Lane B's ?dbg=1 hook. Writes full-frame 1280x720 PNGs. """ import sys, time, socket, subprocess, pathlib @@ -24,59 +25,54 @@ 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_v3_1' +OUTDIR = ROOT / 'docs' / 'shots' / 'release_v4' if '--seed' in sys.argv: SEED = int(sys.argv[sys.argv.index('--seed') + 1]) -# [R16 THE FLIP] shot groups — the DEFAULT boot now has all four flags on, so most shots come from ONE clean -# load (no per-shot flag juggling). Rain is still forced (the default seed rolls clear); ?classic=1 gives the -# before/after comparison. Deterministic, same seed. -GROUPS = [ - {'flags': '', 'street': ['venue_night', 'district_posters', 'queue_night', 'tram_stop'], 'interior': True}, - {'flags': 'weather=rain', 'street': ['rain_verandah'], 'interior': False}, - # the town before v3 — the exact v2 boot; same main-street pose as district_posters for a clean A/B - {'flags': 'classic=1', 'street': ['district_posters'], 'interior': False, 'suffix': '_classic'}, +# (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'), ] -# settle + re-pose a street bookmark once its live/async content (queue, instrument GLBs, poster textures, -# streamed chunks) has landed, then report SETTLED stats. -STREET_JS = r""" -async (name) => { +# 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.shot(name); // initial pose + segment (2 renders) - for (let i = 0; i < 8; i++) await new Promise(r => requestAnimationFrame(r)); // animate loop: queue spawns - const q = (P.gigs && P.venueQueues) ? P.venueQueues.get(P.gigs.venueShopIds[0]) : null; - await new Promise(r => setTimeout(r, 1400)); // async GLB / poster / chunk settle - const stat = D.shot(name); // RE-POSE now that everything is loaded → the settled frame - const queue = (P.gigs && P.queueCountOf) ? P.queueCountOf(P.gigs.venueShopIds[0]) : null; - const posters = (P.plan.posters || []).length; - return { name, draws: stat.drawCalls, tris: stat.tris, queue, posters }; -} -""" - -INTERIOR_JS = r""" -async () => { - const P = window.PROCITY, D = window.DBG, THREE = P.THREE; - const id = P.gigs.venueShopIds[0]; - D.setSegment(5); - // drive the queue live + admit a couple so the interior crowd carries roster identities (continuity) - for (let i = 0; i < 8; i++) await new Promise(r => requestAnimationFrame(r)); - const q = P.venueQueues && P.venueQueues.get(id); - if (q) for (let i = 0; i < 3 && q.count() > 0; i++) q.admitOne(); - D.enterShop(id); await new Promise(r => setTimeout(r, 900)); - const room = P.interiorMode.current; if (!room) return { ok: false }; - const st = room.stage; - P.camera.position.set(st.x + 0.2, 1.6, room.dims.D / 2 - 1.2); - P.camera.lookAt(new THREE.Vector3(st.x, 1.35, st.z)); - // shoot-twice / settled discipline (ledger #5): immediate vs after the instrument GLBs land - P.renderer.info.reset(); P.renderer.render(P.interiorMode.scene, P.camera); - const trisImmediate = P.renderer.info.render.triangles; - await new Promise(r => setTimeout(r, 1600)); - P.renderer.info.reset(); P.renderer.render(P.interiorMode.scene, P.camera); - const info = P.interiorMode.crewInfo; - return { ok: true, draws: P.renderer.info.render.calls, trisImmediate, trisSettled: P.renderer.info.render.triangles, - band: info ? info.band : 0, crowd: info ? info.crowd : 0, fromRoster: info ? info.fromRoster : 0, - venue: (P.plan.shops.find(s => s.id === id) || {}).name, bandName: P.gigs.bandNameOf(id) }; + 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 }; } """ @@ -98,31 +94,18 @@ def ensure_server(): proc.terminate(); sys.exit(f'could not start http.server on :{PORT}') -def boot(pg, flags): - pg.goto(f'{HOST}/index.html?seed={SEED}&dbg=1&{flags}') - pg.wait_for_function('window.DBG && window.DBG.ready === true', timeout=25000) - pg.evaluate("() => { const o=document.getElementById('pc-start'); if(o) o.style.display='none'; }") - try: - pg.wait_for_function('() => window.PROCITY && (!window.PROCITY.fleet || window.PROCITY.fleet.ready)', timeout=15000) - except Exception: - print(' ! fleet never readied — shooting placeholders') - - def contact_html(shots): - cards = [] - for s in shots: - cap = s['caption'] - cards.append( - f'
{s[' - f'
{s["name"]}
{cap}
') + cards = ''.join( + f'
{s[' + f'
{s["name"]}
{s["caption"]}
' for s in shots) return ( - 'PROCITY v3.1 — release tour' + 'PROCITY v4.0 — the release tour' '' - f'

PROCITY v3.1 — release money-shot tour (THE FLIP) · seed {SEED} · settled measurements

' - f'
{"".join(cards)}
') + 'figcaption{padding:9px 13px;color:#cdc4b2}b{color:#ffd9a0}' + f'

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

' + f'
{cards}
') def main(): @@ -131,51 +114,39 @@ def main(): except ImportError: sys.exit('playwright not installed — tools/.venv/bin/python -m playwright install chromium') - print(f"\033[1mv3.1 RELEASE MONEY-SHOT TOUR\033[0m (seed {SEED} → docs/shots/release_v3_1/)") + 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 grp in GROUPS: + 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))) - boot(pg, grp['flags']) - sfx = grp.get('suffix', '') - for name in grp['street']: - stat = pg.evaluate(STREET_JS, name) - fname = f'{name}{sfx}.png' - pg.screenshot(path=str(OUTDIR / fname)) - cap = (f"?classic=1 — the town before v3 (no gig layer, no pools)" if sfx - else f"{stat['draws']} draws · {stat['tris']:,} tris") - if not sfx and stat.get('queue') is not None: cap += f" · queue {stat['queue']}" - if not sfx and name in ('district_posters', 'venue_night', 'queue_night'): cap += f" · {stat['posters']} posters" - shots.append({'name': name + sfx, 'file': fname, 'caption': cap}) - print(f" \033[32m●\033[0m {name}{sfx}: {cap}") - if grp['interior']: - info = pg.evaluate(INTERIOR_JS) - if info.get('ok'): - out = OUTDIR / 'gig_interior.png' - pg.screenshot(path=str(out)) - cap = (f"{info['venue']} · {info['bandName']} · {info['band']}-piece · crowd {info['crowd']} " - f"({info['fromRoster']} from roster) · {info['draws']} draws · " - f"{info['trisImmediate']:,}→{info['trisSettled']:,} tris (pre-load→settled)") - shots.append({'name': 'gig_interior', 'file': 'gig_interior.png', 'caption': cap}) - print(f" \033[32m●\033[0m gig_interior: {cap}") - else: - print(" \033[31m✗\033[0m gig_interior: could not stage") + 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) in [{grp['flags']}]: {errs[0][:100]}") + 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)} shots\033[0m → docs/shots/release_v3_1/ (+ contact.html) · {errs_total} page errors") - sys.exit(1 if errs_total or len(shots) < 6 else 0) + 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__': diff --git a/web/js/world/dbg.js b/web/js/world/dbg.js index 1e49e1a..a2923c3 100644 --- a/web/js/world/dbg.js +++ b/web/js/world/dbg.js @@ -262,11 +262,26 @@ export function installDBG(deps) { }, // scripted interior visit (drives the record_interior shot + the soak memory-baseline gate) - enterShop(shopId) { - const s = (plan.shops || []).find((x) => x.id === shopId); - if (!s) return { error: `no shop ${shopId}` }; + // [Lane F R22 — Lane G's ask] enterShop takes a SELECTOR, not just an id, so cross-repo QA is a + // one-liner on any town without looking ids up first: + // DBG.enterShop(116) → by id + // DBG.enterShop('record') → first shop of that registry type + // DBG.enterShop('The Exchange Hotel')→ by exact name, else name substring (case-insensitive) + // Resolution order is most-specific-first so an id-shaped string can never be shadowed by a name. + enterShop(sel) { + const shops = plan.shops || []; + let s = null; + if (typeof sel === 'number') s = shops.find((x) => x.id === sel); + else if (typeof sel === 'string') { + const q = sel.trim().toLowerCase(); + s = shops.find((x) => String(x.id) === q) // id-as-string + || shops.find((x) => (x.name || '').toLowerCase() === q) // exact name + || shops.find((x) => (x.type || '').toLowerCase() === q) // registry type + || shops.find((x) => (x.name || '').toLowerCase().includes(q)); // name substring + } + if (!s) return { error: `no shop matching ${JSON.stringify(sel)}` }; enterShop(s.id, s.name); - return { entered: s.id, name: s.name }; + return { entered: s.id, name: s.name, type: s.type }; }, exitShop() { window.dispatchEvent(new CustomEvent('procity:exitShop')); return { exited: true }; }, };