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>
This commit is contained in:
m3ultra 2026-07-17 01:06:00 +10:00
parent 1eb0b45bd0
commit 5815dfbd58
11 changed files with 206039 additions and 230846 deletions

View File

@ -1,5 +1,146 @@
# LANE E — cross-lane notes # 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⟩ ## 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 **1. `validate_atlas` — the G2a format gate (ledger #4).** `pipeline/validate_atlas.py`, wired into

View File

@ -50,10 +50,30 @@ TOWNS = {
"bowral_real": {"town": "Bowral", "state": "NSW", "center": (-34.4780, 150.4180), "span_km": 2.2}, "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}, "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}, "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}, "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}, "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}, "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 ──────────────────────────────── # ── 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 # 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 # checks**. I tried the re-bbox first (3.0 km CBD-wide → 1.6 km on the Ruthven St heart, centre

View File

@ -1,37 +1,49 @@
#!/usr/bin/env python3 #!/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 Judges Lane G's tier-1 atlases **from the committed files alone** — no dealgod DB, no network — so the
ladder). This gate is the FORMAT side: it proves G's output is well-formed, inside C's ceilings, and gate runs on m3. Wired into `validate_manifest` (qa gate 3).
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.
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): THE AUTHORITY IS THE RUNTIME (R24 ruling). `stockpack.js` states it: a per-shop pack lives under
index `web/assets/stock_godverse/**/stock_shop_<godverseShopId>_index.json` `assets/stock_godverse/<godverseShopId>/` and carries the SAME `stock_<type>_index.json` name (type picks
{version, atlas_px, cell, atlases[], items:[{id,title,artist,price,price_band,atlas,uv}]} the stock slot); cache identity is `(type, base)`. This gate reads that, not a doc that disagrees with it.
+ 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).
Fail-soft is NOT this gate's business: a missing atlas is legal at runtime (parody canvas, R7 law). This python3 pipeline/validate_atlas.py # 0 = valid (or an honest SKIP), 1 = a bad/misnamed atlas
gate only judges atlases that EXIST a malformed one must fail loudly rather than render garbled sleeves.
""" """
import json, os, sys, glob, struct import json, os, sys, glob, struct
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 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"} 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_ITEM = ("id", "title", "artist", "price", "price_band", "atlas", "uv")
REQUIRED_PROV = ("license", "attribution", "generator", "snapshot") # charter law #3 + determinism MAX_ATLASES = 2 # C §7.2: ≤2 atlases per shop
MAX_ATLASES = 2 # C: 1 atlas/shop, hard cap 2 (each extra = +1 draw) PX_MAX = 2048 # C §7.2: ≤2048² each
PX_TYPICAL, PX_MAX = 1024, 2048 PX_TYPICAL = 1024 # C §7.2: ≤1024² whenever the crate fits it (≤64 items at 256²)
BIG_SHOP_ITEMS = 60 # C: 2048² only for a 60+ item shop (else spill to the type-pack) FITS_1024 = 64
# charter law #3: "No private-individual data, ever" — a real shop's stock row must never carry a person. # 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", 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): def image_size(path):
@ -39,17 +51,16 @@ def image_size(path):
with open(path, "rb") as f: with open(path, "rb") as f:
head = f.read(32) head = f.read(32)
if head[:8] == b"\x89PNG\r\n\x1a\n": if head[:8] == b"\x89PNG\r\n\x1a\n":
w, h = struct.unpack(">II", head[16:24]) return struct.unpack(">II", head[16:24])
return w, h
if head[:4] == b"RIFF" and head[8:12] == b"WEBP": if head[:4] == b"RIFF" and head[8:12] == b"WEBP":
fmt = head[12:16] fmt = head[12:16]
if fmt == b"VP8X": # extended: 24-bit minus-one if fmt == b"VP8X":
b = head[24:30] b = head[24:30]
return (b[0] | b[1] << 8 | b[2] << 16) + 1, (b[3] | b[4] << 8 | b[5] << 16) + 1 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]) w, h = struct.unpack("<HH", head[26:30])
return w & 0x3FFF, h & 0x3FFF 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] b = struct.unpack("<I", head[21:25])[0]
return (b & 0x3FFF) + 1, ((b >> 14) & 0x3FFF) + 1 return (b & 0x3FFF) + 1, ((b >> 14) & 0x3FFF) + 1
raise ValueError("unrecognised image header (want PNG or WebP)") raise ValueError("unrecognised image header (want PNG or WebP)")
@ -57,57 +68,77 @@ def image_size(path):
def validate_index(path): def validate_index(path):
errs, warns = [], [] errs, warns = [], []
name = os.path.basename(path) name = os.path.relpath(path, STOCK_G)
try: try:
idx = json.load(open(path)) idx = json.load(open(path))
except Exception as e: 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) d = os.path.dirname(path)
# ── provenance (charter law #3) — every atlas carries it, and the gate PRINTS the licence ── # ── the NAME must be one the runtime can fetch: stock_<type>_index.json, type ∈ STOCK_TYPES ──
miss = [f for f in REQUIRED_PROV if not str(idx.get(f, "")).strip()] fname = os.path.basename(path)
if miss: ftype = fname[len("stock_"):-len("_index.json")]
errs.append(f"{name}: missing/empty provenance {miss} (charter law #3 — every atlas carries provenance)") 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) ── # ── identity: the per-shop base IS the godverse shop id (the loader keys on it) ──
atlases = idx.get("atlases", []) 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: if not isinstance(atlases, list) or not atlases:
errs.append(f"{name}: no atlases[] listed") errs.append(f"{name}: no atlases[] listed")
atlases = [] atlases = []
if len(atlases) > MAX_ATLASES: items = idx.get("items")
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", [])
if not isinstance(items, list) or not items: if not isinstance(items, list) or not items:
errs.append(f"{name}: no items") errs.append(f"{name}: no items")
items = [] items = []
if len(atlases) > MAX_ATLASES:
# ── C's texture/VRAM ceiling (§7.2) + index↔atlas consistency (the declared size must be REAL) ── errs.append(f"{name}: {len(atlases)} atlases — C §7.2 caps a shop at {MAX_ATLASES}")
declared = idx.get("atlas_px") declared = idx.get("atlas_px")
for a in atlases: for a in atlases:
ap = os.path.join(d, a) ap = os.path.join(d, a)
if not os.path.isfile(ap): if not os.path.isfile(ap):
errs.append(f"{name}: atlas {a!r} listed but missing on disk") errs.append(f"{name}: atlas {a!r} listed but missing on disk"); continue
continue
try: try:
w, h = image_size(ap) w, h = image_size(ap)
except Exception as e: except Exception as e:
errs.append(f"{name}: atlas {a!r} unreadable: {e}") errs.append(f"{name}: atlas {a!r} unreadable: {e}"); continue
continue
if declared and (w != declared or h != declared): if declared and (w != declared or h != declared):
errs.append(f"{name}: atlas {a!r} is {w}×{h} but index declares atlas_px={declared}") errs.append(f"{name}: atlas {a!r} is {w}×{h} but index declares atlas_px={declared}")
big = len(items) >= BIG_SHOP_ITEMS if max(w, h) > PX_MAX:
cap = PX_MAX if big else PX_TYPICAL errs.append(f"{name}: atlas {a!r} {w}×{h} exceeds C's ceiling {PX_MAX}²")
if max(w, h) > cap: elif max(w, h) > PX_TYPICAL and len(items) <= FITS_1024:
errs.append(f"{name}: atlas {a!r} {w}×{h} exceeds C's ceiling {cap}² " warns.append(f"{name}: atlas {a!r} is {w}×{h} for {len(items)} items — C: ≤{PX_TYPICAL}² "
f"({'60+ item shop' if big else f'{len(items)}-item shop → ≤{PX_TYPICAL}²'})") f"whenever the crate fits it (≤{FITS_1024} at 256²)")
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}²")
# ── 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() ids = set()
for i, it in enumerate(items): for i, it in enumerate(items):
if not isinstance(it, dict): 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)): 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 errs.append(f"{name}[{it['id']}]: uv must be [u0,v0,u1,v1] numbers"); continue
u0, v0, u1, v1 = uv 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}") errs.append(f"{name}[{it['id']}]: uv out of range / inverted: {uv}")
return errs, warns return errs, warns
def main(): 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: 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 return 0
all_errs, all_warns = [], [] all_errs, all_warns = [], []
for p in idxs: for p in idxs:
e, w = validate_index(p) e, w = validate_index(p)
all_errs += e; all_warns += w all_errs += e; all_warns += w
idx = {}
try: try:
idx = json.load(open(p)) idx = json.load(open(p))
except Exception: except Exception:
pass idx = {}
print(f" {os.path.basename(p):40} items={len(idx.get('items', [])):>3} " 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"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)) + ')'}") f"{'OK' if not e else 'ERR(' + str(len(e)) + ')'}")
if idx.get("license"): # licence law: the gate SHOWS it, a human clears it lic = (idx.get("provenance") or {}).get("licence") or {}
print(f" licence: {idx['license']}") 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: for w in all_warns:
print(f" WARN {w}") print(f" WARN {w}")
for e in all_errs: for e in all_errs:

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 **`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`. 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 `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 | | state | towns |
|---|---| |---|---|
| NSW (6) | katoomba · newtown · glebe · marrickville · newcastle (Hamilton strip) · bowral | | 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 | | WA (2) | fremantle · northbridge |
| QLD (1) | westend |
| TAS (2) | hobart · launceston | | TAS (2) | hobart · launceston |
| SA (1) | adelaide | | SA (1) | adelaide |
| ACT (1) | braddon | | ACT (1) | braddon |
| NT (1) | darwin | | 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 **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 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. 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. 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 density widening (v4.0-beta, ROUND20 ledger #2)
The fetch covers the **secondhand heroes** plus the everyday main-street **texture** (bakery/cafe/deli/ 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", "schema": "procity-towns-index/1",
"generated": "2026-07-16", "generated": "2026-07-17",
"license": "ODbL 1.0", "license": "ODbL 1.0",
"attribution": "\u00a9 OpenStreetMap contributors", "attribution": "\u00a9 OpenStreetMap contributors",
"towns": [ "towns": [
@ -12,14 +12,6 @@
"roads": true, "roads": true,
"source": "osm" "source": "osm"
}, },
{
"key": "ballarat_real",
"town": "Ballarat",
"state": "VIC",
"shops": 20,
"roads": true,
"source": "osm"
},
{ {
"key": "bendigo_real", "key": "bendigo_real",
"town": "Bendigo", "town": "Bendigo",
@ -172,6 +164,14 @@
"roads": true, "roads": true,
"source": "osm" "source": "osm"
}, },
{
"key": "redhill_real",
"town": "Red Hill",
"state": "QLD",
"shops": 36,
"roads": true,
"source": "osm"
},
{ {
"key": "westend_real", "key": "westend_real",
"town": "West End", "town": "West End",

File diff suppressed because it is too large Load Diff