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>
214 lines
11 KiB
Python
214 lines
11 KiB
Python
#!/usr/bin/env python3
|
||
"""Validate per-shop real-stock atlases — the G2a format gate (v5.0-alpha, R23 #4 · REBUILT R24 #4).
|
||
|
||
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).
|
||
|
||
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.
|
||
|
||
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.
|
||
|
||
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)
|
||
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")
|
||
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", "staff"}
|
||
|
||
|
||
def image_size(path):
|
||
"""(w, h) for PNG/WebP from the header alone — dependency-free, house style (cf. glb_stat.py)."""
|
||
with open(path, "rb") as f:
|
||
head = f.read(32)
|
||
if head[:8] == b"\x89PNG\r\n\x1a\n":
|
||
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":
|
||
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 ":
|
||
w, h = struct.unpack("<HH", head[26:30])
|
||
return w & 0x3FFF, h & 0x3FFF
|
||
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)")
|
||
|
||
|
||
def validate_index(path):
|
||
errs, warns = [], []
|
||
name = os.path.relpath(path, STOCK_G)
|
||
try:
|
||
idx = json.load(open(path))
|
||
except Exception as e:
|
||
return [f"{name}: does not parse: {e}"], [] # present-but-unparseable ⇒ FAIL
|
||
d = os.path.dirname(path)
|
||
|
||
# ── 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")
|
||
|
||
# ── 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 = []
|
||
items = idx.get("items")
|
||
if not isinstance(items, list) or not items:
|
||
errs.append(f"{name}: no items")
|
||
items = []
|
||
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
|
||
try:
|
||
w, h = image_size(ap)
|
||
except Exception as e:
|
||
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}")
|
||
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 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):
|
||
errs.append(f"{name}[{i}]: item is not an object"); continue
|
||
m = [f for f in REQUIRED_ITEM if f not in it]
|
||
if m:
|
||
errs.append(f"{name}[{i}]: missing {m}"); continue
|
||
priv = sorted(set(k.lower() for k in it) & FORBIDDEN_ITEM_KEYS)
|
||
if priv:
|
||
errs.append(f"{name}[{it['id']}]: private-individual field(s) {priv} — charter law #3 forbids it")
|
||
if not str(it["title"]).strip() or not str(it["artist"]).strip():
|
||
errs.append(f"{name}[{it['id']}]: empty title/artist")
|
||
if it["id"] in ids:
|
||
errs.append(f"{name}: duplicate id {it['id']!r}")
|
||
ids.add(it["id"])
|
||
if it["price_band"] not in BANDS:
|
||
errs.append(f"{name}[{it['id']}]: bad price_band {it['price_band']!r}")
|
||
if not isinstance(it["price"], (int, float)) or it["price"] < 0:
|
||
errs.append(f"{name}[{it['id']}]: bad price {it['price']!r}")
|
||
if it["atlas"] not in atlases:
|
||
errs.append(f"{name}[{it['id']}]: atlas {it['atlas']!r} not in atlases[]")
|
||
uv = it["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
|
||
u0, v0, u1, v1 = uv
|
||
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(GLOB))
|
||
if not idxs:
|
||
# 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
|
||
try:
|
||
idx = json.load(open(p))
|
||
except Exception:
|
||
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"{'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:
|
||
print(f" ERR {e}")
|
||
if all_errs:
|
||
print(f"atlas-QA FAIL — {len(all_errs)} error(s), {len(all_warns)} warning(s)")
|
||
return 1
|
||
print(f"atlas-QA OK — {len(idxs)} per-shop atlas(es) valid, {len(all_warns)} warning(s)")
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main())
|