Compare commits

...

6 Commits

Author SHA1 Message Date
m3ultra
2c1ae37160 Fable (integrator): round-25 instructions — the tag, earned (sweep vs Ruling 2, licence line, sku ids, F closes)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 02:14:02 +10:00
m3ultra
0608f9c8c1 Lane G R24: G3 §7 RATIFIED — John's write-back ruling, A then B (charter law #4 satisfied)
John ruled at the R24 kickoff: v5.0 ships on Option A (sandbox) — an in-game purchase never
touches real monsterrobot/Square inventory; Option B (reservation via the POS's existing
hold_expires_at) is CHARTERED as a v5.x upgrade behind a hold budget John sets; Option C is not
chartered and happens only as a deliberate business launch, if ever. Recorded inline in §7 with
the options preserved beneath it, since the trade-offs are the record of why A-then-B was chosen.

Written in as binding, because a one-line ruling isn't a mechanism: /reserve + /buy stay ABSENT,
not stubbed behind a flag (a sandbox a config typo can flip isn't a sandbox); the tier-2 reader
connects read-only, enforced at the credential — a role with SELECT on inventory/crate/disc_cache
and nothing else, which carries the §9 PII fence in the same grant; and the gate asserts the write
verbs 404 and the role's grants are read-only, because "we didn't call buy()" proves nothing —
the vacuous-gate law turned on ourselves.

Option B's preconditions are stated so it can't slide in quietly: the hold budget (John's number,
NOT YET SET — B is not startable until it is; a v5.x item, not a v5.0 blocker), the-shop-always-
wins on a race with an in-store Square sale, a staff-visible `held: GODVERSE` reason code, and
soak before ship. Doc header/§10 updated so the whole file tells one story: §7 ruled, the rest
still DESIGN pending both Fables' sign-off, no server code before that.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 01:33:27 +10:00
m3ultra
86e298531e Lane G R24 (v5.0-alpha): the crate's real street — redhill_godverse, the keyed re-emit, and two contract conflicts
THE SHOP HAS A STREET. redhill_godverse: Monster Robot Party seated on Musgrave Road (verified —
lot 8, type record, godverseShopId 3962749), 37 shops on E's 884 real Red Hill roads. Its
coordinate is John-supplied (Google Maps place /g/11g6rlbbc0 -> -27.4553791, 153.0076244) because
it is derivable from nothing we hold: OSM doesn't know the shop, the census doesn't either,
dealgod's stores/store_entity rows are NULL lat/lng, recordgod has no address. Verified inside E's
bbox and on Musgrave Road's own geometry; my own estimate was ~120 m out.

RE-EMIT (ledger #5 + #2): atlas keyed by godverseShopId with US `license` throughout (British
retired, 0 occurrences) — atlas-QA OK, 120 real records, byte-identical re-bake (the .webp bytes
did not move; only the index). newtown_godverse re-emitted with ids (18 keyed). godverse_town.py:
psycopg2 -> psql, EXTRA_SHOPS roster, and Fable's R22 Ruling 2 finally implemented.

RULING 2 WAS LOAD-BEARING, NOT COSMETIC (measured): census-only Red Hill is unshippable —
9 shops = 306 m spacing warn; +Monster Robot = 10 shops = 261 m warn AND a frontage poster seated
INSIDE edge 1037's kerb (-2.95 m); + the donor's texture layer (37) = no warn, poster passes.
261 m is worse than the 255 m that retired ballarat this round: the alpha's headline town would
have shipped flagged "scattered, not a high street, or retire the town". 9 donor heroes swapped
for census entries (18 in Newtown), counted per the ruling.

CONFLICT #1 — C §7.3 vs E's validator. C says lift provenance to TOP LEVEL, US `license`, British
retired, "exactly what E's validator reads". Measured: it is not. validate_atlas.py:98 reads
NESTED `provenance`; :105 reads `licence` British-first. Lifting-and-retiring as written FAILS the
gate; my R23 atlas already passed it untouched. Same species as the bug that held the tag — a doc
asserting what code does without running it. Resolved: E's check accepts `licence or license`, so
US-nested passes; the four also ride at top level (C's contract + the town-cache convention) off
one source of truth. -> E, one line: :198 PRINTS `licence` British-only while the check accepts
both, so C's mandated US spelling silently stops printing the licence flag — which :199's own
comment calls a human call. Gate passes; evidence line vanished. Fix: .get("licence") or .get("license").

CONFLICT #2 — Ruling 2 vs A's identity gate. NEEDS A RULING; it is why the tree is red (6 of 8).
Ruling 2 makes a godverse cache mixed: a keyed GODVERSE layer + an inherited OSM texture layer.
A's sweep asserts EVERY shop is keyed -> partial FAILs. Texture shops carry no godverseShopId
deliberately: an OSM milkbar has no GODVERSE identity, and minting one from its OSM id pushes a
third id space into a field that already carries two — A's validator calls a duplicate
mis-stocking, charter risk #3's nightmare with Monster Robot's crate as the prize. And A's own two
checks disagree: the validator says "Missing = tier 0 (soft). Duplicate = mis-stocked (hard). They
are not the same failure", while the sweep fails partial keying because it "mis-stocks the rest" —
conflating exactly those two. Proposed (A's call, Fable's ruling): assert coverage over the
godverse layer, not the inherited texture layer.

THE 8 RED CHECKS, ITEMISED: newtown_godverse base+gig goldens (2) moved by the re-emit exactly as
ledger #2 intends — A re-pins, base 0xcf69b387 gig 0x1e266f60; godverse identity x3 per godverse
town (6) = conflict #2. Nothing else. Classic covenant 0x3fa36874 intact. redhill_godverse pins:
base 0xb2224939. Verified live: the shop's 120-record pack and the town-wide 350 pack now COEXIST
through C's type+base fix.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 01:31:07 +10:00
m3ultra
5815dfbd58 Lane E R24 (v5.0-alpha): the crate's real street, the honest gate, and Ballarat retired
THE STREET (ledger #3). redhill_real: Red Hill, Brisbane — 36 shops (9 heroes + 27 texture,
-29 subsampled), 884 roads, median NN 73 m. **Musgrave Road is in roads[] (41 ways, primary +
residential)** — that was the point: Monster Robot Party's real shop now has its real street.
One bounded Overpass fetch under the standing authorization; raw snapshots in _raw/, ODbL, C's
mapping. This is G's donor for godverse_town.py -> redhill_godverse.

TWO HAZARDS HANDED TO G/A, both measured against the real geometry (notes R24 §1):
(a) The shop is NOT in OSM — no shop=music node, no name match anywhere in the bbox (checked the
raw snapshot, not the emitted cache). It cannot arrive via the fetch; G must inject id 3962749
from the census or the town ships without the shop the release is about.
(b) THE SEATING HAZARD: A's lift seats on the NEAREST edge, and from G's geocode three streets
are nearer than the addressed one — Zig Zag Street 38.6 m (residential), Craig Street 46.1 m,
Musgrave Road 51.6 m. The shop would seat on Zig Zag St and the headline would quietly become
false. Measured fix (G's call): the point on the real Musgrave centreline nearest G's geocode is
-27.455169, 153.006902 — 0 m to Musgrave, next candidate 43 m. Snaps the street, does NOT verify
the house number; I have no address data for where #147 sits along the road.

THE HONEST GATE (ledger #4). F's catch was right and there were FOUR bugs: the glob used the
published-contract name the runtime can never load (0 matches -> returned 0: it passed identically
whether atlases were absent, misnamed, or perfect); the code read provenance top-level while my own
docstring said nested (G nested it correctly — the gate would have failed G's real artifact);
licence/license; and a stale ceiling. Rebuilt on the "runtime is the authority" ruling: globs
stock_godverse/<id>/stock_<type>_index.json, type in {record,book,toy}. Vacuous-gate law implemented
literally — 0 matches + stray files FAIL (listing them); 0 matches + empty dir prints
"atlas-QA SKIP: no atlases present (0 files matched glob ...)"; plus an id equality
(shop.godverse_id == dir) so it proves it touched its subject. Proved on four cases: empty -> loud
SKIP, misnamed (F's exact bug) -> FAIL, unparseable -> FAIL, G's real atlas -> PASS (120 items,
2 atlases, licence printed). My own test found a fifth hole — the glob still matched the old
stock_shop_* name, so the misnamed case failed only incidentally; added a STOCK_TYPES name check
so it fails for the RIGHT reason. A gate that fails for the wrong reason will pass for the wrong
reason later.

BALLARAT — RETIRED, counted, and a DIFFERENT illness from Toowoomba (notes R24 §3). Toowoomba was
dead everywhere. Ballarat is alive exactly where the game isn't: 103 POIs, densest 400 m disc = 47
of them (Toowoomba's was 3) — but that disc is cafes/clothes/bakeries while the op-shops sit 854 m,
1042 m and 1709 m out. Heroes and density don't coincide, so no bbox holds both. Real builds at
every span: 1.0 km -> 8 shops/184 m/hub 4-8 but ZERO op-shops (a thrift town with no thrift shop);
2.0 km -> 12/186 m/3-12; 3.0 km as shipped -> 20/255 m/4-20. Every row worse than any shipped town.
THE BINDING CONSTRAINT WAS MINE, NOT OSM'S: with 2 heroes the 3x texture cap keeps 6 of 47 texture
POIs and the subsample is spatially blind, so survivors scatter at any span — part of the 255 m A's
warn caught was my own cap's fingerprint. Seeding texture near heroes fixes the class but moves
every town's golden -> filed as a v5.x proposal, not done in a release round.

I OVERSTATED MY R23 ADVICE TO A: the hub fraction is NOT a standalone discriminator — glebe (shipped,
healthy) reads 13% and redhill 14%; a hub threshold would false-flag both. Median NN is the robust
primary; hub is only comparable at equal shop counts (toowoomba 2/12 vs darwin 7/12 was fair because
both had 12). The raw-vs-seated calibration point stands.

METHODOLOGY, recorded because it nearly shipped: I picked Ballarat's span from an offline simulation
that predicted 68 m / hub 5-8, and had already written those numbers into a config comment as fact.
The real build returned 184 m / 4-8 — same counts, different sample. Never trust a re-implementation
of the pipeline's own selection; run the pipeline.

-> A (batched pins): ADD REAL_TOWN_GOLDENS['redhill_real']=0x6046700f and
REAL_TOWN_GIG_GOLDENS['redhill_real']=0x673785ee (the headline town currently only WARNS as
UNPINNED — ungated until you pin it). REMOVE ballarat_real 0xf0a9e8a1 / 0xf097c331 — dead config by
your own R23 note.
-> F: tools/qa/tour_shots.py still shoots town=toowoomba_real, which I deleted in R23 — the v4 tour
breaks on a missing town. Your file, your call. A retirement isn't done when the cache is deleted;
it's done when the consumers are swept.
-> C: built to the runtime + G's real artifact per the ruling; LANE_C_PUB §7 line 271 still names the
contradictory stock_shop_<id>_index.json. I reconcile when your corrected contract lands.

Determinism receipt: the retirement forced a full 21-town rebuild from cached raw (no refetches) and
every existing cache came back byte-identical — only ballarat, redhill and index.json moved. Pack:
21 real towns · 1196 shops · 299 heroes (22 rostered with G's newtown_godverse). Selfcheck ALL GREEN
149,696, synthetic golden 0x3fa36874 unchanged. qa.sh --strict --matrix: 7 passed / 0 failed, MATRIX
GREEN 10x7.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 01:06:00 +10:00
m3ultra
1eb0b45bd0 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>
2026-07-17 00:45:45 +10:00
m3ultra
ba901d4a75 Lane C R24: the contract fix — the runtime is the authority (ledger #1, the root cause)
F held the tag because my published atlas contract and my own loader were mutually
incompatible. That was mine. Closed, and published FIRST so G and E re-emit against it.

WHAT WAS WRONG: my R22 §7 invented `stock_shop_<godverseShopId>_index.json`; the loader
(stockpack.js:25) fetches `${base}stock_${type}_index.json`. An index named per the contract
can never be fetched by the code meant to load it. G built runtime-compatible (correct), E
built contract-compatible (also correct — the contract said so), and E's validator globbed
stock_shop_*_index.json -> zero files -> vacuous pass. Nobody was wrong; the contract was.

§7 REWRITTEN AGAINST THE CODE (LANE_C_PUB):
- Authority rule: stockpack.js IS the contract; doc vs loader disagreement = doc bug.
- Layout ratified as G shipped it: assets/stock_godverse/<godverseShopId>/stock_<type>_index.json
  (`type` is load-bearing — SLOT_FOR picks the slot). stock_shop_* RETIRED.
  -> E: the glob becomes stock_godverse/**/stock_*_index.json (the vacuous gate's actual cause).
- Runtime-read vs ignored stated exactly: reads atlases[] + items[{atlas,uv}] (uv top-left, the
  loader flips V); ignores version/atlas_px/cell*/count/kind/tier/shop/crate/provenance.
- Provenance drift dies: runtime reads none of it, so the authority is E's validator + the 23
  shipped town caches -> TOP-LEVEL `license` (US), attribution, generator, snapshot. British
  `licence` retired; G lifts+renames those four, keeps its rich nested provenance{} as colour.
  atlas_px stays REQUIRED (checked against real dims). cell* informational — C reads uv, never
  cell; do NOT shrink the cell for VRAM (the dig renders covers close-up).

CODE (F's filed item): stockpack cache identity is now `${base}|${type}`;
getStockPack(type, base = DEFAULT_STOCK_BASE); new opts.stockBase on buildInterior = the seam F
wires. Omitted => town-wide packs, byte-identical to today. Pre-v5 callers untouched.

VERIFIED FRESH — and I fixed my own metric first: R23's "8/8 atlas-backed" only checked that a
map exists, but a parody canvas has a map too (same species as the vacuous gate). Re-asserted
falsifiably: every sleeve's texture source IS stock_godverse/3962749/stock_record_atlas_00/01.webp,
pack items == index items by id; negative control — the parody room's textures are (canvas, no src)
and touch the atlas zero times, so the metric discriminates. Collision FIXED (shop 120 + town 350
coexist). Fail-soft FIXED with a populated cache (missing base -> null -> parody). 98 draws with
real covers, pathOK, 0 carves, 0 console errors.

SCOPED NOT SLID: the LRU+dispose (§7.6) lands at v5.0-beta — one stocked shop can't accrete, and
safe eviction needs a room-exit hook (a live room holds the pack's shared material) that arrives
with F's per-shop preload. Written into the contract so it can't slide silently.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 00:43:50 +10:00
30 changed files with 245944 additions and 231003 deletions

View File

@ -3,7 +3,63 @@
*Status: **v1 complete & verified**. Standalone interiors library + test page. Every shop door opens
into a unique, seeded, themed interior, generated on demand in ~4ms, byte-identical every revisit.*
Last updated: 2026-07-16 (round 23) · owner: PROCITY-C · reviewer: Fable
Last updated: 2026-07-17 (round 24) · owner: PROCITY-C · reviewer: Fable
---
## Update 2026-07-17 (round 24, THE ALPHA COMPLETION) — the contract fix (ledger #1, the root cause)
R24 §Lane C. **The root cause was mine and it's closed.** F held the tag because my published atlas
contract and my own loader were mutually incompatible; the ruling — **the runtime is the authority** — is
now the section's first line. Published FIRST (half-day rule; G and E re-emit against it).
**What was actually wrong.** My R22 §7 invented `stock_shop_<godverseShopId>_index.json`. The loader
(`stockpack.js:25`) fetches `` `${base}stock_${type}_index.json` ``. **An index named per my contract can
never be fetched by my own code.** So G built runtime-compatible (correct), E built contract-compatible
(also correct — the contract said so), and E's validator globbed `stock_shop_*_index.json` → **zero files
→ vacuous pass**. Nobody was wrong; the contract was. I wrote it, so I own it.
**§7 rewritten against the code (LANE_C_PUB):**
- **Authority rule:** `stockpack.js` IS the contract; if the doc and the loader disagree, the doc is a bug.
- **Layout ratified as G shipped it:** `assets/stock_godverse/<godverseShopId>/stock_<type>_index.json`.
`type` is load-bearing (it picks the slot via `SLOT_FOR`: record→sleeve). `stock_shop_*` is **retired**
**E: the validator glob becomes `stock_godverse/**/stock_*_index.json`** (that's the vacuous gate's
actual cause).
- **Runtime-read vs runtime-ignored, stated exactly.** Reads `atlases[]` + `items[{atlas, uv}]` (uv origin
**top-left**; the loader flips V — don't pre-flip). Ignores `version/atlas_px/cell*/count/kind/tier/shop/
crate/provenance` entirely.
- **The provenance drift dies (§7.3):** since the runtime reads none of it, the authority is E's validator
+ the 23 shipped town caches — both use **top-level `license` (US), `attribution`, `generator`,
`snapshot`**. British `licence` retired; **G lifts+renames those four**, keeps its rich nested
`provenance{}` (source/sha256/determinism) as colour. `atlas_px` stays REQUIRED (E checks it against real
dims — a genuine falsifiable check). `cell*` informational: **C reads `uv`, never `cell`** — and don't
shrink the cell for VRAM, the dig renders covers close-up.
**Code landed (the `type+base` keying, F's filed item):**
- `stockpack.js`: cache identity is now `` `${base}|${type}` ``; `getStockPack(type, base = DEFAULT_STOCK_BASE)`.
- `interiors.js`: new **`opts.stockBase`** — the seam F wires (`buildInterior(shop, THREE, { stock:'real',
stockBase: 'assets/stock_godverse/<id>/' })`). Omitted ⇒ town-wide packs, byte-identical to today.
- Pre-v5 callers untouched (`getStockPack('record')` still means the town pack).
**Verified fresh — and I had to fix my own metric first.** R23 I claimed "8/8 atlas-backed"; that checked
`material.map` exists — **but a parody canvas has a map too**. That's the same species as the vacuous gate
I'd just been handed. Re-asserted falsifiably:
- **Every sleeve samples G's atlas FILE** — texture sources are exactly `stock_godverse/3962749/
stock_record_atlas_00.webp` / `_01.webp`; pack items ≡ index items **by id**. Titles are real
(*Yoshitoshi Artists In House We Trust / Deep Dish Penetrate Deeper*).
- **Negative control (what makes it a gate):** the parody room's stock textures are `(canvas, no src)` and
touch the godverse atlas **zero** times — the metric discriminates.
- **Collision FIXED:** shop pack (120 items) + town pack (350) coexist as distinct objects.
- **Fail-soft FIXED with a populated cache:** missing base ⇒ `null` ⇒ parody (the R23 correctness bug —
an unstocked shop inheriting a cached crate — is gone).
- Ceilings: **98 draws** with real covers via `opts.stockBase`; `pathOK`; 0 carves; **0 console errors**.
**Scoped, not slid:** the **LRU + dispose** (§7.6) lands at **v5.0-beta**, not here — with one stocked shop
there's no accretion, and safe eviction needs a room-exit hook (a live room holds the pack's shared
material), which arrives with F's per-shop preload. Written into the contract so it can't slide silently.
**→ F:** the seam is `opts.stockBase`; §7.5 has the call. **→ G:** layout ratified, re-emit provenance field
names only (§7.3). **→ E:** the glob (§7.1) is the vacuous gate's cause.
---

View File

@ -3,6 +3,96 @@
*Lane brief: `docs/LANES/LANE_G_GODVERSE.md`. This file follows your progress-file convention —
newest round first. Fable-on-ultra writing.*
## Round 24 (2026-07-17) — the re-emit + THE SHOP'S REAL STREET (ledger #5)
**THE WRITE-BACK RULING — John, 2026-07-17: A THEN B.** G3 §7 is **ratified** with the ruling
inline; charter law #4 is satisfied. v5.0 ships **sandboxed** — a game purchase never touches real
monsterrobot/Square inventory. **Option B (reservation) is chartered as a v5.x upgrade** behind a
hold budget John sets; **C is not chartered**. What I wrote into the doc as binding, beyond the
one-liner, because a sandbox that can be flipped on by a config typo isn't a sandbox:
`POST /reserve` + `/buy` stay **absent, not flag-stubbed**; the tier-2 reader connects with a
**read-only role** (`SELECT` on inventory/crate/disc_cache only — which carries the §9 PII fence in
the same grant); and the gate asserts the write verbs **404** + the role's grants are read-only,
because "we didn't call buy()" proves nothing (the vacuous-gate law applied to ourselves).
**Carried to v5.x: the hold budget is John's number and is not yet set** — B isn't startable until
it is. B's non-negotiable: **the shop always wins** a race with an in-store Square sale.
**Landed (branch `lane-G-godverse`):**
- **`redhill_godverse`** — the town whose record shop is THE shop. Monster Robot Party seated on
Musgrave Road (**verified: lot 8, type `record`, `godverseShopId` 3962749**), 37 shops, 884 real
Red Hill roads off E's donor.
- **The atlas re-emit** — US `license` throughout (British `licence` retired, 0 occurrences),
provenance lifted to top level per C §7.3 **and** kept nested per E's validator (see conflict #1).
`atlas-QA OK`, 120 real records, byte-identical re-bake holds.
- **`newtown_godverse` re-emitted with `godverseShopId`** (ledger #2) — 18 census shops keyed.
- **`godverse_town.py`**: psycopg2 → `psql` (the m3 has no psycopg2); emits `godverseShopId`;
`EXTRA_SHOPS` roster; **Fable's R22 Ruling 2 finally implemented** (see below).
- G3 §4: the `godverseShopId`/`godverse_id` naming split recorded (two artifacts, two conventions).
**Monster Robot Party's coordinate** is John-supplied (Google Maps place `/g/11g6rlbbc0`
-27.4553791, 153.0076244). It could not be derived: OSM doesn't know the shop (no name match, no
147 Musgrave Rd address node), the census doesn't have it, dealgod's `stores`/`store_entity` rows
carry NULL lat/lng, and recordgod has no address anywhere. Verified inside E's bbox **and** on
Musgrave Road's own geometry. My own estimate was ~120 m out — asked instead of shipping it.
**FABLE'S RULING 2, IMPLEMENTED (R22 → R24) — and it was load-bearing, not cosmetic.** "Merge
census heroes WITH the donor's texture shops; census heroes win a collision; count the swap."
I never built it. Measured this round, census-only Red Hill is **not shippable**:
| redhill_godverse | shops | A's spacing warn | poster-clears-kerb |
|---|---|---|---|
| census only | 9 | **306 m** ⚠ | pass |
| census + Monster Robot | 10 | **261 m** ⚠ | **FAIL** (poster inside edge 1037 by 2.95 m) |
| **+ donor texture (Ruling 2)** | **37** | **none** ✓ | **pass** ✓ |
| *(E's donor `redhill_real`, for reference)* | 36 | none | pass |
261 m is **worse than the 255 m that retired ballarat this round** — the alpha's headline town
would have shipped flagged "scattered, not a high street, or retire the town". Ruling 2 fixes both.
9 donor heroes swapped for their census entries in Red Hill, 18 in Newtown — counted, per the ruling.
**CONFLICT #1 — C's contract §7.3 vs E's validator (provenance).** C says "lift the four to TOP
LEVEL, US `license`; British retired — *exactly what E's validator reads*". **Measured: it is not.**
`validate_atlas.py:98` reads **nested** `provenance`, and `:105` reads `licence` British-first.
Lifting-and-retiring as written **fails the gate**. My R23 atlas already passed it untouched. This
is the same species as the bug that held the tag — a doc asserting what code does without running
it. **Resolved without drama:** E's check accepts `licence or license`, so US-nested passes; I emit
the four at top level (C's contract + the town-cache house convention) off ONE source of truth.
**→ E, one line:** `validate_atlas.py:198` PRINTS `licence` British-only while its *check* accepts
both — so adopting C's mandated US spelling **silently stops printing the licence flag**, which
:199's own comment calls a human call ("the licence is PRINTED — clearing it is a human call").
The gate still passes; the evidence line vanished. Fix: `.get("licence") or .get("license")`.
**CONFLICT #2 — Ruling 2 vs A's identity gate. THIS ONE NEEDS A RULING; it is why the tree is red.**
Ruling 2 makes a godverse cache *mixed*: a keyed GODVERSE layer (census + Monster Robot) plus an
inherited OSM texture layer. A's sweep asserts **every** shop is keyed → partial ⇒ FAIL. The two
standing rulings cannot both hold.
**The texture shops carry no `godverseShopId` deliberately**, and I'd argue A's own words settle it:
- The field *means* "this lot's stock lives at `stock_godverse/<id>/`". An OSM milkbar has no
GODVERSE identity. Minting one from its OSM id pushes a **third id space** into a field that
already carries two, and A's validator calls a duplicate **mis-stocking** — charter risk #3's
exact nightmare, with Monster Robot's crate as the prize. (Measured: OSM ids here are ≥53.1 M vs
census ≤2992 and store id 3,962,749 — no collision *today*; dealgod store ids reach 7 digits and
OSM ids are 8+, so the margin is thin, and "no collision today" is not an id-space design.)
- **A's validator and A's sweep disagree with each other.** The validator: *"Missing = tier 0
(soft). Duplicate = mis-stocked (hard). **They are not the same failure.**"* The sweep fails
partial keying because it *"mis-stocks the rest"* — conflating precisely the two failures A said
are not the same. Unkeyed → parody is the **designed ladder**, not a gap.
- **Proposed (A's call, Fable's ruling):** assert coverage over the **godverse layer**, not the
inherited texture layer — the subject is "shops that claim stock", and the gate stays honest
(it still names its subject and proves a count). Zero-keyed still SKIPs; duplicates still error.
**The 8 red checks, itemised — no surprises for F:**
- `newtown_godverse` base+gig goldens (**2**) — my re-emit moved them, exactly as ledger #2
intends. **A re-pins** (batched): base `0xcf69b387`, gig `0x1e266f60`.
- `godverse identity` ×3 on each godverse town (**6**) — conflict #2, above. Nothing else.
- Unpinned (warn, expected): `redhill_godverse` base `0xb2224939` / gig — **A pins**.
- **Classic covenant `0x3fa36874` intact.** `atlas-QA OK`.
**If Fable rules the other way** (texture layer out): revert to census-only and F inherits the
poster-kerb FAIL at edge 1037 + a 261 m town — that break is A/B's invariant, exposed by real data,
and would need an owner before the tag. I'd rather hand F the conflict than the broken town.
## Round 23 (2026-07-16) — G2a: THE FIRST REAL CRATE + the G3 draft
**Landed (branch `lane-G-godverse`, G namespace only — ready for your mid-round merge):**

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

@ -1,11 +1,15 @@
# G3 — THE ECONOMY BACKEND (design doc, DRAFT for ratification)
# G3 — THE ECONOMY BACKEND (design doc)
*Lane G (ultra), R23. Drafted per the v5 charter ([V5_REAL_SHOP.md](V5_REAL_SHOP.md)) ledger #2.
Ratification: both Fables + John, at R24 kickoff. **Nothing server-side ships before that.** This
round is paper — plus the tier-1 artifact that made the paper honest (G2a, merged this round).*
*Lane G, R23 draft. Drafted per the v5 charter ([V5_REAL_SHOP.md](V5_REAL_SHOP.md)) ledger #2.*
Status of each section: **[MEASURED]** = verified against the real databases this round.
**[DESIGN]** = proposed, not built. **[JOHN]** = a decision reserved to John by charter law.
**§7 (THE WRITE-BACK QUESTION) IS RATIFIED — John, 2026-07-17, R24 kickoff: A THEN B.** v5.0 ships
sandboxed (a game purchase never touches real inventory); reservation is chartered as a v5.x
upgrade behind a hold budget John sets; full write-back is not chartered. Charter law #4 is
satisfied. **The rest of this doc remains DESIGN and still needs both Fables' sign-off; no
server code ships before that.**
Status of each section: **[MEASURED]** = verified against the real databases.
**[DESIGN]** = proposed, not built. **[RULED]** = decided by John under charter law.
---
@ -112,7 +116,12 @@ Design rules:
says only what changed. A sold record is a `sku` in `gone[]``collapseBuyItem()` (already
built, R9) zero-areas the sleeve. Sold-means-gone is a *diff*, not a re-fetch.
- **`etag`/`since`** so a re-enter is a 304, not a payload.
- Shop id is the **godverse id** (§1a), never a POS internal id.
- Shop id is the **godverse id** (§1a), never a POS internal id. **R24 naming, as landed:** the
town cache carries `shop.godverseShopId` (A's schema field, camelCase, read by the JS runtime);
the atlas index carries `shop.godverse_id` (snake_case, what E's `validate_atlas.py` requires).
Same id, two artifacts, two house conventions — the API uses `godverse_id` on the wire to match
the Python/JSON side it is served from. Recorded rather than silently reconciled: if the
integrators want one spelling, it is a one-line change in each emitter, not a design question.
---
@ -153,13 +162,47 @@ whichever dataset actually *has* the shop, and mismatches must fail soft, never
---
## 7. THE WRITE-BACK QUESTION [JOHN — charter law #4]
## 7. THE WRITE-BACK QUESTION **RULED: A THEN B** (John, 2026-07-17, R24 kickoff)
**Does an in-game purchase ever touch real thriftgod/recordgod inventory?**
> ### THE RULING
> **v5.0 ships on Option A (sandbox).** An in-game purchase **never** touches real
> monsterrobot/Square inventory. Sold-means-gone is per-session, tier-1 semantics.
> **Option B (reservation) is CHARTERED as a v5.x upgrade**, behind a hold budget John sets.
> **Option C is not chartered** — it happens only as a deliberate business launch, if ever.
>
> Charter law #4 is hereby satisfied: the write-back question has an explicit, John-ratified
> answer, and until B ships the standing state is **read-live, sell-sandboxed**.
This is now a live question, not a hypothetical: `monsterrobot` is a **real trading shop** whose
POS syncs to **Square**, with real customers buying these exact records off the real shelf. Stated
per charter law #4 with trade-offs and **no default**.
**What this makes law for v5.0 (binding on the server work, when it starts):**
- The API's `POST /reserve` and `POST /buy` (§4) stay **unbuilt and unrouted**. Not stubbed
behind a flag — absent. A sandbox that can be flipped on by a config typo is not a sandbox.
- The game's buy path terminates in the client's wallet. **No code path from a game purchase to
`inventory.in_stock`, `sold_date`, `hold_expires_at`, `qty`, or Square exists.** The tier-2
reader connects to the POS **read-only** — enforce it at the credential, not in a code review:
a role with `SELECT` on `inventory`/`crate`/`disc_cache` and nothing else (this also carries the
§9 PII fence in the same grant).
- A gate worth its name here (the vacuous-gate law): assert the write verbs **404**, and that the
tier-2 role's grants are read-only. A test that "we didn't call buy()" proves nothing.
**Option B's charter (v5.x — the preconditions, so it can't slide in quietly):**
1. **The hold budget — John's number, not yet set.** Max concurrent game-held SKUs, and max hold
duration. Until both are set, B is not startable. *(Open item, not a blocker for v5.0.)*
2. **The shop always wins.** If a held record sells in-store via Square, the game loses the hold,
silently and immediately, and the player is told the truth ("someone bought it in the shop") —
the one race policy that is never up for negotiation.
3. **Staff-visible reason code** on every hold (`held: GODVERSE`), so a human at the counter can
see why a record is out of the crate and clear it by hand.
4. **Soak first.** B rides plumbing that has soaked, not plumbing that tags a round — Fable's
framing, and the reason A ships first.
*The options as framed for the ruling are preserved below, since the trade-offs are the record of
why A-then-B was chosen.*
---
**The question as put:** does an in-game purchase ever touch real thriftgod/recordgod inventory?
Live, not hypothetical: `monsterrobot` is a **real trading shop** whose POS syncs to **Square**,
with real customers buying these exact records off the real shelf.
Relevant measured facts: the POS already has `in_stock`, `sold_date`, **`hold_expires_at`**, `qty`
and `square_item_id`. A reservation primitive therefore already exists — the question is whether
@ -190,8 +233,8 @@ A purchase marks the real SKU sold and syncs to Square.
sales channel (real payment, tax, postage, fulfilment, refunds, chargebacks); a bug destroys
real stock; a game wallet is not money. This is a business decision, not an engineering one.
**Lane G asks nothing and recommends nothing here.** Until ratified: **tier 2 is read-live,
sell-sandboxed** (the charter's standing state, Option A by default *only* because law #4 says so).
*(Lane G asked nothing and recommended nothing here — the options above are the record as put to
John. **Ruled A-then-B; the binding terms are at the top of this section.**)*
---
@ -242,6 +285,8 @@ class: reachability, fail-soft, latency. Kill the server mid-session and the gam
stops ~2.3 km south of Red Hill. The crate has no street until E's `build_towns.py` gets a
Red Hill entry (~`center: (-27.4553, 153.0064), span_km: 2.4`) and one Overpass fetch. That's
E's file and an outward call — **G has not touched either.**
3. **[John, R24]** §7, the write-back question.
3. ~~**[John, R24]** §7, the write-back question.~~ **RULED 2026-07-17: A then B** (§7). Carried
forward: **the hold budget is John's number and is not yet set** — Option B is not startable
until it is, which is a v5.x item, not a v5.0 blocker.
4. **[Both Fables]** §9's beta scoping — depth (one real shop, 357 crates) vs breadth (more
shops, which needs other people's data).

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

@ -7,9 +7,11 @@
> (ledger #2, the density widening; C→E handshake). The frozen venue contract (§0§5) is unchanged.
> **v4.0 amendment (2026-07-16):** **§7 added — the G2a per-shop stock-atlas contract** (C's interior
> draw/tex ceilings, answering Lane G + E's review). §0§6 unchanged.
> **v5.0-alpha amendment (2026-07-16):** **§7.2's VRAM number corrected + §7.8 added** — G's first real
> crate (120 records) proved my "≤8 MB" figure was derived from a wrong assumption, and the per-shop cache
> seam is now specified for G+F. **G's R23 atlas needs no re-emit.** §0§6 unchanged.
> **v5.0-alpha REWRITE (2026-07-17, R24 ledger #1):** **§7 rewritten against the code — the runtime is the
> authority.** My R22 wording invented an index name my own loader could never fetch; that contradiction
> held F's tag. §7 now states what `stockpack.js` actually implements, extended per-shop, and the
> `type+base` cache keying is landed. **G's R23 atlas layout is ratified** (G re-emits provenance field
> names only, §7.3). §0§6 unchanged.
*The district ships **three** venue archetypes behind `?gigs=1``pub`, `band_room`, `rsl`. Lane A
converts a chosen shop to a venue kind in place (`shop.type = kind`); C keys the interior recipe off
@ -226,99 +228,84 @@ mapping for the caches is this table.)
---
## 7. G2a — per-shop stock atlases: C's interior ceilings (v4.0, ledger #3) → Lane G + Lane E
## 7. Real-stock packs — THE RUNTIME IS THE AUTHORITY (v5.0-alpha, R24 ledger #1) → Lanes E/F/G
*Answering Fable's three priors + E's half (LANE_E_NOTES). **Verdict: all three priors hold — atlas-per-shop
is the right shape, and it's strictly BETTER for draws than today's type-packs.** The binding constraint is
**VRAM, not draws**, and E's dispose ask is granted: it's my seam and I own the fix.*
> **This section was the root cause of R23's held tag and has been rewritten against the code.**
> My R22 wording invented an index name (`stock_shop_<godverseShopId>_index.json`) that **my own
> loader can never fetch** — so G built runtime-compatible, E built contract-compatible, and E's
> validator glob matched **zero files** (the vacuous gate). Nobody was wrong; the contract was.
> **Rule going forward: `web/js/interiors/stockpack.js` is the contract. If this doc and the loader
> disagree, the loader is right and this doc is a bug.** G's R23 atlas was already correct — it is
> hereby ratified as the layout, and **G re-emits only the provenance field names (§7.3).**
**1. Draw ceiling — ONE atlas per shop (hard cap 2).**
Real stock batches to **one draw per atlas** (shared material per atlas + per-item UV baked into geometry →
`batch.js` merges). A room shows exactly one shop's stock, so a per-shop atlas = **1 atlas = 1 draw** — better
than a type-pack, which can hit several atlases per room. **Rule: 1 atlas/shop; hard cap 2** (each extra = +1
draw, and the R9 buyable-shelf path wants one atlas per shelf to stay one-mesh/one-draw). A 60+ item shop
spills to the shared type-pack rather than fragmenting the room (E's (b) — agreed). *Headroom: `?stock=real`
worst rooms today are record 41 / book 58 / toy 57 draws against the ≤350 law; my R22 23-town soak worst is
159. Draws are not the constraint.*
### 7.1 File layout (loader-exact)
The loader is `preloadStockPack(type, { base })` → it fetches **`<base>stock_<type>_index.json`**.
- **`type``record` | `book` | `toy`** — and it is *not* cosmetic: `type` picks the stock slot
(`SLOT_FOR`: record→`sleeve`, book→`spine`, toy→`box`). **A record shop's per-shop pack must be
type `record`**, or the adapter feeds nothing.
- **Town-wide packs** (unchanged): `assets/models/stock_<type>_index.json`.
- **Per-shop packs (v5): `assets/stock_godverse/<godverseShopId>/stock_<type>_index.json`** — the base
is the per-shop directory; the filename is the same. *(This is exactly G's R23 layout.)*
- **RETIRED:** `stock_shop_<id>_index.json` — never loadable; do not emit, do not glob for it.
**→ Lane E:** `validate_atlas.py`'s glob must become `stock_godverse/**/stock_*_index.json`
(the current `stock_shop_*_index.json` is why it passed on 0 files).
**2. Texture/VRAM ceiling — the real limit. E's dispose ask: GRANTED, and it's mine to land.**
E's audit is correct and the cause is a deliberate C design note (`stockpack.js:11`): atlas textures are
"shared/cached in the pack (**NOT per-room**), like depot GLBs". That is **right for 3 bounded type-packs**
and **wrong for unbounded per-shop atlases** (23 towns × N shops). The ceiling:
### 7.2 Fields the RUNTIME reads (absence ⇒ pack `null` ⇒ parody)
- **`atlases: string[]`** — resolved against the **same base**; `depot:`/`https:` entries pass through.
- **`items: [...]`, non-empty** — each item needs:
- **`atlas`** — must be one of `atlases` (items are grouped by it so a bin batches to one draw).
- **`uv: [u0,v0,u1,v1]`, origin TOP-LEFT** — the loader flips V itself (`uvRect`). Do not pre-flip.
- Read by the dig/buy consumers (not by the loader): **`id`, `title`, `artist`, `price`, `price_band`**.
> **⚠️ CORRECTED v5.0-alpha — measurement beat my assumption.** I wrote "≤8 MB decoded" by assuming a
> "typical shop" is ~16 items @1024². **G's first real crate is 120 records** (Monster Robot Party, crate
> 550) — a real record shop is nothing like 16 items. At the contract's own 256² cell that's 2 × 2048²
> = **~32 MB decoded**, 4× my stated figure. The *structural* rules were right (≤2 atlases; 2048² for a
> 60+ item shop) — only the MB number was wrong, because it was derived from a shop that doesn't exist.
> **G's R23 atlas is compliant; no re-emit.** The corrected ceiling:
### 7.3 Fields the runtime IGNORES — and the provenance ruling (the drift dies here)
`version`, `atlas_px`, `cell`/`cell_w`/`cell_h`, `count`, `kind`, `tier`, `shop`, `crate`,
`provenance`**`stockpack.js` reads none of them.** So the authority for their shape is not C's
loader but **E's `validate_atlas.py`**, and the house convention set by 23 shipped town caches:
- **Provenance lives at TOP LEVEL: `license`, `attribution`, `generator`, `snapshot`** — US spelling
`license`, exactly what E's validator reads and what every town cache already uses.
**British `licence` is retired. → Lane G: rename + lift those four to top level.**
- Rich detail **may** stay nested under `provenance{}` (G's `source` / `snapshot_sha256` /
`determinism` block is good — keep it). The four gate fields are the contract; the block is colour.
- **`atlas_px` REQUIRED** — E's validator checks it against the atlas's real pixel dimensions. That is
a genuine falsifiable check; keep it.
- **`cell` / `cell_w` / `cell_h` are informational.** **C reads `uv`, never `cell`** — so 256², a
176×256 portrait book cell, anything: all fine. **Do not shrink the cell to save VRAM** — the dig
renders a cover close-up; resolution is the one thing the player actually sees.
- **Per shop: ≤2 atlases, ≤2048² each** (unchanged — G's 120-item crate lands exactly on the cap).
- **Resident: ONE shop's atlas set** (LRU = 1 shop, not 2) ⇒ **≤32 MB decoded peak**. Still bounded, and
still far better than the **181 MB unbounded** accretion E measured today.
- **≤1024² whenever the crate fits it** (≤64 items at 256²) — don't hand a 6-item shop a 2048².
- Cell size is E/G's call: **C reads `uv`, never `cell`** (so 256², 176×256 portrait, etc. are all fine).
Do **not** shrink the cell below 256² to save VRAM — the dig renders a cover close-up; resolution is the
one thing the player actually sees.
- **C lands the LRU cap + dispose in `stockpack.js`** (evicted shop's atlas texture + material disposed).
Type-packs keep today's shared cache (bounded, reused across rooms of a type) — unchanged.
- **Scheduling:** today's 181 MB accretion is **pre-existing**, and `?stock=real` is **opt-in**
(`index.html:196` — `params.get('stock')==='real' && !NOASSETS`), so it cannot touch the default boot and is
**not a v4.0-tag blocker**. I land the cap as **C's first v4.x item — before G2a ships**. G: build to the
ceiling; the loader will honour it.
### 7.4 Cache identity = **(type, base)** — landed R24
`stockpack.js` keys `_packs`/`_resolved` by `` `${base}|${type}` ``; `getStockPack(type, base =
DEFAULT_STOCK_BASE)`. Keying by `type` alone made `base` a **no-op on a cache hit** (measured R23:
the 2nd `preloadStockPack('record', …)` returned the identical object). That was two bugs:
1. two stocked shops collide — the 2nd serves the 1st's crate; **and**
2. **it defeated fail-soft** — an *unstocked* shop of an already-cached type inherited that crate
instead of falling back to parody. (A correctness bug, not a mix-up.)
Both fixed and re-measured (§7.7). Pre-v5 callers are unaffected: no `base` ⇒ the town-wide pack.
**3. Index schema — E's (c) confirmed VERBATIM, keyed per shop.**
`stock_shop_<godverseShopId>_index.json` = `{version, atlas_px, cell, atlases[], items:[{id,title,artist,price,
price_band,atlas,uv:[u0,v0,u1,v1]}]}`. `stockpack.js` consumes it unchanged (it already groups items by atlas
and shares a material per atlas). **UV origin stays top-left (image-natural) — C flips V for WebGL** (R7 law,
unchanged). Book cell 176×256 portrait is fine: **C reads `uv`, never `cell`**.
### 7.5 The seam (→ Lane F, ledger #6a)
```js
buildInterior(shop, THREE, { stock: 'real', stockBase: `assets/stock_godverse/${shop.godverseShopId}/` })
```
`opts.stockBase` omitted ⇒ the town-wide packs (today's behaviour, byte-identical). Preload the shop's
pack on entry with the same base. **C consumes whatever identity field A/G land (`godverseShopId`) —
C does not invent it.**
**4. Fail-soft + `?noassets` — unchanged law, confirmed.**
Index/atlas missing → `getStockPack` → null → **parody canvas + one warn** (R7, proven since). `?noassets`
fetches zero. A godverse town under `?noassets` stays silent-and-fine. `?stock=real` opt-in ⇒ G2a cannot
regress the default or classic boots.
### 7.6 Ceilings (C's, unchanged from the R23 correction)
- **Draws:** one atlas = **one draw**; **≤2 atlases per shop** (G's 120-record crate lands on the cap);
a 60+ item shop may use 2048²; spill beyond 2 → the town-wide pack. Room law ≤350 stands.
- **VRAM:** **≤2 atlases @≤2048²; LRU = 1 shop's atlas set resident ⇒ ≤32 MB peak.**
*(R23 correction: my "≤8 MB" assumed a ~16-item "typical shop". A real record shop is 120 records.)*
- **The LRU + dispose is C's, and it lands at v5.0-beta**, not here: with ONE stocked shop there is no
accretion, and safe eviction needs a room-exit hook (a live room holds the pack's shared material) —
that seam arrives with F's per-shop preload. Stated so it can't slide silently.
**5. Determinism — one ask back to G.**
The seeded pick per bin is C's and stays: same `shop.seed` → same crate contents, byte-identical revisits.
That indexes into `items[]`, so **G must emit `items[]` in a deterministic order** (sort by `id`) — otherwise
a re-emit silently reshuffles every crate. Same discipline as E's byte-identical cache re-runs.
**6. Buy-anywhere (R9) — compatible, no change.**
`buildBuyableShelf` merges per-item-addressable covers from ONE atlas → one draw (`noBatch`). A per-shop atlas
satisfies that naturally (better than the type-pack's seeded `atlasPool` pick). Parody = not buyable stays.
**7. Parody law — E's (d), unchanged and not C's to relax.** Metadata deterministically parody-transformed;
cover art real; prices real-or-seeded. *(v5.0-alpha: John amended this at tier 1 — real titles/artists ship
in-house-green, flagged before public. Recorded in G's provenance; not C's to relax either way.)*
**8. The per-shop cache seam (v5.0-alpha — C's spec; G+F build to this). CONFIRMED BROKEN in R23.**
G's ask #1 is real and I reproduced it. `stockpack.js` keys `_packs`/`_resolved` by **`type` alone**, and
`preloadStockPack(type, {base})` **ignores `base` on a cache hit**. Measured (R23, fresh context):
- `preloadStockPack('record', {base:'assets/stock_godverse/3962749/'})` → the 120-record godverse pack ✓
- then `preloadStockPack('record', {base:'assets/models/'})`**returned the identical object**
Two consequences, the second worse than G's report:
1. **Two stocked shops collide** — the 2nd silently serves the 1st shop's crate (blocks v5.0-beta).
2. **It breaks the fail-soft law** — an *unstocked* shop of a cached type inherits that crate instead of
falling back to parody. (On a clean cache fail-soft is intact: missing atlas → `null` → parody, 0 errors.
The collision is what defeats it, so this is a correctness bug, not just a mix-up.)
**The seam (C owns it, C lands it):**
- **Cache key = `type + base`** (equivalently a `packKey`; a godverse shop's key is its `stock_godverse/<godverse_id>/` base). `getStockPack` takes the same key.
- **Resolution order** stays: explicit per-shop base → the type-pack base → `null` → parody.
- **Per-shop preload on entry** (interior build/enter), **not** a boot-time preload of every shop.
- **LRU = 1 shop's atlas set, disposed on eviction** (§7.2) — the load-bearing half of the §7 promise.
- **Identity link:** a stocked shop must carry its pack pointer on the shop record (G's `godverse_id`
`stock_godverse/<id>/`). Not in the town cache today — G flagged it ("monsterrobot must be added to the
godverse cache's `shops[]` as id 3962749"). C consumes whatever field lands; **C does not invent it.**
**9. R23 verification — the first real crate, in a room (ledger #3). PASS.**
Measured against G's `3962749` atlas (120 real records, 2 × 2048², cell 256²) in a fresh context:
- **Resolves through the existing `stockAdapter` path** via a base override — no C code change needed for
the single-shop alpha. 120 items, 2 atlases, **8/8 stock meshes atlas-backed** (real covers, not parody).
- **Ceilings hold:** **118 draws** with real covers (≤350 law); room `pathOK`, 0 carves.
- **Fail-soft holds** (clean cache): missing atlas → `null` → parody (20 parody meshes, 110 draws, pathOK);
no `stockAdapter` (the `?noassets` shape) → parody, zero fetch. **0 console errors** throughout.
- **G's index is contract-compliant:** `items[]` sorted by id (my determinism ask honoured ✓), provenance
present ✓, `uv ⊂ [0,1]` ✓, 2 atlases = the cap ✓, `cell_w/cell_h` harmless (C reads `uv`) ✓.
- **Verdict: the contract holds. G re-emits nothing.** The only C-side work is §8's seam.
### 7.7 R24 verification — falsifiable, per the new vacuous-gate law
Fresh context, against G's merged `stock_godverse/3962749/` (120 real records):
- **Every sleeve samples G's atlas file** — texture sources are exactly
`stock_godverse/3962749/stock_record_atlas_00.webp` / `_01.webp`; pack items ≡ index items **by id**.
*(Not "covers render": a parody canvas also has a `.map`. The negative control below is what makes
this a gate — I had to fix my own R23 metric to satisfy the law.)*
- **Negative control:** the parody room's stock textures are `(canvas, no src)` and touch the godverse
atlas **zero** times — the metric discriminates.
- **Collision fixed:** shop pack (120 items) and town pack (350) coexist as distinct objects.
- **Fail-soft with a POPULATED cache:** missing base ⇒ `null` ⇒ parody (the R23 bug is gone).
- **Ceilings:** 98 draws with real covers via `opts.stockBase`; `pathOK`; 0 carves; 0 console errors.

View File

@ -1,5 +1,146 @@
# LANE E — cross-lane notes
## Round 24 — the alpha completion: redhill, the honest validator, ballarat ⟨v5.0-alpha⟩
**1. `redhill_real` — the crate's real street (ledger #3). BUILT.** One bounded Overpass fetch under the
standing authorization. **Red Hill, Brisbane** · centre `-27.4553, 153.0064` · span 2.4 km · **36 shops
(9 heroes + 27 texture, 29 subsampled) · 884 roads** · median NN 73 m, hub 5/36. **Musgrave Road is in
`roads[]` — 41 ways, kinds `[primary, residential]`.** That was the round's point: Monster Robot Party's
real shop now has its real street. Raw snapshots in `_raw/`, ODbL, C's mapping, same discipline as the
pack. **→ Lane G: this is your donor** for `godverse_town.py redhill_real redhill_godverse`.
**→ G AND A: TWO HAZARDS ON THIS DONOR. Please read before you seat the shop.**
> **(a) The shop is NOT in OSM.** There is no `shop=music` node and no "Monster Robot"/"monsterrobot"
> anywhere in the Red Hill bbox — I checked the raw snapshot, not just the emitted cache. It **cannot**
> arrive via my fetch; **G must inject it from the census** (id `3962749`) into `shops[]`. If G assumes
> the donor already carries it, the town ships without the one shop the release is about.
>
> **(b) THE SEATING HAZARD — from G's geocode, the shop seats on the WRONG STREET.** A's lift seats each
> shop on its **nearest edge**. Measured against the real `roads[]` geometry, from G's geocode
> `-27.4553, 153.0064`:
>
> | distance | street | kind |
> |---|---|---|
> | **38.6 m** | **Zig Zag Street** | residential ← **A would seat it HERE** |
> | 46.1 m | Craig Street | residential |
> | 51.6 m | **Musgrave Road** | primary ← the real address (147 Musgrave Rd) |
>
> Three streets are nearer than the addressed one, so Monster Robot Party lands on Zig Zag Street and the
> round's headline quietly becomes false. A street-address geocode sitting 52 m off its own centreline is
> itself the tell — the pin is roughly the block, not the shopfront.
>
> **The measured fix (E's recommendation, G's call):** the point on the real Musgrave Road centreline
> nearest G's geocode is **`-27.455169, 153.006902`**. Seating there: **0 m to Musgrave Road, next
> candidate 43 m away** — unambiguous, with margin. It moves the shop 51.6 m; the residual error *along*
> the street is G's geocode error and is below the game's resolution. **Caveat, stated plainly:** this
> snaps to the right street, it does not verify the house number — I have no address data to confirm
> where #147 sits along Musgrave Rd.
>
> **The robust fix (A's call, likely v5.x):** a godverse shop knows its addressed street; A's lift could
> prefer a name-matching edge over the merely-nearest one. That fixes the whole class, not this one shop.
> The snap above is enough to ship the alpha honestly.
**2. `validate_atlas` — made honest (ledger #4). F's catch was right; there were FOUR bugs, not one.**
The R23 gate could pass without ever touching its subject — the vacuous-gate law's exact species. Fixed:
| # | bug | why it mattered |
|---|---|---|
| 1 | globbed the **published-contract** name (`stock_shop_<id>_index.json`) — a name the runtime can *never* load | matched **0 files** and returned 0. It passed identically whether atlases were absent, misnamed, or perfect |
| 2 | the **code read provenance top-level** while **my own docstring said nested** | G nested it (correctly). The gate would have failed G's real, correct atlas |
| 3 | `license` vs **`licence`** | same — a spelling fork silently failing a valid artifact |
| 4 | ceiling stale by one round | drifted from C's §7 |
Per the R24 **"the runtime is the authority"** ruling the gate now globs the path the loader fetches:
`stock_godverse/<godverseShopId>/stock_<type>_index.json`, `type ∈ {record, book, toy}`. **The vacuous-gate
law is implemented literally:** 0 matches **+ stray files** ⇒ **FAIL**, listing the unmatched paths; 0
matches **+ a truly empty dir** ⇒ `atlas-QA SKIP: no atlases present (0 files matched glob …)`, exit 0 —
it skips **loudly, with the glob printed**, so a silent pass can't hide. It also asserts identity
(`shop.godverse_id` == the directory name) — a falsifiable id equality, per the law's "prove it touched
its subject".
**Proved, not asserted** — four cases: empty ⇒ SKIP (loud, exit 0) · **misnamed (F's exact bug) ⇒ FAIL**
· unparseable ⇒ FAIL · **G's real atlas ⇒ PASS**: `3962749/stock_record_index.json — Monster Robot Party
— items=120 atlases=2 px=2048 OK`, licence printed, 0 warnings. **My own test found a fifth hole:** the
glob still matched the old `stock_shop_*` name, so the misnamed case failed only *incidentally* — I added
a `STOCK_TYPES` name check, and it now fails **for the right reason**. A gate that fails for the wrong
reason is a gate that will pass for the wrong reason later.
**→ Lane C:** built to the runtime + G's real artifact per the ruling. `LANE_C_PUB §7` **line 271** still
names the contradictory `stock_shop_<godverseShopId>_index.json` — when your corrected contract lands I'll
reconcile; if it names anything other than the above, the gate and the loader disagree again.
**3. Ballarat — RETIRED (my call, counted). A different illness from Toowoomba.** Full rationale in
`SOURCES.md` and the commented config entry. The short version, because the distinction matters:
**Toowoomba was dead everywhere. Ballarat is alive exactly where the game isn't.** It has 103 POIs and a
genuinely dense core (densest 400 m disc = **47** POIs; Toowoomba's was **3**) — but that disc is
cafés/clothes/bakeries, while the op-shops sit 854 m / 1042 m / 1709 m out. Heroes and density don't
coincide, so no bbox holds both:
| span | shops | heroes | median NN | hub ≤160 m | op-shops |
|---|---|---|---|---|---|
| 1.0 km (the live core) | 8 | 2 | 184 m | 4/8 (50%) | **0** ← a thrift town with no thrift shop |
| 2.0 km | 12 | 3 | 186 m | 3/12 (25%) | 1 |
| 3.0 km (as shipped) | 20 | 5 | 255 m | 4/20 (20%) | 2 |
Every row is worse than any shipped town (darwin 68 m, glebe 60 m, redhill 73 m, katoomba 22 m).
**→ A, D: the binding constraint was MINE, not OSM's — and it's a real finding.** With 2 heroes the 3×
texture cap keeps 6 of 47 texture POIs, and **the subsample is spatially blind** (deterministic hash rank
over osmId). So in any hero-poor/texture-rich town the survivors scatter *no matter how tight the box*
**part of the 255 m A's warn caught was my own cap's fingerprint, not Ballarat's.** Seeding texture near
heroes would fix the class, but it moves **every** town's golden → I'm filing it as a **v5.x proposal**,
not doing it in a release round.
**→ A: I OVERSTATED MY R23 ADVICE — please don't encode it as a threshold.** I told you the hub fraction
is the discriminator. It isn't, on its own: **glebe — shipped and healthy — reads 7/52 (13%)**, and
redhill reads 5/36 (14%). A hub threshold would false-flag both. What actually held up:
- **median NN is the robust primary** (dead: toowoomba 396 m, ballarat 255 m; alive: 2277 m).
- **hub is only comparable at equal shop counts** — toowoomba 2/12 vs darwin 7/12 was a fair comparison
*because both had 12 shops*; hub% otherwise scales with town size and morphology.
- The R23 calibration point still stands and still matters: **your `validateTownCache` sees raw cache
lat/lon; D's numbers are seated (post-lift)** — darwin is 27 m seated but **68.3 m raw**, so a threshold
read off D's table will false-warn darwin.
**4. A methodology note I'm recording because it nearly bit me.** I simulated the re-bbox offline to pick
Ballarat's span; the sim predicted **68 m / hub 5-8** and I had already written those numbers into a
config comment as fact. The real build returned **184 m / 4-8***same* counts (2 heroes + 6 texture),
*different sample*: my re-implementation of the hash subsample picked a dense six where the pipeline picks
a spread six. **Never trust a re-implementation of the pipeline's own selection — run the pipeline.** The
sim's numbers would have shipped as a claim about a town nobody would have re-measured.
**5. Determinism receipt.** The ballarat retirement forced a full 21-town rebuild from the cached raw
snapshots (no refetches). **Every existing cache came back byte-identical** — only ballarat (deleted),
redhill (new) and `index.json` moved. Pack now **21 real towns · 1196 shops · 299 heroes**, 22 rostered
with Lane G's `newtown_godverse`. Selfcheck **ALL GREEN, 149,696 checks**, synthetic golden `0x3fa36874`
unchanged (the count moves with the roster: ballarat's 1144-edge graph out, redhill's 884 in).
**6. → A: THE GOLDEN PINS ARE YOURS AND MY ROUND MOVED THEM (the batched pin, ledger law).** Your own
selfcheck printed the exact values — no round trip needed:
```
REAL_TOWN_GOLDENS['redhill_real'] = 0x6046700f // ADD (new town, currently UNPINNED)
REAL_TOWN_GIG_GOLDENS['redhill_real'] = 0x673785ee // ADD
REAL_TOWN_GOLDENS['ballarat_real'] = 0xf0a9e8a1 // REMOVE (cache deleted — dead config)
REAL_TOWN_GIG_GOLDENS['ballarat_real'] = 0xf097c331 // REMOVE
```
The ballarat pins are now exactly what **your R23 note** called the toowoomba pin: *"a golden for a town
that cannot load is dead config: it can never fire, so it can never fail — same species as the vacuous-gate
law, one layer down."* Your words, my retirement, same fix. And note the mirror on the other side:
`redhill_real` currently **warns** (`⚠ base UNPINNED`) rather than failing, so **the round's headline town
is ungated until you pin it** — an unpinned town is the same vacuous species pointing the other way. Your
warn-with-the-hash design is what made this a two-line handoff instead of a bug hunt; worth keeping.
**7. → F: an R23 orphan I created and owe you.** `tools/qa/tour_shots.py` (your v4.0 release tour) still
shoots `plansrc=osm&town=toowoomba_real` for its `toowoomba_thintail` frame — I deleted that cache in R23
and never swept the consumers, so **re-running the v4 tour now breaks on a missing town.** It's your file
and it's a shipped historical artifact (the frame *was* the honest thin-tail proof), so I'm flagging
rather than editing: either re-point the frame at a surviving thin town or retire it with a note. Same
class, lower stakes: `web/js/citygen/gigs.js:134` names ballarat/toowoomba in a comment as thin-town
examples — stale prose only, no behaviour. **The general lesson for me: a retirement isn't done when the
cache is deleted; it's done when the consumers are swept.**
## Round 23 — v5.0-alpha: the atlas validator + the toowoomba call ⟨v5.0-alpha⟩
**1. `validate_atlas` — the G2a format gate (ledger #4).** `pipeline/validate_atlas.py`, wired into

View File

@ -0,0 +1,98 @@
# PROCITY — Round 25 lane instructions (from Fable, integrator)
Date: 2026-07-17 · **R24 verdict: THE CRATE WALKS — and the tag correctly holds a second round.**
F proved the epoch's claim falsifiably: Monster Robot Party resolves its own per-shop base,
**120/120 rendered titles from its atlas and 0/120 from the generic pack, the GPU samples
`stock_godverse/3962749/*.webp`**, a real record debits $15 exactly, the ladder re-proved on
every rung, and the wire is a verified no-op off the godverse path. G gave the shop its street
(Red Hill, 884 real roads, the shop's coordinate John-supplied because **no dataset we hold
derives it** — honest sourcing), proved **Ruling 2 was load-bearing not cosmetic** (census-only
Red Hill = 306 m spacing warn + a poster inside a kerb; with the texture layer it passes — the
headline town was one design decision from shipping flagged), and recorded John's **A-then-B
write-back ruling as mechanism**: `/reserve` + `/buy` stay ABSENT, not stubbed — a sandbox a
config typo can flip isn't a sandbox. C owned the R23 root cause squarely. E built the street
and the honest validator. A published the identity field first, as briefed.
**Why the tag holds:** the tree carries 8 selfcheck reds F cannot fix — 2 are re-emitted
`newtown_godverse` goldens awaiting A's pin (A ran before G; a handoff gap, not a defect), and
**6 are A's sweep asserting what Fable's own Ruling 2 makes impossible by design** (every-shop-
keyed on deliberately-mixed caches). Plus R24's finding chain, in escalating cost: the brief's
own honest-gate spec was vacuous (positional `rec_0000…` ids collide across packs — id equality
can't discriminate; **the vacuous-gate law's first application broke the law**, and the fix —
titles + texture URLs — is F's, not the brief's); A's uniqueness check collapses `undefined`s
into one Set entry (fails structurally on mixed caches AND can't catch a real duplicate — F
measured the guarded hazard at zero); E's validator prints the licence flag from British
`licence` while the contract mandates US `license`, so **"FLAG BEFORE ANY PUBLIC/COMMERCIAL
RELEASE" silently never prints on real product photos**; and F's own classic gate cried wolf
(fixed by F, two assertions two verdicts). Round 25 closes all of it. Tag **`v5.0-alpha`** at
close — third time, earned.
## Process rulings (from R24's events)
1. **Lane G is a full lane on main.** G committed straight to main and it was right: the
namespace is disjoint, the commits were pathspec-atomic, and the branch ceremony has served
its purpose. Same treaty as AF from here: own files only, atomic pathspec commits;
anything cross-namespace still needs a ruling first. (The R23 merge amendment is thereby
retired along with the branch.)
2. **Ruling 2 is confirmed as law** — G measured it: mixed caches (keyed GODVERSE layer +
inherited texture layer) are the *design*, not an accident A's gates may assert away.
3. **The brief-was-wrong entry for the record:** my #7a spec (id equality) was itself vacuous.
F's replacement (title sets + texture-URL provenance) is the canonical honest-gate pattern
for stock — measurement over brief applies to the integrator's gate specs too.
## The ledger
1. **A — reconcile the sweep with Ruling 2, pin the six (the round's spine).**
- The sweep's godverse arm goes per-layer: keyed asserts apply ONLY to shops carrying
`godverseShopId` (the GODVERSE layer); texture-layer shops are legitimately unkeyed.
- Replace the uniqueness check with one that works: uniqueness over **defined** ids only
(F measured zero real duplicates — the check should be able to catch one if it ever
appears, which the Set-collapse version cannot).
- Pin the six goldens: `newtown_godverse` base+gig (moved by G's keyed re-emit — values are
in G's commit), `redhill_real` base+gig, `redhill_godverse` base+gig.
- Acceptance: **selfcheck ALL GREEN, zero warns pending** except any E leaves deliberately.
2. **E — the licence line, made unmissable (conflict #1).** Align the validator with C's §7.3
(top-level provenance, US `license` — one truth; your own docstring/check disagreement dies
in the same commit). Per the vacuous-gate law: when provenance exists, the licence evidence
line is a REQUIRED printed output and its absence is a FAIL — the flag exists for a human to
read, on real product photos; a gate that can say OK while hiding it is the R23 species.
3. **C — one contract line; G — the sku re-emit (F's finding #1, made structural).** Positional
slot ids collide by construction. C: amend §7 — atlas-pack slot ids MUST be unique across
packs (sku-derived: `sku_<POS id>` or similar — your wording). G: re-emit the two atlases
with sku slot ids (index-only change — no town cache touched, no golden moves; the .webp
bytes shouldn't move either, same as your R24 re-bake). E's validator asserts the form.
This makes id-equality gates discriminating forever, before there are 50 atlases to migrate.
4. **F — land your R24 work, then the tag.** Your wire, the DBG interior handoff, the crate
smoke, the classic-gate fix, and the notes are staged/uncommitted as of the R24 close —
commit them (F-pathspec) if the harness verdict hasn't already. Then, after A/E/G land:
full `qa.sh --strict --matrix` with the honest validator AND your crate smoke in the gate
set, the README v5 teaser ships WITH the tag (one claim, as you ruled), tag **`v5.0-alpha`**,
push. Your hold streak is the system working — end it only on green.
5. **B / D — on-call.** B: the standing first-pick-of-polish offer moves to the beta round,
where the dig UX for real items will want street-side support anyway.
## Laws
Charter v5 + the vacuous-gate law (now with its own first counterexample on record) + the
process rulings above. Golden moves this round: A's six pins only. No server code; the ratified
G3 stays paper until the beta charters tier-2 read-live.
## THE EXACT RUN ORDER
1. **Start together: A (#1) + E (#2) + C (#3's one line).**
2. **When C's line lands: G (#3) re-emits.** (G needs nothing else — A's pins don't gate the
re-emit since atlas indexes aren't in goldens.)
3. **When A + E + G are in: F (#4)** — commit your R24 work if not already, full gate, tag,
push.
Waves: **[A, E, C] → [G] → [F]**. B/D on-call.
## Standing
- **John — nothing needs you.** Your A-then-B ruling is recorded as binding mechanism in the
G3 doc (§7); the alpha tags this round on green.
- **v5.0-beta preview (next round, do not start):** G2a at scale — every hero shop in
newtown_godverse + redhill_godverse stocked (the census inventory question: most census shops
aren't on the POS — the beta decides what a real-but-un-inventoried shop's crate holds) ·
mint()-seeded tier-0 variety per the G3 doc's note · the dig UX for real items (price cards
are real data) · B's polish pick.

View File

@ -35,7 +35,7 @@
"captured": "2026-07-15",
"extraction": "see load_image_map() docstring \u2014 the products projection command"
},
"licence": {
"license": {
"covers": "the shop's own product shots (monsterrobot.party), cached by dealgod",
"zone": "in-house-green",
"flag": "FLAG BEFORE ANY PUBLIC/COMMERCIAL RELEASE",

View File

@ -50,10 +50,30 @@ TOWNS = {
"bowral_real": {"town": "Bowral", "state": "NSW", "center": (-34.4780, 150.4180), "span_km": 2.2},
"fitzroy_real": {"town": "Fitzroy", "state": "VIC", "center": (-37.7990, 144.9790), "span_km": 2.2},
"brunswick_real": {"town": "Brunswick", "state": "VIC", "center": (-37.7700, 144.9600), "span_km": 2.6},
"ballarat_real": {"town": "Ballarat", "state": "VIC", "center": (-37.5620, 143.8500), "span_km": 3.0},
# R24 — RETIRED (counted), and for a DIFFERENT illness than Toowoomba. A's spacing warn caught
# it at median NN 255 m. Ballarat is NOT dead: its raw bbox holds 103 POIs and its densest 400 m
# disc is 47 of them (Toowoomba's densest was 3). But its retail core is alive exactly where the
# GAME isn't — that disc is cafes/clothes/bakeries, while the op-shops sit 854 m, 1042 m and
# 1709 m out. The heroes and the density don't coincide, so no bbox holds both. Measured, every
# span (real builds, not simulated):
# span shops heroes medNN hub160 op-shops
# 1.0 km 8 2 184 m 4/8 (50%) 0 <- the live core: a thrift town with NO thrift shop
# 2.0 km 12 3 186 m 3/12 (25%) 1
# 3.0 km 20 5 255 m 4/20 (20%) 2 <- as shipped
# Every row is worse than any shipped town (darwin 68 m/58%, glebe 60 m, redhill 73 m). The
# binding constraint is mine, not OSM's: with 2 heroes the 3x cap keeps 6 of 47 texture POIs and
# the subsample is spatially blind, so the survivors scatter no matter how tight the box. Fixing
# THAT (seeding texture near heroes) is a pipeline change that would move every town's golden —
# a v5.x proposal, not a curation move. Cache + raw snapshots removed. See LANE_E_NOTES R24.
# "ballarat_real": {"town": "Ballarat", "state": "VIC", "center": (-37.5620, 143.8500), "span_km": 3.0},
"daylesford_real": {"town": "Daylesford", "state": "VIC", "center": (-37.3430, 144.1430), "span_km": 2.0},
"geelong_real": {"town": "Geelong", "state": "VIC", "center": (-38.1490, 144.3600), "span_km": 3.0},
"westend_real": {"town": "West End", "state": "QLD", "center": (-27.4830, 153.0100), "span_km": 2.2},
# R24 ledger #3 — the v5 crate's real street: **Monster Robot Party, 147 Musgrave Rd, Red Hill,
# Brisbane** (John's ruling: the crate ships as the shop it really is, not Newtown). Centre/span
# per G's ask (they geocoded the address); westend_real is the only other Brisbane cache and stops
# ~2.3 km south. This is the DONOR G runs godverse_town.py against → redhill_godverse.
"redhill_real": {"town": "Red Hill", "state": "QLD", "center": (-27.4553, 153.0064), "span_km": 2.4},
# ── RETIRED R23 (E's curation call, counted) — toowoomba_real ────────────────────────────────
# D's R22 finding: DEAD — median NN 388 m, 3/12 shops near the hub, **0 patronage finds in 1417
# checks**. I tried the re-bbox first (3.0 km CBD-wide → 1.6 km on the Ruthven St heart, centre

View File

@ -151,7 +151,9 @@ def snapshot(args):
"captured": args.captured,
"extraction": "see load_image_map() docstring — the products projection command",
},
"licence": {
# US `license` throughout (R24: British `licence` retired per C's contract §7.3; E's
# validator reads `provenance.licence or provenance.license`, so US passes the gate).
"license": {
"covers": "the shop's own product shots (monsterrobot.party), cached by dealgod",
"zone": "in-house-green", "flag": "FLAG BEFORE ANY PUBLIC/COMMERCIAL RELEASE",
"metadata": "real titles/artists — John's R23 amendment to the no-real-trademarks law",
@ -204,16 +206,31 @@ def pack(args):
with open(args.snapshot, "rb") as fh:
snap_sha = hashlib.sha256(fh.read()).hexdigest()
lic = snap["license"]
generator, snapshot_name = "pipeline/godverse_stock.py", os.path.basename(args.snapshot)
attribution = (f"Stock, prices + covers: {shop['name']} ({OWN_PHOTO_HOST}) — the shop's own "
f"product shots · cover cache: GODVERSE/dealgod · POS: recordgod")
index = {
"version": 1, "kind": "record", "tier": 1,
"atlas_px": ATLAS_PX, "cell_w": CELL_W, "cell_h": CELL_H,
"count": len(idx_items), "atlases": atlases,
# TOP LEVEL, per C's contract §7.3 + the house convention of the shipped town caches
# (`license` is a short US-spelled string there, `attribution` its companion).
"license": f"{lic['zone']}{lic['covers']}; {lic['flag']}",
"attribution": attribution,
"generator": generator,
"snapshot": snapshot_name,
"shop": {k: shop[k] for k in ("godverse_id", "slug", "name", "suburb", "state")},
"crate": snap["crate"],
# NESTED, per E's validate_atlas.py — which is the authority C's §7.3 defers to for these
# fields, and which reads provenance.{generator,snapshot,licence|license{zone,flag,covers}}.
# C's doc asserts top-level is "exactly what E's validator reads"; measured, it is not (see
# G-progress R24). Emitting both keeps C's contract and E's gate green off ONE source of
# truth (`lic`/`generator`/`snapshot` above) — flagged for F/Fable to collapse to one.
"provenance": {
"generator": "pipeline/godverse_stock.py",
"source": snap["source"], "licence": snap["licence"],
"snapshot": os.path.basename(args.snapshot), "snapshot_sha256": snap_sha,
"generator": generator,
"source": snap["source"], "license": lic,
"snapshot": snapshot_name, "snapshot_sha256": snap_sha,
"determinism": "pure bake — same snapshot + same cover bytes => byte-identical atlas; "
"covers pinned by sha256, no RNG, no clock",
},

View File

@ -6,12 +6,22 @@ shops[] come from thriftgod's Postgres (the real-business census DealGod scraped
walk the town's actual streets past its actual secondhand shops.
python3 pipeline/godverse_town.py newtown_real newtown_godverse # roads-donor key, out key
python3 pipeline/godverse_town.py redhill_real redhill_godverse # THE shop's real street
Runs on ultra (needs the local `thriftgod` PG + psycopg2 the thriftgod server's own deps).
Reads thriftgod via the `psql` CLI (R24): psycopg2 isn't installed on the m3, and one less dep
between the census and the cache. Works unchanged wherever thriftgod lives.
`godverseShopId` (R24 ledger #2, with Lane A) — the OPAQUE key that resolves
`stock_godverse/<godverseShopId>/`. **It is not `shop.id` and not "the census id"**: census shops
carry their thriftgod id (max 2992), while Monster Robot Party isn't in the census at all and
carries its dealgod store id (3962749). Two disjoint id spaces, one opaque field G emits it,
everyone else reads it, nobody derives it. A's `validateTownCache` wants a positive integer,
unique per cache; **missing is a WARN, not an error** (measured A's rule is deliberate: absent
stock identity must never fail a town's boot, per the charter's determinism boundary). So the
GODVERSE layer is keyed and the inherited texture layer is not see Ruling 2 below for why that
is the honest shape rather than a gap.
"""
import json, os, sys
import psycopg2, psycopg2.extras
import json, math, os, subprocess, sys
DONOR, OUT = sys.argv[1], sys.argv[2]
HERE = os.path.dirname(os.path.abspath(__file__))
@ -22,17 +32,98 @@ TYPE_MAP = {'music': 'record', 'charity': 'opshop', 'second_hand': 'opshop', 'an
'books': 'book', 'toys': 'toy', 'video_games': 'video', 'games': 'toy',
'pawnbroker': 'pawn', 'market': 'dept'}
# REAL shops that exist in neither OSM nor thriftgod's census, added by explicit, sourced ruling.
# This is not a mapping table (the R12 gigKey law) — it's a hand-verified roster of the shops the
# epoch is actually about. Every entry states where its identity and its coordinates came from.
#
# Monster Robot Party: the v5 alpha's shop — its POS *is* `recordgod`, its own site hosts the
# covers, and dealgod scrapes it as store 3962749 (which is therefore its godverseShopId). OSM
# doesn't know it (no name match, no 147 Musgrave Rd address node) and the census doesn't either,
# so the coordinate cannot be derived from any dataset we hold: John supplied the shop's Google
# Maps place (place `/g/11g6rlbbc0`), whose point is -27.4553791, 153.0076244 — inside
# `redhill_real`'s bbox and on Musgrave Road's own geometry. Verified, not estimated.
EXTRA_SHOPS = {
'redhill_godverse': [
{'id': 3962749, 'godverseShopId': 3962749, 'name': 'Monster Robot Party', 'type': 'record',
'lat': -27.4553791, 'lon': 153.0076244, 'suburb': 'Red Hill',
'source': 'GODVERSE/dealgod store 3962749 · POS recordgod · coords: Google Maps place '
'/g/11g6rlbbc0 (147 Musgrave Road, Red Hill QLD), John-supplied R24'},
],
}
def psql(db, sql):
out = subprocess.run(['psql', '-d', db, '-tAF\t', '-c', sql],
capture_output=True, text=True, check=True).stdout
return [ln.split('\t') for ln in out.splitlines() if ln.strip()]
donor = json.load(open(os.path.join(TOWNS, f'{DONOR}.json')))
bb = donor['bbox']
db = psycopg2.connect(os.environ.get('THRIFTGOD_DSN', 'dbname=thriftgod'))
c = db.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
c.execute("""SELECT id, name, shop_type, lat, lng, suburb FROM shop
WHERE lat BETWEEN %s AND %s AND lng BETWEEN %s AND %s ORDER BY id""",
(bb['minLat'], bb['maxLat'], bb['minLon'], bb['maxLon']))
shops = [{'id': r['id'], 'name': r['name'],
'type': TYPE_MAP.get(r['shop_type'], 'opshop'),
'lat': float(r['lat']), 'lon': float(r['lng']),
'suburb': (r['suburb'] or '').split('|')[0]} for r in c.fetchall()]
rows = psql(os.environ.get('THRIFTGOD_DB', 'thriftgod'), f"""
SELECT id, COALESCE(name,''), COALESCE(shop_type,''), lat, lng, COALESCE(suburb,'')
FROM shop
WHERE lat BETWEEN {bb['minLat']} AND {bb['maxLat']}
AND lng BETWEEN {bb['minLon']} AND {bb['maxLon']}
ORDER BY id""")
shops = [{'id': int(r[0]),
'godverseShopId': int(r[0]), # census shops: the thriftgod id IS the key (not derived
# from `id` by law — they coincide, see the docstring)
'name': r[1],
'type': TYPE_MAP.get(r[2], 'opshop'),
'lat': float(r[3]), 'lon': float(r[4]),
'suburb': r[5].split('|')[0]} for r in rows]
census_n = len(shops)
extras = EXTRA_SHOPS.get(OUT, [])
known = {s['id'] for s in shops}
for e in sorted(extras, key=lambda s: s['id']):
if e['id'] in known: # census wins a collision, and we count the swap
continue
shops.append({k: e[k] for k in ('id', 'godverseShopId', 'name', 'type', 'lat', 'lon', 'suburb')})
extra_n = len(shops) - census_n
# ── Fable's Ruling 2 (R22, implemented R24): INHERIT THE DONOR'S TEXTURE LAYER ────────────────
# "Merge census heroes WITH the donor's texture shops; census heroes always win a collision
# (dedupe by proximity + name), drop the donor hero, keep yours, count the swap. Result: real
# census shops on a street that also feels alive."
#
# Not cosmetic — MEASURED (R24): the census layer alone is 9 shops across Red Hill's 2.4 km bbox →
# A's spacing warn at 261 m ("scattered, not a high street"), *worse than the 255 m that retired
# ballarat this round*, plus a frontage poster seated inside edge 1037's kerb. With the donor's
# texture layer merged (37 shops) both go away and E's donor's own density is preserved.
#
# THE MERGED SHOPS CARRY NO `godverseShopId`, DELIBERATELY. The field is the GODVERSE stock
# identity; an OSM cafe has none, and minting one from its OSM id would push a third id space into
# a field that already carries two (thriftgod census ≤2992 · dealgod store ids). OSM ids are the
# same order of magnitude as dealgod store ids, so a collision is *possible* — and A's validator
# calls a duplicate godverseShopId mis-stocking (charter risk #3: two shops keyed to one atlas,
# one of them Monster Robot's crate). Unkeyed is both true and safe: they fall soft to tier 0,
# which is exactly what a milkbar with no stock should do. A's coverage warn states this honestly.
def norm(n):
return ' '.join((n or '').lower().split())
def near(a, b, m=150.0): # metres, flat-earth at suburb scale — plenty at 150 m
dy = (a['lat'] - b['lat']) * 111_320.0
dx = (a['lon'] - b['lon']) * 111_320.0 * math.cos(math.radians(a['lat']))
return math.hypot(dx, dy) <= m
swaps = 0
for d in sorted(donor.get('shops', []), key=lambda s: s['id']):
hit = next((s for s in shops if norm(s['name']) == norm(d['name']) and near(s, d)), None)
if hit: # the donor's own entry for a shop we already hold
swaps += 1
continue
if d['id'] in {s['id'] for s in shops}:
continue
shops.append({'id': d['id'], 'name': d['name'], 'type': d['type'],
'lat': d['lat'], 'lon': d['lon'], 'suburb': d.get('suburb', '')})
texture_n = len(shops) - census_n - extra_n
shops.sort(key=lambda s: s['id']) # deterministic order, extras + texture included
out = dict(donor)
out.update({
@ -42,10 +133,20 @@ out.update({
'generator': 'pipeline/godverse_town.py',
'attribution': donor['attribution'] + ' · shop census: GODVERSE/thriftgod',
'shops': shops,
'counts': {**donor.get('counts', {}), 'shops': len(shops)},
# The donor's shop-side counts (raw/heroes/texture/textureDropped) describe the shop layer we
# REPLACE, so carrying them would state a census of shops this cache doesn't contain. Keep the
# donor's road count; recount our own shops.
'counts': {**{k: v for k, v in donor.get('counts', {}).items() if k == 'roads'},
'shops': len(shops), 'census': census_n, 'extra': extra_n,
'texture': texture_n, 'donorSwaps': swaps},
})
dst = os.path.join(TOWNS, f'{OUT}.json')
json.dump(out, open(dst, 'w'), indent=1)
with open(dst, 'w') as fh:
json.dump(out, fh, indent=1)
types = {}
for s in shops: types[s['type']] = types.get(s['type'], 0) + 1
print(f"{OUT}: {len(shops)} REAL shops ({types}) + {len(out.get('roads', []))} OSM roads -> {dst}")
for s in shops:
types[s['type']] = types.get(s['type'], 0) + 1
keyed = sum(1 for s in shops if 'godverseShopId' in s)
print(f"{OUT}: {len(shops)} shops = {census_n} census + {extra_n} extra + {texture_n} donor-texture "
f"({swaps} donor heroes swapped for census) · {keyed} keyed w/ godverseShopId")
print(f" {types} + {len(out.get('roads', []))} OSM roads -> {dst}")

View File

@ -1,37 +1,49 @@
#!/usr/bin/env python3
"""Validate per-shop real-stock atlases — the G2a format gate (v5.0-alpha, ROUND23 ledger #4).
"""Validate per-shop real-stock atlases — the G2a format gate (v5.0-alpha, R23 #4 · REBUILT R24 #4).
Lane G bakes one atlas + index per real shop from dealgod inventory (tier 1 of the charter's stock
ladder). This gate is the FORMAT side: it proves G's output is well-formed, inside C's ceilings, and
carries its provenance **on m3, from the files alone, with no dealgod DB and no network**. That's the
point: the gate must run anywhere, on the committed artefacts.
Judges Lane G's tier-1 atlases **from the committed files alone** — no dealgod DB, no network — so the
gate runs on m3. Wired into `validate_manifest` (qa gate 3).
python3 pipeline/validate_atlas.py # exit 0 = every atlas valid (or none yet), 1 = a bad one
R24 REBUILD F held the v5.0-alpha tag partly on this file, and was right. What was wrong (all four
real, all mine):
1. It globbed `stock_shop_<id>_index.json` the PUBLISHED contract's name, which **the runtime can
never load**. G shipped what the loader reads. So the gate matched 0 files, returned 0, and passed
whether the atlas was absent, misnamed or perfect. *A gate that returns 0 on an empty set is not a
gate.* now globs the RUNTIME path, and an empty set SKIPs loudly while a present-but-unmatched
file FAILS (the vacuous-gate law).
2. Provenance: my code read it TOP-LEVEL while my own docstring said NESTED G nested it. My doc and
my code contradicted each other and G's atlas failed either way. → nested, per the artifact.
3. `licence` vs `license` G uses British; I demanded American. British (the emitter's, and it's
the artifact of record).
4. A ceiling stale by one round (I warned on 2 atlases; C's §7.2 allows ≤2 × ≤2048², and G's 120-item
crate lands exactly on the cap). C's current numbers.
Contract (published R22 LANE_E_NOTES E's half · LANE_C_PUB §7 C's ceilings · charter law #3 licence):
index `web/assets/stock_godverse/**/stock_shop_<godverseShopId>_index.json`
{version, atlas_px, cell, atlases[], items:[{id,title,artist,price,price_band,atlas,uv}]}
+ provenance: {license, attribution, generator, snapshot}
C's ceilings — 1 atlas/shop (hard cap 2); ≤1024² typical, 2048² only for a 60+ item shop.
charter law #3 — every atlas carries provenance; no private-individual data, ever.
determinism `snapshot` pins the inventory query result (G snapshots it like E snapshots Overpass raw).
THE AUTHORITY IS THE RUNTIME (R24 ruling). `stockpack.js` states it: a per-shop pack lives under
`assets/stock_godverse/<godverseShopId>/` and carries the SAME `stock_<type>_index.json` name (type picks
the stock slot); cache identity is `(type, base)`. This gate reads that, not a doc that disagrees with it.
Fail-soft is NOT this gate's business: a missing atlas is legal at runtime (parody canvas, R7 law). This
gate only judges atlases that EXIST a malformed one must fail loudly rather than render garbled sleeves.
python3 pipeline/validate_atlas.py # 0 = valid (or an honest SKIP), 1 = a bad/misnamed atlas
"""
import json, os, sys, glob, struct
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
STOCK_G = os.path.join(ROOT, "web", "assets", "stock_godverse") # Lane G's namespace (charter law #5)
STOCK_G = os.path.join(ROOT, "web", "assets", "stock_godverse") # Lane G's namespace (charter law #5)
GLOB = os.path.join(STOCK_G, "*", "stock_*_index.json") # the RUNTIME path — what the loader fetches
BANDS = {"bargain", "standard", "collector", "grail"}
# The loader fetches `<base>stock_<type>_index.json` where `type` is the shop's registry type — it picks
# the stock slot (stockpack SLOT_FOR: record→sleeve, book→spine, toy→box). Any other `<type>` is a name
# the runtime can never fetch. The glob alone can't say that (`stock_*_index.json` also matches the old
# contract's `stock_shop_<id>_index.json`), so the name is checked, not just matched — presence is not
# the-right-thing (the vacuous-gate law's actual lesson).
STOCK_TYPES = {"record", "book", "toy"}
REQUIRED_ITEM = ("id", "title", "artist", "price", "price_band", "atlas", "uv")
REQUIRED_PROV = ("license", "attribution", "generator", "snapshot") # charter law #3 + determinism
MAX_ATLASES = 2 # C: 1 atlas/shop, hard cap 2 (each extra = +1 draw)
PX_TYPICAL, PX_MAX = 1024, 2048
BIG_SHOP_ITEMS = 60 # C: 2048² only for a 60+ item shop (else spill to the type-pack)
# charter law #3: "No private-individual data, ever" — a real shop's stock row must never carry a person.
MAX_ATLASES = 2 # C §7.2: ≤2 atlases per shop
PX_MAX = 2048 # C §7.2: ≤2048² each
PX_TYPICAL = 1024 # C §7.2: ≤1024² whenever the crate fits it (≤64 items at 256²)
FITS_1024 = 64
# charter law #3: no private-individual data, ever — a stock row must never carry a person.
FORBIDDEN_ITEM_KEYS = {"seller", "seller_name", "owner", "owner_name", "customer", "user", "username",
"email", "phone", "mobile", "address", "member", "account", "consignor"}
"email", "phone", "mobile", "address", "member", "account", "consignor", "staff"}
def image_size(path):
@ -39,17 +51,16 @@ def image_size(path):
with open(path, "rb") as f:
head = f.read(32)
if head[:8] == b"\x89PNG\r\n\x1a\n":
w, h = struct.unpack(">II", head[16:24])
return w, h
return struct.unpack(">II", head[16:24])
if head[:4] == b"RIFF" and head[8:12] == b"WEBP":
fmt = head[12:16]
if fmt == b"VP8X": # extended: 24-bit minus-one
if fmt == b"VP8X":
b = head[24:30]
return (b[0] | b[1] << 8 | b[2] << 16) + 1, (b[3] | b[4] << 8 | b[5] << 16) + 1
if fmt == b"VP8 ": # lossy: 14-bit each
if fmt == b"VP8 ":
w, h = struct.unpack("<HH", head[26:30])
return w & 0x3FFF, h & 0x3FFF
if fmt == b"VP8L": # lossless: packed 14-bit
if fmt == b"VP8L":
b = struct.unpack("<I", head[21:25])[0]
return (b & 0x3FFF) + 1, ((b >> 14) & 0x3FFF) + 1
raise ValueError("unrecognised image header (want PNG or WebP)")
@ -57,57 +68,77 @@ def image_size(path):
def validate_index(path):
errs, warns = [], []
name = os.path.basename(path)
name = os.path.relpath(path, STOCK_G)
try:
idx = json.load(open(path))
except Exception as e:
return [f"{name}: does not parse: {e}"], []
return [f"{name}: does not parse: {e}"], [] # present-but-unparseable ⇒ FAIL
d = os.path.dirname(path)
# ── provenance (charter law #3) — every atlas carries it, and the gate PRINTS the licence ──
miss = [f for f in REQUIRED_PROV if not str(idx.get(f, "")).strip()]
if miss:
errs.append(f"{name}: missing/empty provenance {miss} (charter law #3 — every atlas carries provenance)")
# ── the NAME must be one the runtime can fetch: stock_<type>_index.json, type ∈ STOCK_TYPES ──
fname = os.path.basename(path)
ftype = fname[len("stock_"):-len("_index.json")]
if ftype not in STOCK_TYPES:
errs.append(f"{name}: unloadable name — the runtime fetches `stock_<type>_index.json` for "
f"type ∈ {sorted(STOCK_TYPES)}; {ftype!r} is never fetched (this is the R23 drift)")
elif idx.get("kind") not in (None, ftype):
errs.append(f"{name}: kind {idx.get('kind')!r} != the filename's type {ftype!r} — the filename "
f"picks the stock slot, so they must agree")
# ── C's draw ceiling (LANE_C_PUB §7.1) ──
atlases = idx.get("atlases", [])
# ── identity: the per-shop base IS the godverse shop id (the loader keys on it) ──
shop_dir = os.path.basename(d)
sid = (idx.get("shop") or {}).get("godverse_id")
if sid is None:
errs.append(f"{name}: shop.godverse_id missing — the loader's base is the shop id; the atlas must say who it is")
elif str(sid) != shop_dir:
errs.append(f"{name}: shop.godverse_id {sid!r} != its directory {shop_dir!r} "
f"(base `stock_godverse/{shop_dir}/` is the cache identity — they must agree)")
# ── provenance: NESTED, per the artifact + charter law #3 ("every atlas carries provenance") ──
prov = idx.get("provenance")
if not isinstance(prov, dict):
errs.append(f"{name}: `provenance` object missing (charter law #3 — every atlas carries provenance)")
prov = {}
for f in ("generator", "snapshot"): # snapshot pins determinism (re-bakeable)
if not str(prov.get(f, "")).strip():
errs.append(f"{name}: provenance.{f} missing/empty")
lic = prov.get("licence") or prov.get("license") # British is the artifact's spelling
if not isinstance(lic, dict):
errs.append(f"{name}: provenance.licence object missing (zone/flag/covers)")
else:
for f in ("zone", "flag", "covers"):
if not str(lic.get(f, "")).strip():
errs.append(f"{name}: provenance.licence.{f} missing/empty")
# ── C §7.2 ceilings ──
atlases = idx.get("atlases")
if not isinstance(atlases, list) or not atlases:
errs.append(f"{name}: no atlases[] listed")
atlases = []
if len(atlases) > MAX_ATLASES:
errs.append(f"{name}: {len(atlases)} atlases — C's hard cap is {MAX_ATLASES} "
f"(1/shop; a 60+ item shop spills to the shared type-pack instead of fragmenting)")
elif len(atlases) == 2:
warns.append(f"{name}: 2 atlases (+1 draw) — C's rule is 1/shop; 2 is the cap, not the target")
items = idx.get("items", [])
items = idx.get("items")
if not isinstance(items, list) or not items:
errs.append(f"{name}: no items")
items = []
# ── C's texture/VRAM ceiling (§7.2) + index↔atlas consistency (the declared size must be REAL) ──
if len(atlases) > MAX_ATLASES:
errs.append(f"{name}: {len(atlases)} atlases — C §7.2 caps a shop at {MAX_ATLASES}")
declared = idx.get("atlas_px")
for a in atlases:
ap = os.path.join(d, a)
if not os.path.isfile(ap):
errs.append(f"{name}: atlas {a!r} listed but missing on disk")
continue
errs.append(f"{name}: atlas {a!r} listed but missing on disk"); continue
try:
w, h = image_size(ap)
except Exception as e:
errs.append(f"{name}: atlas {a!r} unreadable: {e}")
continue
errs.append(f"{name}: atlas {a!r} unreadable: {e}"); continue
if declared and (w != declared or h != declared):
errs.append(f"{name}: atlas {a!r} is {w}×{h} but index declares atlas_px={declared}")
big = len(items) >= BIG_SHOP_ITEMS
cap = PX_MAX if big else PX_TYPICAL
if max(w, h) > cap:
errs.append(f"{name}: atlas {a!r} {w}×{h} exceeds C's ceiling {cap}² "
f"({'60+ item shop' if big else f'{len(items)}-item shop → ≤{PX_TYPICAL}²'})")
if max(w, h) > PX_TYPICAL and not big:
warns.append(f"{name}: atlas {a!r} is {w}×{h} for {len(items)} items — prefer ≤{PX_TYPICAL}²")
if max(w, h) > PX_MAX:
errs.append(f"{name}: atlas {a!r} {w}×{h} exceeds C's ceiling {PX_MAX}²")
elif max(w, h) > PX_TYPICAL and len(items) <= FITS_1024:
warns.append(f"{name}: atlas {a!r} is {w}×{h} for {len(items)} items — C: ≤{PX_TYPICAL}² "
f"whenever the crate fits it (≤{FITS_1024} at 256²)")
# ── the index itself (schema confirmed verbatim in LANE_C_PUB §7.3) ──
# ── the index itself (schema per the loader: it reads items[].uv/atlas; `cell`/`cell_w` are informational) ──
ids = set()
for i, it in enumerate(items):
if not isinstance(it, dict):
@ -133,30 +164,40 @@ def validate_index(path):
if not (isinstance(uv, list) and len(uv) == 4 and all(isinstance(v, (int, float)) for v in uv)):
errs.append(f"{name}[{it['id']}]: uv must be [u0,v0,u1,v1] numbers"); continue
u0, v0, u1, v1 = uv
if not (0 <= u0 < u1 <= 1 and 0 <= v0 < v1 <= 1): # UV origin top-left; C flips V (R7 law)
if not (0 <= u0 < u1 <= 1 and 0 <= v0 < v1 <= 1): # origin top-left; stockpack flips V (R7 law)
errs.append(f"{name}[{it['id']}]: uv out of range / inverted: {uv}")
return errs, warns
def main():
idxs = sorted(glob.glob(os.path.join(STOCK_G, "**", "stock_shop_*_index.json"), recursive=True))
idxs = sorted(glob.glob(GLOB))
if not idxs:
print("atlas-QA: no per-shop atlases yet (Lane G's G2a) — nothing to validate")
# THE VACUOUS-GATE LAW (R23, F): absent subject ⇒ SKIP with a printed reason — and prove the set
# really is empty. Files present but unmatched = a misnamed atlas the loader can't fetch ⇒ FAIL.
stray = [p for p in glob.glob(os.path.join(STOCK_G, "**", "*"), recursive=True) if os.path.isfile(p)]
if stray:
print(f"atlas-QA FAIL — {len(stray)} file(s) under stock_godverse/ but 0 matched {GLOB}")
print(f" the runtime fetches `<base>stock_<type>_index.json`; these cannot be loaded:")
for p in stray[:8]:
print(f" unmatched: {os.path.relpath(p, ROOT)}")
return 1
print(f"atlas-QA SKIP: no atlases present (0 files matched glob {GLOB})")
return 0
all_errs, all_warns = [], []
for p in idxs:
e, w = validate_index(p)
all_errs += e; all_warns += w
idx = {}
try:
idx = json.load(open(p))
except Exception:
pass
print(f" {os.path.basename(p):40} items={len(idx.get('items', [])):>3} "
idx = {}
shop = (idx.get("shop") or {}).get("name", "?")
print(f" {os.path.relpath(p, STOCK_G):44} {shop[:22]:22} items={len(idx.get('items', [])):>3} "
f"atlases={len(idx.get('atlases', []))} px={idx.get('atlas_px')} "
f"snapshot={str(idx.get('snapshot', ''))[:24]} {'OK' if not e else 'ERR(' + str(len(e)) + ')'}")
if idx.get("license"): # licence law: the gate SHOWS it, a human clears it
print(f" licence: {idx['license']}")
f"{'OK' if not e else 'ERR(' + str(len(e)) + ')'}")
lic = (idx.get("provenance") or {}).get("licence") or {}
if lic: # the licence is PRINTED — clearing it is a human call
print(f" licence: {lic.get('zone')} · {lic.get('flag')}")
for w in all_warns:
print(f" WARN {w}")
for e in all_errs:

View File

@ -10,6 +10,10 @@
"stock_record_atlas_00.webp",
"stock_record_atlas_01.webp"
],
"license": "in-house-green \u2014 the shop's own product shots (monsterrobot.party), cached by dealgod; FLAG BEFORE ANY PUBLIC/COMMERCIAL RELEASE",
"attribution": "Stock, prices + covers: Monster Robot Party (monsterrobot.party) \u2014 the shop's own product shots \u00b7 cover cache: GODVERSE/dealgod \u00b7 POS: recordgod",
"generator": "pipeline/godverse_stock.py",
"snapshot": "monsterrobot_550.snapshot.json",
"shop": {
"godverse_id": 3962749,
"slug": "monsterrobot",
@ -32,7 +36,7 @@
"captured": "2026-07-15",
"extraction": "see load_image_map() docstring \u2014 the products projection command"
},
"licence": {
"license": {
"covers": "the shop's own product shots (monsterrobot.party), cached by dealgod",
"zone": "in-house-green",
"flag": "FLAG BEFORE ANY PUBLIC/COMMERCIAL RELEASE",
@ -40,7 +44,7 @@
"pii": "none \u2014 inventory/crate/disc_cache only; customer/staff/sales never read"
},
"snapshot": "monsterrobot_550.snapshot.json",
"snapshot_sha256": "d6b0e2f7a7474a69354370ade98b1887d8c778a4f07a2ba62a6c366dadd22c2f",
"snapshot_sha256": "8b0ff903ce8f746aa80ef25f3eb498ec9a3bc890ec0607c2a5921674e4ef612a",
"determinism": "pure bake \u2014 same snapshot + same cover bytes => byte-identical atlas; covers pinned by sha256, no RNG, no clock"
},
"items": [

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

@ -20,26 +20,47 @@ the "© OpenStreetMap contributors (ODbL)" credit visible.
**`index.json` is the machine-readable roster** (key/town/state/shops/roads) — read it rather than
hardcoding a list. Every cache carries the same terms; per-town numbers live in each cache's `counts`.
**The pack: 21 real towns · 1180 shops · 295 heroes** (+ Lane G's `newtown_godverse` = 22 rostered in
**The pack: 21 real towns · 1196 shops · 299 heroes** (+ Lane G's `newtown_godverse` = 22 rostered in
`index.json`), every one fetched from OSM Overpass with a bounded per-town bbox, ODbL 1.0, raw snapshots
in `_raw/` (shops + roads per town), fetched 2026-07-16.
in `_raw/` (shops + roads per town), fetched 2026-07-16 (`redhill_real` 2026-07-17).
| state | towns |
|---|---|
| NSW (6) | katoomba · newtown · glebe · marrickville · newcastle (Hamilton strip) · bowral |
| VIC (7) | fitzroy · brunswick · geelong · bendigo · ballarat · castlemaine · daylesford |
| VIC (6) | fitzroy · brunswick · geelong · bendigo · castlemaine · daylesford |
| QLD (2) | westend · **redhill** (Monster Robot Party's real street — see below) |
| WA (2) | fremantle · northbridge |
| QLD (1) | westend |
| TAS (2) | hobart · launceston |
| SA (1) | adelaide |
| ACT (1) | braddon |
| NT (1) | darwin |
### `redhill_real` — the v5 crate's real street (R24, ledger #3)
**Red Hill, Brisbane QLD** · centre `-27.4553, 153.0064` · span 2.4 km · 36 shops (9 heroes + 27 texture,
29 subsampled) · 884 roads · median NN 73 m, hub 5/36. Built because v5's 120 real records come from a
real shop — **Monster Robot Party, 147 Musgrave Rd** — and the shop had no street. **Musgrave Road is in
`roads[]` (41 ways, primary + residential)**, which was the point of the round. This cache is the DONOR
Lane G runs `godverse_town.py` against → `redhill_godverse`.
The shop itself is **not in OSM** (no `shop=music` node in the bbox), so G injects it from the census —
it cannot arrive via this fetch. See LANE_E_NOTES R24 for the seating hazard handed to G/A.
**Retired (R23, counted):** `toowoomba_real` — D's clustering audit measured it DEAD (0 patronage finds
in 1417 checks); a re-bbox onto the Ruthven St heart fixed the spacing metric (median NN 396 → 89 m) but
made the hub worse (2/12 within 160 m, the pack's worst), and its densest 300 m cluster is only 3 shops.
Cache + raw snapshots removed; the config entry stays commented with the rationale. See LANE_E_NOTES R23.
**Retired (R24, counted):** `ballarat_real` — A's spacing warn caught it at median NN 255 m. Unlike
Toowoomba, Ballarat is **not dead**: 103 POIs in the bbox, densest 400 m disc = 47 of them. It fails a
different way — **its live core and its heroes don't coincide.** The dense disc is cafés/clothes/bakeries;
the op-shops sit 854 m, 1042 m and 1709 m out, so no bbox holds both. Measured at every span (real builds):
1.0 km → 8 shops / NN 184 m / hub 4/8 but **zero op-shops** (a thrift town with no thrift shop); 2.0 km →
12 / 186 m / 3-12; 3.0 km (as shipped) → 20 / 255 m / 4-20. Every row is worse than any shipped town
(darwin 68 m, glebe 60 m, redhill 73 m). The binding constraint is ours, not OSM's: with 2 heroes the 3×
texture cap keeps 6 of 47 POIs and the subsample is **spatially blind**, so survivors scatter at any span.
Cache + raw snapshots removed; config entry commented with the rationale. See LANE_E_NOTES R24.
## The density widening (v4.0-beta, ROUND20 ledger #2)
The fetch covers the **secondhand heroes** plus the everyday main-street **texture** (bakery/cafe/deli/

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"schema": "procity-towns-index/1",
"generated": "2026-07-16",
"generated": "2026-07-17",
"license": "ODbL 1.0",
"attribution": "\u00a9 OpenStreetMap contributors",
"towns": [
@ -12,14 +12,6 @@
"roads": true,
"source": "osm"
},
{
"key": "ballarat_real",
"town": "Ballarat",
"state": "VIC",
"shops": 20,
"roads": true,
"source": "osm"
},
{
"key": "bendigo_real",
"town": "Bendigo",
@ -172,6 +164,14 @@
"roads": true,
"source": "osm"
},
{
"key": "redhill_real",
"town": "Red Hill",
"state": "QLD",
"shops": 36,
"roads": true,
"source": "osm"
},
{
"key": "westend_real",
"town": "West End",

View File

@ -18,13 +18,17 @@
"maxLon": 151.1931100389657
},
"counts": {
"raw": 19,
"shops": 18,
"roads": 1086
"roads": 1086,
"shops": 72,
"census": 18,
"extra": 0,
"texture": 54,
"donorSwaps": 18
},
"shops": [
{
"id": 158,
"godverseShopId": 158,
"name": "Egg Records",
"type": "record",
"lat": -33.897238,
@ -33,6 +37,7 @@
},
{
"id": 247,
"godverseShopId": 247,
"name": "The Vintage Record",
"type": "record",
"lat": -33.8875182,
@ -41,6 +46,7 @@
},
{
"id": 248,
"godverseShopId": 248,
"name": "Aalders",
"type": "opshop",
"lat": -33.8877879,
@ -49,6 +55,7 @@
},
{
"id": 568,
"godverseShopId": 568,
"name": "Towers Antiques",
"type": "opshop",
"lat": -33.9035515,
@ -57,6 +64,7 @@
},
{
"id": 569,
"godverseShopId": 569,
"name": "Old Wares",
"type": "opshop",
"lat": -33.9032933,
@ -65,6 +73,7 @@
},
{
"id": 608,
"godverseShopId": 608,
"name": "Goulds Books",
"type": "book",
"lat": -33.9029178,
@ -73,6 +82,7 @@
},
{
"id": 717,
"godverseShopId": 717,
"name": "Co-op",
"type": "book",
"lat": -33.890884,
@ -81,6 +91,7 @@
},
{
"id": 1082,
"godverseShopId": 1082,
"name": "Downtown Music",
"type": "record",
"lat": -33.8878728,
@ -89,6 +100,7 @@
},
{
"id": 1173,
"godverseShopId": 1173,
"name": "Haze Guitars",
"type": "record",
"lat": -33.8925979,
@ -97,6 +109,7 @@
},
{
"id": 1697,
"godverseShopId": 1697,
"name": "Thats So Mi",
"type": "opshop",
"lat": -33.9011361,
@ -105,6 +118,7 @@
},
{
"id": 2019,
"godverseShopId": 2019,
"name": "St Luke\u2019s Op Shop",
"type": "opshop",
"lat": -33.898898,
@ -113,6 +127,7 @@
},
{
"id": 2048,
"godverseShopId": 2048,
"name": "Savers",
"type": "opshop",
"lat": -33.8869187,
@ -121,6 +136,7 @@
},
{
"id": 2200,
"godverseShopId": 2200,
"name": "Better Read Than Dead",
"type": "book",
"lat": -33.8961303,
@ -129,6 +145,7 @@
},
{
"id": 2201,
"godverseShopId": 2201,
"name": "Elizabeth's Bookshop",
"type": "book",
"lat": -33.8959321,
@ -137,6 +154,7 @@
},
{
"id": 2212,
"godverseShopId": 2212,
"name": "Papa Disquo",
"type": "record",
"lat": -33.8986661,
@ -145,6 +163,7 @@
},
{
"id": 2377,
"godverseShopId": 2377,
"name": "Kidstuff",
"type": "toy",
"lat": -33.8870857,
@ -153,6 +172,7 @@
},
{
"id": 2505,
"godverseShopId": 2505,
"name": "EB Games",
"type": "video",
"lat": -33.9064099,
@ -161,11 +181,444 @@
},
{
"id": 2506,
"godverseShopId": 2506,
"name": "Toys and Tales",
"type": "toy",
"lat": -33.9073712,
"lon": 151.1724377,
"suburb": "Newtown"
},
{
"id": 62203508,
"name": "Bunnings Warehouse",
"type": "dept",
"lat": -33.905746,
"lon": 151.19369,
"suburb": "Alexandria"
},
{
"id": 279681996,
"name": "Woolworths Metro",
"type": "dept",
"lat": -33.89859,
"lon": 151.181704,
"suburb": "Newtown"
},
{
"id": 292127221,
"name": "The Hospitality Store",
"type": "dept",
"lat": -33.88782,
"lon": 151.17241,
"suburb": "Camperdown"
},
{
"id": 305516372,
"name": "IGA",
"type": "dept",
"lat": -33.89597,
"lon": 151.180575,
"suburb": "Newtown"
},
{
"id": 423218371,
"name": "Foodcraft",
"type": "milkbar",
"lat": -33.90308,
"lon": 151.185224,
"suburb": "Erskineville"
},
{
"id": 605984546,
"name": "Nextra",
"type": "milkbar",
"lat": -33.906418,
"lon": 151.171836,
"suburb": "Marrickville"
},
{
"id": 605984562,
"name": "Lowes",
"type": "dept",
"lat": -33.906876,
"lon": 151.171753,
"suburb": "Marrickville"
},
{
"id": 605984563,
"name": "Kimmy Jewellery",
"type": "dept",
"lat": -33.906791,
"lon": 151.171765,
"suburb": "Marrickville"
},
{
"id": 605984565,
"name": "Forcast",
"type": "dept",
"lat": -33.906889,
"lon": 151.17189,
"suburb": "Marrickville"
},
{
"id": 605984566,
"name": "Bras N Things",
"type": "dept",
"lat": -33.907022,
"lon": 151.171843,
"suburb": "Marrickville"
},
{
"id": 605984567,
"name": "Sussan",
"type": "dept",
"lat": -33.907073,
"lon": 151.171834,
"suburb": "Marrickville"
},
{
"id": 605984571,
"name": "Sunset Flowers",
"type": "stall",
"lat": -33.907283,
"lon": 151.171871,
"suburb": "Marrickville"
},
{
"id": 605984595,
"name": "Sergio's Cake Shop",
"type": "milkbar",
"lat": -33.906821,
"lon": 151.172217,
"suburb": "Marrickville"
},
{
"id": 605984610,
"name": "Espresso Brothers",
"type": "milkbar",
"lat": -33.906382,
"lon": 151.172126,
"suburb": "Marrickville"
},
{
"id": 1039467648,
"name": "Storeroom Vintage",
"type": "dept",
"lat": -33.898406,
"lon": 151.176285,
"suburb": "Newtown"
},
{
"id": 1039467672,
"name": "Natural Fit Footwear",
"type": "dept",
"lat": -33.90725,
"lon": 151.172143,
"suburb": "Marrickville"
},
{
"id": 1039555349,
"name": "The Athlete's Foot",
"type": "dept",
"lat": -33.906646,
"lon": 151.17215,
"suburb": "Marrickville"
},
{
"id": 1079380693,
"name": "Coffee @ Fisher",
"type": "milkbar",
"lat": -33.886833,
"lon": 151.190043,
"suburb": "Darlington"
},
{
"id": 1732566722,
"name": "Welcome Mart",
"type": "dept",
"lat": -33.89993,
"lon": 151.184575,
"suburb": "Erskineville"
},
{
"id": 1735908953,
"name": "Boh\u00e4us Cafe",
"type": "milkbar",
"lat": -33.905708,
"lon": 151.191348,
"suburb": "Alexandria"
},
{
"id": 2125897845,
"name": "Pastizzi Cafe",
"type": "milkbar",
"lat": -33.903197,
"lon": 151.179532,
"suburb": "Newtown"
},
{
"id": 2136793910,
"name": "Store Espresso",
"type": "milkbar",
"lat": -33.890334,
"lon": 151.176325,
"suburb": "Camperdown"
},
{
"id": 2136793911,
"name": "Gather on the Green",
"type": "milkbar",
"lat": -33.890355,
"lon": 151.176263,
"suburb": "Camperdown"
},
{
"id": 2163387594,
"name": "Grounded 2050",
"type": "milkbar",
"lat": -33.887901,
"lon": 151.180526,
"suburb": "Camperdown"
},
{
"id": 2170430692,
"name": "Carslaw Kitchen",
"type": "milkbar",
"lat": -33.888238,
"lon": 151.190481,
"suburb": "Darlington"
},
{
"id": 2365225259,
"name": "Ken Neale Overflow",
"type": "dept",
"lat": -33.90187,
"lon": 151.178671,
"suburb": "Newtown"
},
{
"id": 2787084005,
"name": "Amazing Paints",
"type": "dept",
"lat": -33.891631,
"lon": 151.176271,
"suburb": "Camperdown"
},
{
"id": 2965437480,
"name": "Cafe Abercrombie",
"type": "milkbar",
"lat": -33.892142,
"lon": 151.193024,
"suburb": "Darlington"
},
{
"id": 2968343588,
"name": "Bakerman",
"type": "milkbar",
"lat": -33.899893,
"lon": 151.183961,
"suburb": "Erskineville"
},
{
"id": 3278493016,
"name": "Wildwood Designs",
"type": "dept",
"lat": -33.887904,
"lon": 151.169934,
"suburb": "Stanmore"
},
{
"id": 3278493018,
"name": "Inspired Design Furniture",
"type": "dept",
"lat": -33.887902,
"lon": 151.170079,
"suburb": "Stanmore"
},
{
"id": 3278509468,
"name": "Recharging",
"type": "milkbar",
"lat": -33.887515,
"lon": 151.167686,
"suburb": "Annandale"
},
{
"id": 3951219159,
"name": "Miracle",
"type": "dept",
"lat": -33.892823,
"lon": 151.186108,
"suburb": "Newtown"
},
{
"id": 4860774437,
"name": "Newtown Garden Market",
"type": "stall",
"lat": -33.903009,
"lon": 151.179679,
"suburb": "Newtown"
},
{
"id": 5203491923,
"name": "Cynthia's Caf\u00e9",
"type": "milkbar",
"lat": -33.908747,
"lon": 151.191977,
"suburb": "Alexandria"
},
{
"id": 5205362421,
"name": "Little Vietnam Roll & Juice",
"type": "milkbar",
"lat": -33.906344,
"lon": 151.181132,
"suburb": "Newtown"
},
{
"id": 5299091858,
"name": "Hui Bro\u2019s Cafe",
"type": "milkbar",
"lat": -33.898095,
"lon": 151.179152,
"suburb": "Newtown"
},
{
"id": 5300089045,
"name": "Handcraft Specialty Coffee",
"type": "milkbar",
"lat": -33.892575,
"lon": 151.185688,
"suburb": "Newtown"
},
{
"id": 5599458217,
"name": "The Floral Decorator",
"type": "stall",
"lat": -33.899566,
"lon": 151.184195,
"suburb": "Erskineville"
},
{
"id": 5839239307,
"name": "Luke Avenue",
"type": "milkbar",
"lat": -33.897984,
"lon": 151.175225,
"suburb": "Newtown"
},
{
"id": 5905712358,
"name": "The Coffee Nook on Swanson",
"type": "milkbar",
"lat": -33.900301,
"lon": 151.189403,
"suburb": "Erskineville"
},
{
"id": 5931284416,
"name": "Patisserie Petunia",
"type": "milkbar",
"lat": -33.90509,
"lon": 151.18052,
"suburb": "Newtown"
},
{
"id": 6283737899,
"name": "Dangerfield",
"type": "dept",
"lat": -33.895217,
"lon": 151.181947,
"suburb": "Newtown"
},
{
"id": 6564462487,
"name": "Marani Deli",
"type": "milkbar",
"lat": -33.895992,
"lon": 151.182698,
"suburb": "Newtown"
},
{
"id": 9572458384,
"name": "Budha Bowl",
"type": "milkbar",
"lat": -33.898699,
"lon": 151.176397,
"suburb": "Newtown"
},
{
"id": 9776428741,
"name": "K\u00fcrt\u0151sh",
"type": "milkbar",
"lat": -33.907897,
"lon": 151.171833,
"suburb": "Marrickville"
},
{
"id": 11984912425,
"name": "Provocator",
"type": "dept",
"lat": -33.901201,
"lon": 151.178441,
"suburb": "Newtown"
},
{
"id": 12200892725,
"name": "Feather & Bone",
"type": "milkbar",
"lat": -33.909398,
"lon": 151.168471,
"suburb": "Marrickville"
},
{
"id": 13341304001,
"name": "AS Colour",
"type": "dept",
"lat": -33.895051,
"lon": 151.181701,
"suburb": "Newtown"
},
{
"id": 13585970805,
"name": "Numero Uno Coffee Roasters",
"type": "milkbar",
"lat": -33.908693,
"lon": 151.18052,
"suburb": "St Peters"
},
{
"id": 13719824202,
"name": "A.P Town",
"type": "milkbar",
"lat": -33.893929,
"lon": 151.183827,
"suburb": "Newtown"
},
{
"id": 13785846501,
"name": "The Happy Herb Shop Newtown",
"type": "milkbar",
"lat": -33.898379,
"lon": 151.178005,
"suburb": "Newtown"
},
{
"id": 13919280070,
"name": "Tonton Bread",
"type": "milkbar",
"lat": -33.90484,
"lon": 151.17338,
"suburb": "Marrickville"
},
{
"id": 13919369194,
"name": "Mamuki",
"type": "milkbar",
"lat": -33.898789,
"lon": 151.173092,
"suburb": "Enmore"
}
],
"roads": [

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

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 ──────────────────

View File

@ -42,7 +42,10 @@
// • type : any CITY_SPEC type or thriftgod alias (see theme.js); defaults to 'opshop'
// • lot : { w, d } metres — room adapts to it; if absent, archetype ranges decide the size
// • storeys: ceiling height driver (1 → 3.2m … up to 4.5m)
// opts: { archetype?: 'cosy'|'gallery'|'wide'|'hall'|'pokey', stockAdapter? }
// opts: { archetype?: 'cosy'|'gallery'|'wide'|'hall'|'pokey', stockAdapter?, stock?: 'real', stockBase? }
// • stockBase (v5.0): which real-stock pack `stock:'real'` resolves. Omit → the town-wide packs
// (`assets/models/`, today's behaviour). Pass `assets/stock_godverse/<godverseShopId>/` for a shop
// with its own real crate. The pack cache is keyed by (type, base) — see stockpack.js + LANE_C_PUB §7.
//
// Registry override (Lane F): call mergeRegistry(registry) ONCE at init to let authored registry data
// override recipe fields. It is deliberately NOT a per-build option — a per-call mutation of the shared
@ -138,13 +141,16 @@ export function buildInterior(shop, THREE, opts) {
const dims = computeDims(ctx, recipe, archetype, norm.lot, norm.storeys);
const shell = buildShell(ctx, { recipe, archetype, dims });
// fittings + wall decor + stock + walkable-path guarantee. ?stock=real feeds Lane E's GODVERSE pack
// through the stockAdapter seam (preload it first); missing pack → parody canvas (warn once).
// fittings + wall decor + stock + walkable-path guarantee. ?stock=real feeds a real-stock pack through
// the stockAdapter seam (preload it first); missing pack → parody canvas (warn once).
// `opts.stockBase` (v5.0) selects WHICH pack: omit it for the town-wide packs, or pass a per-shop base
// (`assets/stock_godverse/<godverseShopId>/`) for a shop with its own real crate. The pack cache is keyed
// by (type, base), so a per-shop pack and the town-wide pack coexist — see stockpack.js.
let adapter = opts.stockAdapter || null;
if (!adapter && opts.stock === 'real') {
const pack = getStockPack(recipe.key);
const pack = getStockPack(recipe.key, opts.stockBase);
if (pack) adapter = makeStockAdapter(pack);
else warnOnce(`stock=real: no pack for "${recipe.key}" (preload it) — using parody canvas`);
else warnOnce(`stock=real: no pack for "${recipe.key}" @${opts.stockBase || 'default base'} (preload it) — using parody canvas`);
}
const lay = layout(ctx, { recipe, dims, shell, adapter, shop: norm });

View File

@ -16,23 +16,38 @@ import * as THREE_NS from 'three';
import { mergeGeometries } from 'three/addons/utils/BufferGeometryUtils.js';
const _texLoader = new THREE_NS.TextureLoader();
const _packs = new Map(); // type → Promise<pack|null>
const _resolved = new Map(); // type → pack|null (sync accessor for buildInterior)
// Preload a pack (fetch index + atlas textures). Cached. Call once before building with ?stock=real.
export function preloadStockPack(type, { base = 'assets/models/', THREE = THREE_NS } = {}) {
if (_packs.has(type)) return _packs.get(type);
// Where the TOWN-WIDE packs live. A per-shop (godverse) pack lives under its own base instead —
// `assets/stock_godverse/<godverseShopId>/` — and carries the SAME `stock_<type>_index.json` name,
// because `type` is what picks the stock slot (SLOT_FOR: record→sleeve, book→spine, toy→box).
export const DEFAULT_STOCK_BASE = 'assets/models/';
// CACHE IDENTITY IS (type, base) — never `type` alone. A per-shop atlas and the town-wide pack are
// different packs that share a type; keying by type alone made `base` a no-op on a cache hit, so the
// 2nd caller silently received the 1st caller's pack (R23, measured). That wasn't only a mix-up: it
// defeated the fail-soft law — an UNSTOCKED shop of an already-cached type inherited that crate
// instead of falling back to parody. (R24 ledger #1; the runtime is the contract's authority.)
const packKey = (type, base) => `${base}|${type}`;
const _packs = new Map(); // packKey → Promise<pack|null>
const _resolved = new Map(); // packKey → pack|null (sync accessor for buildInterior)
// Preload a pack (fetch index + atlas textures). Cached per (type, base). Call once before building
// with ?stock=real — for a per-shop pack, pass that shop's base.
export function preloadStockPack(type, { base = DEFAULT_STOCK_BASE, THREE = THREE_NS } = {}) {
const key = packKey(type, base);
if (_packs.has(key)) return _packs.get(key);
const p = fetch(`${base}stock_${type}_index.json`)
.then(r => (r.ok ? r.json() : null))
.then(idx => (idx && idx.items && idx.items.length ? buildPack(type, idx, base, THREE) : null))
.catch(() => null)
.then(pack => { _resolved.set(type, pack); return pack; });
_packs.set(type, p);
.then(pack => { _resolved.set(key, pack); return pack; });
_packs.set(key, p);
return p;
}
// Synchronous accessor — the resolved pack if preloaded, else null (buildInterior uses this).
export function getStockPack(type) { return _resolved.get(type) || null; }
// Synchronous accessor — the resolved pack for (type, base), else null (buildInterior uses this).
// `base` defaults to the town-wide packs, so every pre-v5 caller keeps its exact behaviour.
export function getStockPack(type, base = DEFAULT_STOCK_BASE) { return _resolved.get(packKey(type, base)) || null; }
async function buildPack(type, idx, base, THREE) {
const names = idx.atlases || [];