# LANE A — CITYGEN · progress (PROCITY-A) *Status: **all deliverables landed; self-check all-green (1727/1727); rounds 2–8 closed** (see below). `qa.sh --strict` GREEN.* *Date: 2026-07-15 · owner: PROCITY-A (Opus 4.8)* ## 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 `