PROCITY/docs/A-progress.md
m3ultra 110c82833b Lane A round 21: venue cluster-adjacency bias — D's relocation finding (ledger #1)
D found katoomba's pub 1,000m from the retail cluster — a gig with no passing crowd. Measuring all
five towns first showed it was SYSTEMIC: 11 of 15 venues sat with <3 shops within 160m (newtown's were
0/0/1 — every venue stranded). After the bias: 0 of 15. Katoomba's pub 1 → 9. Castlemaine cleared without
needing the floor exception the brief anticipated.

- pickVenues builds a CLUSTER-ADJACENT field first (>= CLUSTER_MIN 3 shops within CLUSTER_R 160m), then the
  kind flavors (pub corner / band_room fringe / rsl off-spine) bias WITHIN that field rather than overriding
  it (per the brief: biases, not absolutes) — a fringe band_room in the cluster beats one a kilometre from
  anyone. A town with no cluster at all falls back to the pool.
- SYNTHETIC FROZEN BY CONSTRUCTION, not by luck. The brief assumed a blanket filter would be a no-op because
  'the synthetic town is one dense cluster anyway'. Measurement says otherwise: 4 synthetic candidates PER
  SEED sit under the threshold (min cluster 0-1), so a blanket filter WOULD have moved the frozen golden.
  Gated on plan.source === 'osm' (undefined on synthetic) — impossible rather than unlikely. The marched
  fixtures are 'osm' but genuinely dense (min cluster 3/11/13) ⇒ verified no-op.
- Only the GIG goldens moved (venue selection is gig-layer; base town goldens untouched): katoomba
  0xbf586b77, newtown 0xacdb3eee, fremantle 0xcf3426fc, bendigo 0x646b2c23, castlemaine 0xf9713a69.
  classic 0x3fa36874 / gig 0xb1d48ea1 / marched fixtures FROZEN. selfcheck ALL GREEN 66419/66419;
  scaffold + consistency GREEN; cross-process deterministic.
- FYI: E's v4.0 pack (glebe/marrickville/newcastle) already boots clean through my loader (validates + full
  suites pass), listed UNPINNED — I pin the pack in R22; it does not gate the beta tag.

Handshake → C + D: cluster bias landed, goldens re-pinned, start your verifies (before/after table in
LANE_A_NOTES §R21).

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

599 lines
48 KiB
Markdown
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.

# LANE A — CITYGEN · progress (PROCITY-A)
*Status: **all deliverables landed; self-check all-green (66419/66419); rounds 221 closed** (see below).
R21: venue cluster-adjacency bias — D's relocation finding resolved (venues under 3 shops/160 m: 11 of 15
→ 0); gig goldens re-pinned; synthetic frozen by construction. R20: poster cap (katoomba 438→31) + E's
density widening absorbed (5 towns at 36× shops). classic/gig/marched-fixtures frozen.*
*Date: 2026-07-16 · owner: PROCITY-A (Opus 4.8)*
## Round 21 (2026-07-16) — v4.0-beta: the venue cluster-adjacency bias (ledger #1, the round's spine)
D filed it: katoomba's pub sat 1,000 m from the retail cluster — a gig with no passing crowd. Measuring all
five towns first showed it was **systemic: 11 of 15 venues** had <3 shops within 160 m (newtown's were
0/0/1). After the bias: **0 of 15**. Katoomba's pub went **1 → 9**; castlemaine cleared without needing the
floor exception Fable anticipated.
- `pickVenues` now builds a **cluster-adjacent field** (≥`CLUSTER_MIN` 3 shops within `CLUSTER_R` 160 m),
then the kind flavors (pub corner / band_room fringe / rsl off-spine) bias **within** it rather than
overriding it (per the brief: biases, not absolutes). No cluster at all falls back to the pool.
- **Synthetic frozen BY CONSTRUCTION**, not by luck. The brief assumed a blanket filter was a no-op because
"the synthetic town is one dense cluster anyway" measurement says otherwise: **4 synthetic candidates
per seed** sit under the threshold (min cluster 01), so a blanket filter *would* have moved the frozen
golden. Gated on `plan.source === 'osm'` (undefined on synthetic). Marched fixtures are 'osm' but genuinely
dense (min 3/11/13) verified no-op.
- **Only the GIG goldens moved** (venue selection is gig-layer): katoomba `0xbf586b77`, newtown `0xacdb3eee`,
fremantle `0xcf3426fc`, bendigo `0x646b2c23`, castlemaine `0xf9713a69`. Base goldens untouched.
classic/gig/marched fixtures frozen. selfcheck ALL GREEN **66419/66419**; scaffold + consistency GREEN;
cross-process deterministic.
- FYI: E's v4.0 pack (`glebe`, `marrickville`, `newcastle`) already boots clean through my loader (validates
+ full suites pass), listed UNPINNED **I pin the pack in R22**; it does not gate the beta tag.
Handshake fired C + D: cluster bias landed, goldens re-pinned, start your verifies.
## Round 20 (2026-07-16) — v4.0-beta THE FIVE TOWNS: the poster cap (density absorb pends E)
Two Lane-A deliverables; the first (independent) landed this commit, the second waits on E's widened caches.
1. **Poster cap (ledger #1) — LANDED.** My R19 finding (real katoomba: 438 spine posters, driving F's one
budget edge, `venue_night` 200,965 tris) is fixed. `gigs.js buildPosters` caps the spine pole run at
**`POSTER_PER_SHOP × shops`** (1.5/shop, floor 12) a seeded subsample of the run. **Scaled by shop
count, not edge count** (the whole point: a sparse real town has few shops but a big graph). Result:
katoomba **438 → 31** posters (~2× the synthetic feel), fremantle 32, castlemaine 14. **No-op on the
synthetic town** (493 shops cap ~740 its ~15 posters), so the synthetic gig golden `0xb1d48ea1` is
frozen. Posters are gig-layer, so the real-town BASE goldens are unchanged I added
`REAL_TOWN_GIG_GOLDENS` (the full district+poster plan per town: katoomba `0xbd332e83`, …) to
regression-guard the capped output byte-exact. selfcheck ALL GREEN **51675/51675**; scaffold + consistency
GREEN; cross-process deterministic. F: re-measure `venue_night`, the +965 edge should close.
2. **Density absorb (ledger #2) — LANDED.** E's five caches at **36× shops** (292 total: katoomba 2080,
fremantle 2180, newtown 1872, bendigo 936, castlemaine 624) absorbed; **all five place 3 venues**;
the poster cap auto-scaled off the live shop count as designed. **Capacity widened** at 4× the dominant
drop cause was overflow (edge-side full), so `NODE_CLEAR` 63 + a **roads-only `RGAP`** 20.5 (shared
marched `GAP` untouched fixtures frozen): drops **7.9% → 5.5%** (16/292), residual **counted by cause**
(`dropped_overflow`/`_overlap`/`_stranded`). Stopped there deliberately the next lever (spilling
overflow onto a different street) buys ~1% by putting a shop on a road it isn't on. **Venue facade bias
extended to `pub`** (R16 exempted it; on a real graph at density the pub's frontage poster overhung a
cross street newtown 0.91 m); still a no-op on synthetic. **Goldens re-pinned once** (base + gig, all
five). **Fixed:** the real-cache loader now skips E's new `index.json` (a selector index, not a cache).
selfcheck ALL GREEN **51866/51866**; scaffold + consistency GREEN; cross-process deterministic.
Handshake fired all: **densities absorbed, goldens pinned.** F: re-measure `venue_night` (poster cap)
and gate the five-town matrix.
## Round 19 (2026-07-16) — v4.0-alpha CLOSE: fragmentation ruling + findings hardened + goldens pinned
A was the round's spine (the AEA finalization deferred from R18). Every filed finding resolved; the tag is
F's to land.
1. **Fragmentation (D's finding — Fable ruled, A owns details).** The naïve real graph fragmented
(katoomba **105 components**, 58% street-metres on islands). Fixed in `plan_osm.buildRealRoads`:
(a) **junction-protected DouglasPeucker** a point shared by 2 ways is a junction; simplify only
between junctions, never through one (the collinear tee-off drop). Alone 94100% main-net.
(b) **cull/bridge** main component + near-fragments joined (`JOIN_TOL` 12 m; shop-island bridge 200 m);
far **shopless** islands culled. **Result: 1 connected component on all five towns** (katoomba 100%
main-net, 2 islands culled). Every dropped shop **counted** (`norm.dropped`) katoomba 19/20 (1 crowd
drop, the R18 overflow prediction measured), the other four kept all.
2. **E's poster-clearance finding fixed** (`gigs.js`): a spine poster overhanging a real crossing carriageway
is skipped measured exactly as the selfcheck kerb floor, a **no-op on synthetic** (gig golden frozen).
The 23 real-town clearance failures 0.
3. **Goldens pinned** (the finalization): all 5 real towns on their real-roads output
(katoomba `0xb7ffbca2`, newtown `0x6f984c81`, fremantle `0xfb197000`, bendigo `0x21179b3b`,
castlemaine `0x7c47f639`). synthetic/marched-fixtures/classic/default **frozen** the alpha changed
nothing outside the cache-schema path. selfcheck ALL GREEN **51670/51670**; scaffold + consistency GREEN.
4. **New finding filed (beta, non-blocking):** spine-poster density scales with edge count (katoomba 438
posters vs ~14 synthetic; draws stay low via instancing) a beta posters-per-town cap.
Handshake fired: `katoomba_real` boots real roads, golden pinned, findings resolved B/C/D/F. D re-measures
(1 component / 100% / 0 stranded); F applies B's `BIG_CITY` one-liner + gates + tags `v4.0-alpha`.
## Round 18 (2026-07-16) — v4.0-alpha REAL ROADS: cache schema v2 + the real-graph lift (the epoch headline)
Lane A is the round's long pole (serial spine AEA). Two commits:
**Commit 1 — cache schema v2 (`roads[]`), published FIRST** (`4dc80c2`) so E's `build_towns.py` can fetch
road geometry. A v2 cache adds `roads:[{kind,pts:[[lat,lon],…]}]` (`kind` = OSM `highway=*`);
`validateTownCache` accepts schema `…/1` AND `…/2` (roads absent marched fallback, nothing regresses);
`ROAD_KIND` maps the class to a CityPlan edge kind. Full contract in `web/assets/towns/README.md`.
**Commit 2 — the real-graph lift** (`plan_osm.buildRealRoads`): real OSM ways the CityPlan street graph,
**same schema out** (B/C/D/F consume it unchanged). project simplify (DouglasPeucker ε6 m) snap
coincident points (≤3 m) to shared nodes = real intersections straight-segment edges classify `main`
by OSM class + shop density seat each shop on its nearest real edge (real order/side), marched +
overlap-resolved. **Passes the full structuralSuite + gig district + determinism** on a committed synthetic
irregular graph (main + two cross streets + intersection + acute angle + dead-end lane + curve). Robust on
messy inputs (single road, coincident dups, disconnected ways, all-roads-too-short marched fallback).
Frozen (the alpha changes only the roads path): classic `0x3fa36874`, gig `0xb1d48ea1`, osm melbourne/
katoomba/silverton, + E's 4 trailing real caches pinned on v1 (bendigo/castlemaine/fremantle/newtown).
`katoomba_real` stays UNPINNED its golden re-pins to the real-ROADS output when E lands its roads (the
AEA finalization). selfcheck ALL GREEN **16810/16810**; scaffold + consistency GREEN.
Fidelity decision (charter risk #4): real edge + real order + real side + regularised spacing NOT
pixel-exact (real shops overlap; no-overlap is load-bearing). Alpha failure list (LANE_A_NOTES §R18): (1)
overflow drop on short edges (`norm.dropped`); (2) intersections = shared coordinates only (crossings
without a shared OSM node disconnected edges a D sim-graph risk); (3) blocks are per-edge frontage
strips not planar faces (a B ground-render audit item); (4) crowded-intersection overlap-resolve drop.
Handshake: schema v2 E (done). "katoomba_real boots with real roads, golden pinned" B/C/D fires when
E's roads land (katoomba is v1/marched until then). B/C/D audit prep isn't blocked on me.
## Round 17 (2026-07-16) — v3.2 THE SCOUT: the town-cache contract + plan_osm hardened for real data
Lane A ran **first** (E's `build_towns.py` builds to the contract). Scope fence held: **no new plan
features, no golden moved** the scout proves the *existing* `?plansrc=osm` contract eats real geography.
Delivered (one commit):
1. **The town-cache contract (published FIRST, ledger #6).** `plan_osm.js` gains `validateTownCache()`
(the authoritative `{ok, errors[], warnings[]}` check), `registerTownCache()`, a `generatePlanOSM(…,
{cache})` seam, and `TOWN_CACHE_SCHEMA`/`MIN_TOWN_SHOPS`/`MAX_TOWN_SPAN_M`. Shape: `{schema, key, town,
source, license, attribution, center{lat,lon}, shops[{id,name,type,lat,lon,suburb?}]}`. Home declared:
`web/assets/towns/<key>.json` (+ a `README.md` with the full spec). Documented in CITY_SPEC (v3.2 section)
+ LANE_A_NOTES §R17 (the E handshake).
2. **`plan_osm` hardened** blank name registry label; the validator gates finite center/coords, a
6-shop floor, and warns on unknown types (→opshop), dup ids, and **span > 5 km** (the mega-strip). Proven
against thriftgod's real 2,828-shop `city_source.json`: boots a valid plan, 0 bad coords, 0 "undefined"
names. Marching regularises messy real coords, so dup/odd positions are a non-issue.
3. **Selfcheck: contract stress suite + real-cache path.** A committed synthetic-adversarial suite (valid
messy caches boot + pass structural/gig; invalid ones reject; wide-spread warns; the registry seam works),
plus an auto-loader for `web/assets/towns/*.json` validates, runs the suites, pins per-town goldens.
Empty until E lands caches (guarded, green). ALL GREEN **15277/15277**; scaffold + consistency GREEN.
4. **The v4 risk list** (LANE_A_NOTES §R17, for Fable's charter): (a) **the mega-strip** an unbounded
Overpass bbox marches a whole region (2,828 shops) into an 11.6 km strip; town SELECTION is E's real
quality lever, warned at >5 km, the generator can't rescue an over-broad query; (b) sparse compact towns
(a 2 km inner-Canberra radius held only 6 shops); (c) settled — osm facades clear (rows 54 m apart), so
the R16 clearance bias is a no-op on real towns.
Handshake open: E builds `build_towns.py` to the contract; E→A each cache → I pin its golden.
## Round 16 (2026-07-16) — v3.1 THE FLIP: corner-poster clearance bias (#6) + no-spine osm fixture (#7)
Lane A ran **first** among the parallel lanes (F bookends with the flip; A's golden feeds F's default-boot
gate). The R15 finding was that a corner venue's frontage poster can sit near a crossing kerb.
Delivered (one commit):
1. **Corner-poster fix — via `pickVenues`, not a nudge.** I proved the brief's proposed fix (nudge along
the facade) is geometrically impossible: the crossing edge is **parallel** to the facade and spans it
(0/187 along-facade slides help). Root cause: band_room/rsl land on through-lots on narrow warehouse
blocks, so the `+Z` wall (B's door side) faces the block's other street, overlapping its carriageway by
up to ~3 m. **Surfaced to Fable; ruling: bias `pickVenues`.** It now prefers a band_room/rsl candidate
whose `+Z` facade clears every carriageway by ≥ `POSTER_CLEAR` (0.5 m), broadening off-spine when the
fringe is uniformly narrow. Result: **0/851** frontage posters below floor across 400 seeds (worst
0.51 m); pub untouched (hero shot unmoved). Feasibility pre-checked: a clear lot always exists.
2. **Gig golden re-pinned `0x4f4a549d → 0xb1d48ea1`** — the sanctioned move (posters moved); synthetic/
classic `0x3fa36874` frozen. §v3 FROZEN marker records the amendment.
3. **Selfcheck: exemption → floor.** R15's blanket frontage exemption is now a real invariant — frontage
posters must clear their kerb by ≥ `POSTER_CLEAR` (exported from the barrel), spine posters keep ≥ 0.
4. **No-spine osm fixture `silverton`** (ledger #7): a constructed single-row town (all shops one latitude
⇒ 0 `main` edges). Closes the R14 gap (both real fixtures had a spine); proves `pickVenues`' no-spine
branch + `buildPosters`' no-main fallback. Base golden pinned `0xd4b351c9`; sweep asserts 0 main edges +
graceful 24-venue placement. Selfcheck **ALL GREEN 14769/14769**; scaffold + consistency GREEN.
5. **CITY_SPEC §v3 amended** (golden line, poster clearance-bias prose, no-spine note, recorded amendment).
Left the prime-flag-law blockquote for F (the v3.1 classic-law + flags-table is F's §v3 amendment).
For F: default-boot golden = `0xb1d48ea1`; `?classic=1` regression target = `0x3fa36874`.
## Round 15 (2026-07-16) — the frontage-poster +Z flip (ledger #1) + one sanctioned golden move
Lane A ran **first** (B's hero re-shoot + F's tour hang off it). The R14 review found every venue's
own-frontage poster had been on the building's **Z back** since R13 — ~16 m behind the street face
(B measured 15.9 m), invisible from the street, which is why John's `queue_night` framed no poster.
Delivered (one commit):
1. **Frontage-poster fix** (`gigs.js` buildPosters item 1): flipped to the **+Z street facade** (B's
canon) AND `ry = lot.ry + π` so the printed face points `+Z` at the queue-side camera (else `DoubleSide`
renders a mirrored band name). Proven: pub + rsl frontage posters now sit **0.06 m from B's door**,
printed-face·(+Z street) = **1.000**. Spine posters untouched.
2. **Gig golden re-pinned `0x1f636349 → 0x4f4a549d`** — the round's ONE sanctioned move (posters moved);
synthetic `0x3fa36874` frozen. §v3 FROZEN marker records the amendment (amendment law).
3. **Selfcheck poster check refined**: the brief expected the 400-seed off-carriageway assert to stay
green, but the `+Z` facade is the block-interior side in *plan* coords, so **corner venues** (a band_room/
rsl fronting one street with another behind — seed 1 lot#481, 1.58 m from a crossing centreline) put the
frontage poster near that street. That's B's building geometry (the door is there too), not a mid-road
defect — so the no-bitumen rule now **exempts building-bound frontage posters** and keeps it for
free-standing **spine** posters. Sweep green again: **14264/14264**.
4. **CITY_SPEC §v3 amended** (poster prose → `+Z` facade + `ry+π`; golden line; frontage/spine carriageway
split; recorded amendment in the FROZEN marker). LANE_A_NOTES §R15 hands B/F the new golden + a
corner-venue eyeball note.
Convention settled: the visible venue face is `+Z` (`buildings.js` facade `zf=+d/2`; `venue.js` door/queue
`+Z`). The selfcheck's `facing=(sin,cos)` "faces frontEdge" is the sim/GLB convention, 180° off the
visual front — the same π-ambiguity D fixed for the fleet in R13.
## Round 14 (2026-07-16) — v3.0 RELEASE: invariant sweep + CITY_SPEC §v3 freeze
Release round, Lane A scope = harden + freeze (no new systems). **No golden moved** (synthetic
`0x3fa36874`, gig `0x1f636349` both re-verified frozen); the only code touched is `selfcheck.js`
(test-only), so no seeded stream gained a draw — the release-round law holds.
Delivered:
1. **400-seed district invariant sweep** (`selfcheck.js`, ALL GREEN **14264/14264** in ~0.7s). Factored
the district checks out of `gigSuite` into `districtInvariants(plan,label)` (behaviour-preserving — the
4 hero-seed gigSuite runs are byte-identical), then swept **seeds 1400 + the large edge seeds**
asserting the whole contract each: 24 venues, ≥1 pub, never the openLate shop, no band twice per night
town-wide, per-venue 46 nights, every poster off the carriageway. Added a distribution assert that the
seeded count **spans {2,3,4}** across 400 seeds (146/121/133) — proves the range is exercised, not pinned.
`structuralSuite` still runs on the 6 hero SEEDS + osm parity (unchanged); the sweep runs the light
district checks only, so 400 gig-on generations stay ~0.5s.
2. **osm graceful placement** in the sweep: `katoomba` (19 shops, **no warehouse district**) still lands
24 valid venues incl. a pub for every hero seed — proving `pickVenues`' off-spine fallback. Honest
scope noted: both osm fixtures keep a main spine, so this proves the no-warehouse branch (not no-spine).
3. **Poster-check hardening**: kept the `roadWidth/2` nearest-edge formulation (a literal `poleOffset`
compare would false-fail corner posters); added a `1e-6` epsilon on the strict `<` to immunise the
exact kerb-line boundary against r2 rounding (only reachable on a no-main-spine fallback; not hit today).
4. **CITY_SPEC §v3 frozen at `v3.0`** — §v2-style FROZEN marker, final wording pass over venues table,
genre map, gigKey contract, corridor law, gig golden. Genres final (pubrock/grunge/covers, Fable's ruling).
Coordination: the R12 single-venue **alias line stays in §v3 for F to delete atomically** with the
`gig_state.js` getters (debt #1) — deleting it now would be doc-ahead-of-code (alias still live) and would
race F on the same lines; the freeze marker carves it out explicitly. Flagged to F that the runtime block
omits `nightOf(id)`, and to B that `venue.js:228` is a second bare-string edit site. **Not touched:**
`web/assets/manifest.json` shows a concurrent Lane E `glb_law` edit in the shared tree — left for E.
## Round 13 (2026-07-15) — v3.0-beta THE DISTRICT: 24 venues + the week + town-wide posters + verge fix
Lane A ran **first** again (C/D/B/F all hang off the district `plan.gigs`). Same flag, same prime-flag
law: **flags-off byte-identical — all v2 goldens frozen** (synthetic `0x3fa36874`, osm unchanged).
Self-check **ALL GREEN 3273/3273**; `scaffold_check` + `consistency` GREEN. The gig **entry shapes are
unchanged** — the alpha's one-venue readers now iterate 24. Interfaces published in `LANE_A_NOTES.md`
(§Round 13) and `CITY_SPEC.md` (§v3.0-beta) in this same commit.
Delivered (all behind `?gigs=1`):
1. **24 venues, three kinds**`pickVenues` converts a seeded 24 plain shops in place: `pub` (spine
end), `band_room` (warehouse fringe), `rsl` (off-spine), 4th = a 2nd pub at the far end. Each carries
`venueKind` + `genreKey` (`pubrock`/`grunge`/`covers`). Registry gains weight-0 `band_room`+`rsl` types
(facades, hours, footprints). Placement degrades gracefully so osm towns still get venues. Never the
openLate landmark; venues occupy distinct lots.
2. **The gigKey contract (debt #1)**`gigKeyFor(genreKey) => 'gig-'+genreKey`, the single exported
genre→bed mapping (no table anywhere). C/E/B/F all route through it; F deletes the `GIG_BED` bridge.
3. **`plan.gigs` goes district** — 7 nights × every venue, each playing a seeded **46** (dark the rest,
reads true). `gigId` unique town-wide; cover skewed by kind (pub 50%/`$210`, RSL 45%/`$25`, band_room
70%/`$26`). **No band plays two venues on one night**; custom names spread night-major across the week.
4. **`plan.posters` goes town-wide** — tonight's playing venues each get a frontage poster + a seeded spine
run (2/main edge, either verge). Different venues share pole runs — the district reading.
5. **Verge offset (debt #5 PAID)** — the R12 bug (spine posters at `node.x+2` = mid-bitumen on a 28 m
corridor) is fixed by the new corridor law: `roadWidth`/`vergeBand`/`poleOffset(edge)` in registry.js,
re-exported from the barrel — the road-vs-verge split of `edge.width`, published as **functions** (no
plan fields ⇒ base goldens frozen). Selfcheck now asserts no poster stands on a carriageway.
6. **Registry `pub.interior` corrected** `'band_room'`→`'pub'` (was an unread field; now avoids a
name-collision with the new `band_room` venue type — C keys recipes off `shop.type`, not this field).
Golden re-pin: gig golden `0xa6ae5a5e`→**`0x1f636349`** (multi-venue changed gig-plan output; re-pinned
in-commit per CITY_SPEC amendment law). New selfcheck coverage: 24 venues + known kinds + `type≡venueKind`
+ genre map + gigKey ≡ `gig-`+genreKey; 46 gigs/venue, ≤1 gig/night/venue, no same-night band clash
town-wide; posters advertise night-0 and **clear every carriageway**. Sample seed 20261990 → 3 venues:
The Exchange Hotel (pub), Wangaratta RSL, The Vinyl Shed (band room, dark tonight); 14 gigs across the week.
**Handshake note for Lane C:** `RECIPES.band_room`/`RECIPES.rsl` don't exist in `theme.js` yet — until C
adds them those venues fall back to the op-shop interior. That's the order (A→C). **Not run by A:** the
browser flags/scale gates + depot (Gate 3, John-blocked) — F's end-of-round acceptance, and they'd test
R12 runtime that hasn't adapted to the new venue count yet.
## Round 12 (2026-07-15) — v3.0-alpha GIG LAYER (`?gigs=1`): venue + gigs + band names + closing-time debt
Lane A ran **first** this round (the whole round hangs off the `plan.gigs` handshake). Everything is
behind `?gigs=1` (prime flag law): **flags-off is byte-identical — all v2 goldens frozen** (synthetic
`0x3fa36874`, osm melbourne `0x34cfdec0`, katoomba `0x0f652510`). Self-check **ALL GREEN 3074/3074**
(+ the whole gig suite ×2 sources), `qa.sh --strict` GREEN (6/6). Verified in-browser both modes.
**Architecture — a post-hoc augmentation layer, not a generator change.** `web/js/citygen/gigs.js`
`withGigs(plan, seed, opts)` takes a finished plan and adds the venue + gigs + posters; the selector
(`index.js generatePlanFor`) applies it **only** when `opts.gigs`. So `generatePlan` is untouched
(goldens can't move) and the layer is source-agnostic (synthetic + osm both get gigs).
Delivered (all behind the flag):
1. **Venue** — one `pub` per town: a spine-END plain shop converted in place to `type:'pub'` (keeps lot
id/geometry ⇒ enterable, no overlap), `shop.venue=true`, hours `[17,23]`. **Never** the openLate shop.
Registry gains a weight-0 `pub` type (facades `djsim-pub`+`federation`, `interior:'band_room'` for
Lane C, fittings). Weight-0 ⇒ never auto-placed ⇒ base plan unchanged.
2. **`plan.gigs`** — 7 nightly entries `{gigId, venueShopId, bandName, genreKey:'pubrock', night, startSeg:5,
endSeg:5, cover}`; `cover` seeded ~half free / half `$2$10`. F picks tonight's + charges at the door.
3. **Band names**`bandName(seed)` generator (90s Aussie pub-rock wordlists) **+ the drop-in**
`web/assets/custom_bands.json` (`{bands:[…]}`, ships with 10 examples incl. "The Feral Galahs"):
present → priority into the pool (up to 7 surface); absent → pure generator, zero fetch (`?noassets`).
Deterministic given (seed, file). The shell (F) does the async load and passes `customBands` in.
4. **`plan.posters`** — ~6 seeded poster placements `{id, gigId, x, z, ry}` (venue + spine) for Lane B.
5. **Closing-time debt PAID**`isOpen(shopOrHours, hour)` exported from the barrel: the canonical
half-open law (`open ≤ h < close`) as code, for D's gig-night patronage surge (was prose in round 9).
6. **Debug map** `map.html?gigs=1` shows the venue ring + "♪ PUB" label, poster pins, and a panel gig
line ("♪ The Thornbury Hotel · tonight: Screaming Utes · $10 cover · 7 nights · 6 posters"). Loads the
band drop-in for the view. Flags-off map is unchanged (adds a benign `pub (0)` legend row).
New self-check coverage: `gigSuite` runs the full structural suite (venue-aware the pub is exempt
from the single-late-shop rule via `!sh.venue`, a no-op on base plans) + gig invariants (one pub, gig
schema, cover {0}[2,10], band names resolved, posters ref real gigs), on synthetic + both osm towns;
a `?gigs`-offbase byte-identity check per seed; a custom-band priority + determinism check; and a
**gig golden `0xa6ae5a5e`**. CITY_SPEC gained a "CityPlan v3 gig layer" section; handshake interfaces
for C/D/B/F are in `LANE_A_NOTES.md`. Sample seed 20261990 venue "The Thornbury Hotel" (a converted
spine-end pawn shop), openLate video preserved.
## Round 9 (2026-07-15) — freeze the v2 contract + closing-time ruling (ship round)
Small round; the most "done" lane makes it official.
1. **CITY_SPEC v2 contract FROZEN** added a "CityPlan v2 frozen producer contract" section: the
producer API (`generatePlanFor`/`generatePlan`/`generatePlanOSM`/`osmTownKeys`/`chunkIndex`), the
goldens table (synthetic `0x3fa36874`, osm/melbourne `0x34cfdec0`, osm/katoomba `0x0f652510`, keyed
(seed,plansrc,town)), the town-key mechanism, osm normalization rules, and the
hours/openLate/storeys laws all matching code as shipped (F diffs claims vs code in review).
Refreshed the stale round-6 "Plan sources" blockquote to the multi-town reality. Layer-1 changes now
require a CITY_SPEC amendment + golden re-pin in the same commit.
2. **Recipe sanity — dry-ran end-to-end** (not just eyeballed): appended a throwaway Newtown (42 shops)
to `OSM_TOWNS`, ran selfcheck the full parity suite auto-covered it (1931/1932, only the golden
unpinned) and printed a ready-to-paste `newtown: 0x9045e577,` line. Reverted; no 3rd town shipped
(didn't fall out free). Improved selfcheck to print that `⚠ UNPINNED — add to OSM_GOLDENS` line so
the recipe is genuinely mechanical; fixed the recipe wording to match.
3. **Closing-time occupant ruling** (D's on-call question) ratifies what D's `sim.js` already does and
freezes it: half-open hours, no new entry when closed, **occupants drain (finish their bounded
dwell) never popped**, player not force-ejected, openLate video is the night destination. In
CITY_SPEC (contract law) + `LANE_A_NOTES.md` (for D). No code change asked of D.
Self-check **ALL GREEN 1727/1727**; `qa.sh --strict` GREEN. Only doc + one selfcheck log-line touched;
no generator change all three goldens unchanged.
## Round 8 (2026-07-15) — OSM to parity + a second town (carried R7 brief, FULLY LANDED)
## Round 8 (2026-07-15) — OSM to parity + a second town (carried R7 brief, FULLY LANDED)
The carried brief (slipped R7) is done all four items, not a partial.
1. **Selfcheck parity**: extracted the full invariant suite into `structuralSuite(plan, label)` and
run it on **every (source, town)** synthetic + both OSM towns now get identical coverage
(storeys, one-shop-per-lot, solid-use, facing, within/cross-block overlap, corridor coverage,
determinism, exactly-one-openLate). **ALL GREEN 1727/1727** (was 1362 OSM went from a subset to
the full suite ×2 towns).
2. **Second town**: **Katoomba** (Blue Mountains, 19 shops, op-shop/book heavy) a small-regional
contrast to inner-Melbourne (95, book heavy). `?plansrc=osm&town=katoomba`. `osm_fixture.js` is now
`OSM_TOWNS { melbourne, katoomba }` + `DEFAULT_TOWN`; `generatePlanFor(seed, source, { town })`.
Golden pinned `0x0f652510`. **Melbourne golden `0x34cfdec0` and synthetic `0x3fa36874` unchanged.**
3. **"Add a town" recipe** documented in `LANE_A_NOTES.md` (R8) extraction `OSM_TOWNS` entry
pin golden tell F. Mechanical.
4. **Normalization + logging**: importer normalizes real data to the contracts (unknown kindopshop,
hours clamp so only one shop is openLate, video-preferred landmark w/ fallback) and **logs** what it
changed via `{ report }` selfcheck prints e.g. `melbourne: 95 shops · normalized {typesRemapped:0,
hoursClamped:5, openLate:video}`. The contract never bends; the data does.
Extraction one-liner (Katoomba, reusable see recipe): cell 0.05 around (-33.714, 150.312), same
KIND map + dedupe + sort-by-id as Melbourne (cell 0.06 around -37.803, 144.967). Screenshots:
`docs/shots/laneA/osm-{melbourne,katoomba}.png`. `qa.sh --strict` GREEN.
## Round 6 (2026-07-14) — the second plan source (`?plansrc=osm`), FULLY LANDED
The carried-over OSM plan-source seam (no-showed rounds 45) is **done** not the escape-hatch
partial, the whole thing, invariants green. A second CityPlan producer behind the same contract, so
Lanes BF consume it unchanged.
- **`osm_fixture.js`** 95 REAL inner-Melbourne secondhand/charity/music/book/toy/antique shops
(book 36, toy 14, opshop 13, record 12, video 11, pawn 9), extracted deterministically from
thriftgod's `city_source.json` Overpass cache. Checked in, imported as a module **zero network**.
- **`plan_osm.js`** `generatePlanOSM(seed)`: projects real lat/lon metres, bins shops into
latitude-band avenues off a spine (real row + real eastwest order preserved), marches them into
a valid, non-overlapping CityPlan, centres on origin, reports its true 362×486 bbox. Real shop
names kept ("Central Catholic Bookshop"). Exactly-one-openLate-video rule matches synthetic.
- **`index.js`** `generatePlanFor(seed, source)` selector; default `'synthetic'` is byte-identical
(golden `0x3fa36874` untouched). `generatePlanOSM` exported for F.
- **`selfcheck.js`** parameterized by source: OSM runs the full structural invariant suite
(frontEdge/facing/no-overlap/chunk-coverage/finite/JSON/determinism/openLate) + its own golden
**`0x34cfdec0`**; synthetic-only brief checks (market/arcade/dept/milkbar) deliberately skipped.
- **`map.html`** `?plansrc=osm` renders it; screenshot `docs/shots/laneA/osm-melbourne.png`.
- Seam + goldens + shape-differences documented for F's determinism gate in `LANE_A_NOTES.md` (R6).
**Verified**: 95/95 shops placed, 0 bad frontEdges / 0 overlaps / 0 backwards / full chunk coverage,
deterministic, synthetic path byte-identical. Self-check **ALL GREEN 1362/1362**; `qa.sh --strict` GREEN.
Honest cut (documented): street geometry is regularized (real row+order drive it, exact projected
metres do not) and shop-name parody is deferred.
## Round 4 (2026-07-14) — v1-tag round: 2 confirmations, 1 hardening fix
**A1 — storeys checker (post-F2): CONFIRMED.** `tools/qa/consistency_check.mjs` `permittedMax` scoping
(Lane F's F2, matching my R3 spec) gives **0 storeys warnings** across 7 seeds; `qa.sh --strict` GREEN
(4/4). False-negative test: injected a real `video@2` into a scratch copy of the checker's input
caught on every seed. No benign-anchor false positives, no real-drift false negatives.
**A2 — openLate contract: AUDITED + HARDENED.** F's night gate `hours[1] >= 22` was matching 415
shops/seed (regular videos jittered from base-21 to 22), not the single landmark it assumes. Fixed in
plan.js so `hours[1] >= 22` `openLate` (exactly one, always the video rental): regular shops close
by 21:00; only the deterministically-chosen video landmark reaches 22 (`LATE_HOUR`). This also makes
the generator finally match what `web/index.html:159-161` and `soak.py:68` already assumed
("at night only the openLate shop is open"). Added selfcheck assertions; verified across 15 seeds.
** GOLDEN.hash: `0x098eec2b` `0x3fa36874`** intended output change from the A2 fix (not drift;
F2 is tooling-only and didn't touch generation). Same lots/shops/types/names/skins; only a few videos'
close hour moved 2221. Details for Fable in `LANE_A_NOTES.md` (round 4).
**Self-check:** `✓ ALL GREEN — 1301/1301` (was 1283; +18 are the new openLate exactly-one / fieldthreshold
/ is-video assertions). Seed 20261990 "Boolarra Heads": 681 lots, 493 shops, unchanged.
> **⚠ Fable — commit-label mixup (no action needed on the code):** my round-2 changes are committed
> and correct, but a shared-working-tree race landed them under Lane C's message —
> commit **`4235060` "Lane C: keeper stand pose…" actually contains only my 5 Lane A files** (a
> concurrent `git commit` grabbed my staged index between my `add` and my `commit`). Lane C's real
> keeper-pose changes are still uncommitted & intact. Nothing is pushed, so relabel at your leisure
> when the tree is quiet (`git commit --amend`). Systemic: lanes should commit atomically with
> `git commit -- <paths> -m …` to avoid this. Self-check green 1283/1283; content is safe.
## TL;DR
`generatePlan(citySeed) → CityPlan` is done: a deterministic, JSON-serializable, <1ms pure-data
town generator matching CITY_SPEC schema v1, plus `chunkIndex`, the shared shop-type registry, a
seeded 90s-AU name generator, a Canvas-2D map debugger, and a node self-check that asserts the
whole acceptance contract. `node web/js/citygen/selfcheck.js` prints **ALL GREEN (1082/1082)**.
Five seeds render as recognisable towns (screenshots in `docs/shots/laneA/`).
## Deliverables (all present)
| # | file | what |
|---|---|---|
| 1 | [web/js/citygen/plan.js](../web/js/citygen/plan.js) | `generatePlan(citySeed)`, `chunkIndex(plan)`, `CHUNK`, `chunkKey` |
| 2 | [web/js/citygen/names.js](../web/js/citygen/names.js) + [wordlists.js](../web/js/citygen/wordlists.js) | seeded shop+town names, 50+ patterns, short signboard forms |
| 3 | [web/js/core/registry.js](../web/js/core/registry.js) | `SHOP_TYPES` (all 9), district weights, enums, pure helpers |
| 4 | [web/map.html](../web/map.html) | standalone Canvas-2D plan viewer (pan/zoom/hover, layer toggles, seed box, regen, export JSON, chunk grid) |
| 5 | [web/js/citygen/selfcheck.js](../web/js/citygen/selfcheck.js) | `node web/js/citygen/selfcheck.js` full acceptance harness |
| + | [web/package.json](../web/package.json) | `{"type":"module"}` so `node` runs the self-check as ESM (see *Treaty notes*) |
| + | `docs/shots/laneA/seed-*.png` | 5 seeds screenshotted (20261990, 1, 42, 777, 8675309) |
## What the generator produces (matches the CITY_SPEC design brief)
- **Main-street spine** 7 stations SN through the origin, x-jitter ±30m, 28m corridor, `kind:'main'`.
Continuous narrow-frontage (69m) retail both sides.
- **Cross streets** at 4 stations; the two central ones are wider "second high streets", the outer
two are grittier `backstreets`. All `kind:'side'`, ±8° jitter.
- **Market square** west of the origin a tidy 5×8 grid of `stall` lots + the `dept` anchor
fronting the spine.
- **Arcade** a `kind:'arcade'` (width 5) pedestrian lane cutting east through a mid-spine block,
lined both sides with tiny 35m record-heavy shops.
- **Warehouse fringe** sparse big lots beyond one spine end (N or S, seeded), mostly `infill`
with the odd pawn shop.
- **Residential collar** a loose ring road of `house` lots with **24 corner milk bars** embedded.
- **Laneways** (`kind:'lane'`) behind the central blocks, with `yard` lots for back-door flavour.
Sample (seed 20261990 "Boolarra Heads"): 27 nodes, 22 edges, 35 blocks, 711 lots, 523 shops, 152 chunks.
## Self-check coverage (`node web/js/citygen/selfcheck.js`)
Determinism (two runs byte-identical) · <100ms (actually <1ms) · every edgereal nodes · every blockreal
district · every lotreal block + **valid frontEdge** + positive size · every shopreal lot + known type +
**facade skin exists on disk** + sane hours + named · one-shop-per-lot · **no overlapping lots within a
block** (rotated-rect OBB / SAT test) · **chunkIndex covers every lot** & buckets reference real ids ·
JSON round-trip lossless · design-brief presence (spine, arcade, market, stalls, dept anchor, 24 milkbars) ·
all registry + used facades exist in `web/assets/gen/`. Run over 6 seeds **1082/1082**.
## House-law compliance
- **Zero `Math.random` and zero THREE imports in `js/citygen/*` and `registry.js`** (grep-clean).
All randomness flows through `web/js/core/prng.js`. (map.html uses `Math.random` only to pick a
*random seed to view* the generation it then runs is fully seeded; the map is pure Canvas 2D,
no THREE.)
- JSON-serializable, deterministic, <100ms verified.
- Registry is flat data every lane imports; facade pools are real files in `web/assets/gen/`.
## Treaty / cross-lane notes for Fable
1. **`web/package.json` added.** Needed so `node web/js/citygen/selfcheck.js` runs the ES modules
(browsers use `<script type=module>`; node needs `"type":"module"`). It's additive infra
python `http.server` and browsers ignore it, no deps, no build. Not in the ownership table;
flagging for your blessing. Other lanes can now node-test their pure modules too.
2. **Schema extensions (non-breaking).** Added `kind` to each `block` (its district-kind string,
alongside the spec's `district` **id**) so the map & Lane B can theme a block without a district
lookup. `block.district` is the **district id**; `districts[id].kind` is the kind. Everything
else matches schema v1 exactly.
3. **`ry` convention (Lane B please read):** a lot's `ry` is the Y-rotation so a GLB modelled
facing **Z** ends up with its facade's outward normal pointing at its `frontEdge` street.
Derivation + a worked example are in `plan.js` (`marchStrip`). `frontEdge` is a valid edge id
for every lot (stalls/dept front the nearest spine edge).
4. **`pipeline/gen_names.py` intentionally NOT shipped.** The Lane A brief offered an *optional*
Ollama word-expansion script, but `pipeline/*` is **Lane E's** owned dir per the CITY_SPEC
ownership table. Wordlists are hand-authored and checked in; runtime does zero network calls.
If you want the Ollama expansion, it should live in Lane E's pipeline or be re-assigned.
## How to eyeball it
```
cd web && python3 -m http.server 8130
# http://localhost:8130/map.html (default seed)
# http://localhost:8130/map.html?seed=42 (any uint32 seed; ?seed= drives the screenshot harness)
node web/js/citygen/selfcheck.js # acceptance harness, prints ALL GREEN
```
## Open questions for Fable
- Retail density: a core seed yields ~500 shops (continuous frontage, as briefed). Happy with that,
or dial down for Lane B's draw-call budget? (It's one constant per band; trivial to tune.)
- Want the residential collar to be a true closed ring vs the current 4-edge loop? (Current reads
fine on the map; a fuller street graph is a later enhancement.)
## Adversarial review + fixes (round 1)
Ran a 6-dimension multi-agent review of the whole lane (27 agents: determinism · schema/spec ·
geometry · chunk/JSON/perf · cross-lane contract · code quality), each finding independently
verified by a skeptic agent that ran the real generator to confirm or refute. **21 findings raised,
15 confirmed real, 6 refuted.** All confirmed defects are now **fixed and re-verified**; the
self-check grew new assertions so none of these classes can regress silently.
| # | defect (confirmed) | sev | fix |
|---|---|---|---|
| D1 | market stalls + dept anchor faced **backwards** (`ryEast` sign inverted facades pointed west, away from the spine they front) | HIGH | `ryEast = atan2(-1,0)`; self-check now asserts every lot faces its `frontEdge` |
| D2 | **cross-block lot overlaps** at spine×cross-street corners (≈45 building-lot pairs/seed, 8.7m) self-check only tested *within* a block | HIGH | reserve corner (rung near-spine inset 1434m) + a deterministic overlap-resolution pass (demote later lot to `infill`); self-check now runs **global** cross-block OBB/SAT over building lots |
| D3 | `chunkIndex` **skipped chunks** (32m point-sampling on a 64m grid); the origin chunk under the spine was omitted; road width ignored | HIGH | AmanatidesWoo grid supercover + kerb rails (centreline ± half-width); self-check now asserts edge coverage vs a dense supercover |
| D4 | market stalls' `frontEdge` pointed at the wrong spine segment (z-band mismatch, up to 122m off) | MED | stalls `spineEdges[2]`, dept `spineEdges[3]` |
| D5 | name generator emitted **doubled possessives** (`Maccass`) `{First}*s` fired both the `*` and the literal `s` | HIGH | dropped the redundant `*`; simplified `fill()`; self-check rejects unresolved tokens |
| D6 | map.html drew the main road with `var(--roadmain)`, which canvas can't parse the spine rendered near-invisible | MED | pass the bare custom-property name to `getcss()` (now a proper brown spine) |
| D7 | registry facades/fittings drifted from the CITY_SPEC table | LOW | **amended CITY_SPEC's shop-types table** (in this commit, flagged) to match the registry lanes import |
| + | `_sbl` Map cache was memoized onto the plan polluted exported JSON with `"_sbl":{}` | LOW | moved the cache to a module-level var, reset per regen |
| + | `cornerBoost` could push single-storey types (video/milkbar) above their registry max | LOW | gated to types with registry max 2 |
| + | no golden-fingerprint / cross-revision drift guard; lot coords never checked finite | | added a committed golden hash for seed 20261990 + finiteness assertions |
| + | undocumented `block.kind` / `block.district`-is-id; dead stall guard | | documented the schema in CITY_SPEC; removed dead code |
**Refuted (6)** investigated and dismissed with reasons, e.g. "the determinism test is a
tautology" (true that it only catches in-process nondeterminism so I *added* the golden fingerprint
for real drift detection) and "`package.json` isn't in the ownership table" (correct, but it's
required infra now added to the table).
**Post-fix self-check:** `✓ ALL GREEN — 1271/1271` (was 1082; +189 checks are the new
facing/cross-block-overlap/edge-coverage/finiteness/fingerprint assertions). Seed 20261990
"Boolarra Heads": 27 nodes, 22 edges, 35 blocks, 681 lots, 493 shops, 174 chunks. All 5 seed
screenshots re-captured. Golden fingerprint `0xb5d5cc13`.
## Round 2 — cross-lane integration responses (2026-07-14)
Picked the lane back up after BF integrated against it. Worked through the notes those lanes left
for Lane A (`docs/LANES/LANE_F_NOTES.md §83.57`, `B-progress.md`, `E-progress.md`) and closed
every request that lands in my files. **Self-check green at 1283/1283; golden fingerprint refreshed
`0xb5d5cc13` `0x098eec2b`; map.html re-verified in-browser (renders a town, chunk-grid overlay
works, zero console errors).** No screenshot refresh needed the changes don't alter the plan's 2D
appearance (the map colours by type/use, not by facade or hours).
| # | request (who asked) | what I changed | files |
|---|---|---|---|
| R1 | **"Which field marks the open-late shop, and what's the `hours` encoding?"** (Lane F §8); "exactly one late-night shop per town" (F §3.5) | The old `sr()<0.06` per-shop dice yielded **0…many** late shops and because its formula was `max(close,19)+0..2` usually **none past 22:00**, so F's `hours[1]>=22` gate was flaky/false. Replaced it with **exactly one deterministic `openLate:true` landmark** per town, chosen after de-confliction from a naturally-late type (video/milk bar, never a stall), closing **22:00 or 23:00**. Encoding documented: `hours=[open,close]`, 24h ints, `0≤open<close≤23`. | `plan.js`, `CITY_SPEC.md` |
| R2 | **Skin-name double-prefix trap** registry stores full filenames, `skins.js` expected bare keys `facade-facade-…jpg.jpg` 404s (Lane F §7); **"pick one and say so"** on field types (Lane B) | Added a **Field-type contract** to CITY_SPEC: all ids + id-refs are **integers**; `facadeSkin`/registry `facades` entries are **full filenames** under `web/assets/gen/`, loaded verbatim (extended to E's ground/awning/sky/wall/interior skins). Lane A is the source of truth; consumer fixtures conform to it. | `CITY_SPEC.md` |
| R3 | **`chunkIndex` furniture-drop** verge props at a perpendicular offset bucketed into a chunk that omitted their edge (Lane B) | Rewrote edge bucketing from a 3-rail supercover to a **gap-free corridor rasterisation**: each edge is split into 64m pieces and every cell of each piece's `±width/2` corridor-rect AABB is bucketed. Now *provably* covers the full road+verge band. Self-check samples 9 offsets across the band (incl. between-rail) to prove it. | `plan.js`, `selfcheck.js` |
| R4 | **`stall` pool has only 1 facade** (Lane E validator wants 2) | Added `facade-corrugated.jpg` (market-shed backdrop) to the `stall` pool matches Lane E's manifest facadetype map, which already tags corrugated for stall. Every type now has 2 facades. | `registry.js`, `CITY_SPEC.md` |
**New self-check assertions (regression guards):** exactly one `openLate` shop per seed; the late
shop closes 22:00 and isn't a stall; `chunkIndex` covers the full road corridor sampled densely
across the verge band (not just the centreline).
**Empirical result** (5 seeds): every town gets one late-night video store e.g. seed 20261990
"Yass VIDEO" open till 23:00, seed 42 "MOVIES" till 23:00, seed 1 "RENTALS" till 22:00. Both
stall facades now appear in the market square.
### Notes for Fable / other lanes
- **Lane B can drop its private furniture-rasterisation workaround** if it likes `chunkIndex.edges`
now covers the whole corridor at the source, so any consumer (incl. Lane D footpath NPCs) can trust it.
- **Orientation:** CITY_SPEC Units/axes + Layer-1 schema) already specifies GLB front = **Z** at
`ry=0`, world facing `(sin ry, cos ry)` toward `frontEdge`; the self-check enforces it and B renders
it correctly. Lane B's note prose says "+Z" informally flagging only so the **Z** spec stays the
single reference for Lane C/E when modelling facades. No code change; it works.
- **Still-open design questions from round 1** (retail density ~493/core vs Lane B's draw budget; true
closed residential ring) remain parked for your call both are one-constant tunes, not rework.
## Round 3 — verification round (2026-07-14, per ROUND3_INSTRUCTIONS "Lane A — on-call + one verification")
Small round as briefed. All three tasks done; `bash tools/qa.sh --strict` **GREEN** (4/4 gates,
selfcheck 1283/1283, manifest 0 errors).
**1. F finding #5 — "shops with storeys outside the registry type range" → verified STALE / CLOSED.**
Scanned every shop across 8 seeds against the *correct* permitted range. Result: **zero violations.**
`video`/`milkbar`/`stall` (registry `[1,1]`) are observed at 1 storey only (e.g. 412 video shops, none
at 2 so F's specific "video at 2" case does not occur). The only shops above their registry `max`
are the tall-capable types (`record`/`opshop`/`toy`/`book`/`pawn` `[1,2]`, `dept` `[2,3]`) reaching 3
via the intentional **corner anchor** exactly the CITY_SPEC "occasional 3-storey corner anchor". My
round-1 `cornerBoost` gate (only boosts registry max 2) already prevents the reported bug.
- **This is the source of `qa.sh`'s 5 consistency warnings** ("N shops have storeys outside their
registry range ask Lane A"). **They are benign** all intended corner anchors, not planregistry
drift. *Lane F: you can silence them by scoping the check to `permittedMax = max≥2 ? min(max+1,3) : max`
instead of the strict registry `max`.*
- **Hardened anyway** (the finding's real lesson): the selfcheck's storeys assertion was too lax
(`<= Math.max(mx,3)` would have passed a video at 3). Tightened to the exact permitted range, so a
single-storey type exceeding 1 (or any type past `min(max+1,3)`) now fails loudly. Proved non-vacuous
(rejects `video@2`, `record@4`, `dept@1`; allows `record@3`). Documented the `storeys` rule in the
CITY_SPEC schema so the intended-vs-drift question can't recur.
**2. Golden fingerprint confirmed.** Committed `GOLDEN.hash = 0x098eec2b` matches the current selfcheck
fingerprint on main. No output drift. (selfcheck.js assertion tightening doesn't touch the plan, so the
hash is unchanged.)
**3. Hours / `openLate` contract — ready for its first real consumer (Lane F §3.5).** No changes needed:
`hours=[open,close]` (24h ints, `0≤open<close≤23`), **exactly one `openLate:true` landmark per town**
(closes 22:00, video/milkbar, never a stall). ROUND3_INSTRUCTIONS §Lane F restates it correctly, so
F's night-gate wiring can consume it as-is. Full contract in CITY_SPEC schema + the round-2 notes above.