Closes my own R19 finding + F's one open budget edge. A sparse REAL town has few shops but a big edge graph, so the 'two posters per main edge' pole run reads busy (katoomba 438) and drove venue_night to 200,965 tris (+0.5% over the 200k soft ceiling). - gigs.js buildPosters caps the spine pole run at POSTER_PER_SHOP × shops (1.5/shop, floor 12) — a seeded subsample. SCALED BY SHOP COUNT, not edge count (the whole point). katoomba 438→31 posters (~2x the synthetic feel), fremantle 32, castlemaine 14. NO-OP on the shop-dense synthetic town (493 shops ⇒ cap ~740 ≫ its ~15 posters), so the synthetic gig golden 0xb1d48ea1 stays FROZEN. - Posters are gig-layer (withGigs), so the real-town BASE goldens are unchanged (correctly green). Added REAL_TOWN_GIG_GOLDENS (the full district+capped-poster plan per town: katoomba 0xbd332e83, …) to regression-guard the capped output byte-exact, not just via districtInvariants. classic/gig/marched fixtures frozen; selfcheck ALL GREEN 51675/51675; scaffold + consistency GREEN; cross-process deterministic. → F: re-measure venue_night, the +965 edge should close. Density absorb (ledger #2) pends E's widened caches — the poster cap already reads the live shop count, so it auto-scales to the widened towns. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
46 KiB
LANE A — CITYGEN · progress (PROCITY-A)
Status: all deliverables landed; self-check all-green (51675/51675); rounds 2–19 closed, R20 poster-cap landed (see below). v4.0-beta: spine-poster cap scaled by shop count (katoomba 438→31, closes F's venue_night edge); real-town gig goldens pinned. classic/gig/marched-fixtures frozen. Density absorb pends E's widened caches. Date: 2026-07-16 · owner: PROCITY-A (Opus 4.8)
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.
- Poster cap (ledger #1) — LANDED. My R19 finding (real katoomba: 438 spine posters, driving F's one
budget edge,
venue_night200,965 tris) is fixed.gigs.js buildPosterscaps the spine pole run atPOSTER_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 golden0xb1d48ea1is frozen. Posters are gig-layer, so the real-town BASE goldens are unchanged — I addedREAL_TOWN_GIG_GOLDENS(the full district+poster plan per town: katoomba0xbd332e83, …) to regression-guard the capped output byte-exact. selfcheck ALL GREEN 51675/51675; scaffold + consistency GREEN; cross-process deterministic. → F: re-measurevenue_night, the +965 edge should close. - Density absorb (ledger #2) — PENDING E. E re-fetches general retail (C's class→archetype table) and re-emits the five caches at 3–6× shop count. When they land I load-test seating/overflow/venue placement and re-pin the real goldens once. The poster cap already reads the live shop count → auto-scales to the widened towns.
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.
- 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_TOL12 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. - 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. - Goldens pinned (the finalization): all 5 real towns on their real-roads output
(katoomba
0xb7ffbca2, newtown0x6f984c81, fremantle0xfb197000, bendigo0x21179b3b, castlemaine0x7c47f639). synthetic/marched-fixtures/classic/default frozen — the alpha changed nothing outside the cache-schema path. selfcheck ALL GREEN 51670/51670; scaffold + consistency GREEN. - 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):
- The town-cache contract (published FIRST, ledger #6).
plan_osm.jsgainsvalidateTownCache()(the authoritative{ok, errors[], warnings[]}check),registerTownCache(), ageneratePlanOSM(…, {cache})seam, andTOWN_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(+ aREADME.mdwith the full spec). Documented in CITY_SPEC (v3.2 section)- LANE_A_NOTES §R17 (the E handshake).
plan_osmhardened — 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-shopcity_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.- 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. - 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):
- 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+Zwall (B's door side) faces the block's other street, overlapping its carriageway by up to ~3 m. Surfaced to Fable; ruling: biaspickVenues. It now prefers a band_room/rsl candidate whose+Zfacade 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. - Gig golden re-pinned
0x4f4a549d → 0xb1d48ea1— the sanctioned move (posters moved); synthetic/ classic0x3fa36874frozen. §v3 FROZEN marker records the amendment. - 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. - No-spine osm fixture
silverton(ledger #7): a constructed single-row town (all shops one latitude ⇒ 0mainedges). Closes the R14 gap (both real fixtures had a spine); provespickVenues' no-spine branch +buildPosters' no-main fallback. Base golden pinned0xd4b351c9; sweep asserts 0 main edges + graceful 2–4-venue placement. Selfcheck ALL GREEN 14769/14769; scaffold + consistency GREEN. - 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):
- Frontage-poster fix (
gigs.jsbuildPosters item 1): flipped to the +Z street facade (B's canon) ANDry = lot.ry + πso the printed face points+Zat the queue-side camera (elseDoubleSiderenders 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. - Gig golden re-pinned
0x1f636349 → 0x4f4a549d— the round's ONE sanctioned move (posters moved); synthetic0x3fa36874frozen. §v3 FROZEN marker records the amendment (amendment law). - Selfcheck poster check refined: the brief expected the 400-seed off-carriageway assert to stay
green, but the
+Zfacade 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. - CITY_SPEC §v3 amended (poster prose →
+Zfacade +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:
- 400-seed district invariant sweep (
selfcheck.js, ALL GREEN 14264/14264 in ~0.7s). Factored the district checks out ofgigSuiteintodistrictInvariants(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.structuralSuitestill 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. - 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 — provingpickVenues' off-spine fallback. Honest scope noted: both osm fixtures keep a main spine, so this proves the no-warehouse branch (not no-spine). - Poster-check hardening: kept the
roadWidth/2nearest-edge formulation (a literalpoleOffsetcompare would false-fail corner posters); added a−1e-6epsilon on the strict<to immunise the exact kerb-line boundary against r2 rounding (only reachable on a no-main-spine fallback; not hit today). - 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):
- 2–4 venues, three kinds —
pickVenuesconverts 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 carriesvenueKind+genreKey(pubrock/grunge/covers). Registry gains weight-0band_room+rsltypes (facades, hours, footprints). Placement degrades gracefully so osm towns still get venues. Never the openLate landmark; venues occupy distinct lots. - 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 theGIG_BEDbridge. plan.gigsgoes district — 7 nights × every venue, each playing a seeded 4–6 (dark the rest, reads true).gigIdunique 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.plan.postersgoes 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.- 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 ofedge.width, published as functions (no plan fields ⇒ base goldens frozen). Selfcheck now asserts no poster stands on a carriageway. - Registry
pub.interiorcorrected'band_room'→'pub'(was an unread field; now avoids a name-collision with the newband_roomvenue type — C keys recipes offshop.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):
- Venue — one
pubper town: a spine-END plain shop converted in place totype:'pub'(keeps lot id/geometry ⇒ enterable, no overlap),shop.venue=true, hours[17,23]. Never the openLate shop. Registry gains a weight-0pubtype (facadesdjsim-pub+federation,interior:'band_room'for Lane C, fittings). Weight-0 ⇒ never auto-placed ⇒ base plan unchanged. plan.gigs— 7 nightly entries{gigId, venueShopId, bandName, genreKey:'pubrock', night, startSeg:5, endSeg:5, cover};coverseeded ~half free / half$2–$10. F picks tonight's + charges at the door.- Band names —
bandName(seed)generator (90s Aussie pub-rock wordlists) + the drop-inweb/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 passescustomBandsin. plan.posters— ~6 seeded poster placements{id, gigId, x, z, ry}(venue + spine) for Lane B.- 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). - Debug map —
map.html?gigs=1shows 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 benignpub (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.
- CITY_SPEC v2 contract FROZEN — added a "CityPlan v2 — frozen producer contract" section: the
producer API (
generatePlanFor/generatePlan/generatePlanOSM/osmTownKeys/chunkIndex), the goldens table (synthetic0x3fa36874, osm/melbourne0x34cfdec0, osm/katoomba0x0f652510, 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. - 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-pastenewtown: 0x9045e577,line. Reverted; no 3rd town shipped (didn't fall out free). Improved selfcheck to print that⚠ UNPINNED — add to OSM_GOLDENSline so the recipe is genuinely mechanical; fixed the recipe wording to match. - Closing-time occupant ruling (D's on-call question) — ratifies what D's
sim.jsalready 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.
- 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). - 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.jsis nowOSM_TOWNS { melbourne, katoomba }+DEFAULT_TOWN;generatePlanFor(seed, source, { town }). Golden pinned0x0f652510. Melbourne golden0x34cfdec0and synthetic0x3fa36874unchanged. - "Add a town" recipe documented in
LANE_A_NOTES.md(R8) — extraction →OSM_TOWNSentry → pin golden → tell F. Mechanical. - 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'scity_source.jsonOverpass 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 (golden0x3fa36874untouched).generatePlanOSMexported 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 golden0x34cfdec0; synthetic-only brief checks (market/arcade/dept/milkbar) deliberately skipped.map.html—?plansrc=osmrenders it; screenshotdocs/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 concurrentgit commitgrabbed my staged index between myaddand mycommit). 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 withgit 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 | generatePlan(citySeed), chunkIndex(plan), CHUNK, chunkKey |
| 2 | web/js/citygen/names.js + wordlists.js | seeded shop+town names, 50+ patterns, short signboard forms |
| 3 | web/js/core/registry.js | SHOP_TYPES (all 9), district weights, enums, pure helpers |
| 4 | 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 | node web/js/citygen/selfcheck.js — full acceptance harness |
| + | 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. Allkind:'side', ±8° jitter. - Market square west of the origin — a tidy 5×8 grid of
stalllots + thedeptanchor 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
infillwith the odd pawn shop. - Residential collar — a loose ring road of
houselots with 2–4 corner milk bars embedded. - Laneways (
kind:'lane') behind the central blocks, withyardlots 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.randomand zero THREE imports injs/citygen/*andregistry.js(grep-clean). All randomness flows throughweb/js/core/prng.js. (map.html usesMath.randomonly 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
web/package.jsonadded. Needed sonode web/js/citygen/selfcheck.jsruns the ES modules (browsers use<script type=module>; node needs"type":"module"). It's additive infra — pythonhttp.serverand 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.- Schema extensions (non-breaking). Added
kindto eachblock(its district-kind string, alongside the spec'sdistrictid) so the map & Lane B can theme a block without a district lookup.block.districtis the district id;districts[id].kindis the kind. Everything else matches schema v1 exactly. ryconvention (Lane B please read): a lot'sryis the Y-rotation so a GLB modelled facing −Z ends up with its facade's outward normal pointing at itsfrontEdgestreet. Derivation + a worked example are inplan.js(marchStrip).frontEdgeis a valid edge id for every lot (stalls/dept front the nearest spine edge).pipeline/gen_names.pyintentionally NOT shipped. The Lane A brief offered an optional Ollama word-expansion script, butpipeline/*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.edgesnow 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)towardfrontEdge; 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 topermittedMax = max≥2 ? min(max+1,3) : maxinstead of the strict registrymax. - 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 pastmin(max+1,3)) now fails loudly. Proved non-vacuous (rejectsvideo@2,record@4,dept@1; allowsrecord@3). Documented thestoreysrule 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.