All 8 tree reds cleared. selfcheck ALL GREEN 156,211/156,211; scaffold + consistency green; classic
0x3fa36874 and gig 0xb1d48ea1 frozen. F: the tag is unblocked from my side, nothing of mine is pending.
SIX OF THE EIGHT REDS WERE MY OWN GATE ASSERTING RULING 2 AWAY. I built the R24 identity arm on an
assumption that was true when I wrote it and false the moment G shipped: that a godverse cache is ALL
census shops. Ruling 2 makes it MIXED -- a keyed GODVERSE layer plus an inherited OSM texture layer that
G measured to be load-bearing, not cosmetic (census-only Red Hill: 306 m spacing warn + a poster inside a
kerb). So newtown_godverse (18 keyed + 54 texture) and redhill_godverse (10 + 27) were HEALTHY TOWNS MY
GATE CALLED BROKEN -- it fired 54/72 on a good town. Keyed asserts now run PER-LAYER over the keyed subset
only; unkeyed texture shops raise nothing. The validator's "N/M missing" warn dies with it. The one honest
requirement left at that layer: a `godverse+osm` cache must have a NON-EMPTY godverse layer (else it's an
osm cache wearing the wrong `source`) -- still a warn, for the R24 charter reasons that still hold.
THE UNIQUENESS CHECK WAS BROKEN IN BOTH DIRECTIONS. `new Set(all ids).size === shops.length` collapses
every `undefined` into ONE Set entry, so it (a) failed structurally on any mixed cache -- 54 texture shops
became one entry -- and (b) could HIDE A REAL DUPLICATE behind that same collapse. Now over DEFINED ids
only. And per the vacuous-gate law I made it prove it bites rather than asserting that it does:
validateTownCache rejects duplicates at the front door, so a dup can never reach the plan that way -- the
test feeds one STRAIGHT TO THE LIFT (generatePlanOSM does not validate) and asserts the predicate catches
it, with a healthy mixed cache passing the same predicate. A check nobody can watch fail is not a check.
NEW -- THE ORPHANED-ATLAS GATE (my own R24 finding, now enforced instead of filed). The lift legitimately
drops shops (overlap-resolve, counted since R19). Dropping a shop that HAS a committed atlas means real
stock keyed to a shop that isn't in the town -- exactly the failure I filed in R24, and one F's #7b could
only catch in a browser at the end of a round. Now a node gate: for each godverse town, every
stock_godverse/<id>/ on disk belonging to that cache MUST be seated, else FAIL. Subject-aware -- it SKIPs
by name when a town has no atlas, rather than passing quietly.
It already found one, benign today: redhill's lift drops keyed shop 2286 "Empire Revival" (10 keyed in
cache -> 9 seated). No atlas, so nothing is orphaned and the gate passes -- but it prints, because a drop
must never be silent (the R19/R20 ruling). G: stocking Empire Revival at the beta would orphan it. Monster
Robot Party is seated (lot 8), which is the one that matters this round.
THE SIX PINS, cross-checked two ways before landing -- byte-identical across two fresh processes, AND
matching the three values G stated independently in 86e2985:
newtown_godverse base 0xcf69b387 gig 0x1e266f60 (moved by G's keyed re-emit)
redhill_real base 0x6046700f gig 0x673785ee
redhill_godverse base 0xb2224939 gig 0xc6f80e18
No other golden moved -- the diff is exactly these six lines.
Two lines still print; both deliberate, neither pending:
⊘ SKIP newtown_godverse orphaned-atlas -- correct, the only committed atlas (3962749) keys to Red Hill.
⚠ redhill_godverse: 1 keyed shop dropped -- the honest disclosure above.
Committed strictly by pathspec: F's six staged files sit untouched, exactly as its session left them.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
146 lines
8.5 KiB
Markdown
146 lines
8.5 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**).
|
||
|
||
**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.
|
||
|
||
```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).
|