The optional shop key that resolves `stock_godverse/<godverseShopId>/`. Schema STAYS v2 (optional field ⇒ every shipped cache still valid, the 21 osm caches untouched by construction). Published to web/assets/towns/README.md — the doc G and E build to — plus CITY_SPEC. selfcheck ALL GREEN (+15 checks), synthetic 0x3fa36874 frozen, NO golden moved. THE BRIEF'S "THE POS SHOP ID IS THE ID — NO MAPPING TABLE" IS FALSE FOR THE ONE SHOP THIS ALPHA IS ABOUT. G's own docstring says it: Monster Robot Party is NOT in thriftgod's census, so its id is its DEALGOD STORE ID 3962749, while census shops use thriftgod ids (max 2992) -- two disjoint spaces, deliberately kept clear of each other. Had I coded "godverseShopId == shop.id" or derived it from the census, the headline shop could never be keyed. So it is an OPAQUE key: no derivation from `id`, no equality assert, no mapping table (the R12 gigKey law -- one key, zero mapping; G emits, everyone reads). THE FIELD HAD TO SURVIVE THE LIFT OR IT WAS DECORATION. plan_osm builds a FRESH shop object and copies nothing unknown, and the runtime reads plan.shops[], never the cache -- so a cache-only field could never reach F's per-shop base. It's now carried through, CONDITIONALLY: absent ⇒ the key isn't written ⇒ JSON.stringify byte-identical ⇒ every pinned golden holds until G re-emits (that re-emit IS the sanctioned move). Proven end-to-end: a keyed cache puts `stock_godverse/3962749/` on the plan with census ids (101..107) coexisting alongside it. ARMS SPLIT BY HAZARD, NOT TIDINESS: - malformed -> ERROR. It names a path segment; "3962749" vs 3962749 is the same folder but different JSON, and that coercion drift is the exact species that broke C's atlas contract (licence/license). - duplicate -> ERROR. Two shops keyed to one atlas IS mis-stocking -- charter risk #3 forbids it by name. - missing on a godverse cache -> WARN + fail-soft to tier 0. RECORDED DEPARTURE (Fable's to overrule): ledger #2 says validateTownCache "requires" the field; I made MISSING a warn. Three reasons. (1) The charter's determinism boundary is "world = seeded, frozen, gated; stock = data tiers" -- an error inverts it, failing to load a TOWN because its STOCK identity is absent; tier 0 must always boot. (2) Charter risk #3 names the remedy for identity gaps: "fail-soft to tier 0, never mis-stocked" -- missing IS the soft case, duplicate is the hard one; they are different failures and now get different arms. (3) Sequencing: newtown_godverse carries 0 ids today, it is G's to re-emit in wave 2 (their namespace, no census DB on m3), so a hard error would red the tree for C and E through all of wave 1 to say what the warn already says. The requirement keeps its teeth where R23 taught us to put them -- the VACUOUS-GATE LAW: selfcheck FAILS on partial keying (a half-keyed town silently mis-stocks the rest) and prints an explicit "⊘ SKIP ... 0/18 shops carry godverseShopId. Subject absent ... binds the moment one id lands" at zero. It names its subject, proves it touched it with a count, and cannot pass vacuously. FILED FOR G BEFORE THEY START (measured in wave 1, so it costs them minutes not a wave): redhill_godverse cannot contain Monster Robot from EITHER source. godverse_town.py takes roads from the OSM donor and shops from the census, and the shop is in neither -- not in the census (G's R23 finding), and NOT in E's redhill_real, which I checked the moment it landed: 36 shops, ZERO of type `record`, nothing matching /monster|robot/. As written the adapter yields a Red Hill of census charity shops while the atlas keys to a shop that isn't in the town -- F's #7b id-equality gate would fail at the end of the round. It must be injected explicitly; the opaque-key design exists precisely to serve that case. Second trap on the same path: redhill_godverse must still clear MIN_TOWN_SHOPS (6) from the census bbox or the cache is rejected outright (hard error). E's donor is otherwise good: valid, no warns, 73.3 m median spacing. HOUSEKEEPING: removed toowoomba's two stale pins -- E retired the cache in R23, and a golden for a town that cannot load can never fire (the vacuous-gate law, one layer down). E's attempt also proved my R23 spacing metric is GAMEABLE: the re-bbox fixed the median (395.7 -> 89.4 m, "passes") while hub density fell 3/12 -> 2/12, the pack's worst. It is necessary, not sufficient -- D's hub test is the complement, and E used both and refused to game it. Ballarat (255 m) is still flagged and still E's call; I pin what moves. PINS NOT TAKEN YET, DELIBERATELY: redhill_real is untracked/in-flight (E still editing build_towns.py), and redhill_godverse + the re-keyed newtown_godverse don't exist until wave 2. Pinning a moving file buys a false green or a spurious red. The round's law says A pins these BATCHED -- that batch lands when E commits and G's caches are in. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
134 lines
7.6 KiB
Markdown
134 lines
7.6 KiB
Markdown
# 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`)
|
||
|
||
```jsonc
|
||
{
|
||
"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*.
|
||
|
||
```jsonc
|
||
"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"` 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.
|
||
|
||
```jsonc
|
||
{
|
||
"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`/`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 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).
|