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:
parent
ba901d4a75
commit
1eb0b45bd0
@ -1,10 +1,50 @@
|
||||
# LANE A — CITYGEN · progress (PROCITY-A)
|
||||
|
||||
*Status: **all deliverables landed; self-check all-green (161,372/161,372); rounds 2–23 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 2–24 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)
|
||||
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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`
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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 ──────────────────
|
||||
|
||||
Loading…
Reference in New Issue
Block a user