Lane A round 24 (v5.0-alpha): the identity field — godverseShopId, published first (ledger #2)

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>
This commit is contained in:
m3ultra 2026-07-17 00:45:45 +10:00
parent ba901d4a75
commit 1eb0b45bd0
6 changed files with 313 additions and 9 deletions

View File

@ -1,10 +1,50 @@
# LANE A — CITYGEN · progress (PROCITY-A)
*Status: **all deliverables landed; self-check all-green (161,372/161,372); rounds 223 closed** (see below).
R23 (v5.0-alpha): D's thin-tail finding is law — `validateTownCache` warns above **150 m median shop
spacing**; flags 2 of 23 towns (toowoomba 396 m, **ballarat 255 m — beyond the brief**). Warn, never error.
R22: the pack absorbed — 23 towns, 36 goldens in one commit. classic `0x3fa36874` / gig `0xb1d48ea1` frozen.*
*Date: 2026-07-16 · owner: PROCITY-A (Opus 4.8)*
*Status: **all deliverables landed; self-check all-green (152,030/152,030); rounds 224 closed** (see below).
R24 (v5.0-alpha): the **`godverseShopId`** identity field — schema v2 (optional), carried through the lift
onto `plan.shops[]`, **no golden moved**. R23: the spacing warn (150 m). classic `0x3fa36874` /
gig `0xb1d48ea1` frozen. (Pack is 21 real + 1 godverse — E retired toowoomba in R23.)*
*Date: 2026-07-17 · owner: PROCITY-A (Opus 4.8)*
## Round 24 (2026-07-17) — v5.0-alpha: the identity field (ledger #2)
`shop.godverseShopId` — the optional key resolving `stock_godverse/<id>/`. Schema **stays v2**; absent
everywhere except godverse caches, so the 21 osm caches are untouched by construction. Published to
`web/assets/towns/README.md` (the doc G/E build to) + CITY_SPEC. selfcheck **ALL GREEN 152,030/152,030**
(+15), synthetic `0x3fa36874` frozen, **no golden moved** — by design (see below).
- **The brief's "the POS shop id IS the id" is false for the one shop that matters.** G's own docstring:
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 on one field ⇒ built it as an **opaque
key**: no derivation from `id`, no equality assert, no mapping table (the R12 gigKey law).
- **The field had to survive the lift, or it was decoration.** `plan_osm` builds a *fresh* shop object and
copies nothing unknown, so a cache-only field would never reach the runtime — and the runtime reads
`plan.shops[]`, never the cache. 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).
- **Arms split by hazard:** malformed → **error** (it's a path segment; `"3962749"` vs `3962749` is the
same coercion drift that broke the atlas contract), duplicate → **error** (two shops, one atlas = the
mis-stock charter risk #3 forbids), missing on a godverse cache → **warn + fail-soft to tier 0**.
- **Recorded departure:** the ledger said missing should be an *error*. It would invert the charter's
determinism boundary (the **town** failing to load because **stock** identity is absent), contradict risk
#3's own remedy ("fail-soft to tier 0"), and red the tree for C and E through all of wave 1 — G re-emits
in wave 2 and it's their namespace, not mine. Teeth kept where R23 says to put them: selfcheck **fails on
partial keying**, prints an explicit **SKIP + count** at zero. Flagged for Fable to overrule.
- **Filed for G before they start (measured, wave 1):** `redhill_godverse` can't contain Monster Robot from
**either** source. `godverse_town.py` takes roads from the OSM donor and shops from the census; the shop is
in neither — not in thriftgod's census (G's R23 finding), and **not in E's `redhill_real`** (I checked it
as it landed: 36 shops, **zero of type `record`**, nothing matching /monster|robot/). Needs explicit
injection, or F's #7b id-equality gate fails at the end of the round. Also flagged: the census bbox must
still clear `MIN_TOWN_SHOPS` (6) or the cache is rejected outright.
- **Pins deliberately NOT taken this round yet.** `redhill_real` is untracked/in-flight (E is 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 once E commits and G's caches are in. `redhill_real` reads clean meanwhile: valid, no
warns, 73.3 m spacing.
- **Housekeeping:** removed toowoomba's two stale pins (E retired the cache in R23 — a golden for a town
that can't load can never fire). E's attempt also proved my R23 spacing metric is **gameable**: the
re-bbox fixed the median (395.7 → 89.4 m) while hub density fell 3/12 → 2/12. Necessary, not sufficient —
D's hub test is the complement. **Ballarat (255 m) still flagged, still E's call; I pin whatever moves.**
## Round 23 (2026-07-16) — v5.0-alpha: the spacing warn (ledger #5)

View File

@ -426,6 +426,33 @@ auto-loads `web/assets/towns/*.json` — validates, runs the structural + gig su
**Provenance:** OSM is **ODbL**; attribution ships with the data (E owns `SOURCES.md` + the `license`/
`attribution` fields; raw Overpass responses cached so re-runs never re-fetch).
### `godverseShopId` — the stock identity (v5.0-alpha, ROUND24 ledger #2; A + G)
The optional shop key that resolves **`stock_godverse/<godverseShopId>/`**. **Schema stays v2** — optional
field, absent everywhere except godverse caches, so every shipped cache stays valid *by construction*.
**It is NOT `shop.id` and NOT "the census id"** — the round doc's "the POS shop id IS the id" holds for
census shops but breaks on the shop the alpha is about. Two disjoint id spaces ride one opaque field:
census shops use thriftgod `shop.id` (max **2992**), while **Monster Robot Party is not in thriftgod's
census at all** and uses its **dealgod store id 3962749**. So: never derive it from `id`, never assert
equality, never build a mapping table (the R12 gigKey law — one key, zero mapping). **G emits, all read.**
**`validateTownCache` arms** (split by hazard, not tidiness): a **malformed** id (not a positive integer)
is an **error** — it names a path segment, and `"3962749"` vs `3962749` is exactly the coercion drift that
broke the atlas contract; a **duplicate** id is an **error** — two shops sharing one atlas *is* mis-stocking
(charter risk #3: "never mis-stocked"); a **missing** id on a `source: "godverse+osm"` cache is a **warn**,
and those shops fall soft to **tier 0**. Missing is deliberately *not* an error: the charter's determinism
boundary says *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, not a defect. The teeth live in `selfcheck`:
**partial keying FAILS** (a half-keyed town silently mis-stocks the rest) and zero keying prints an explicit
**SKIP** naming the town and count, per the round's vacuous-gate law. *(Recorded departure from the ledger's
"requires" — reasons above; Fable's to overrule.)*
**It survives the lift:** `plan_osm` copies it onto `plan.shops[]` (the runtime reads the plan, never the
cache — otherwise the field is decoration and F's per-shop `base` can never resolve). Absent ⇒ the key is
**not written**, so `JSON.stringify` is byte-identical and **every pinned golden holds until G re-emits**;
that re-emit is the sanctioned golden move.
### Schema v2 — real roads (v4.0-alpha, ROUND18 REAL ROADS)
> The gap the scout left: the marched fallback lays real shops onto **synthetic** parallel avenues.

View File

@ -2,6 +2,73 @@
Durable answers to questions other lanes raised against Lane A. Full status in `A-progress.md`.
## Round 24 (2026-07-17) — v5.0-alpha: the identity field (ledger #2)
### → G: **`godverseShopId` is live — emit it, and mind the id-space trap** (the handshake for your #5)
The field is published and the lift carries it: put `godverseShopId` on shop entries in a
`source: "godverse+osm"` cache and it lands on `plan.shops[]`, which is what F's per-shop `base` reads.
Contract in `web/assets/towns/README.md`; `validateTownCache` enforces it; selfcheck gates it.
**The round doc says "the POS shop id IS the id — no mapping table". That's true for census shops and
false for the shop this whole alpha is about** — and your own docstring is what told me:
| shop | id source | 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 | **no** |
So I built it as an **opaque key**, not a census id: no equality assert, no derivation from `id`, no mapping
table. Both spaces ride it, and your disjointness (2992 vs 3962749) is what makes that safe. **Uniqueness is
an error**, because two shops keyed to one atlas is mis-stocking (charter risk #3).
**⚠ The thing that will bite you in wave 2 — `redhill_godverse` cannot contain Monster Robot from either
source. Measured, not inferred.** `godverse_town.py` takes **roads from the OSM donor** and **shops from
thriftgod's census**. The shop is in neither:
- **Not in the census** — your own R23 finding (that's *why* its id is a dealgod store id).
- **Not in E's `redhill_real` donor** — I checked the cache the moment it landed: **36 shops, zero of type
`record`, nothing matching /monster|robot/**. OSM's Red Hill doesn't know the shop exists.
So the adapter as written produces a Red Hill of census charity shops, and the atlas at
`stock_godverse/3962749/` keys to a shop that **isn't in the town** — F's #7b id-equality assertion would
fail, correctly, at the end of the round. **Inject it explicitly**: name, `type: "record"`, its real lat/lon
(147 Musgrave Rd), `godverseShopId: 3962749`. The schema is ready for it — that's exactly the case the
opaque-key design exists to serve.
**Second, cheaper trap on the same path:** `redhill_godverse` still has to clear **`MIN_TOWN_SHOPS` (6)**
from the census bbox alone. Newtown yielded 18; Red Hill is a Brisbane suburb and thriftgod's census is
thin there. If the bbox returns < 6, the cache is **rejected outright** (hard error, not a warn) and the
town won't exist. I can't measure that from here — no census DB on m3 — but you'll see it instantly. If it
bites, the count includes any shop you inject.
**E's donor is otherwise good for you:** valid, no warnings, **73.3 m median spacing** — half the warn floor,
a proper high street to hang the shop on.
### → F: the seam you need for #6a
`plan.shops[].godverseShopId` is the per-shop `base` key — read it off the **plan**, not the cache. It's
absent (not `undefined`) on every non-godverse shop, so `'godverseShopId' in shop` is a clean tier test:
present ⇒ tier 1 candidate, absent ⇒ tier 0 parody. That absence is also why **no golden moved this round**.
### → Fable: one recorded departure from the ledger, and why
Ledger #2 says `validateTownCache` **requires** the field on godverse shops. I implemented **missing → warn**
(malformed and duplicate → hard errors). Three reasons, and it's yours to overrule:
1. **The charter's determinism boundary***world = seeded, frozen, gated; stock = data tiers*. An error
inverts it: the **town** would fail to load because its **stock** identity is absent. Tier 0 must boot.
2. **Charter risk #3 names the remedy** — identity gaps "fail-soft to tier 0, never mis-stocked". Missing
*is* the soft case; duplicate is the hard one. They're different failures and now get different arms.
3. **Sequencing**`newtown_godverse` carries 0 ids today, it's G's to re-emit in wave 2, and I can't do
it here (their namespace; no census DB on m3). 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 still has teeth, in the place R23 taught us to put them: selfcheck **fails on partial
keying** and prints an explicit **SKIP with the count** at zero — it cannot pass vacuously.
### Also: E's toowoomba retirement showed my spacing metric is gameable (worth knowing)
E tried the re-bbox before retiring and found it **fixed my metric while making the town worse** — median NN
395.7 → 89.4 m ("passes") but hub density 3/12 → 2/12, the pack's worst. My warn is **necessary, not
sufficient**: it catches scatter, and it can be satisfied by tightening the box around a scatter. D's hub
test is the complement, and E used both. Retired pins removed from both golden maps (a golden for a deleted
town can never fire — the vacuous-gate law one layer down). **Ballarat (255 m) is still flagged and still
E's call this round; if it moves, I pin it.**
## Round 23 (2026-07-16) — v5.0-alpha: the spacing warn (D's thin-tail finding as law)
### → E: **the warn is live — and it flags TWO towns, not one** (the handshake for your #4)

View File

@ -42,6 +42,49 @@ strip (the R17 mega-strip risk — a whole region of 2,918 shops became an 11.6
`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`

View File

@ -94,6 +94,20 @@ export function medianShopSpacing(cache) {
return nn.length % 2 ? nn[m] : (nn[m - 1] + nn[m]) / 2;
}
// ── the identity field (ROUND24 ledger #2, with Lane G) ──────────────────────────────────────────
// `shop.godverseShopId` — the OPTIONAL external key that says "this lot's stock lives at
// `stock_godverse/<godverseShopId>/`". Absent everywhere except godverse caches; schema stays v2
// (an optional field breaks no shipped cache, and the 22 osm caches are untouched by construction).
//
// IT IS NOT `shop.id`, AND IT IS NOT "THE CENSUS ID" — that assumption breaks on the one shop the
// v5 alpha is about. Two id spaces ride this field, and G verified they are disjoint:
// census shops → thriftgod `shop.id` (max 2992) — here godverseShopId does equal `id`
// Monster Robot → dealgod store 3962749 — NOT in thriftgod's census at all
// So it's 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).
const GODVERSE_SOURCE = 'godverse+osm';
const isGodverseCache = cache => cache && cache.source === GODVERSE_SOURCE;
// ── roads[] (schema v2, ROUND18) ─────────────────────────────────────────────────────────────────
// OSM `highway=*` → CityPlan edge kind. The graph lift refines this with shop density (a `side` road
// dense with shops becomes the `main` spine). Anything unmapped defaults to 'side'.
@ -147,6 +161,38 @@ export function validateTownCache(cache) {
if (blankName) warnings.push(`${blankName} blank shop name(s) → defaulted to type label`);
if (unknownType) warnings.push(`${unknownType} unknown shop type(s) → remapped to opshop`);
if (dupId) warnings.push(`${dupId} duplicate shop id(s)`);
// godverseShopId (ROUND24 ledger #2) — the stock identity. Split by what is actually a hazard:
// · MALFORMED → error. It names a path segment (`stock_godverse/<id>/`); a string "3962749" and
// the number 3962749 are the same folder but different JSON, and that coercion drift is exactly
// the species F caught in the atlas contract (licence/license). One truth: a positive integer.
// · DUPLICATE → error. Two shops keyed to one atlas IS mis-stocking — charter risk #3 says
// mismatches "fail-soft to tier 0, NEVER mis-stocked". This is the arm that earns its keep.
// · MISSING on a godverse cache → WARN, not error. See the note below — this is a deliberate
// departure from the round doc's "requires", and it is Fable's to overrule.
let badGid = 0, dupGid = 0, withGid = 0; const gids = new Set();
for (const s of cache.shops) {
if (!s || s.godverseShopId === undefined || s.godverseShopId === null) continue;
withGid++;
if (!Number.isSafeInteger(s.godverseShopId) || s.godverseShopId <= 0) { badGid++; continue; }
if (gids.has(s.godverseShopId)) dupGid++; else gids.add(s.godverseShopId);
}
if (badGid) errors.push(`${badGid} shop(s) with a malformed godverseShopId (want a positive integer — it is a path segment, not a label)`);
if (dupGid) errors.push(`${dupGid} duplicate godverseShopId(s) — two shops cannot share one stock atlas (mis-stock)`);
// WHY MISSING IS A WARN AND NOT AN ERROR (ROUND24 ledger #2 says "requires"; recorded as a
// departure, with reasons, per measurement-over-brief):
// 1. The charter's determinism boundary: "world = seeded, frozen, gated. Stock = data tiers."
// An error here inverts it — the TOWN would fail to load because its STOCK identity is absent.
// Tier 0 must always boot; a shop with no id is a parody shop, which is the designed ladder.
// 2. Charter risk #3 names the remedy for identity gaps: "fail-soft to tier 0, never mis-stocked."
// Missing = tier 0 (soft). Duplicate = mis-stocked (hard). They are not the same failure.
// 3. Sequencing: `newtown_godverse` carries 0 ids today and is G's to re-emit in wave 2 (their
// namespace, and the census DB isn't on this machine). An error would red the tree for C and E
// through all of wave 1 to convey what this warn already says.
// The teeth live in selfcheck: PARTIAL coverage fails, and zero coverage prints an explicit SKIP
// naming the town and the count (the vacuous-gate law) — so this cannot pass unnoticed.
if (isGodverseCache(cache) && withGid < cache.shops.length) {
warnings.push(`${cache.shops.length - withGid}/${cache.shops.length} shop(s) missing godverseShopId on a '${GODVERSE_SOURCE}' cache → those shops fall soft to tier 0 (parody); G re-emits`);
}
// wide-spread warning: an over-broad Overpass bbox marches into an unusable multi-km strip (the R17
// real-data risk). Metadata only — plan_osm still boots a valid plan; it just isn't a coherent town.
let mnLat = Infinity, mxLat = -Infinity, mnLon = Infinity, mxLon = -Infinity;
@ -237,8 +283,17 @@ export function generatePlanOSM(citySeed, town = DEFAULT_TOWN, opts = {}) {
// never renders "undefined" (build_towns.py filters most, but plan_osm must not trust its input).
const nm = (typeof fixtureShop.name === 'string' && fixtureShop.name.trim()) ? fixtureShop.name : (reg.label || type);
if (nm !== fixtureShop.name) norm.namesDefaulted = (norm.namesDefaulted || 0) + 1;
shops.push({ id, lot: lotId, type, name: nm, sign: signOf(nm),
seed, facadeSkin, storeys, hours: [open, close] });
const shop = { id, lot: lotId, type, name: nm, sign: signOf(nm),
seed, facadeSkin, storeys, hours: [open, close] };
// ROUND24 ledger #2: carry the stock identity THROUGH the lift. Without this the field would be a
// cache-only decoration — the runtime reads `plan.shops[]`, never the cache, so F's per-shop `base`
// (`stock_godverse/<godverseShopId>/`) could never resolve. Conditional by design: absent ⇒ the key
// is not written ⇒ `JSON.stringify` is byte-identical ⇒ every pinned town golden holds today, and
// the field starts flowing the moment G re-emits (that re-emit IS the sanctioned golden move).
if (Number.isSafeInteger(fixtureShop.godverseShopId) && fixtureShop.godverseShopId > 0) {
shop.godverseShopId = fixtureShop.godverseShopId;
}
shops.push(shop);
return id;
}

View File

@ -435,6 +435,57 @@ for (const [label, cache, needle] of [
ok(Math.abs(got - 100) < 0.5, `medianShopSpacing: 100 m pair measures ${got.toFixed(1)} m (projection sane)`);
}
}
// (c3) the IDENTITY FIELD (ROUND24 ledger #2, with G) — `shop.godverseShopId`, the key that resolves
// `stock_godverse/<id>/`. It is NOT `shop.id` and NOT "the census id": Monster Robot Party isn't in
// thriftgod's census at all, so its id is its dealgod store id (3962749), while census shops use
// thriftgod ids (max 2992). Two disjoint spaces, one opaque field — so these assert the FIELD's
// properties, never a relationship to `id`.
{
const gv = shops => ({ ...mkCache(shops), source: 'godverse+osm' });
const withIds = cluster(8).map((s, i) => ({ ...s, godverseShopId: 3962749 + i }));
// a well-formed godverse cache validates clean and carries no spurious warn
const good = validateTownCache(gv(withIds));
ok(good.ok, `godverse cache: valid with ids` + (good.ok ? '' : `${good.errors.join('; ')}`));
ok(!good.warnings.some(w => /godverseShopId/.test(w)), `godverse cache: fully-keyed cache raises no identity warn`);
// MALFORMED → error. A path segment, not a label: the string "3962749" is the same folder but
// different JSON, and that coercion drift is the species F caught in the atlas contract.
for (const [label, bad] of [
['string id', '3962749'], ['float id', 3962749.5], ['zero', 0], ['negative', -5], ['NaN', NaN],
]) {
const v = validateTownCache(gv(withIds.map((s, i) => i === 1 ? { ...s, godverseShopId: bad } : s)));
ok(!v.ok && v.errors.some(e => /malformed godverseShopId/.test(e)), `godverse cache: ${label} REJECTED`);
}
// DUPLICATE → error. Two shops keyed to one atlas is mis-stocking (charter risk #3: "never mis-stocked").
{
const v = validateTownCache(gv(withIds.map((s, i) => i === 1 ? { ...s, godverseShopId: withIds[0].godverseShopId } : s)));
ok(!v.ok && v.errors.some(e => /duplicate godverseShopId/.test(e)), `godverse cache: duplicate id REJECTED (mis-stock)`);
}
// MISSING → warn + fail-soft, NOT an error (the charter's determinism boundary: the world must not
// fail to load because stock identity is absent). Partial coverage still warns and still boots.
{
const v = validateTownCache(gv(withIds.map((s, i) => i < 2 ? { ...s, godverseShopId: undefined } : s)));
ok(v.ok, `godverse cache: missing ids stay VALID (fail-soft to tier 0, never a boot failure)`);
ok(v.warnings.some(w => /2\/8 shop\(s\) missing godverseShopId/.test(w)), `godverse cache: missing ids WARN with a count (${v.warnings.find(w => /godverseShopId/.test(w)) || 'NO WARN'})`);
}
// a plain osm cache must never be asked for ids
ok(!validateTownCache(mkCache(cluster(8))).warnings.some(w => /godverseShopId/.test(w)), `osm cache: no identity warn (godverse-only requirement)`);
// THE SEAM THAT MATTERS: the id survives the lift onto plan.shops. Without this the field is a
// cache-only decoration and F's per-shop base can never resolve — the runtime reads the PLAN.
{
const cache = gv(withIds);
const p = generatePlanOSM(20261990, 'gvtest', { cache });
const keyed = p.shops.filter(s => s.godverseShopId !== undefined);
ok(keyed.length === p.shops.length, `godverse lift: every seated shop carries godverseShopId (${keyed.length}/${p.shops.length})`);
ok(keyed.every(s => Number.isSafeInteger(s.godverseShopId) && s.godverseShopId > 0), `godverse lift: ids survive as positive integers`);
ok(new Set(keyed.map(s => s.godverseShopId)).size === keyed.length, `godverse lift: ids stay unique through the lift (no atlas collision)`);
// and an unkeyed cache writes NO key — that absence is what keeps all 22 pinned goldens frozen today
const plain = generatePlanOSM(20261990, 'plaintest', { cache: mkCache(cluster(8)) });
ok(plain.shops.every(s => !('godverseShopId' in s)), `plain lift: the key is ABSENT, not undefined (byte-identical JSON ⇒ goldens frozen)`);
}
}
// (d) the registry seam: register → osmTownKeys() sees it → generatePlanOSM resolves it
{
registerTownCache('regtest', mkCache(cluster(14)));
@ -511,7 +562,10 @@ const REAL_TOWN_GOLDENS = {
newtown_godverse: 0x24e63feb,
newtown_real: 0x741c18ec,
northbridge_real: 0x1a645848,
toowoomba_real: 0x8b635722,
// toowoomba_real RETIRED by E in R23 (cache deleted) — pin removed with it. A golden for a town that
// cannot load is dead config: it can never fire, so it can never fail. Same species as the round's
// vacuous-gate law, one layer down. (E tried the re-bbox first and it GAMED my spacing metric — median
// NN 395.7 → 89.4 m "passes" while hub density fell 3/12 → 2/12, the pack's worst. Right call.)
westend_real: 0x5b1e5ffe,
};
// ROUND20: the GIG-layer golden per real town (district + capped posters) — the base golden above is
@ -541,7 +595,7 @@ const REAL_TOWN_GIG_GOLDENS = {
newtown_godverse: 0xc5ed0b92,
newtown_real: 0xacdb3eee,
northbridge_real: 0x83a1d5be,
toowoomba_real: 0x9d34ccfe,
// toowoomba_real RETIRED by E in R23 — see the note on the base map above.
westend_real: 0xb2c7ddb6,
};
// `index.json` is E's towns INDEX (key/town/state/shops/roads for B's selector), not a town cache — skip it.
@ -564,6 +618,24 @@ for (const f of townFiles) {
const gwant = REAL_TOWN_GIG_GOLDENS[key];
if (gwant === undefined) console.log(` ⚠ real/${key}: gig UNPINNED — add REAL_TOWN_GIG_GOLDENS['${key}'] = 0x${ghash.toString(16).padStart(8, '0')}`);
else ok(ghash === (gwant >>> 0), `real/${key}: gig golden 0x${ghash.toString(16)} matches pinned 0x${(gwant >>> 0).toString(16)}`);
// ROUND24 ledger #2 — the identity gate on the REAL godverse caches, written to the round's own
// vacuous-gate law: it names its subject, it proves it touched it (a count), and it CANNOT pass while
// its subject is absent. Zero ids ⇒ explicit SKIP with the reason and the count. Partial ⇒ FAIL (a
// half-keyed town silently mis-stocks half its shops). Full ⇒ assert the ids reach plan.shops.
if (cache.source === 'godverse+osm') {
const keyed = cache.shops.filter(s => Number.isSafeInteger(s && s.godverseShopId) && s.godverseShopId > 0).length;
if (keyed === 0) {
console.log(` ⊘ SKIP real/${key}: godverse identity — 0/${cache.shops.length} shops carry godverseShopId. ` +
`Subject absent: G re-emits with ids (R24 ledger #2). This gate binds the moment one id lands.`);
} else {
ok(keyed === cache.shops.length,
`real/${key}: godverse identity — ${keyed}/${cache.shops.length} shops keyed (partial keying mis-stocks the rest)`);
const seated = generatePlanOSM(20261990, key, { cache }).shops;
const lifted = seated.filter(s => s.godverseShopId !== undefined).length;
ok(lifted === seated.length, `real/${key}: godverse identity survives the lift (${lifted}/${seated.length} seated shops keyed)`);
ok(new Set(seated.map(s => s.godverseShopId)).size === seated.length, `real/${key}: godverse ids unique on the plan (no shared atlas)`);
}
}
}
// ── 4. every facade skin referenced by the registry exists on disk ──────────────────