THE STREET (ledger #3). redhill_real: Red Hill, Brisbane — 36 shops (9 heroes + 27 texture, -29 subsampled), 884 roads, median NN 73 m. **Musgrave Road is in roads[] (41 ways, primary + residential)** — that was the point: Monster Robot Party's real shop now has its real street. One bounded Overpass fetch under the standing authorization; raw snapshots in _raw/, ODbL, C's mapping. This is G's donor for godverse_town.py -> redhill_godverse. TWO HAZARDS HANDED TO G/A, both measured against the real geometry (notes R24 §1): (a) The shop is NOT in OSM — no shop=music node, no name match anywhere in the bbox (checked the raw snapshot, not the emitted cache). It cannot arrive via the fetch; G must inject id 3962749 from the census or the town ships without the shop the release is about. (b) THE SEATING HAZARD: A's lift seats on the NEAREST edge, and from G's geocode three streets are nearer than the addressed one — Zig Zag Street 38.6 m (residential), Craig Street 46.1 m, Musgrave Road 51.6 m. The shop would seat on Zig Zag St and the headline would quietly become false. Measured fix (G's call): the point on the real Musgrave centreline nearest G's geocode is -27.455169, 153.006902 — 0 m to Musgrave, next candidate 43 m. Snaps the street, does NOT verify the house number; I have no address data for where #147 sits along the road. THE HONEST GATE (ledger #4). F's catch was right and there were FOUR bugs: the glob used the published-contract name the runtime can never load (0 matches -> returned 0: it passed identically whether atlases were absent, misnamed, or perfect); the code read provenance top-level while my own docstring said nested (G nested it correctly — the gate would have failed G's real artifact); licence/license; and a stale ceiling. Rebuilt on the "runtime is the authority" ruling: globs stock_godverse/<id>/stock_<type>_index.json, type in {record,book,toy}. Vacuous-gate law implemented literally — 0 matches + stray files FAIL (listing them); 0 matches + empty dir prints "atlas-QA SKIP: no atlases present (0 files matched glob ...)"; plus an id equality (shop.godverse_id == dir) so it proves it touched its subject. Proved on four cases: empty -> loud SKIP, misnamed (F's exact bug) -> FAIL, unparseable -> FAIL, G's real atlas -> PASS (120 items, 2 atlases, licence printed). My own test found a fifth hole — the glob still matched the old stock_shop_* name, so the misnamed case failed only incidentally; added a STOCK_TYPES name check so it fails for the RIGHT reason. A gate that fails for the wrong reason will pass for the wrong reason later. BALLARAT — RETIRED, counted, and a DIFFERENT illness from Toowoomba (notes R24 §3). Toowoomba was dead everywhere. Ballarat is alive exactly where the game isn't: 103 POIs, densest 400 m disc = 47 of them (Toowoomba's was 3) — but that disc is cafes/clothes/bakeries while the op-shops sit 854 m, 1042 m and 1709 m out. Heroes and density don't coincide, so no bbox holds both. Real builds at every span: 1.0 km -> 8 shops/184 m/hub 4-8 but ZERO op-shops (a thrift town with no thrift shop); 2.0 km -> 12/186 m/3-12; 3.0 km as shipped -> 20/255 m/4-20. Every row worse than any shipped town. THE BINDING CONSTRAINT WAS MINE, NOT OSM'S: with 2 heroes the 3x texture cap keeps 6 of 47 texture POIs and the subsample is spatially blind, so survivors scatter at any span — part of the 255 m A's warn caught was my own cap's fingerprint. Seeding texture near heroes fixes the class but moves every town's golden -> filed as a v5.x proposal, not done in a release round. I OVERSTATED MY R23 ADVICE TO A: the hub fraction is NOT a standalone discriminator — glebe (shipped, healthy) reads 13% and redhill 14%; a hub threshold would false-flag both. Median NN is the robust primary; hub is only comparable at equal shop counts (toowoomba 2/12 vs darwin 7/12 was fair because both had 12). The raw-vs-seated calibration point stands. METHODOLOGY, recorded because it nearly shipped: I picked Ballarat's span from an offline simulation that predicted 68 m / hub 5-8, and had already written those numbers into a config comment as fact. The real build returned 184 m / 4-8 — same counts, different sample. Never trust a re-implementation of the pipeline's own selection; run the pipeline. -> A (batched pins): ADD REAL_TOWN_GOLDENS['redhill_real']=0x6046700f and REAL_TOWN_GIG_GOLDENS['redhill_real']=0x673785ee (the headline town currently only WARNS as UNPINNED — ungated until you pin it). REMOVE ballarat_real 0xf0a9e8a1 / 0xf097c331 — dead config by your own R23 note. -> F: tools/qa/tour_shots.py still shoots town=toowoomba_real, which I deleted in R23 — the v4 tour breaks on a missing town. Your file, your call. A retirement isn't done when the cache is deleted; it's done when the consumers are swept. -> C: built to the runtime + G's real artifact per the ruling; LANE_C_PUB §7 line 271 still names the contradictory stock_shop_<id>_index.json. I reconcile when your corrected contract lands. Determinism receipt: the retirement forced a full 21-town rebuild from cached raw (no refetches) and every existing cache came back byte-identical — only ballarat, redhill and index.json moved. Pack: 21 real towns · 1196 shops · 299 heroes (22 rostered with G's newtown_godverse). Selfcheck ALL GREEN 149,696, synthetic golden 0x3fa36874 unchanged. qa.sh --strict --matrix: 7 passed / 0 failed, MATRIX GREEN 10x7. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|---|---|---|
| .. | ||
| _raw | ||
| adelaide_real.json | ||
| bendigo_real.json | ||
| bowral_real.json | ||
| braddon_real.json | ||
| brunswick_real.json | ||
| castlemaine_real.json | ||
| darwin_real.json | ||
| daylesford_real.json | ||
| fitzroy_real.json | ||
| fremantle_real.json | ||
| geelong_real.json | ||
| glebe_real.json | ||
| hobart_real.json | ||
| index.json | ||
| katoomba_real.json | ||
| launceston_real.json | ||
| marrickville_real.json | ||
| newcastle_real.json | ||
| newtown_godverse.json | ||
| newtown_real.json | ||
| northbridge_real.json | ||
| README.md | ||
| redhill_real.json | ||
| SOURCES.md | ||
| westend_real.json | ||
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.shopsan 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 3962749 — not 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"and3962749are the same folder but different JSON; that coercion drift is the species that produced thelicence/licenseatlas 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 inselfcheck, 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. < 2points → the road is dropped (warning); an unmappedkindfalls toside(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/secondary → main; tertiary/unclassified/
residential/living_street/road → side; service/track → lane; pedestrian/footway/path/
steps/cycleway → arcade. Fidelity (charter risk #4, A's knob): ship the geometry roughly as OSM
has it; plan_osm simplifies (Douglas–Peucker) — 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 soosmTownKeys()andgeneratePlanFor(seed, 'osm', { town: key })see it.web/js/citygen/selfcheck.jsauto-loads every*.jsonhere: validates it, runs the full structural + gig suites across the hero seeds, and pins a per-town golden (unpinned → prints the value to paste).