# LANE A — CITYGEN · progress (PROCITY-A) *Status: **all deliverables landed; self-check ALL GREEN (156,352/156,352); rounds 2–30 closed**. R30 (v7.0-alpha ledger #6): the rotation boundary VERIFIED — F's `day` salt rides runtime streams only, plan generation structurally cannot receive it; all goldens frozen, zero re-pins.* ## Round 30 (2026-07-18) — the rotation boundary verify (ledger #6, small round as briefed) One job: prove F's day-salted stock rotation (207bcff) never reaches plan generation. **It holds.** Three measured legs (full detail in LANE_A_NOTES §30): (1) grep-zero for day identifiers over the proven `citygen/**` glob (8 files / 2,546 lines, census printed; pattern proven non-vacuous against `interior_mode.js`); (2) structural absence — `generatePlanFor` consumes no day-shaped opt and neither shell call site passes one; (3) day-inertness byte-measured — plan hash identical with `{day, stockDay}` hostilely injected as 1 and 37 vs absent, 3 seeds × synthetic+osm, 6/6. The salt's actual reach: `stk-*` stock-pick stream labels + dig `binSeed` + `setWeekNight` night-selection over the plan-time 7-night week (day picks a night, never generates one). Selfcheck ALL GREEN 156,352/156,352, fingerprint `0x5f76e76` frozen, every town pin green. No files of mine changed except these two notes. ## Round 27 wave 5 (2026-07-17) — THE FACADE FIX (the epoch's biggest catch) The ruling asked me to land B's one-liner in the real-roads seating and to **measure the marched path rather than assume it was clean**. I measured all three paths, and the ruling's premise was wrong in the half that mattered: **synthetic is broken too**. Not "every real town since R18" — **every town, every path, since v1**. Confirmed numerically (0 toward / 681 away, dot −1.0000) and then with my own eyes: `?classic=1`, synthetic main street = three blank boxes; walk into the block and NUMBAT PLAYTHINGS is there, facing the dirt. 637 of 681 lots pointed their facade at **no street at all**. - **Four sites, one sign.** `plan.js:122` (synthetic strip), `plan.js:244` (market row), `plan_osm:330` (marched avenue), `plan_osm:443` (real roads — B's proven one-liner). All `atan2(outward)` where the intent was `atan2(−outward)`, and **every one says so in its own comment**. The code never did what it said; CITY_SPEC:71 blessed the result by writing the SIM convention into the LOTS contract. After: synthetic 681/681, marched + all 3 fixtures 100%, real-roads katoomba **0/72 → 72/72** (B's number). - **The covenant moved, on John's ruling.** Fixing synthetic necessarily moves `0x3fa36874`, frozen "forever" since R16 — not Lane A's call, so I stopped and asked. Ruling: **amend and fix all paths** (the covenant is anti-drift machinery, not a defect preservative). **51 goldens re-pinned in one commit**; amendment recorded in CITY_SPEC as a one-time, ruling-gated precedent. - **A second bug the fix exposed, fixed with it:** `buildRealRoads` seated blocks at `KERB = 4 m` from the **centreline**, but a `main` carriageway is 10 m wide (kerb at 5 m) — **every main-street building has stood 1 m inside the road since R18**. Invisible until the shopfront turned around and its poster landed in the traffic lane. Blocks now seat behind the kerb per A's own corridor law. Drops 6.4% → **6.6%**, counted. - **The cross-convention gate (the lesson made law).** It does not restate a spec line — that is the trap F proved (72/72 "facing" over blank walls). It replicates **Lane B's own** `buildings.js` maths and asks whether the quad the *renderer* draws lands nearer the street than the lot centre. Fires if A's `ry` **or** B's `zf` drifts. It would have caught this on day one. - **→ F (blocking their re-gate):** `scaffold_check` is RED until F re-pins the covenant hashes duplicated in **F's own files** (`flags_check.py` `GOLDEN_HASH`/`GIG_GOLDEN`, `scaffold_check.mjs` `TOWN_GOLDENS`, two tour captions). Exact values handed over in LANE_A_NOTES §R27w5; I can't touch F's files. *Previous status: 156,212/156,212; rounds 2–27 closed (see below). R27 (v5.0 close): **CITY_SPEC v5 section** (tier ladder · `sourcing` vs `tier` · the three id-space fences · the manifest · Ruling 2) + the **charter risk list judged line by line** — risk #1 deliberately CARRIED, not pre-retired, because F's kill-the-server gate is what retires it. classic `0x3fa36874` / gig `0xb1d48ea1` frozen; **no golden moved this epoch**.* *Date: 2026-07-17 · owner: PROCITY-A (Opus 4.8)* ## Round 27 (2026-07-17) — v5.0 THE LIVE CRATE: the docs freeze (ledger #6) Lane A's half of the epoch close. Docs only; **no code, no golden moved** — correct, since tier 2 touches no plan, no cache, no atlas bytes. selfcheck **ALL GREEN 156,212/156,212**. - **CITY_SPEC v5 section**, written from the artifacts rather than the briefs (the epoch's own lesson): the **tier ladder** (0 parody / 1 static real / 2 live, each degrading softly); **`sourcing` is not `tier`** — both ship in every index, `tier: 1` is the rung and a mint atlas is *still* rung 1, while `sourcing: "real"|"mint"` is provenance (15 keyed atlases = **1 real + 14 mint**); the **three id-space fences**; the **manifest** (existence declared, never probed — the root fix for fail-soft-404 vs the zero-console-errors law; 15 entries, 15 dirs); **Ruling 2**; and A's single seam, `godverseShopId`. - **The id fences, verified not restated.** The plan-id/godverse-id trap is still live in the data and I reproduced it: in `redhill_godverse`, bare **`31`** is "Wholefood Cafe" as a plan id and "Silky Oak Furnature" as a godverse id — the exact ambiguity that made F's `enterShop(31)` assert vacuously on the wrong shop. It's a standing rule, not a fixed bug, and the spec says so. - **The risk list judged, without pre-retiring anyone's gate.** #1 (tier-2 latency) **CARRIED** — written in wave 2, F's reader doesn't exist yet, and *nothing retires that line except the gate going green*; retiring it from a doc while the code is unwritten is the precise species this epoch spent four holds catching. #2 **retired** (measured: max 2 atlases/shop vs C's cap of 2; mint at 16 items/1024², only the real POS shop at 2048²; the mecca case served by crate rotation — the remedy the risk itself named). #3 **retired — designed out, not solved**: it assumed fuzzy name-matching, and there is no matcher; Monster Robot is in *neither* census nor OSM, so no matcher could ever have found it. #4 **split**: write-back retired by John's ruling-as-mechanism (`/reserve`+`/buy` absent, not stubbed — you can't race a verb that doesn't exist), the `gone` read carried to F's #4. Two new carried: **#6 orphaned atlas**, **#7 "real" is one shop wide** (14/15 are mint — honest and marked, but the headline rests on one shop). - **The gate that armed itself.** My R25 orphan gate SKIPped on `newtown_godverse` while no atlas keyed there; G's mint atlases landed and it **began binding unattended** — 156,211 → **156,212**, and that +1 is the gate arming itself. Near-miss carried: redhill's lift drops keyed shop **2286 "Empire Revival"**; G stocked neighbour **2287**. Stocking 2286 today would orphan it. ## Round 25 (2026-07-17) — v5.0-alpha: the sweep vs Ruling 2 + the six pins (ledger #1) All 8 tree reds cleared — **6 were my own gate asserting Ruling 2 away**, 2 were goldens awaiting my pin. selfcheck **ALL GREEN 156,211/156,211**, scaffold + consistency green, classic frozen. - **My R24 gate encoded an assumption that Ruling 2 falsified.** I built the identity arm believing a godverse cache is *all* census shops. It's **mixed** by design — a keyed GODVERSE layer + an inherited OSM texture layer that G measured to be load-bearing (census-only Red Hill: 306 m spacing warn + a poster inside a kerb). So `newtown_godverse` (18 keyed + 54 texture) and `redhill_godverse` (10 + 27) were **healthy towns my gate called broken**. Keyed asserts now run **per-layer over the keyed subset only**; the "N/M missing" warn is gone. The one honest requirement left: a `godverse+osm` cache must have a **non-empty** godverse layer (else it's an osm cache with the wrong `source`) — still a warn. - **The uniqueness check was broken in both directions.** `new Set(all ids).size === shops.length` collapses every `undefined` into one entry, so it failed structurally on mixed caches *and* could hide a real duplicate behind the same collapse. Now over **defined ids only** — and, per the vacuous-gate law, proven able to fail: the validator rejects dups at the front door, so the test feeds one **straight to the lift** (`generatePlanOSM` doesn't validate) and asserts the predicate catches it. - **New: the orphaned-atlas gate** — my own R24 finding, enforced rather than filed. A committed `stock_godverse//` whose shop the lift dropped = real stock keyed to a shop that isn't in the town. Node-side, subject-aware (SKIPs by name when a town has no atlas), so it catches in seconds what F's #7b could only catch in a browser at the end of a round. **It already found one, benign today:** redhill drops keyed shop 2286 "Empire Revival" (unstocked ⇒ no orphan ⇒ gate passes). Filed for G: stocking it at the beta would orphan. Monster Robot Party is seated (lot 8). - **The six pins**, cross-checked two ways before landing — byte-identical across two fresh processes, and matching the values G stated independently in `86e2985`: `newtown_godverse` `0xcf69b387`/`0x1e266f60`, `redhill_real` `0x6046700f`/`0x673785ee`, `redhill_godverse` `0xb2224939`/`0xc6f80e18`. ## Round 24 (2026-07-17) — v5.0-alpha: the identity field (ledger #2) `shop.godverseShopId` — the optional key resolving `stock_godverse//`. Schema **stays v2**; absent everywhere except godverse caches, so the 21 osm caches are untouched by construction. Published to `web/assets/towns/README.md` (the doc G/E build to) + CITY_SPEC. selfcheck **ALL GREEN 152,030/152,030** (+15), synthetic `0x3fa36874` frozen, **no golden moved** — by design (see below). - **The brief's "the POS shop id IS the id" is false for the one shop that matters.** G's own docstring: Monster Robot Party is **not in thriftgod's census**, so its id is its **dealgod store id 3962749**, while census shops use thriftgod ids (max 2992). Two disjoint spaces on one field ⇒ built it as an **opaque key**: no derivation from `id`, no equality assert, no mapping table (the R12 gigKey law). - **The field had to survive the lift, or it was decoration.** `plan_osm` builds a *fresh* shop object and copies nothing unknown, so a cache-only field would never reach the runtime — and the runtime reads `plan.shops[]`, never the cache. It's now carried through, **conditionally**: absent ⇒ the key isn't written ⇒ `JSON.stringify` byte-identical ⇒ **every pinned golden holds until G re-emits** (that re-emit is the sanctioned move). - **Arms split by hazard:** malformed → **error** (it's a path segment; `"3962749"` vs `3962749` is the same coercion drift that broke the atlas contract), duplicate → **error** (two shops, one atlas = the mis-stock charter risk #3 forbids), missing on a godverse cache → **warn + fail-soft to tier 0**. - **Recorded departure:** the ledger said missing should be an *error*. It would invert the charter's determinism boundary (the **town** failing to load because **stock** identity is absent), contradict risk #3's own remedy ("fail-soft to tier 0"), and red the tree for C and E through all of wave 1 — G re-emits in wave 2 and it's their namespace, not mine. Teeth kept where R23 says to put them: selfcheck **fails on partial keying**, prints an explicit **SKIP + count** at zero. Flagged for Fable to overrule. - **Filed for G before they start (measured, wave 1):** `redhill_godverse` can't contain Monster Robot from **either** source. `godverse_town.py` takes roads from the OSM donor and shops from the census; the shop is in neither — not in thriftgod's census (G's R23 finding), and **not in E's `redhill_real`** (I checked it as it landed: 36 shops, **zero of type `record`**, nothing matching /monster|robot/). Needs explicit injection, or F's #7b id-equality gate fails at the end of the round. Also flagged: the census bbox must still clear `MIN_TOWN_SHOPS` (6) or the cache is rejected outright. - **Pins deliberately NOT taken this round yet.** `redhill_real` is untracked/in-flight (E is still editing `build_towns.py`), and `redhill_godverse` + the re-keyed `newtown_godverse` don't exist until wave 2. Pinning a moving file buys a false green or a spurious red. The round's law says A pins these **batched** — that batch lands once E commits and G's caches are in. `redhill_real` reads clean meanwhile: valid, no warns, 73.3 m spacing. - **Housekeeping:** removed toowoomba's two stale pins (E retired the cache in R23 — a golden for a town that can't load can never fire). E's attempt also proved my R23 spacing metric is **gameable**: the re-bbox fixed the median (395.7 → 89.4 m) while hub density fell 3/12 → 2/12. Necessary, not sufficient — D's hub test is the complement. **Ballarat (255 m) still flagged, still E's call; I pin whatever moves.** ## Round 23 (2026-07-16) — v5.0-alpha: the spacing warn (ledger #5) D's R22 finding made law: `MIN_TOWN_SHOPS` counts shops, but what makes a town read alive is **clustering**. `validateTownCache` now warns when `medianShopSpacing(cache) > MAX_MEDIAN_SPACING_M` (**150 m**) — the metric and threshold both exported from the barrel, so no lane re-derives them. **Warn, never error**: a flagged cache still boots a valid district (toowoomba shipped green through the whole v4.0 pack), E curates, the validator flags. selfcheck **ALL GREEN 161,372/161,372** (+72), synthetic `0x3fa36874` frozen, no golden moved. - **The threshold is measured, not picked.** Fable's brief offered darwin 27 m / toowoomba 388 m as "plenty of daylight for a line". Measuring all 23 towns showed the pack is a near-continuum, and the line is **forced** into `(119.3, 254.9]` — braddon (119.3 m) is alive by D's own hub test (7 shops within 160 m, identical to darwin's 7), ballarat (254.9 m) is not. 150 m sits in the pack's widest gap (**×2.14**). - **Two towns flag, not one.** The brief names toowoomba; the measurement adds **ballarat (255 m)**, which nobody had looked at — and which is independently one of the three towns my R22 cluster fallback fired on. Filed for E as a curation call, counted either way. - **The brief's anchors were in the wrong space, and it matters.** D's 27 m is **plan** space (post-lift); `validateTownCache` is **cache** space (raw lat/lon — the only thing it has at ingest). Darwin is 68.3 m in cache space, not 27. Coding a threshold interpolated between D's figures would have mixed spaces silently. Cache space is right *here*, and provably safe: the lift compresses dense towns (darwin plan/cache **0.40**) but not sparse ones (toowoomba **0.98**, ballarat **1.01**), so the error lives at the dense end and the warn only fires at the sparse end. Verified all 23 in both spaces: **one town disagrees** (`newtown_godverse`, 94.4 cache → 158.3 plan) — filed for G/Fable, not acted on, since no cache-space line catches it without false-positiving braddon, and its sparsity is census *coverage*, which re-bboxing can't fix. - Cost: ~1 ms/town at registration (25.6 ms for all 23 including fitzroy's 160 shops), against a <100 ms plan budget. Pending: re-pin whatever E's toowoomba re-bbox moves — **only** that. ## Round 22 (2026-07-16) — v4.0 THE EPOCH CLOSE: the pack absorbed + 36 goldens pinned (ledger #2) E's 17 new towns + Lane G's `newtown_godverse` fed through the lift and pinned in ONE golden commit (`REAL_TOWN_GOLDENS` + `REAL_TOWN_GIG_GOLDENS`: 5 → **23 towns each**, zero unpinned). **The headline: everything built this epoch held at 23-town scale, first try.** 1,210 shops → 1,133 seated (77 dropped, **6.4%**, counted by cause); **1 connected component and 3 venues on every one of the 23 towns**. `fitzroy` — 160 shops, 2× anything absorbed before — landed 139 seated, 1 component, 3 venues, 212 posters, all invariants green. The junction-protected DP, fragmentation ruling, cluster bias, poster cap and capacity fence needed **no new hardening**: the pack needed absorbing, not fixing. - **Graded cluster fallback (the one refinement).** Three towns have no candidate at the floor at all (`ballarat`/`toowoomba`/`newtown_godverse` top out at 2 shops within 160 m); R21's bias switched off there, stranding venues at 0. It now falls back to the **densest available** — ballarat 0,2,0 → **2,2,2**, godverse 0,1,1 → **2,2,2**, toowoomba 1,0,0 → 1,2,1. Fires only where the floor is unreachable ⇒ **no already-pinned town moved** (verified). Acceptance at pack scale: **20 of 23 towns have every venue ≥3**; the three above sit at their town's genuine ceiling — the carved-out case, stated explicitly. - **Drop outliers, counted not hidden:** `daylesford` 19% (32 shops on a 171-road graph — the thinnest in the pack, all overflow), `fitzroy` 13%. `RGAP` is already 0.5 m and `NODE_CLEAR` 3 m; the only lever left is spilling onto a different street, which buys a few percent by putting a shop on a road it isn't on. Same ruling as R20: **drop and count beats teleport**. - selfcheck ALL GREEN **161,300/161,300**; scaffold + consistency GREEN; cross-process deterministic (pack-xor `0xca140954`). classic / gig / marched fixtures frozen. Handshake fired → C + D + F: the pack is in, 23 towns pinned, start your verifies. **Ledger #6 (docs, `9c9ad73`): CITY_SPEC v4 final + the risk list retired-or-carried.** Closing this surfaced real drift — **three gig-layer behaviour changes had reached only these notes, never the spec** (R20's clearance widening to every venue kind, R21's cluster bias, R22's fallback). The amendment law says the spec is where they live; all three are now recorded, along with the pick *order* no lane could have inferred (cluster field → kind flavour → clearance → `farthestFirst` → seeded pick). Risk list judged against the pack, not the 5-town sample it was written for: **#1 retired** (widest town geelong 3,829 m = 77% of the 5 km law), **#2 retired** (thinnest darwin 12 shops = 2× `MIN_TOWN_SHOPS`), **#3 retired plan-side / carried for the runtime lanes** (a green plan is not a green town — C/D/F's verifies close it), **#4 retired** (DP knob measured; 1 component on all 23), **#6 NEW carried** (thin-graph seating capacity). **Ledger #1 (tri diet): not A's — B's numbers decided it.** The driver is E's four furniture GLBs (91.5%); posters are **222 tris**, buildings **348**. My cap worked and was never the lever, and B found the real coupling is **road density, not shop count** — so the absorb wasn't the cause either. My guess (buildings/LOD) was wrong at 0.2%; the measurement won, as it should. ## 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 0–1), 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 **3–6× shops** (292 total: katoomba 20→80, fremantle 21→80, newtown 18→72, bendigo 9→36, castlemaine 6→24) 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` 6→3 + a **roads-only `RGAP`** 2→0.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 A→E→A 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 Douglas–Peucker** — a point shared by ≥2 ways is a junction; simplify only between junctions, never through one (the collinear tee-off drop). Alone → 94–100% 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 A→E→A). 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 (Douglas–Peucker ε≈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 A→E→A 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/.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 2–4-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 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 `