# LANE A — CITYGEN · progress (PROCITY-A) *Status: **all deliverables landed; self-check all-green (14264/14264); rounds 2–14 closed** (see below). v3.0 RELEASE round: 400-seed district invariant sweep landed + CITY_SPEC §v3 frozen at `v3.0`. Goldens unchanged (synthetic `0x3fa36874`, gig `0x1f636349`) — no golden moved, no rng draw added this round.* *Date: 2026-07-16 · owner: PROCITY-A (Opus 4.8)* ## 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 1–400 + the large edge seeds** asserting the whole contract each: 2–4 venues, ≥1 pub, never the openLate shop, no band twice per night town-wide, per-venue 4–6 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 2–4 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: 2–4 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 2–4. 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. **2–4 venues, three kinds** — `pickVenues` converts a seeded 2–4 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 **4–6** (dark the rest, reads true). `gigId` unique town-wide; cover skewed by kind (pub 50%/`$2–10`, RSL 45%/`$2–5`, band_room 70%/`$2–6`). **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: 2–4 venues + known kinds + `type≡venueKind` + genre map + gigKey ≡ `gig-`+genreKey; 4–6 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`-off≡base 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 kind→opshop, 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 4–5) is **done** — not the escape-hatch partial, the whole thing, invariants green. A second CityPlan producer behind the same contract, so Lanes B–F 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 east–west 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 4–15 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 22→21. 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 / field⟺threshold / 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 -- -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 S→N through the origin, x-jitter ±30m, 28m corridor, `kind:'main'`. Continuous narrow-frontage (6–9m) 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 3–5m 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 **2–4 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 edge→real nodes · every block→real district · every lot→real block + **valid frontEdge** + positive size · every shop→real 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, 2–4 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 `