PROCITY/web/assets/towns
m3ultra 86e298531e Lane G R24 (v5.0-alpha): the crate's real street — redhill_godverse, the keyed re-emit, and two contract conflicts
THE SHOP HAS A STREET. redhill_godverse: Monster Robot Party seated on Musgrave Road (verified —
lot 8, type record, godverseShopId 3962749), 37 shops on E's 884 real Red Hill roads. Its
coordinate is John-supplied (Google Maps place /g/11g6rlbbc0 -> -27.4553791, 153.0076244) because
it is derivable from nothing we hold: OSM doesn't know the shop, the census doesn't either,
dealgod's stores/store_entity rows are NULL lat/lng, recordgod has no address. Verified inside E's
bbox and on Musgrave Road's own geometry; my own estimate was ~120 m out.

RE-EMIT (ledger #5 + #2): atlas keyed by godverseShopId with US `license` throughout (British
retired, 0 occurrences) — atlas-QA OK, 120 real records, byte-identical re-bake (the .webp bytes
did not move; only the index). newtown_godverse re-emitted with ids (18 keyed). godverse_town.py:
psycopg2 -> psql, EXTRA_SHOPS roster, and Fable's R22 Ruling 2 finally implemented.

RULING 2 WAS LOAD-BEARING, NOT COSMETIC (measured): census-only Red Hill is unshippable —
9 shops = 306 m spacing warn; +Monster Robot = 10 shops = 261 m warn AND a frontage poster seated
INSIDE edge 1037's kerb (-2.95 m); + the donor's texture layer (37) = no warn, poster passes.
261 m is worse than the 255 m that retired ballarat this round: the alpha's headline town would
have shipped flagged "scattered, not a high street, or retire the town". 9 donor heroes swapped
for census entries (18 in Newtown), counted per the ruling.

CONFLICT #1 — C §7.3 vs E's validator. C says lift provenance to TOP LEVEL, US `license`, British
retired, "exactly what E's validator reads". Measured: it is not. validate_atlas.py:98 reads
NESTED `provenance`; :105 reads `licence` British-first. Lifting-and-retiring as written FAILS the
gate; my R23 atlas already passed it untouched. Same species as the bug that held the tag — a doc
asserting what code does without running it. Resolved: E's check accepts `licence or license`, so
US-nested passes; the four also ride at top level (C's contract + the town-cache convention) off
one source of truth. -> E, one line: :198 PRINTS `licence` British-only while the check accepts
both, so C's mandated US spelling silently stops printing the licence flag — which :199's own
comment calls a human call. Gate passes; evidence line vanished. Fix: .get("licence") or .get("license").

CONFLICT #2 — Ruling 2 vs A's identity gate. NEEDS A RULING; it is why the tree is red (6 of 8).
Ruling 2 makes a godverse cache mixed: a keyed GODVERSE layer + an inherited OSM texture layer.
A's sweep asserts EVERY shop is keyed -> partial FAILs. Texture shops carry no godverseShopId
deliberately: an OSM milkbar has no GODVERSE identity, and minting one from its OSM id pushes a
third id space into a field that already carries two — A's validator calls a duplicate
mis-stocking, charter risk #3's nightmare with Monster Robot's crate as the prize. And A's own two
checks disagree: the validator says "Missing = tier 0 (soft). Duplicate = mis-stocked (hard). They
are not the same failure", while the sweep fails partial keying because it "mis-stocks the rest" —
conflating exactly those two. Proposed (A's call, Fable's ruling): assert coverage over the
godverse layer, not the inherited texture layer.

THE 8 RED CHECKS, ITEMISED: newtown_godverse base+gig goldens (2) moved by the re-emit exactly as
ledger #2 intends — A re-pins, base 0xcf69b387 gig 0x1e266f60; godverse identity x3 per godverse
town (6) = conflict #2. Nothing else. Classic covenant 0x3fa36874 intact. redhill_godverse pins:
base 0xb2224939. Verified live: the shop's 120-record pack and the town-wide 350 pack now COEXIST
through C's type+base fix.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 01:31:07 +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 R24 (v5.0-alpha): the crate's real street, the honest gate, and Ballarat retired 2026-07-17 01:06:00 +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 24 (v5.0-alpha): the identity field — godverseShopId, published first (ledger #2) 2026-07-17 00:45:45 +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 R24 (v5.0-alpha): the crate's real street, the honest gate, and Ballarat retired 2026-07-17 01:06:00 +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).

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").
  • Missing on a source: "godverse+osm" cache → WARN, and those shops fall soft to tier 0 (parody). 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. The teeth are in selfcheck, where partial keying FAILS (a half-keyed town silently mis-stocks the rest) and zero keying prints an explicit SKIP with the count, per the vacuous-gate law.

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).