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>
599 lines
48 KiB
Markdown
599 lines
48 KiB
Markdown
# LANE A — CITYGEN · progress (PROCITY-A)
|
||
|
||
*Status: **all deliverables landed; self-check all-green (66419/66419); rounds 2–21 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 3–6× 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 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/<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 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 -- <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 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 `<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 14→34m) + 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 | Amanatides–Woo 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** (`Macca’s’s`) — `{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 B–F integrated against it. Worked through the notes those lanes left
|
||
for Lane A (`docs/LANES/LANE_F_NOTES.md §8/§3.5/§7`, `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 facade→type 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 plan↔registry
|
||
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.
|