PROCITY/web/assets/towns
m3ultra bee7563a76 Lane E R27 (v5.0): provenance v5-final — and the roster gap the freeze caught (ledger #6)
THE BUG THE FREEZE FOUND: redhill_godverse was ON DISK but NOT IN THE ROSTER — for three rounds. The v5
provenance table needed real numbers, so I measured instead of carrying the v4 row forward. The count
didn't match: 23 caches on disk, 22 rostered. The missing one was redhill_godverse — THE TOWN THE ENTIRE
v5 EPOCH WALKS TO.

Mine, squarely. index.json is Lane E's file (write_index()). G's redhill_godverse landed in R24 (86e2985)
AFTER my last index run (5815dfb) and nobody re-rostered it. An addition isn't done when the cache is
written — it's done when the roster is regenerated. That is the exact mirror of my own R24 lesson ("a
retirement isn't done when the cache is deleted; it's done when the consumers are swept"): I wrote the
lesson for deletions and missed its reflection for additions.

WHY FOUR GREEN ROUNDS NEVER CAUGHT IT — a gate-shape lesson. Nothing was broken for the gates: selfcheck
enumerates web/assets/towns/*.json DIRECTLY (so it saw the town; A even pinned its goldens) and F's crate
smoke boots BY KEY (?town=redhill_godverse). The roster's only consumer is B's HUD selector (hud.js, my
R20 ledger #2 design). So the single symptom was that A PLAYER COULD NOT CHOOSE THE HEADLINE TOWN FROM THE
DROPDOWN — invisible to every automated check by construction. A gate that reads around the artifact under
test cannot test it: the vacuous-gate species, one layer out.
Fixed: re-ran write_index() -> 23 rostered, 23 on disk, 0 missing. Only index.json moved (every cache
byte-identical again). selfcheck ALL GREEN 156,212, golden 0x3fa36874 untouched. qa.sh --strict --matrix
7 passed / 0 failed / 0 warn.

-> F (#5, the tour): the money shot walks Musgrave Road into Monster Robot Party. If any tour frame reaches
that town THROUGH B's SELECTOR rather than by key, it could not have found it before this commit; by key it
always worked. Worth one glance at your framing path.
-> B: the dropdown now carries both godverse towns. Your `${t.state ? ', ' + t.state : ''}` handles their
empty state gracefully — nothing needed, and the defensive write is why this cost nothing cosmetically.
-> G: your godverse caches carry no `state`, so they roster as state:''. Harmless (B degrades cleanly). For
"Red Hill, QLD" in the picker, emit `state` — write_index() already prefers the cache's own field.

PROVENANCE v5-FINAL. The ⟨v5.0-FINAL⟩ rows are measured, not inherited — the town-pack row still claimed
v4's "22 towns / 1192 shops", stale since my R24 ballarat retirement and redhill build (now 21 real / 1196
shops / 299 heroes, + both godverse towns = 23 rostered). Added the two v5 art rows: the real-stock atlas
(1 crate, 120 items, Monster Robot's own POS, sku_ ids) and the mint atlases (14 crates, 224 items, seeded
from real dealgod listings, mint_ ids). These are the table's FIRST AMBER ROWS — everything before v5 was
green web-ok; real product photography is the first asset class this project ships that a human must clear
before public release.

I VERIFIED MY OWN PROSE AND IT CAUGHT ME: I first wrote the mint crates as "6 record · 6 book · 3 toy" —
that is the breakdown of all FIFTEEN crates, which includes Monster Robot's REAL record crate. The 14 mint
crates are 5 record · 6 book · 3 toy. A count attributed to the wrong population, one line from being
frozen into the release record. Every other claim in the table re-measured true (21/1196/299/8 states/23
rostered/roster==disk/72/37/120 items/224 items/both zones in-house-green).

THE LICENCE LINE'S STORY, recorded as a mechanism. The amber rows are guarded by exactly one printed line,
so the freeze says why it took two rounds to make honest: R23's glob matched 0 files and returned 0; R25's
permissive `or` fallback let the print go silent while the check still passed — the flag stopped reaching a
human on real product photos while the gate said OK. Now: one spelling, one lookup with one owner, and the
line is COUNTED (judged N => N lines, or FAIL). Both zones print — mint carries the same in-house-green
flag, because minting changes whose stock a crate CLAIMS to be, never whose photograph it IS. Frozen today
at: atlas-QA OK — 15 per-shop atlas(es) valid, 15 licence line(s) shown, 0 warning(s).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 13:59:49 +10:00
..
_raw Lane E R24 (v5.0-alpha): the crate's real street, the honest gate, and Ballarat retired 2026-07-17 01:06:00 +10:00
adelaide_real.json Lane E R21 (v4.0): the town pack — 22 towns, 1192 shops (DATA-ONLY, non-gating) 2026-07-16 21:06:46 +10:00
bendigo_real.json Lane E R20 (v4.0-beta): the density widening — widened fetch, 5 caches re-emitted, towns index 2026-07-16 19:30:50 +10:00
bowral_real.json Lane E R21 (v4.0): the town pack — 22 towns, 1192 shops (DATA-ONLY, non-gating) 2026-07-16 21:06:46 +10:00
braddon_real.json Lane E R21 (v4.0): the town pack — 22 towns, 1192 shops (DATA-ONLY, non-gating) 2026-07-16 21:06:46 +10:00
brunswick_real.json Lane E R21 (v4.0): the town pack — 22 towns, 1192 shops (DATA-ONLY, non-gating) 2026-07-16 21:06:46 +10:00
castlemaine_real.json Lane E R20 (v4.0-beta): the density widening — widened fetch, 5 caches re-emitted, towns index 2026-07-16 19:30:50 +10:00
darwin_real.json Lane E R21 (v4.0): the town pack — 22 towns, 1192 shops (DATA-ONLY, non-gating) 2026-07-16 21:06:46 +10:00
daylesford_real.json Lane E R21 (v4.0): the town pack — 22 towns, 1192 shops (DATA-ONLY, non-gating) 2026-07-16 21:06:46 +10:00
fitzroy_real.json Lane E R21 (v4.0): the town pack — 22 towns, 1192 shops (DATA-ONLY, non-gating) 2026-07-16 21:06:46 +10:00
fremantle_real.json Lane E R20 (v4.0-beta): the density widening — widened fetch, 5 caches re-emitted, towns index 2026-07-16 19:30:50 +10:00
geelong_real.json Lane E R21 (v4.0): the town pack — 22 towns, 1192 shops (DATA-ONLY, non-gating) 2026-07-16 21:06:46 +10:00
glebe_real.json Lane E R21 (v4.0): the town pack — 22 towns, 1192 shops (DATA-ONLY, non-gating) 2026-07-16 21:06:46 +10:00
hobart_real.json Lane E R21 (v4.0): the town pack — 22 towns, 1192 shops (DATA-ONLY, non-gating) 2026-07-16 21:06:46 +10:00
index.json Lane E R27 (v5.0): provenance v5-final — and the roster gap the freeze caught (ledger #6) 2026-07-17 13:59:49 +10:00
katoomba_real.json Lane E R20 (v4.0-beta): the density widening — widened fetch, 5 caches re-emitted, towns index 2026-07-16 19:30:50 +10:00
launceston_real.json Lane E R21 (v4.0): the town pack — 22 towns, 1192 shops (DATA-ONLY, non-gating) 2026-07-16 21:06:46 +10:00
marrickville_real.json Lane E R21 (v4.0): the town pack — 22 towns, 1192 shops (DATA-ONLY, non-gating) 2026-07-16 21:06:46 +10:00
newcastle_real.json Lane E R21 (v4.0): the town pack — 22 towns, 1192 shops (DATA-ONLY, non-gating) 2026-07-16 21:06:46 +10:00
newtown_godverse.json Lane G R24 (v5.0-alpha): the crate's real street — redhill_godverse, the keyed re-emit, and two contract conflicts 2026-07-17 01:31:07 +10:00
newtown_real.json Lane E R20 (v4.0-beta): the density widening — widened fetch, 5 caches re-emitted, towns index 2026-07-16 19:30:50 +10:00
northbridge_real.json Lane E R21 (v4.0): the town pack — 22 towns, 1192 shops (DATA-ONLY, non-gating) 2026-07-16 21:06:46 +10:00
README.md Lane A round 25 (v5.0-alpha): the sweep reconciled with Ruling 2 + the six pins (ledger #1) 2026-07-17 02:23:57 +10:00
redhill_godverse.json Lane G R24 (v5.0-alpha): the crate's real street — redhill_godverse, the keyed re-emit, and two contract conflicts 2026-07-17 01:31:07 +10:00
redhill_real.json Lane E R24 (v5.0-alpha): the crate's real street, the honest gate, and Ballarat retired 2026-07-17 01:06:00 +10:00
SOURCES.md Lane E R27 (v5.0): provenance v5-final — and the roster gap the freeze caught (ledger #6) 2026-07-17 13:59:49 +10:00
westend_real.json Lane E R21 (v4.0): the town pack — 22 towns, 1192 shops (DATA-ONLY, non-gating) 2026-07-16 21:06:46 +10:00

Real town caches — the home (web/assets/towns/)

Lane A owns the contract; Lane E fills this directory (ROUND17 ledger #6, the real-map scout).

Each real Australian town is one file, <key>.json, in the town-cache contract shape — the processed output of pipeline/build_towns.py (Overpass → dedupe/suburb/parody-name/seeded-hours). plan_osm.js marches it exactly like the checked-in fixtures in web/js/citygen/osm_fixture.js; the scout proves the existing CityPlan contract eats real data — no new plan fields, no new geometry.

The contract (authoritative: validateTownCache() in web/js/citygen/plan_osm.js)

{
  "schema": "procity-town-cache/1",
  "key": "katoomba_real",                    // matches the filename; distinct from the hand-made fixture key
  "town": "Katoomba",                        // display name
  "source": "osm",
  "license": "ODbL 1.0",                     // REQUIRED — OSM is ODbL
  "attribution": "© OpenStreetMap contributors",
  "generator": "pipeline/build_towns.py",    // optional provenance
  "fetchedAt": "2026-07-16",                 // optional
  "center": { "lat": -33.71, "lon": 150.31 },// REQUIRED — the equirectangular projection origin
  "bbox": { "minLat": …, "minLon": …, "maxLat": …, "maxLon": … },  // optional metadata
  "counts": { "raw": 512, "shops": 96 },     // optional metadata
  "shops": [
    { "id": 318855426, "name": "Canty's Bookshop", "type": "book", "lat": -35.32, "lon": 149.18, "suburb": "Fyshwick" }
    // …
  ]
}

Hard requirements (else the cache is rejected and does not boot):

  • center.{lat,lon} finite numbers.
  • shops an array of MIN_TOWN_SHOPS (6) entries (up to 4 venues + the one openLate landmark + a spare).
  • every shop has finite lat/lon.

Absorbed as warnings (still boots): a blank name defaults to the type label; an unknown type remaps to opshop; duplicate ids; and — importantly — a cache spanning > 5 km warns "likely more than one town". A cache MUST be ONE compact town: an over-broad Overpass bbox marches into an unusable multi-km strip (the R17 mega-strip risk — a whole region of 2,918 shops became an 11.6 km avenue). Bound the query.

type should be a registry SHOP_TYPE (record/opshop/toy/book/video/pawn/milkbar/dept/stall); build_towns.py maps OSM shop=* tags to these (unknowns land on opshop).

godverseShopId — the stock identity (ROUND24, v5.0-alpha; Lane A + Lane G)

The optional key that says "this shop's real stock lives at web/assets/stock_godverse/<godverseShopId>/". The schema stays v2 — it's an optional field, so every shipped cache stays valid and the 21 osm caches are untouched by construction.

"shops": [
  { "id": 158, "name": "Egg Records", "type": "record", "lat": -33.897, "lon": 151.179,
    "suburb": "Newtown", "godverseShopId": 158 },        // census shop: happens to equal `id`
  { "id": 9001, "name": "Monster Robot Party", "type": "record", "lat": -27.455, "lon": 153.006,
    "suburb": "Red Hill", "godverseShopId": 3962749 }    // NOT in the census: a dealgod store id
]

It is NOT shop.id, and it is NOT "the census id". That shortcut breaks on the one shop the v5 alpha is about. Two id spaces ride this field, and G verified they're disjoint:

shop source of the id equals shop.id?
census shops (newtown_godverse) thriftgod shop.id — max 2992 yes, incidentally
Monster Robot Party dealgod store 3962749not in thriftgod's census at all no

So treat it as an opaque key: never derive it from id, never assume equality, never build a mapping table (the R12 gigKey lesson — one key, zero mapping; G emits it, everyone else reads it).

A godverse cache is MIXED — that's the design (Ruling 2, law as of R25). It carries a keyed GODVERSE layer (census shops + injected shops like Monster Robot) plus an inherited OSM texture layer that is legitimately unkeyed. The texture layer is load-bearing, not cosmetic — G measured that census-only Red Hill was unshippable (306 m spacing warn, a poster inside a kerb) and the texture layer fixed it. Real shape today: newtown_godverse = 18 keyed + 54 texture; redhill_godverse = 10 keyed + 27 texture. Unkeyed shops are not a gap and no gate may assert them away.

Rules — split by what is actually a hazard, not by tidiness:

  • Malformed → ERROR. Must be a positive integer. It names a path segment, so "3962749" and 3962749 are the same folder but different JSON; that coercion drift is the species that produced the licence/license atlas break. One truth.
  • Duplicate within a cache → ERROR. Two shops keyed to one atlas is mis-stocking (charter risk #3: "fail-soft to tier 0, never mis-stocked").
  • An entirely empty GODVERSE layer → WARN. A godverse+osm cache with no id on any shop is an osm cache wearing the wrong source. Still a warn, not an error: the charter's determinism boundary is world = seeded and gated, stock = data tiers, so a town must never fail to load because its stock identity is absent — tier 0 is the designed ladder.
  • Mixed → silent. Some keyed, some not, is normal and raises nothing.

selfcheck gates the keyed subset per-layer: the layer reaches the plan, ids are unique over defined ids only, the lift invents no identity — plus an orphaned-atlas gate (a committed atlas whose shop the lift dropped = real stock keyed to a shop that isn't in the town → FAIL). Dropped keyed shops are printed, never silent.

It survives the lift. plan_osm copies it onto plan.shops[], because the runtime reads the plan, never the cache — without that the field would be decoration and F's per-shop base could never resolve. Absent ⇒ the key is not written (not undefined), so JSON.stringify is byte-identical and every pinned town golden holds until G re-emits.

Schema v2 — roads[] (ROUND18, v4.0-alpha REAL ROADS)

A v2 cache adds an optional roads[] — the town's real OSM street geometry. When present, plan_osm builds the CityPlan street graph from the real roads and seats each shop on its nearest real edge (its real street, real order, real side) instead of marching shops onto synthetic parallel avenues. roads absent (or schema procity-town-cache/1) ⇒ the marched fallback — every shipped v1 cache stays valid.

{
  "schema": "procity-town-cache/2",
  // … all v1 fields (center, shops, license, …) …
  "roads": [
    { "id": 12345678,                 // optional (OSM way id)
      "kind": "primary",              // REQUIRED — the OSM highway=* class (see the map below)
      "name": "Katoomba Street",      // optional
      "pts": [ [-33.7175, 150.3110], [-33.7140, 150.3112], [-33.7100, 150.3115] ] }  // REQUIRED — ≥2 [lat,lon]
    // …
  ]
}

Road validity (a bad road is a hard error; a soft one is absorbed):

  • each road is { kind: string, pts: [[lat,lon], …] } with ≥ 2 finite [lat,lon] points — else rejected.
  • < 2 points → the road is dropped (warning); an unmapped kind falls to side (warning).

kind → CityPlan edge kind (ROAD_KIND in plan_osm.js; the lift then promotes a shop-dense side road to the main spine): motorway/trunk/primary/secondarymain; tertiary/unclassified/ residential/living_street/roadside; service/tracklane; pedestrian/footway/path/ steps/cyclewayarcade. Fidelity (charter risk #4, A's knob): ship the geometry roughly as OSM has it; plan_osm simplifies (DouglasPeucker) — don't pre-decimate below ~1 point per 5 m.

Bound the roads to the town (the bbox law): fetch highway=* within the same per-town bbox as the shops so the graph is the town's streets, not a region's.

Provenance (Lane E)

OSM data is ODbL. Ship attribution with the data: this README, a SOURCES.md (E's licence table), and the license/attribution fields on every cache. Cache the raw Overpass responses too so re-runs never re-fetch.

Wiring

  • plan_osm.generatePlanOSM(seed, key, { cache }) marches a cache directly; registerTownCache(key, cache) adds it to the runtime registry so osmTownKeys() and generatePlanFor(seed, 'osm', { town: key }) see it.
  • web/js/citygen/selfcheck.js auto-loads every *.json here: validates it, runs the full structural + gig suites across the hero seeds, and pins a per-town golden (unpinned → prints the value to paste).