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>
This commit is contained in:
m3ultra 2026-07-16 22:32:32 +10:00
parent b0e4ce103e
commit 8ac3b18b36
11 changed files with 188 additions and 123 deletions

View File

@ -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.060.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 2021 (v4.0-beta THE FIVE TOWNS) — F never ran R20; closed R21 ## Round 2021 (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 **R20: F never ran** (no gate, no tag) — the honest record is in LANE_F_NOTES §20. Unlike R18 (where F ran

View File

@ -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 | | `?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=<key>` | — | 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 | | `?plansrc=osm&town=<key>` | — | 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 **23 real Australian towns — 1,210 real shops, every state and territory — on their actual streets.** Not
their **actual OSM street graphs**, at real retail density: secondhand heroes keep their footfall while the "inspired by": Fitzroy's shops sit on Fitzroy's roads, at their real positions, because the pipeline eats
street around them gets ~4× busier (texture, never a takeover). Venues bias toward the retail cluster, the OpenStreetMap and marches it through the same CityPlan contract the procedural town uses. **Pick one from
tram runs only where it fronts a real high street (Newtown/Castlemaine — elsewhere it was "a highway bus", the in-HUD selector and walk it.** Everything built across v1v3 comes along unchanged — the venues and their
so it's fenced), and the in-HUD **town selector** reads the towns index, so you can just pick one. Every seeded week of gigs, the 4-piece band with the seated drummer, the crowd that follows you into the pub, the
town: deterministic, districted, silent under `?noassets`, `?classic=1` still byte-identical to v2. interiors behind every door, weather, the tram — because they all hang off the plan contract and never had
See [`docs/shots/v4_beta/`](docs/shots/v4_beta/) and the charter, [`docs/V4_REAL_MAP.md`](docs/V4_REAL_MAP.md). 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 | | `?seed=N` | `20261990` | town seed |
**Release tour:** [`docs/shots/release_v3_1/contact.html`](docs/shots/release_v3_1/) — the money shots on **Release tour:** [`docs/shots/release_v3_1/contact.html`](docs/shots/release_v3_1/) — the money shots on

View File

@ -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 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 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. 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** — 79× 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.

Binary file not shown.

After

Width:  |  Height:  |  Size: 809 KiB

View File

@ -0,0 +1 @@
<!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><h1>PROCITY v4.0 — THE REAL MAP <small>· 23 real Australian towns · seed 20261990 · settled</small></h1><div class="grid"><figure><img src="fremantle_hero.png" alt="fremantle_hero" loading="lazy"><figcaption><b>fremantle_hero</b><br>the hero — Fremantle, real streets, golden hour · Fremantle · 79 shops / 1056 edges · retail heart: 10 shops/60m · 126 draws · 77,322 tris</figcaption></figure><figure><img src="fitzroy_mecca.png" alt="fitzroy_mecca" loading="lazy"><figcaption><b>fitzroy_mecca</b><br>the mecca — Fitzroy, the densest town in the pack · Fitzroy · 139 shops / 2408 edges · retail heart: 7 shops/60m · 166 draws · 95,824 tris</figcaption></figure><figure><img src="toowoomba_thintail.png" alt="toowoomba_thintail" loading="lazy"><figcaption><b>toowoomba_thintail</b><br>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 · Toowoomba · 12 shops / 1459 edges · retail heart: 1 shops/60m · 78 draws · 78,496 tris</figcaption></figure><figure><img src="synthetic_control.png" alt="synthetic_control" loading="lazy"><figcaption><b>synthetic_control</b><br>the control — the procedural town, unchanged · Boolarra Heads · 493 shops / 22 edges · 180 draws · 29,158 tris</figcaption></figure><figure><img src="classic_covenant.png" alt="classic_covenant" loading="lazy"><figcaption><b>classic_covenant</b><br>?classic=1 — the v2 covenant, byte-identical forever · Boolarra Heads · 493 shops / 22 edges · 164 draws · 26,472 tris</figcaption></figure></div>

Binary file not shown.

After

Width:  |  Height:  |  Size: 778 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 662 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 809 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 835 KiB

View File

@ -1,22 +1,23 @@
#!/usr/bin/env python3 #!/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 The v4 thesis in five frames, into docs/shots/release_v4/ (+ contact.html). The towns ARE the product:
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)
Discipline (ROUND15 ledger #5 + B's note): every shot is measured SETTLED — after the async instrument fremantle_hero the golden-hour hero: a real shopfront corridor with the density crowd on it
GLBs / poster textures / chunk stream land (shoot-twice), from a clean per-group load (no orphan-chunk fitzroy_mecca the mecca (160 shops, 2× anything absorbed before this epoch)
inflation); the queue is driven LIVE (rAF is throttled in automation, so we run the animate loop + admit toowoomba_thintail the thin tail (12 shops): proof the floor still reads as a town
explicitly rather than trust auto-drain). 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] 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 import sys, time, socket, subprocess, pathlib
@ -24,59 +25,54 @@ ROOT = pathlib.Path(__file__).resolve().parent.parent.parent
PORT = 8130 PORT = 8130
HOST = f'http://127.0.0.1:{PORT}' HOST = f'http://127.0.0.1:{PORT}'
SEED = 20261990 SEED = 20261990
OUTDIR = ROOT / 'docs' / 'shots' / 'release_v3_1' OUTDIR = ROOT / 'docs' / 'shots' / 'release_v4'
if '--seed' in sys.argv: if '--seed' in sys.argv:
SEED = int(sys.argv[sys.argv.index('--seed') + 1]) 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 # (file, query, segment, cluster-pose?, caption-prefix). seg 3 = late-afternoon gold; seg 2 = midday.
# load (no per-shot flag juggling). Rain is still forced (the default seed rolls clear); ?classic=1 gives the # cluster=True → F's spatial-cluster pose (real towns); cluster=False → the bookmark (synthetic is one dense
# before/after comparison. Deterministic, same seed. # cluster already, so street_noon frames it correctly).
GROUPS = [ V4_TOUR = [
{'flags': '', 'street': ['venue_night', 'district_posters', 'queue_night', 'tram_stop'], 'interior': True}, ('fremantle_hero', 'plansrc=osm&town=fremantle_real', 3, True, 'the hero — Fremantle, real streets, golden hour'),
{'flags': 'weather=rain', 'street': ['rain_verandah'], 'interior': False}, ('fitzroy_mecca', 'plansrc=osm&town=fitzroy_real', 3, True, 'the mecca — Fitzroy, the densest town in the pack'),
# the town before v3 — the exact v2 boot; same main-street pose as district_posters for a clean A/B ('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'),
{'flags': 'classic=1', 'street': ['district_posters'], 'interior': False, 'suffix': '_classic'}, ('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, # THE POSING PRIMITIVE (F's R21 finding). On a real graph, per-EDGE shop counts are ~1 — so "the edge with
# streamed chunks) has landed, then report SETTLED stats. # the most shops" frames an empty arterial (it did, first run: the Fitzroy mecca came out as bare bitumen).
STREET_JS = r""" # The town's retail heart is a SPATIAL cluster: find the shop lot with the most neighbours inside 60 m,
async (name) => { # 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; const P = window.PROCITY, D = window.DBG;
D.shot(name); // initial pose + segment (2 renders) D.setSegment(seg);
for (let i = 0; i < 8; i++) await new Promise(r => requestAnimationFrame(r)); // animate loop: queue spawns let hub = -1;
const q = (P.gigs && P.venueQueues) ? P.venueQueues.get(P.gigs.venueShopIds[0]) : null; if (!cluster) { D.shot('street_noon'); }
await new Promise(r => setTimeout(r, 1400)); // async GLB / poster / chunk settle else {
const stat = D.shot(name); // RE-POSE now that everything is loaded the settled frame const plan = P.plan, nById = new Map(plan.streets.nodes.map(n => [n.id, n]));
const queue = (P.gigs && P.queueCountOf) ? P.queueCountOf(P.gigs.venueShopIds[0]) : null; const lots = (plan.lots || []).filter(l => l.use === 'shop' || l.use === 'anchor');
const posters = (P.plan.posters || []).length; let best = null;
return { name, draws: stat.drawCalls, tris: stat.tris, queue, posters }; 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];
INTERIOR_JS = r""" const a = nById.get(e.a), b2 = nById.get(e.b);
async () => { const dx = b2.x - a.x, dz = b2.z - a.z, L = Math.hypot(dx, dz) || 1, ux = dx / L, uz = dz / L;
const P = window.PROCITY, D = window.DBG, THREE = P.THREE; let t = (best.x - a.x) * ux + (best.z - a.z) * uz;
const id = P.gigs.venueShopIds[0]; t = Math.max(6, Math.min(L - 6, t));
D.setSegment(5); const back = Math.min(26, t);
// drive the queue live + admit a couple so the interior crowd carries roster identities (continuity) D.teleport(a.x + ux * (t - back), a.z + uz * (t - back), Math.atan2(ux, uz));
for (let i = 0; i < 8; i++) await new Promise(r => requestAnimationFrame(r)); } else { D.shot('street_noon'); }
const q = P.venueQueues && P.venueQueues.get(id); }
if (q) for (let i = 0; i < 3 && q.count() > 0; i++) q.admitOne(); await new Promise(r => setTimeout(r, 1800)); // crowd + chunks + skins + GLBs settle
D.enterShop(id); await new Promise(r => setTimeout(r, 900)); const hud = document.getElementById('pc-hud'); if (hud) hud.style.display = 'none'; // beauty shot
const room = P.interiorMode.current; if (!room) return { ok: false }; for (let i = 0; i < 3; i++) await new Promise(r => requestAnimationFrame(r));
const st = room.stage; const i = D.info();
P.camera.position.set(st.x + 0.2, 1.6, room.dims.D / 2 - 1.2); return { name: P.plan.name, shops: (P.plan.shops || []).length, hub,
P.camera.lookAt(new THREE.Vector3(st.x, 1.35, st.z)); edges: (P.plan.streets.edges || []).length, draws: i.drawCalls, tris: i.tris };
// 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) };
} }
""" """
@ -98,31 +94,18 @@ def ensure_server():
proc.terminate(); sys.exit(f'could not start http.server on :{PORT}') 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): def contact_html(shots):
cards = [] cards = ''.join(
for s in shots: f'<figure><img src="{s["file"]}" alt="{s["name"]}" loading="lazy">'
cap = s['caption'] f'<figcaption><b>{s["name"]}</b><br>{s["caption"]}</figcaption></figure>' for s in shots)
cards.append(
f'<figure><img src="{s["file"]}" alt="{s["name"]}" loading="lazy">'
f'<figcaption><b>{s["name"]}</b><br>{cap}</figcaption></figure>')
return ( return (
'<!doctype html><meta charset="utf-8"><title>PROCITY v3.1 — release tour</title>' '<!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}' '<style>body{background:#14110c;color:#f4efe6;font:14px/1.4 -apple-system,Segoe UI,Roboto,sans-serif;margin:24px}'
'h1{font-weight:700}.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(420px,1fr));gap:18px}' '.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}' 'figure{margin:0;background:#1d1810;border-radius:10px;overflow:hidden}img{width:100%;display:block}'
'figcaption{padding:8px 12px;color:#cdc4b2}b{color:#ffd9a0}</style>' 'figcaption{padding:9px 13px;color:#cdc4b2}b{color:#ffd9a0}</style>'
f'<h1>PROCITY v3.1 — release money-shot tour (THE FLIP) <small>· seed {SEED} · settled measurements</small></h1>' f'<h1>PROCITY v4.0 — THE REAL MAP <small>· 23 real Australian towns · seed {SEED} · settled</small></h1>'
f'<div class="grid">{"".join(cards)}</div>') f'<div class="grid">{cards}</div>')
def main(): def main():
@ -131,51 +114,39 @@ def main():
except ImportError: except ImportError:
sys.exit('playwright not installed — tools/.venv/bin/python -m playwright install chromium') 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) OUTDIR.mkdir(parents=True, exist_ok=True)
srv = ensure_server() srv = ensure_server()
shots, errs_total = [], 0 shots, errs_total = [], 0
try: try:
with sync_playwright() as p: with sync_playwright() as p:
b = p.chromium.launch() 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}) pg = b.new_page(viewport={'width': 1280, 'height': 720})
errs = [] errs = []
pg.on('pageerror', lambda e: errs.append(str(e))) pg.on('pageerror', lambda e: errs.append(str(e)))
boot(pg, grp['flags']) pg.goto(f'{HOST}/index.html?{query}&dbg=1')
sfx = grp.get('suffix', '') pg.wait_for_function('window.PROCITY && window.PROCITY.plan && window.DBG && window.DBG.ready', timeout=25000)
for name in grp['street']: pg.evaluate("() => { const o=document.getElementById('pc-start'); if(o) o.style.display='none'; }")
stat = pg.evaluate(STREET_JS, name) try:
fname = f'{name}{sfx}.png' pg.wait_for_function('() => !window.PROCITY.fleet || window.PROCITY.fleet.ready', timeout=15000)
pg.screenshot(path=str(OUTDIR / fname)) except Exception:
cap = (f"?classic=1 — the town before v3 (no gig layer, no pools)" if sfx pass
else f"{stat['draws']} draws · {stat['tris']:,} tris") r = pg.evaluate(SHOT_JS, {'seg': seg, 'cluster': cluster})
if not sfx and stat.get('queue') is not None: cap += f" · queue {stat['queue']}" pg.screenshot(path=str(OUTDIR / f'{name}.png'))
if not sfx and name in ('district_posters', 'venue_night', 'queue_night'): cap += f" · {stat['posters']} posters" 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 + sfx, 'file': fname, 'caption': cap}) shots.append({'name': name, 'file': f'{name}.png', 'caption': caption})
print(f" \033[32m●\033[0m {name}{sfx}: {cap}") print(f" \033[32m●\033[0m {name}: {caption}")
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")
errs_total += len(errs) 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() pg.close()
b.close() b.close()
finally: finally:
if srv: srv.terminate() if srv: srv.terminate()
(OUTDIR / 'contact.html').write_text(contact_html(shots)) (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") 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) < 6 else 0) sys.exit(1 if errs_total or len(shots) < len(V4_TOUR) else 0)
if __name__ == '__main__': if __name__ == '__main__':

View File

@ -262,11 +262,26 @@ export function installDBG(deps) {
}, },
// scripted interior visit (drives the record_interior shot + the soak memory-baseline gate) // scripted interior visit (drives the record_interior shot + the soak memory-baseline gate)
enterShop(shopId) { // [Lane F R22 — Lane G's ask] enterShop takes a SELECTOR, not just an id, so cross-repo QA is a
const s = (plan.shops || []).find((x) => x.id === shopId); // one-liner on any town without looking ids up first:
if (!s) return { error: `no shop ${shopId}` }; // 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); 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 }; }, exitShop() { window.dispatchEvent(new CustomEvent('procity:exitShop')); return { exited: true }; },
}; };