PROCITY/web/assets/towns/README.md
m3ultra a493c0a13c Lane A round 25 (v5.0-alpha): the sweep reconciled with Ruling 2 + the six pins (ledger #1)
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>
2026-07-17 02:23:57 +10:00

146 lines
8.5 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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