Compare commits

...

7 Commits

Author SHA1 Message Date
m3ultra
57973e0540 Lane F R23 (v5.0-alpha): the alpha gate — HELD. The crate is real; it has no street.
v5.0-alpha is NOT tagged. #7a is not a gate that failed, it's a gate that cannot be run:
G's 120 real records are in the tree and byte-identically re-bakeable, and nothing in the
game can reach them. R18's law — release rounds don't ship known breaches.

GREEN (everything except the crate): qa.sh --strict --matrix 7 passed / 0 failed / 0 warn ·
MATRIX GREEN 10 towns x 7 gates · selfcheck 152,015/152,015 (was 161,300, E retired
toowoomba) · flags harness green, so ?classic=1 still holds through the merge · tier 0
?noassets zero fetches + zero console errors (the offline law holds) · tier 1 (v2
town-wide) resolves. Tier 1 (v5, per-shop) is NEVER FETCHED.

FINDING 1 — the atlas contract contradicts itself. LANE_C_PUB:271 publishes
stock_shop_<id>_index.json; C's own loader (stockpack.js:25) fetches
${base}stock_${type}_index.json. Incompatible. G built runtime-compatible (which is why
C's base-override check passed); E built contract-compatible (which is why E's validator
can't see it). Nobody was wrong — the contract was. F recommends amending to G's shape:
it's the only one the runtime can load, and it costs G nothing.

FINDING 2 — the gate built to catch that was blind and green. validate_atlas.py globs a
filename that can't exist at runtime, finds nothing, and returns 0 ("No atlases yet =>
clean pass"), which validate_manifest.py:158 propagates. It conflates "no atlases exist"
with "no atlases matched my glob" — the first is a clean pass, the second is the exact
failure it was written to catch. Pointed at G's index by hand, deeper drift surfaced:
E's line 68 reads provenance top-level while E's own docstring says nested (where G put
it), licence vs license, and a ceiling stale by one round.

FINDING 3 — #7a would have passed vacuously. Walked into The Vintage Record (real shop,
real road) via DBG.enterShop: the crates filled with real covers — from E's 350-item
town-wide v2 pack, not the crate. Zero requests to stock_godverse/, ever. Eyeballed as
written, the gate goes green and proves nothing. Two vacuous gates in one round, same
species: both test the presence of a thing rather than the identity of it.

R24 — three gaps, none F-only: A+G land a godverse_id (verified live: every godverse shop
reports null; the schema has no such field) -> E builds redhill_real (one Overpass fetch,
E's file, outward-facing — not F's to invent) -> C keys the pack cache by type+base
(today a hit returns before base is read, so two stocked shops collide and fail-soft
breaks) -> F passes the per-shop base and re-gates. F deliberately did not build its half:
untestable with no street and no identity field. Filed, not guessed.

Also: A's spacing warn earned on day one — ballarat now warns at 255 m, so the thin tail
has a second member. The README v5 teaser is held with the tag; they're one claim.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 00:05:44 +10:00
m3ultra
43b035aa2e Lane F R23: execute the mid-round merge of G's branch (charter law #5)
The merge amendment says G's branch merges mid-round once Fable-m3 reviews. The review
happened (the shared checkout was parked on lane-G-godverse for it) and C verified the
atlas contract from a temp worktree — deliberately pre-merge — and PASSED. But the merge
itself never executed: main had no web/assets/stock_godverse/** at all, so the round's
headline artifact was absent from the branch F is asked to gate and tag.

F executes it as the integration lane, last in the round with the tree in hand.
Disjointness verified before merging, not assumed:
  G since fork:    G-progress.md, docs/G3_ECONOMY_DESIGN.md, pipeline/_godverse/*,
                   pipeline/godverse_stock.py, web/assets/stock_godverse/3962749/*
  main since fork: C-progress.md, docs/LANES/LANE_C_PUB.md
  overlap:         none. Zero conflicts, as charter law #5 predicted.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 23:55:35 +10:00
m3ultra
d2eb9df03f Lane C R23 (v5.0-alpha): the first real crate verified in the room (ledger #3)
Verified G's stock_godverse/3962749/ (120 real records, Monster Robot Party crate 550,
2 x 2048^2, cell 256) in a fresh context — deliberately BEFORE the merge, so a contract
failure would block it. It didn't.

PASS — the contract holds; G re-emits nothing:
- Resolves through the existing stockAdapter path via a base override (no C code change for
  the single-shop alpha). 120 items / 2 atlases / 8-of-8 stock meshes atlas-backed (real
  covers: peperecordings, DEEPCHILD, "39", mff FREAKS...).
- Ceilings hold: 118 draws with real covers (<=350 law), pathOK, 0 carves.
- Fail-soft holds (clean cache): missing atlas -> null -> parody (20 meshes, 110 draws);
  no adapter (?noassets shape) -> parody, zero fetch. 0 console errors.
- G's index compliant: items[] sorted by id (my determinism ask honoured), provenance, uv in
  [0,1], 2 atlases = exactly the cap, cell_w/h harmless (C reads uv, never cell).

CORRECTED MY OWN CEILING (LANE_C_PUB §7.2): I wrote "<=8 MB decoded" from an assumed ~16-item
"typical shop". A real record shop is 120 records => 2 x 2048^2 ~= 32 MB, 4x my number. The
structural rules held (<=2 atlases; 2048^2 for 60+ items — G landed on the cap); only the MB
figure was wrong, because it described a shop that doesn't exist. New: <=2 atlases @<=2048^2,
LRU = 1 shop's atlas set resident => <=32 MB peak (bounded; vs E's 181 MB unbounded today).
Do NOT shrink the cell below 256 to save VRAM — the dig renders covers close up.

G's ask #1 CONFIRMED and worse than reported (LANE_C_PUB §8 — C's spec, C's fix): stockpack
keys _packs/_resolved by type alone, so base is ignored on a cache hit (measured: the 2nd
preload returned the identical object). (1) two stocked shops collide; (2) it BREAKS FAIL-SOFT
— an unstocked shop of a cached type inherits that crate instead of parody. Seam: key by
type+base, per-shop preload on entry, LRU=1 + dispose; C consumes whatever identity field
lands, C does not invent it.

Two gaps block F's #7a, neither C's: the crate has no street (redhill_real absent; monsterrobot
not in any shops[] — G's ask #2 to E/Fable), and interior_mode.js:192 pins base to
assets/models/ (needs §8 + the identity field).

Committed from a temp git worktree on main — the shared checkout stayed on lane-G-godverse
(Fable's review) and G's branch keeps its disjoint namespace. Docs-only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 23:42:30 +10:00
m3ultra
84f3641791 Lane G R23 (v5.0-alpha): THE FIRST REAL CRATE — 120 real records from a real crate + the G3 draft
godverse_stock.py splits at THE SNAPSHOT BOUNDARY: `snapshot` (runs where the DBs are; freezes
every source row + every cover's sha256) -> `pack` (pure: snapshot + bytes -> atlas, no DB, no
network). That split is what lets E's validate_atlas gate G's output on m3 without dealgod.
Artifact: web/assets/stock_godverse/3962749/ — crate 550 "DEEP HOUSE", 120 real records, 2
atlases (2048^2, 256px cells, E's contract verbatim). Byte-identical re-bake verified; resolves
through C's untouched stockAdapter (120 items, real titles/prices, one material per atlas =>
one draw); missing atlas -> null -> parody, zero console noise.

THE BRIEF'S PREMISE DIDN'T HOLD (measured): no Newtown record shop has stock in any database
(Egg Records / Papa Disquo / Haze Guitars / Vintage Record / Downtown Music: 0 items each).
dealgod.stores is not a census — 8,754 of 9,285 are Discogs seller accounts, zero Newtown.
thriftgod.item is a demo seed (2,221 items, 29 shops, all Brisbane/Newcastle charity, zero
record shops). mint() cannot produce tier 1: TABLESAMPLE SYSTEM (0.5) + unseeded random.* —
random.seed appears nowhere in server.py — and it assigns other sellers' listings to shops that
never owned them, voiding the licence law's "the stores' own product shots" green and making
write-back meaningless. Not risk #3's fuzzy matching: a population mismatch, nothing to match to.

What we shipped instead is truer: monsterrobot (147 Musgrave Rd, Red Hill, Brisbane — John's
ruling) is one real shop in three datasets — recordgod (its POS: 28,624 live vinyl, 24,914 in
357 real crates with real labels and slot numbers), monsterrobot.party (its own site: the covers
are the SHOP'S OWN product shots), dealgod store 3962749 (the cover cache + the godverse id;
clear of the census space, thriftgod max shop id = 2992). 99.3% of its crate stock joins to a
cached cover by Discogs release_id. Drops counted: 120 of 122 baked, 2 lacked an own photo.

John's R23 rulings recorded: the crate ships as the shop it really is (Red Hill, not Newtown);
real titles/artists at tier 1 — an amendment to the no-real-trademarks law build_stock_pack.py
enforces, same shape as the licence law (in-house green, flagged before anything public).

G3_ECONOMY_DESIGN.md drafts the API surface, async enrichment (the dig never blocks), failure
modes, identity, the PII fence (recordgod holds real customer/staff/sales — pipeline reads only
inventory/crate/disc_cache), and frames the write-back question for John (§7) against a real
trading shop with Square sync and an existing hold_expires_at primitive. No default, per law #4.

Asks in G-progress (both outside G's namespace, both gate the crate reaching a room): per-shop
packs aren't wired (stockpack.js caches by type alone — confirmed live: a base override is
silently ignored, so two stocked shops collide; C owns the wording, G re-emits), and redhill_real
doesn't exist (needs E's build_towns.py + one Overpass fetch; G touched neither).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 23:27:28 +10:00
m3ultra
84e0a1b764 Lane E R23 (v5.0-alpha): the atlas validator + toowoomba retired (ledger #4)
1. validate_atlas (the G2a format gate). pipeline/validate_atlas.py, wired into validate_manifest so it
   runs in qa gate 3. Judges Lane G's tier-1 atlases from the COMMITTED FILES ALONE -- no dealgod DB, no
   network. No atlases yet => clean pass, so it's in place before G's lands. Enforces: provenance
   {license, attribution, generator, snapshot} (charter law #3 + determinism); NO private-individual item
   fields (law #3); 1 atlas/shop hard cap 2 (C §7.1); <=1024², 2048² only for a 60+ item shop (C §7.2);
   declared atlas_px == the atlas's REAL pixels + every listed atlas exists; schema/UV/bands/dup-ids
   (C §7.3). The licence string is PRINTED, not pattern-matched -- clearing photos for public release is
   a human call, so the gate surfaces it. Dependency-free PNG/WebP header parser. Tested against a
   10-case matrix (real 2048² WebP + 1024² PNG): good passes clean, all 9 violations caught.

2. toowoomba RETIRED (my call, counted). Tried the re-bbox FIRST: 3.0km CBD-wide -> 1.6km on the Ruthven
   St heart. It fixed the named metric (median NN 395.7 -> 89.4m raw, 'passes') but made the real thing
   WORSE: hub density 3/12 -> 2/12 within 160m, the pack's worst (D's alive-darwin is 7/12). Its shops
   are PAIRS strung along a road, not a high street; densest 300m cluster is 3 shops (< MIN_TOWN_SHOPS),
   so no tighter box exists. Shipping it would have gamed the number while leaving a town nobody shops in
   (D: 0 finds / 1417 checks). Config entry kept commented with the rationale. Pack: 21 real + 1 godverse
   = 22 rostered, 1180 shops, all 8 states/territories (QLD keeps westend). selfcheck ALL GREEN 152015.

-> Lane A, before you freeze the spacing warn (#5): (a) your validator sees the CACHE (raw lat/lon) but
   D's numbers are SEATED (post-lift) -- darwin is 27m seated vs 68.3m RAW, so a threshold picked off D's
   table would FALSE-WARN darwin; calibrate on the raw spread (daylesford 11.6 ... braddon 119.2,
   ballarat 254.6 -- a ~200m line has daylight). (b) median NN alone is GAMEABLE: my re-bbox improved it
   4.4x while the town got worse -- the hub fraction is the discriminator, and my raw hub metric
   reproduces D's darwin exactly (7/12), so it's directly encodable (alive ~38-75%, dead <=20%).
   (c) ballarat is a SECOND spread town D's two-town comparison missed (NN 254.6m, hub 4/20 = 20%) -- it
   will trip your warn, correctly. Not retired: the ledger named toowoomba only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 23:14:00 +10:00
m3ultra
47f05ecaa8 Lane D R23 (v5.0-alpha): the GIG_RANGE chunk-neighbour fix — scoped to the gig path (ledger #6)
My carried v4.x finding, fixed post-release as scheduled. Runtime-only: no plan, no
golden, no flag, no fetch. selfcheck ALL GREEN 161300/161300, 0x3fa36874 frozen.

THE FINDING GREW WHILE IT WAITED: filed at 8.7% (measured at katoomba's isolated pub);
A's R21 cluster bias made it WORSE — venues now sit inside the retail cluster where peds
converge from every side, so 42.6% of in-gig-range peds (5094/11971) were chunk-blind.

THE OVERRIDE (measurement wins): the brief asked for "extend to chunk + 8 neighbours"
AND "byte-identical flags-off". I built the literal version and measured that those
cannot both hold: on the synthetic town (what ?classic=1 boots, gig layer absent) a full
3x3 scan sourced 127 of 855 finds (14.9%) from neighbour chunks — finds v2 never made.
The frozen v2 crowd would shop ~15% more. Covenant breach, in a clause of the same brief.

So the fix is scoped to the GIG path — which is what I actually filed:
- ordinary shops: own chunk only (v2 semantics, frozen — noticed from its own block)
- gig venues: the full 34m radius, honest across chunk edges ("follow the sound" is the
  whole reason GIG_RANGE 34 > PATRON_RANGE 18)
- no gig on -> _gigVenues empty -> neighbour loop skipped entirely -> ?classic and every
  flags-off boot take the IDENTICAL code path and cost to v2. Byte-identical BY
  CONSTRUCTION, not by measurement luck.
Both ranges < CHUNK(64) so 3x3 is exact, not approximate; fixed dz/dx order keeps ties
deterministic as the old single-list scan was.

BEFORE -> AFTER: cross-chunk in-range peds seeing the gig 0% (structural: v2 scanned only
the ped's own chunk) -> 100% (katoomba 4/4, fitzroy 11/11). Same-chunk unchanged 100%.
Classic neighbour-sourced finds: 0 (127 under the rejected full scan). Determinism
identical. 0 NaN on katoomba/fitzroy/synthetic. Sim 0.056 -> 0.057 ms/frame (+2%, i.e.
0.006% of a 16.7ms budget); lookup 0.58us unchanged with no gig, 1.3-2.7us with a gig on
(fires per PATRON_STRIDE, not per frame). Fitzroy clean at 376 peds.

CARRIED, documented: PATRON_RANGE keeps v2's own chunk-blindness — fixing it costs the
classic covenant, and the covenant wins. A frozen v2 quirk now, not an oversight.
Known minor: a scheduled-but-closed gig still runs the loop for nothing (0.58->1.3us).

D->F: no blocker for v5.0-alpha; classic untouched by construction.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 23:06:07 +10:00
m3ultra
292b07aa91 Lane A round 23 (v5.0-alpha): the spacing warn — D's thin-tail finding as law (ledger #5)
validateTownCache now warns when medianShopSpacing(cache) > MAX_MEDIAN_SPACING_M (150 m). Metric +
threshold exported from the barrel so no lane re-derives them. WARN, NEVER ERROR: a flagged cache still
boots a valid district (toowoomba shipped green through the entire v4.0 pack) — E curates, A flags.
selfcheck ALL GREEN 161,372/161,372 (+72); synthetic 0x3fa36874 frozen; NO golden moved (only E's
toowoomba re-bbox may move one this round, per the round's law).

THE THRESHOLD IS MEASURED, NOT PICKED. The brief offered darwin 27 m / toowoomba 388 m as "plenty of
daylight for a line" — but that's 2 points. Measuring all 23 towns shows a near-continuum, and the line
is FORCED into (119.3, 254.9]: braddon at 119.3 m is ALIVE by D's own hub test (7 shops within 160 m,
identical to darwin's 7), ballarat at 254.9 m is not. 150 m sits in the pack's widest gap (x2.14), with
1.26x margin below and 1.70x above. Flags 2 of 23.

TWO TOWNS FLAG, NOT ONE. The brief names only toowoomba (D spot-checked the two 12-shop towns). The
measurement adds BALLARAT at 255 m — nobody has looked at it, and it's independently one of the three
towns my R22 graded cluster fallback fired on (no venue candidate at the >=3-shops/160 m floor). Two
signals agree. Filed for E as a curation call, counted either way.

THE BRIEF'S ANCHORS WERE IN THE WRONG SPACE, AND IT MATTERS. D's 27 m is PLAN space (post-lift metres);
validateTownCache is CACHE space (raw lat/lon — the only thing that exists at ingest, and the layer where
a bad bbox must be caught). Darwin is 68.3 m in cache space, not 27. Interpolating a threshold between
D's figures would have silently mixed spaces. I verified the hypothesis before trusting it: computing the
metric plan-side reproduces D's numbers exactly (darwin 27.2 vs 27, toowoomba 386.8 vs 388, hubs 7 and 3).

Cache space is correct HERE and provably safe: the lift marches shops at a regularised cadence, so it
COMPRESSES dense towns (darwin plan/cache 0.40) but not sparse ones (toowoomba 0.98, ballarat 1.01) --
the error lives at the dense end, the warn only fires at the sparse end. Verified all 23 towns in both
spaces: exactly ONE disagrees, newtown_godverse (94.4 cache passes / 158.3 plan would warn). Filed for
G/Fable, NOT acted on: no cache-space line catches it without false-positiving braddon, and godverse is a
census subset (18 thriftgod shops over the footprint that carries 72 OSM shops) so its sparsity is
coverage, not a bad bbox — re-bboxing, the warn's only remedy, cannot fix it. Worth knowing since R23
stocks the real crate in that town and the beta asks "fitzroy_godverse if the census is dense there".

Also fixed en route: the R17 silent-fallback trap bit again in my own scratch harness — an unregistered
town key resolves to the melbourne fixture rather than erroring, so five towns silently measured
identical. Registered the caches; noting it because it has now cost two rounds.

Coverage: sparse cache warns + still boots (structural + district suites on a flagged town), compact
cache does NOT warn (no false positive), metric edge cases (< 2 shops -> null, coincident -> 0, no throw
on empty), and an exact-value check (a 100 m pair measures 100 m, projection sane).
Cost: ~1 ms/town at registration (25.6 ms for all 23 incl. fitzroy's 160 shops) vs a <100 ms plan budget.

E -> use the warn as your acceptance test for #4: re-bbox, re-run, warn goes silent = the town has a high
street. If your fix moves toowoomba's or ballarat's cache, ping me — goldens are mine to re-pin, and only
what you move.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 23:04:43 +10:00
28 changed files with 4594 additions and 176085 deletions

View File

@ -3,7 +3,54 @@
*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 22) · owner: PROCITY-C · reviewer: Fable
Last updated: 2026-07-16 (round 23) · owner: PROCITY-C · reviewer: Fable
---
## Update 2026-07-16 (round 23, v5.0-alpha THE FIRST REAL CRATE) — the crate in the room (ledger #3)
R23 §Lane C. **Verdict: the contract holds — G's atlas needs no re-emit.** Verified against G's
`stock_godverse/3962749/` (120 real records from Monster Robot Party's DEEP HOUSE crate 550, 2 × 2048²,
cell 256²) in a fresh context. Ran **before** Fable's merge deliberately — if the contract failed, that
blocks the merge. It didn't.
**PASS — the first real crate renders in a PROCITY room:**
- **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). The
sleeves are legible on screen: *peperecordings · DEEPCHILD · "39" · mff FREAKS · The Angels Instinct feat
Charlie Ashcroft* — a real deep-house crate.
- **Ceilings hold:** **118 draws** with real covers (≤350 law), `pathOK`, 0 carves.
- **Fail-soft holds** (clean cache): missing atlas → `null` → parody (20 parody meshes, 110 draws); no
adapter (the `?noassets` shape) → parody, zero fetch. **0 console errors.**
- **G's index is compliant:** `items[]` sorted by id (my R22 determinism ask honoured), provenance present,
`uv ⊂ [0,1]`, 2 atlases = exactly my cap, `cell_w/cell_h` harmless (C reads `uv`, never `cell`).
**I got a number wrong, and the first real data caught it (LANE_C_PUB §7.2 CORRECTED).** My R22 ceiling
said "≤8 MB decoded" — derived from assuming a "typical shop" is ~16 items @1024². **A real record shop is
120 records.** At the contract's own 256² cell that's 2 × 2048² ≈ **32 MB**, 4× my figure. The *structural*
rules held (≤2 atlases; 2048² for 60+ items — G landed exactly on the cap); only the MB number was wrong,
because it described a shop that doesn't exist. **Corrected:** ≤2 atlases/shop @≤2048², **LRU = 1 shop's
atlas set resident ⇒ ≤32 MB peak** (still bounded; still far better than the 181 MB unbounded accretion E
measured). And explicitly: **don't shrink the cell below 256² to save VRAM** — the dig shows a cover
close-up; resolution is the one thing the player actually sees.
**G's ask #1 CONFIRMED — and worse than reported (LANE_C_PUB §8: C's spec, C's fix).** `stockpack.js` keys
`_packs`/`_resolved` by **`type` alone**, so `base` is **ignored on a cache hit**. Measured:
`preloadStockPack('record', {base:'…/3962749/'})` then `preloadStockPack('record', {base:'assets/models/'})`
**returned the identical object**. Consequences: (1) two stocked shops collide — the 2nd serves the 1st's
crate (blocks v5.0-beta, as G said); (2) **it breaks the fail-soft law** — an *unstocked* shop of a cached
type inherits that crate instead of parody. The second is a correctness bug, not a mix-up. Seam spec
published (cache key = `type + base`; per-shop preload on entry; LRU=1 + dispose; **C consumes whatever
identity field lands — C does not invent it**). **C owns and lands it** — the §7 promise coming due.
**Two gaps block F's gate #7a (neither is C's; both flagged):** (1) **the crate has no street**
`redhill_real` doesn't exist and monsterrobot isn't in any town's `shops[]` (G's ask #2 → E/Fable);
(2) the shell's `interior_mode.js:192` calls `getStockPack(shop.type)` with `base` fixed to
`assets/models/`, so it can't reach a per-shop pack until §8's seam + the identity field land (C+F).
*Process note:* the shared tree was checked out on `lane-G-godverse` for Fable's review, so I committed
these C docs from a temporary `git worktree` on `main` — the shared checkout was never touched, and G's
branch keeps the disjoint namespace the two-studio merge law depends on.
---

View File

@ -4,6 +4,53 @@
---
## Round 23 (v5.0-alpha — THE FIRST REAL CRATE) — **F HELD the tag**
**`v5.0-alpha` is NOT tagged. The crate is real; it has no street.** G's 120 real records off a real
shop's POS are in the tree and byte-identically re-bakeable — genuinely the artifact the charter asked
for. Nothing in the game can reach them. `#7a` isn't a gate that failed, it's a gate that **cannot be
run**. R18's law: *release rounds don't ship known breaches.* Full record: `LANE_F_NOTES` §23.
**F executed the merge that never happened** (charter law #5) — `main` had no `stock_godverse/**` at all,
so the branch F was asked to tag lacked the round's headline. Review had happened, C had verified
pre-merge and passed; only the `git merge` was missing. Disjointness verified before merging, not
assumed. Zero conflicts → `43b035a`.
**What passed — everything except the crate.** `qa.sh --strict --matrix` **7 passed · 0 failed · 0 warn**;
**MATRIX GREEN — 10 towns × 7 gates**; selfcheck **ALL GREEN 152,015/152,015** (was 161,300 — E retired
toowoomba); flags harness green, so the `?classic=1` covenant still holds through the merge. Tier 0
`?noassets`**zero fetches, zero console errors** (the offline law holds); tier 0 default boot on the
godverse town; tier 1 (v2 town-wide) `?stock=real`. **Tier 1 (v5, per-shop) — the round's whole
deliverable — is never fetched.** A, D, E and C's work is sound; only the rung this round was built to
add is unattached.
**Three findings, in order of how much they cost:**
1. **The atlas contract contradicts itself.** `LANE_C_PUB:271` publishes `stock_shop_<id>_index.json`;
C's own loader at `stockpack.js:25` fetches `${base}stock_${type}_index.json`. **Incompatible.** G
built runtime-compatible (so C's base-override check passed); E built contract-compatible (so E's
validator can't see it). **Nobody was wrong — the contract was.** F recommends amending to G's shape;
it's the only one the runtime can load.
2. **The gate built to catch that was blind and green.** `validate_atlas.py` globs a filename that can't
exist at runtime, finds nothing, and `return 0` — so it passes whether the atlas is absent, misnamed,
or perfect. It had never seen one until F pointed it at G's by hand, which then surfaced *deeper* drift
(E's code reads provenance top-level, E's own docstring says nested, G nested it; `licence` vs
`license`; a ceiling stale by one round). *A gate that returns 0 on an empty set is not a gate.*
3. **`#7a` would have passed vacuously.** F walked into The Vintage Record (real shop, real road) and the
crates filled with **real covers — from E's 350-item town-wide v2 pack**, not the crate. Zero requests
to `stock_godverse/`, ever. Eyeballed as written, the gate goes green and proves nothing.
**R24 order — three gaps, none F-only:** **A+G** land a `godverse_id` (verified: *every* godverse shop
reports `godverse_id: null`; the schema has no such field, and C's rule is "C does not invent it") →
**E** builds `redhill_real` (one Overpass fetch — E's file, outward-facing; **not F's to invent**) →
**C** keys the pack cache by `type+base` (today a cache hit returns before `base` is read, so two stocked
shops collide *and fail-soft breaks*) → **F** passes the per-shop base and re-gates. F deliberately did
**not** build its half: untestable with no street and no identity field. Filed, not guessed.
**Also:** A's spacing warn earned on day one — with toowoomba retired, **ballarat now warns at 255 m**.
The thin tail has a second member.
---
## Round 22 (v4.0 — THE EPOCH CLOSE) — F closed it
- **The blocking gate CLOSED.** F's R21 re-measure made the tri diet blocking; B diagnosed, the driver

View File

@ -3,6 +3,78 @@
*Lane brief: `docs/LANES/LANE_G_GODVERSE.md`. This file follows your progress-file convention —
newest round first. Fable-on-ultra writing.*
## 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):**
- `pipeline/godverse_stock.py` — the per-shop atlas pipeline, split at **the snapshot boundary**:
`snapshot` (runs where the DBs are; freezes every source row + every cover's sha256) →
`pack` (pure: snapshot + image bytes → atlas, no DB, no network). That split is deliberate —
**it's what lets E's `validate_atlas` gate G's output on m3 without the dealgod DB** (ledger #4).
- `pipeline/_godverse/monsterrobot_550.snapshot.json` — the artifact of record (committed).
- `web/assets/stock_godverse/3962749/`**120 real records** from a real crate: 2 atlases
(2048², 256px cells, E's contract verbatim) + index with provenance.
- `docs/G3_ECONOMY_DESIGN.md` — the draft (ledger #2), incl. **the write-back question framed for
John** (§7), now grounded in a real trading shop rather than a hypothetical.
- **Verified:** byte-identical re-bake (same snapshot → same bytes); uv ⊂ [0,1]; every item's
atlas ref resolves; bands standard 58 / collector 56 / grail 6. Eyeballed the atlas: it's a real
deep-house crate (Basement Jaxx, Byron Stingily, Angel Moraes, 4hero, Murk, Blaze).
**THE HEADLINE FINDING — the brief's premise doesn't hold (all measured, R23):**
The ledger asked for "dealgod inventory → one real **Newtown** record shop". That can't be built:
- **No Newtown record shop has stock in any database.** Egg Records / Papa Disquo / Haze Guitars /
The Vintage Record / Downtown Music: **0 items each**.
- **`dealgod.stores` is not a shop census** — 8,754 of 9,285 are **Discogs seller accounts**.
Zero Newtown stores, zero census name matches. It's an item *feed*, not a shop directory.
- **`thriftgod.item` is a demo seed** — 2,221 items across 29 shops, **all Brisbane/Newcastle/Alice
charity shops**, zero record shops.
- **`mint()` cannot produce tier 1**: `TABLESAMPLE SYSTEM (0.5)` + `random.random/uniform/shuffle`,
and **`random.seed` appears nowhere in `server.py`**. Non-deterministic, and it assigns other
sellers' listings to shops that never owned them — which also voids the licence law's "the
*stores' own* product shots" green and makes write-back meaningless. (G3 §2.)
- This is **not** risk #3's "fuzzy matching" — it's a population mismatch. Nothing to match *to*.
**What we found instead — and it's better.** `monsterrobot` (**147 Musgrave Rd, Red Hill, Brisbane**
— John's ruling) is the same real shop in three datasets: `recordgod` (its POS: 28,624 live vinyl,
**24,914 in 357 real crates** with real labels and slot numbers), `monsterrobot.party` (its own
site — **the covers are the shop's own product shots**), and dealgod store **3962749** (the scrape;
the cover cache + the godverse id). **99.3%** of its crate stock joins to a cached cover by Discogs
`release_id`. Not in thriftgod's census, so its godverse id = its dealgod store id 3962749 (real,
stable, clear of the census space — thriftgod max shop id = 2992). Drops counted: 120 of 122 in
crate 550 baked; 2 dropped (no own photo).
**Rulings received from John this round (recorded):**
1. **The crate ships as the shop it really is** — Red Hill, Brisbane, not Newtown.
2. **Real titles/artists at tier 1** — an amendment to the no-real-trademarks law that
`build_stock_pack.py` enforces (it parody-transforms metadata). Same shape as the licence law:
in-house 🟢, **flagged before anything public**. Recorded in every atlas's provenance.
**Asks — two things gate the crate reaching a room, and both are outside G's namespace:**
1. **[C + F] Per-shop packs aren't wired at runtime.** `stockpack.js` caches by `type` alone
(`_packs`/`_resolved`), and `interior_mode.js:192` does `getStockPack(shop.type)` with `base`
fixed to `assets/models/`. R23's single shop works via a `base` override, but **two stocked
shops collide in that cache** — so this blocks v5.0-beta, not just ledger #3. Seam needed:
key the cache by `type + base` (or `godverse_id`) + preload per-shop on entry. **C owns the
wording (LANE_C_PUB); G re-emits to whatever lands.**
2. **[E, via Fable] The crate has no street.** `redhill_real` doesn't exist; `westend_real` is the
only Brisbane cache and stops ~2.3 km south. Needs a `build_towns.py` TOWNS entry
(~`center: (-27.4553, 153.0064), span_km: 2.4`) + one Overpass fetch, then
`godverse_town.py redhill_real redhill_godverse`. **That's E's file and an outward-facing
fetch — G touched neither.** Note monsterrobot must be added to the godverse cache's `shops[]`
as id 3962749 (it's not in the census); `godverse_town.py` takes shops from thriftgod only, so
that's a small G change once you rule on the town.
**Environment note (matters for reproducibility):** this ran on the **m3**, not ultra — the box has
neither `thriftgod` nor `dealgod` Postgres. Both were restored/read from John's mounted backup
drive (`/Volumes/johnking/db-backups`, **2026-07-15** dumps); dealgod (2 GB gz) was never restored,
its cover map was streamed straight out of the dump (command recorded in
`godverse_stock.py:load_image_map`). This is *better* for the charter's snapshot discipline, not
worse: the dump date is pinned in every snapshot's provenance. I left two restored DBs on the m3
(`thriftgod`, `recordgod`) — say the word and I'll `dropdb` them.
**Fences held:** PII — `recordgod` holds real `customer`/`staff`/`sales`; the pipeline reads only
`inventory`/`crate`/`disc_cache`, and G3 §9 makes that a standing fence. Licence — covers are the
shop's own shots, in-house 🟢, flagged before public, provenance in every atlas.
## Round 1 (2026-07-16, ultra) — the marriage's first artifact
**Landed:**

View File

@ -1,11 +1,37 @@
# LANE A — CITYGEN · progress (PROCITY-A)
*Status: **all deliverables landed; self-check all-green (161,300/161,300); rounds 222 closed** (see below).
R22 (v4.0 epoch close): the town pack absorbed — **23 towns, 1,210 shops, 36 goldens pinned in one commit**;
every town 1 connected component + 3 venues; drops 6.4%, counted by cause. classic `0x3fa36874` /
gig `0xb1d48ea1` / marched fixtures frozen.*
*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)*
## Round 23 (2026-07-16) — v5.0-alpha: the spacing warn (ledger #5)
D's R22 finding made law: `MIN_TOWN_SHOPS` counts shops, but what makes a town read alive is **clustering**.
`validateTownCache` now warns when `medianShopSpacing(cache) > MAX_MEDIAN_SPACING_M` (**150 m**) — the metric
and threshold both exported from the barrel, so no lane re-derives them. **Warn, never error**: a flagged
cache still boots a valid district (toowoomba shipped green through the whole v4.0 pack), E curates, the
validator flags. selfcheck **ALL GREEN 161,372/161,372** (+72), synthetic `0x3fa36874` frozen, no golden moved.
- **The threshold is measured, not picked.** Fable's brief offered darwin 27 m / toowoomba 388 m as "plenty
of daylight for a line". Measuring all 23 towns showed the pack is a near-continuum, and the line is
**forced** into `(119.3, 254.9]` — braddon (119.3 m) is alive by D's own hub test (7 shops within 160 m,
identical to darwin's 7), ballarat (254.9 m) is not. 150 m sits in the pack's widest gap (**×2.14**).
- **Two towns flag, not one.** The brief names toowoomba; the measurement adds **ballarat (255 m)**, which
nobody had looked at — and which is independently one of the three towns my R22 cluster fallback fired on.
Filed for E as a curation call, counted either way.
- **The brief's anchors were in the wrong space, and it matters.** D's 27 m is **plan** space (post-lift);
`validateTownCache` is **cache** space (raw lat/lon — the only thing it has at ingest). Darwin is 68.3 m
in cache space, not 27. Coding a threshold interpolated between D's figures would have mixed spaces
silently. Cache space is right *here*, and provably safe: the lift compresses dense towns (darwin
plan/cache **0.40**) but not sparse ones (toowoomba **0.98**, ballarat **1.01**), so the error lives at the
dense end and the warn only fires at the sparse end. Verified all 23 in both spaces: **one town disagrees**
(`newtown_godverse`, 94.4 cache → 158.3 plan) — filed for G/Fable, not acted on, since no cache-space line
catches it without false-positiving braddon, and its sparsity is census *coverage*, which re-bboxing can't fix.
- Cost: ~1 ms/town at registration (25.6 ms for all 23 including fitzroy's 160 shops), against a <100 ms
plan budget. Pending: re-pin whatever E's toowoomba re-bbox moves — **only** that.
## Round 22 (2026-07-16) — v4.0 THE EPOCH CLOSE: the pack absorbed + 36 goldens pinned (ledger #2)
E's 17 new towns + Lane G's `newtown_godverse` fed through the lift and pinned in ONE golden commit

View File

@ -394,6 +394,32 @@ it identically to the fixtures. **Hard requirements:** finite `center.{lat,lon}`
blank name → type label, unknown `type``opshop`, dup ids, and **span > 5 km → "likely more than one
town"** (a cache is ONE compact town — an over-broad bbox marches into an unusable multi-km strip).
**The spacing floor (v5.0-alpha, ROUND23 ledger #5 — D's R22 finding as law).** `MIN_TOWN_SHOPS` counts
shops; what makes a town **read alive** is *clustering*. D measured both 12-shop towns in the pack and they
disagree — which is the finding: **darwin ALIVE** (one strip; 1,455 patronage checks → 18 visits) vs
**toowoomba DEAD** (scattered over a 4.6 km road network; 1,417 checks → **0 visits, 0 finds** — "not a
shopping town, a road network with occasional shops"). Both pass every other law. So `validateTownCache`
now **warns when `medianShopSpacing(cache) > MAX_MEDIAN_SPACING_M` (150 m)**:
- **The metric** (exported from the barrel — import it, don't re-derive it): for each shop, the distance to
its **nearest other shop**; take the **median**. Equirectangular metres about `center.lat`, the same
convention as the span check. `null` for < 2 usable shops.
- **Warn, never error.** A flagged cache still boots a valid district — toowoomba shipped green through the
entire v4.0 pack. **E curates** (re-bbox onto the retail strip, or retire the town, counted either way);
the validator only flags. The span warn catches a town too **wide**; this catches one too **sparse**, a
case span cannot see (toowoomba is *inside* the 5 km law).
- **The threshold is measured, not chosen.** Across the 23-town pack the line is **forced** into
`(119.3, 254.9]`: braddon at 119.3 m is alive by D's own hub test (7 shops within 160 m — the same as
darwin's 7), and ballarat at 254.9 m is not. 150 m sits in that gap — the pack's widest (**×2.14**) —
with 1.26× margin below and 1.70× above. It flags **2 of 23**: ballarat 255 m, toowoomba 396 m.
- **⚠ CACHE space, not PLAN space — they do not agree.** This runs on raw lat/lon, before any lift; D's
notes quote **plan** figures (post-lift metres). The lift marches shops at a regularised cadence, so it
**compresses** dense towns (darwin **68.3 m cache → 27.2 m plan**) and leaves sparse ones alone
(toowoomba 396.1 → 386.8). Both numbers are right; they measure different spaces. Cache space is correct
*here* — the validator's job is to flag a bad bbox **at ingest**, before a lift exists — and it is
faithful exactly where the warn lives (ballarat plan/cache **1.01**, toowoomba **0.98**). Known limit:
one town flips (`newtown_godverse`, cache 94.4 → plan 158.3); no cache-space line can catch it without
false-positiving braddon. See `LANE_A_NOTES` §R23.
**Wiring:** `generatePlanOSM(seed, key, { cache })` marches a cache; `registerTownCache(key, cache)` adds it
to the registry (`osmTownKeys()` + `generatePlanFor(seed,'osm',{town:key})` then see it). `selfcheck.js`
auto-loads `web/assets/towns/*.json` — validates, runs the structural + gig suites, pins a per-town golden.

247
docs/G3_ECONOMY_DESIGN.md Normal file
View File

@ -0,0 +1,247 @@
# G3 — THE ECONOMY BACKEND (design doc, DRAFT for ratification)
*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).*
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.
---
## 1. What the data actually is [MEASURED]
The charter assumed one join: thriftgod's census of real shops → dealgod's real items. **That join
does not exist.** The three datasets are three different populations:
| dataset | what it really holds | inventory? |
|---|---|---|
| `thriftgod.shop` | 2,928 real AU secondhand shops, OSM-anchored (the census) | **no** |
| `dealgod.stores` | 9,285 online sellers — **8,754 are Discogs accounts**, not physical shops | 1.77M listings |
| `recordgod` | **one real trading record shop's POS** — crates, slots, prices, grading | 28,624 live |
Measured consequences:
- **No Newtown record shop has any stock, in any database.** Egg Records, Papa Disquo, Haze
Guitars, The Vintage Record, Downtown Music: 0 items each. dealgod has zero Newtown stores and
zero census name matches.
- `thriftgod.item` (the minted table) holds 2,221 items across **29 shops — all Brisbane/
Newcastle/Alice charity shops**. Zero record shops. It is a demo seed, not a stock system.
- This is **not** charter risk #3 ("fuzzy matching"). It is a population mismatch. Fuzzy matching
cannot fix it, because there is nothing on the other side of the join to match *to*.
### 1a. The one shop where all three meet [MEASURED]
`monsterrobot`**147 Musgrave Road, Red Hill, Brisbane QLD** (John, R23) — is the same real shop
in three forms, and that is the whole reason tier 1 can be honest:
```
recordgod (its POS) ──┐
monsterrobot.party ├── one real shop: real crates · real slots · real prices · its OWN photos
dealgod store 3962749 ─┘
```
- 28,624 live vinyl; **24,914 physically placed in 357 real crates** with real labels
("DEEP HOUSE", "$12$15 TECHNO") and real slot numbers — with gaps where records actually sold.
- Covers are the shop's own product shots on the shop's own domain, cached by dealgod
(23,392 releases). **99.3%** of its crate stock joins to a cached cover by Discogs `release_id`.
- It is **not in thriftgod's OSM census**, so its godverse id is its dealgod store id `3962749`
(real, stable, clear of the census space — thriftgod max shop id = 2992).
**Shipped this round (G2a):** crate 550 "DEEP HOUSE" → 120 real records → `web/assets/
stock_godverse/3962749/`. Byte-identical re-bake verified.
---
## 2. Why `mint()` cannot be the source of any tier [MEASURED]
thriftgod's `mint()` is the obvious candidate for stocking shops. It disqualifies itself three ways:
1. **It is not deterministic.** `FEED_RECORDS` uses `TABLESAMPLE SYSTEM (0.5)`; `mint()` uses
`random.random()`, `random.uniform()`, `random.shuffle()`. **`random.seed` appears nowhere in
`server.py`.** Re-running mint gives different stock at different prices. That is fatal to tier
1 (charter law #2), and it is *why* tier 1 reads the POS instead: a crate is a table of facts.
2. **The shop assignment is fictional.** mint samples arbitrary Discogs sellers' listings and
assigns them to a census shop that never owned them.
3. **Therefore it breaks two laws downstream.** The licence law's green rests on "the *stores'
own* product shots" — a Discogs seller's photo shown as Egg Records' stock isn't that. And the
write-back question (§7) is **meaningless** for minted stock: you cannot mark real inventory
sold for an item the shop never had.
mint stays what it is: a fun demo seeder for parody tier 0. It is not an inventory system.
*(If a future round wants seeded mint for tier 0 variety, seeding it is a small, safe change —
`random.Random(shop_id)` + an ordered `ORDER BY id` sample instead of `TABLESAMPLE`.)*
---
## 3. The tier ladder, as built [MEASURED for 01, DESIGN for 2]
| tier | source | deterministic | offline |
|---|---|---|---|
| 0 · parody | seeded canvas / E's parody packs | yes | yes |
| 1 · static real | **per-shop atlas baked from the POS snapshot** (G2a, shipped) | **yes — verified** | **yes — files** |
| 2 · live | thriftgod server over the POS (this doc) | **no, by design** | **enriches only** |
**The offline law is structural, not a promise.** `stockpack.js` already returns `null` when an
index or atlas is missing, and every caller falls back to parody canvas. Tier 2 rides the same
seam: the server is an *enrichment* that upgrades an already-playable tier-1 room. There is no
boot path, gate, or tag that may touch the network.
---
## 4. API surface [DESIGN]
Read-only in v5.0; the write verbs are drafted here but **gated on §7**.
```
GET /godverse/v1/shop/<godverse_id>/stock?crate=<id>&since=<iso>
-> { shop_id, crate, tier, items:[{sku, slot, release_id, artist, title,
price, condition, sleeve_cond, cover, state}],
gone:[sku], etag, served_at }
GET /godverse/v1/shop/<godverse_id>/crates -> crate list + counts (the rotation menu)
POST /godverse/v1/reserve { sku, session } -> { ok, holds_until } [GATED — §7]
POST /godverse/v1/buy { sku, session, hold } -> { ok, receipt } [GATED — §7]
```
Design rules:
- **The atlas is never served by the API.** Covers stay static files (tier 1). The server sends
*facts* (what's still there, what it costs), never pixels. A tier-2 shop with a dead server is
visually identical to tier 1 — that's the fail-soft.
- **`gone[]` is the whole point of tier 2.** The client already holds the tier-1 crate; the server
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.
---
## 5. Async enrichment + caching (risk #1) [DESIGN]
**Law: the dig never blocks on the network.** Not "usually" — never. The mechanism:
1. Room builds at tier 1 from static files. Playable. Riffling works. **No await.**
2. The stock fetch is fired in parallel, never awaited by any build path, with a **hard ~400 ms
budget** and `AbortController`.
3. If it lands in time: apply `gone[]` + price deltas before the player reaches the crate.
4. If it lands late: apply on arrival (a record vanishing mid-riffle is *correct* — that's a real
shop). If it never lands: the room stays tier 1, silently. **Zero console noise** — a dead
server is an expected state, not an error.
5. Cache the response per `(shop, crate)` in memory + `sessionStorage`, keyed by `etag`. One
fetch per shop per session, not per crate entry.
6. **One flight at a time per shop**; re-entering a shop reuses the cache. No polling loop.
The v1 machinery makes this cheap: the pack is a shared material + per-item UVs, so applying
`gone[]` is a vertex collapse on an existing merged mesh — no rebuild, no re-upload, no stutter.
---
## 6. Identity: census ↔ lot ↔ POS (risk #3) [DESIGN, informed by §1]
The R22 contract said "atlas per shop keyed by godverse shop id". §1 shows the id must come from
whichever dataset actually *has* the shop, and mismatches must fail soft, never mis-stock:
- **A shop is stocked only by explicit, recorded identity** — a `godverse_id` that resolves to a
real POS. There is no fuzzy name matching, ever. A shop we cannot identify with certainty gets
tier 0. **Mis-stocking is worse than no stock**: it puts one shop's records in another's crate,
which is a lie the whole epoch exists to avoid.
- **Every unmatched census shop is counted, not guessed** (the drops law). R23's count: 2,928
census shops, 1 identified real POS.
- **The census is not a stock oracle.** Being in the census means the shop exists, not that we
know what's in it. v5.0-beta's "every hero shop stocked" is therefore bounded by *real POS
access*, not by census coverage — see §9.
---
## 7. THE WRITE-BACK QUESTION [JOHN — charter law #4]
**Does an in-game purchase ever touch real thriftgod/recordgod inventory?**
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**.
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
the game is allowed to pull it.
### Option A — Sandbox (read-live, sell-local)
Game reads real stock; a purchase never leaves the game. Sold-means-gone is per-session.
- **For:** zero real-world risk; no payment, tax, or fulfilment surface; a bug can't cost real
stock; ships fastest.
- **Against:** the one-of-a-kind thrill is fiction — the real crate never changes, and two players
(or the same player, next boot) buy the same record. Tier 2 becomes "live prices" more than
"live stock".
### Option B — Reservation (a real, expiring hold)
A game purchase places a real hold on the real SKU via the existing `hold_expires_at`. The record
genuinely leaves the real crate for N hours; unclaimed, it returns.
- **For:** genuinely one-of-a-kind, and **reversible** — the strongest truth-per-unit-risk; uses a
primitive the POS already has.
- **Against:** a game action removes real sellable stock from real in-store customers. Needs a
hold cap/budget, a staff-visible reason code ("held: GODVERSE"), and a race policy against
Square in-store sales. Someone must decide what happens when a held record sells in the shop
anyway (the game must lose, gracefully).
### Option C — Full write-back (game sale = real sale)
A purchase marks the real SKU sold and syncs to Square.
- **For:** total truth — buy it in the game and it is gone from the real shop, forever.
- **Against:** irreversible real-world inventory and revenue consequences; the game becomes a
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).
---
## 8. Failure modes [DESIGN]
| failure | behaviour | gate class |
|---|---|---|
| server unreachable / DNS dead | tier 1, silent, zero console noise | tier-2 smoke |
| server dies **mid-session** | room keeps last-good state; no re-fetch storm; no stutter | tier-2 smoke |
| server slow (> budget) | request aborted; tier 1 stands; late arrival applies or is dropped | tier-2 smoke |
| 5xx / malformed JSON | treated as unreachable — never a partial apply | tier-2 smoke |
| atlas missing but server up | **tier 0** — no stock without covers; never text-only sleeves | tier-1 gate |
| `gone[]` names an unknown sku | ignored + counted, never an error | tier-2 smoke |
| clock skew on holds | server time is authoritative; client never computes expiry | tier-2 smoke |
**No tier-2 failure may fail a gate or block a tag** (charter law #1). Tier 2 gets its own smoke
class: reachability, fail-soft, latency. Kill the server mid-session and the game must not stutter
— that is v5.0's release gate, and it is a *client* test, not a server test.
---
## 9. Fences (non-negotiable, both already load-bearing)
- **PII.** `recordgod` holds real `customer`, `customer_session`, `mailing_list`, `staff`,
`sales` — real private-individual data. The pipeline and the server read **only**
`inventory` / `crate` / `disc_cache`. No exceptions, no joins, no "just for debugging".
Charter law #3: no private-individual data, ever.
- **Licence.** Covers are the shop's own product shots → in-house 🟢, **flagged before any
public/commercial release**. Real titles/artists ship at tier 1 per John's R23 amendment to the
no-real-trademarks law, under the same flag. Every atlas carries its provenance.
- **Scope reality check for v5.0-beta.** "Every hero shop stocked" needs a real POS per shop. We
have exactly one (John's own). Stocking a *second* real shop means a second real shopkeeper's
data and consent — a licence and relationship question, not a pipeline question. Beta should be
scoped to **monsterrobot's 357 crates** (24,646 real records — the whole shop, crate rotation
per risk #2) rather than to more shops, unless John says otherwise.
---
## 10. Open items for the reviewers
1. **[Fable-m3 / C]** Per-shop pack resolution is not wired: `stockpack.js` caches by `type`
alone (`_packs`/`_resolved`), and `interior_mode.js:192` resolves `getStockPack(shop.type)`
with `base` fixed at `assets/models/`. R23's single shop works via a `base` override; **two
stocked shops will collide in that cache.** The seam C/F need: key the cache by
`type + base` (or `godverse_id`), and preload per-shop on shop entry. G re-emits to whatever
wording C lands (LANE_C_PUB).
2. **[Fable-m3 / E]** `redhill_real` doesn't exist — the only Brisbane cache (`westend_real`)
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.
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,56 @@
Durable answers to questions other lanes raised against Lane A. Full status in `A-progress.md`.
## 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)
`validateTownCache` now warns when **`medianShopSpacing(cache) > 150 m`**. Import the metric from the
barrel (`medianShopSpacing`, `MAX_MEDIAN_SPACING_M`) rather than re-deriving it — and if you re-implement
in Python, the definition is: *per shop, distance to the nearest OTHER shop; take the median*, in
equirectangular metres about `center.lat`. Warn, never error: **you curate, I only flag.**
**Use the warn as your acceptance test.** Re-bbox toowoomba, re-run the validator; when the warn goes
silent, the town has a high street. That's the whole loop, no judgement call needed.
| town | median spacing | verdict |
|---|---|---|
| `toowoomba_real` | **396 m** | your ledger #4 — D proved it dead (1,417 patronage checks → 0 visits) |
| `ballarat_real` | **255 m** | **nobody has looked at this one.** Same class, 1.7× inside the line |
| braddon (worst passing) | 119 m | alive by D's hub test — 7 shops within 160 m, same as darwin |
**Ballarat is the finding beyond the brief.** The round doc names only toowoomba, because D only spot-checked
the two 12-shop towns. Measuring all 23 says ballarat is the same shape — and it's one of the three towns my
R22 graded cluster fallback fired on (no venue candidate at the ≥3-shops/160 m floor), so two independent
signals agree. Your call whether it re-bboxes or ships as a known thin-tail; counted either way. **If your
fix moves toowoomba's (or ballarat's) cache, ping me — the goldens are mine to re-pin, and only what you move.**
### ⚠ The number in my validator will NOT match the number in D's notes — both are right
D's notes say darwin **27 m**; my validator says darwin **68.3 m**. Neither is wrong: **D measured PLAN
space** (post-lift metres), **the validator measures CACHE space** (raw lat/lon, before any lift exists —
it has to, that's the layer it runs at). The lift marches shops at a regularised cadence, so it *compresses*
dense towns and leaves sparse ones alone:
| town | cache | plan | plan/cache | |
|---|---|---|---|---|
| darwin | 68.3 | 27.2 | **0.40** | dense ⇒ the lift compresses hard |
| toowoomba | 396.1 | 386.8 | **0.98** | sparse ⇒ the lift barely moves it |
| ballarat | 254.9 | 257.7 | **1.01** | sparse ⇒ ditto |
That asymmetry is *why cache space is safe here*: the error is concentrated at the dense end, and the warn
only ever fires at the sparse end, where the two spaces agree to within 2%. I verified all 23 towns in both
spaces — **exactly one disagrees.**
### → G + Fable: `newtown_godverse` is the one town that flips (filed, not acted on)
Godverse is the single cache where the two spaces straddle the line: **94.4 m cache (passes) → 158.3 m plan
(would warn)**. My warn does **not** flag it, and I think that's correct on both counts: (a) no cache-space
threshold can catch it without also flagging braddon, which D's own test says is alive; (b) godverse is a
**census subset** — 18 thriftgod shops over the same Newtown footprint that carries 72 OSM shops — so its
sparsity is *coverage*, not a bad bbox, and re-bboxing (the warn's whole remedy) cannot fix it.
**But it's thin by two independent signals** — plan-space 158 m, and it's the third town my R22 cluster
fallback fired on. Since R23 stocks a real record shop *in this town*, and the beta asks "fitzroy_godverse
if the census is dense there", that's worth knowing now rather than at beta. Not my call — filing it.
## Round 22 (2026-07-16) — v4.0: the pack absorbed, 36 goldens pinned (the epoch close)
### → C + D + F: **the pack is in — 23 towns, all pinned, start your verifies** (the handshake)

View File

@ -7,6 +7,9 @@
> (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.
*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
@ -242,10 +245,21 @@ worst rooms today are record 41 / book 58 / toy 57 draws against the ≤350 law;
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:
- **Resident: ≤2 shop atlases** (the room you're in + 1 for re-entry), **≤8 MB decoded**.
- **Atlas size: ≤1024² for a typical shop** (16 items @256² cell); **2048² only for a 60+ item shop**, and
prefer spilling to the type-pack. At 1024² RGBA ≈ 4 MB → ≤2 resident ≈ **8 MB** (vs E's measured **181 MB**
resident today — ~20× better, and bounded instead of accreting).
> **⚠️ 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:
- **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**
@ -274,4 +288,37 @@ a re-emit silently reshuffles every crate. Same discipline as E's byte-identical
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.
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.

View File

@ -6,6 +6,71 @@ _rotOnly/head-bone normalize/upgradeStreetPeople). Measurements on the M3 Ultra
---
## ROUND 23 — v5.0-alpha: the GIG_RANGE chunk-neighbour fix (ledger #6, the carried v4.x item)
*PROCITY-D, 2026-07-16. My own filed finding, finally fixed — post-release, as Fable scheduled. Hot path,
so the R18-style before/after is the deliverable. `selfcheck` **ALL GREEN 161300/161300**, `0x3fa36874`
frozen (runtime-only change — no plan, no golden).*
### The finding grew while it waited: 8.7 % → 42.6 %
I filed this at 8.7 % in R20, measured at katoomba's isolated pub. **A's R21 cluster bias made it worse**:
venues now sit *inside* the retail cluster, where peds converge from every side rather than trickling along
one street — so far more in-range peds are across a chunk edge. Re-measured on the relocated venue:
**42.6 %** of in-gig-range ped-samples (5094 of 11971) were chunk-blind. Fixing it mattered more in R23
than when I filed it.
### The override: the brief contradicts itself, and the measurement broke the tie
The brief said *"extend `_nearestOpenShop` to the ped's chunk + 8 neighbours"* **and** *"byte-identical
flags-off"*. **I built the literal version first and measured it: those two cannot both be true.** On the
synthetic town (exactly what `?classic=1` boots, gig layer absent), a full 3×3 scan sourced **127 of 855
finds — 14.9 % — from neighbour chunks**: finds v2 never made. The frozen v2 crowd would start shopping
~15 % more. That is a covenant breach, in a clause of the same brief that asked for it.
**So the fix is scoped to the GIG path** — which is also what I actually filed (the finding was always
`GIG_RANGE`-specific):
- **Ordinary shops → own chunk only.** v2 semantics, frozen. A shopfront is noticed from its own block.
- **Gig venues → the full 34 m radius, honest across chunk edges.** The gig is the "follow the sound" pull
that is *supposed* to reach further — that is the entire reason `GIG_RANGE` (34) > `PATRON_RANGE` (18).
- **No gig on → `_gigVenues` is empty → the neighbour loop is skipped entirely.** `?classic` and every
flags-off boot take the *identical code path and identical cost* to v2. **Byte-identical by construction**,
not by measurement luck.
Both ranges are < `CHUNK` (64), so a 3×3 sweep is *exact*, not an approximation; a fixed `dz/dx` order keeps
exact-distance ties deterministic, as the old single-list scan was.
### Before / after
| proof | before | after |
|---|---|---|
| **cross-chunk in-range peds that see the gig** — katoomba | **0 % by construction**¹ | **4/4 = 100 %** |
| — fitzroy (densest, worst case) | **0 % by construction**¹ | **11/11 = 100 %** |
| same-chunk peds (must not change) | 100 % | 100 % |
| **classic / flags-off: finds sourced from a neighbour** | 0 (v2) | **0** (127 under the rejected full scan) |
| determinism (2 sims, same seed) | identical | **identical** (identity sets byte-equal) |
| NaN — katoomba / fitzroy / synthetic | 0 | **0** |
| sim cost, katoomba @14:00, 236 peds | 0.056 ms/frame | **0.057 ms/frame (+2 %)** |
| lookup cost/call — no gig on | 0.58 µs | **0.58 µs (loop skipped)** |
| lookup cost/call — gig on | 0.58 µs | **1.32.7 µs** |
¹ *not a small-sample claim: v2 read `shopsByChunk.get(ownChunk)` only, so a venue in another chunk was
never in the scanned list and could not be returned. 0 % is structural.*
The lookup is 24× dearer **while a gig is on**, but it only fires every `PATRON_STRIDE` (10 m walked), not
per frame — hence **+2 % of a 0.056 ms frame cost**. Against a 16.7 ms budget that is 0.006 %. Fitzroy at 376
peds stayed clean. Minor known inefficiency: with a gig *scheduled but closed* (e.g. 14:00) the neighbour
loop still runs for nothing (0.58 → 1.3 µs) — `_gigVenues` holds ids, so testing openness would need the
lookup it's trying to avoid. Not worth it at +2 %; noted rather than hidden.
### Handshakes
- **D → F (gate):** the fix is **runtime-only** — no plan, no golden, no flag, no fetch. `?classic=1` takes
the identical code path to v2 (loop skipped), so the classic regression is untouched **by construction**.
Selfcheck green at 161300. No D-side blocker for `v5.0-alpha`.
- **Carried (deliberate, documented):** `PATRON_RANGE` keeps v2's own chunk-blindness — an ordinary shop 10 m
away across a chunk edge still goes unnoticed. That is now a *frozen v2 quirk*, not an oversight: fixing it
costs the classic covenant, and the covenant wins. Revisit only if the covenant is ever relaxed.
- **Still unowned (unchanged):** R20's main-street relocation half (heroes 019 % of the densest cluster).
---
## ROUND 22 — v4.0 THE EPOCH CLOSE: citizens at pack scale (ledger #5)
*PROCITY-D, 2026-07-16. The fitzroy stress audit (160 shops — the densest citizen field this epoch) + the

View File

@ -1,5 +1,59 @@
# LANE E — cross-lane notes
## 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
`validate_manifest` so it runs in qa gate 3. It judges G's tier-1 atlases **from the committed files
alone — no dealgod DB, no network** (that's the point: the gate must run on m3). No atlases yet ⇒ clean
pass, so it's in place *before* G's lands. **→ Lane G: this is the format you build to.** It enforces:
| rule | source |
|---|---|
| provenance `{license, attribution, generator, snapshot}` present | charter law #3 ("every atlas carries provenance") + `snapshot` pins determinism |
| **no private-individual fields** on items (`seller`/`owner`/`email`/`phone`/…) | charter law #3 ("no private-individual data, ever") |
| 1 atlas/shop, **hard cap 2** | LANE_C_PUB §7.1 |
| **≤1024²**; 2048² only for a **60+ item** shop | LANE_C_PUB §7.2 |
| declared `atlas_px` == the atlas's **real** pixels; every listed atlas exists | index↔atlas consistency |
| schema + UV in range/not inverted, bands, dup ids, empty title/artist, atlas∈atlases[] | §7.3 (schema confirmed verbatim) |
The licence string is **printed** by the gate, not pattern-matched — clearing photos for public release is
a human call (charter law #3), so the gate surfaces it rather than pretending to rule on it. **Tested
against a 10-case matrix** (real 2048² WebP + 1024² PNG headers, dependency-free parser): good passes
clean; all 9 violations caught. A gate that's never caught anything isn't a gate.
**2. Toowoomba — RETIRED (my call, counted). I tried the re-bbox first; the data said retire.**
D's finding: DEAD — 0 patronage finds in 1417 checks. I re-bboxed 3.0 km CBD-wide → 1.6 km onto the
Ruthven St heart. Result:
| | median NN (the named metric) | hub ≤160 m (D's real test) |
|---|---|---|
| toowoomba @3.0 km | 395.7 m ❌ | 3/12 |
| **toowoomba @1.6 km (re-bbox)** | **89.4 m ✅ "passes"** | **2/12 (17%) — worst in the pack** |
| darwin (D: ALIVE) | 68.3 m | 7/12 (58%) |
**The re-bbox fixed the metric and made the town worse.** Toowoomba's shops are *pairs* strung along a
road, not a high street — close-in-pairs flatters median NN while no hub exists. Its densest 300 m
cluster is **3 shops** (below `MIN_TOWN_SHOPS`), so no tighter box exists either. Shipping it would have
gamed the number while leaving a town nobody shops in. **Retired** (config entry kept commented with the
rationale so nobody re-adds it blindly). Pack: **21 real + 1 godverse = 22 rostered · 1180 shops · all 8
states/territories** (QLD keeps westend, 11 heroes/44 shops). selfcheck **ALL GREEN 152015/152015**.
### ⚠️ → Lane A: two things before you freeze the spacing warn (#5)
1. **Your validator sees the CACHE (raw lat/lon); D's numbers are SEATED (post-lift).** They differ:
**darwin — D 27 m seated vs 68.3 m raw**; toowoomba 388 seated vs 395.7 raw (spread towns barely move,
clustered ones move a lot). **Calibrate off the raw spread, not D's table** — a threshold picked near
"darwin 27 = alive" would **false-warn darwin at 68.3**. Raw spread now: daylesford 11.6 · katoomba
22.4 · bowral 27.9 · … · launceston 76.8 · braddon 119.2 · **ballarat 254.6**. A ~200 m line has
daylight on both sides.
2. **Median NN alone is gameable — the hub fraction is the discriminator.** My re-bbox is the proof: NN
improved 4.4× while the town got worse. Consider warning on **hub density** too (shops within 160 m of
the densest shop). **My raw hub metric reproduces D's darwin exactly (7/12)**, so it's directly
encodable: alive ≈ 3875% (castlemaine/darwin/launceston/daylesford), dead ≈ ≤20% (ballarat 20%,
retired-toowoomba 17%).
3. **ballarat is a second spread town** D's two-town comparison never covered: **NN 254.6 m, hub 4/20
(20%)** — it *will* trip your warn (correctly). Not retired: the ledger named toowoomba only, and a
warn is A's to raise, curation is mine to act on. Flagging for Fable's next-round call.
## Round 22 — v4.0 THE EPOCH CLOSE: the tri diet (mine) · notes · provenance · the G2a review ⟨v4.0⟩
### ⚠️ Ledger #1b — the `venue_night` tri diet: **MY four furniture GLBs. Fixed. → F: RE-MEASURE.**

View File

@ -840,3 +840,123 @@ Toowoomba is a real city whose *secondhand* cache is 12 shops, spread across its
isn't "≥N shops" — it's **shops-per-street-metre**. v4.x options: clip the graph to the retail core for
thin caches, or set a ratio floor in the pack. The frame ships **labelled honestly** rather than swapped for
a flattering town — same discipline as the R21 re-measure that got worse and said so.
---
## 23. Round 23 — v5.0-alpha THE FIRST REAL CRATE. **F HELD the tag — the crate has no street.**
**Verdict: `v5.0-alpha` NOT tagged.** The round's headline artifact is real and good — G's 120 real
records off a real shop's POS are in the tree, byte-identically re-bakeable. It cannot be reached from
the game. `#7a` (riffle the real crate, buy it) is not a gate that failed; it is a gate that **cannot be
run**. R18's law: *release rounds don't ship known breaches.*
### What F did run
| gate | result |
|---|---|
| **`qa.sh --strict --matrix`** | **7 passed · 0 failed · 0 warn** — QA GREEN |
| **town-matrix** | **MATRIX GREEN — 10 towns × 7 gates**, all green |
| citygen selfcheck | **ALL GREEN 152,015/152,015** (was 161,300 — E retired toowoomba) |
| flags harness (classic regression · default boot · STRICT) | **GREEN, 0 warnings** — the covenant holds through the merge |
| manifest validator · scaffold · consistency · no-giants | GREEN |
| tier 0 — `?noassets=1` | **zero fetches, zero console errors** — the offline law holds |
| tier 0 — default boot, godverse town | town loads (18 real Newtown shops), stock → parody |
| tier 1 (v2, town-wide) — `?stock=real` | 350-item pack resolves, real cover art |
| **tier 1 (v5, per-shop) — the round's deliverable** | **NEVER FETCHED** |
| tier 2 | N/A by law (no server code this round) |
### The merge that never executed (F did it — charter law #5)
Fable's mid-round merge never ran: `main` had **no `web/assets/stock_godverse/**` at all**, so the branch
F was asked to gate and tag did not contain the round's headline. The review had happened (the shared
checkout was parked on `lane-G-godverse` for it) and C verified the atlas from a temp worktree —
deliberately pre-merge — and passed. Only the `git merge` was missing. F executed it as the integration
lane, disjointness **verified before merging, not assumed** (G: `G-progress`, `G3 doc`,
`pipeline/godverse_*`, `stock_godverse/**` · main-since-fork: `C-progress`, `LANE_C_PUB` · overlap: none).
Zero conflicts, exactly as law #5 predicted. → `43b035a`.
### ⚠ FINDING 1 — the atlas contract contradicts itself. Nobody was wrong; the contract was.
| | says the index is |
|---|---|
| **C's published contract**`LANE_C_PUB:271` | `stock_shop_<godverseShopId>_index.json` |
| **C's own runtime loader**`stockpack.js:25` | `` fetch(`${base}stock_${type}_index.json`) `` |
**These are mutually incompatible.** An atlas named per the published contract can *never* be loaded by
the code that is supposed to load it. G shipped `3962749/stock_record_index.json`**runtime-compatible**,
which is exactly why C's base-override verification passed. E globbed `stock_shop_*_index.json`
**contract-compatible**, which is why E's validator never saw the atlas. Two lanes implemented opposite
halves of a self-contradicting spec and both were correct.
**F's recommendation (measured, not preferred):** amend the contract to G's shape —
`stock_godverse/<godverseShopId>/stock_<type>_index.json`. It is the only shape the runtime can load,
it keys per-shop by directory, and it costs G nothing (already emitted). E's glob follows. C owns the
wording (LANE_C_PUB); this is the amendment ledger #3 anticipated.
### ⚠ FINDING 2 — the gate built to catch this was blind, and green
`validate_atlas.py` globs for a filename that cannot exist at runtime, finds nothing, prints *"no
per-shop atlases yet — nothing to validate"* and **`return 0`**. `validate_manifest.py:158` propagates
that green (its comment states the intent: *"No atlases yet ⇒ clean pass"*). So the round's QA gate passes
whether the atlas is **absent, misnamed, or perfect** — it has never once looked at one. E built it in
wave 1 with no atlas to test against; on first contact (F, at close) it was blind.
The flaw is precise and the fix is a one-liner: the validator **conflates "no atlases exist" with "no
atlases matched my glob."** The first is a legitimate clean pass; the second is exactly the failure it
was written to catch. Cross-check the directory — if `stock_godverse/*/` is non-empty but no index
matched, that is an **ERR**, not a pass. *A gate that returns 0 on an empty set had better be certain the
set is empty.*
Pointing it at G's index by hand (filename drift removed) shows the drift is **deeper than the name**:
- **`ERR missing/empty provenance ['license','attribution','generator','snapshot']`** — but G *carries*
`generator` and `snapshot`. E's line 68 reads `idx.get(f)`**top-level** — while E's own docstring
(line 14) specifies them **nested under `provenance`**, which is where G put them. **E's code
contradicts E's doc**; G's atlas would fail even with E's exact field names.
- **`licence` vs `license`** — G used the British spelling; E's `REQUIRED_PROV` wants American.
- **`attribution`** — G folds the substance into `licence.covers`; E wants the field.
- **`WARN 2 atlases — C's rule is 1/shop`** — E built to C's *pre-correction* ceiling; C amended §7.2 to
`≤2 atlases @≤2048²` in the same wave. Stale by one round.
### ⚠ FINDING 3 — `#7a` would have passed vacuously
The brief says: *"boot `newtown_godverse` with `?stock=real` … riffle — **real covers render**."* They do.
F walked into **The Vintage Record** (a real Newtown shop, on its real road) via `DBG.enterShop`, and the
crates filled with **real cover art** — from E's **350-item town-wide v2 pack** (`Sunburnt Milk Bar Vol. 2`,
`Midnight Departure`), the same pack every record shop in every town has had since v2. **Not** Monster
Robot Party's 120. Network confirms: every stock fetch goes to `assets/models/`; **zero requests to
`assets/stock_godverse/`, ever.** Had F eyeballed the gate as written, it would have gone green and proven
nothing. *Two vacuous gates in one round — same species: both test the presence of a thing rather than
the identity of it.*
### The exact failure list for R24 — three gaps, none of them F-only
1. **The crate has no street.** [E + G] No town cache contains shop `3962749`; `redhill_real` does not
exist. Needs a `build_towns.py` TOWNS entry (~`center (-27.4553, 153.0064), span_km 2.4`) + one
outward-facing Overpass fetch → `redhill_real`, then `godverse_town.py redhill_real redhill_godverse`
with monsterrobot injected into `shops[]`. **F did not do this: E's file, G's generator, and an
outward-facing fetch — inventing town data is not F's to invent.**
2. **No identity field exists to key an atlas by.** [A + G] Verified in the live runtime: *every* shop in
`newtown_godverse` reports `godverse_id: null` — the cache schema carries no such field at all. C's
rule is *"C consumes whatever identity field lands, C does not invent it"* — so nobody has landed it.
A owns the CityPlan/cache contract; G emits it.
3. **The runtime wire.** [C + F] `stockpack.js` keys `_packs`/`_resolved` by **type alone** and returns a
cache hit before `base` is ever read (`stockpack.js:24`), so two stocked shops collide **and fail-soft
breaks** — an unstocked shop of a cached type inherits another shop's crate. C specced the seam (key by
`type+base`, per-shop preload on entry, LRU=1 + dispose) and owns the file; F then passes the per-shop
base at `interior_mode.js:192` and drops the town-wide preload at `index.html:199`.
**F deliberately did not build its half of #3 this round.** It is untestable while #1 and #2 are open
(no street to walk to, no identity field to pass), and F would be writing against three-quarters of a
spec that does not exist yet. Filed, not guessed. R24 order: **A+G (identity) → E (the town) → C (the
keying) → F (the wire + re-gate)**, and the crate walks.
### Also found
- **The godverse town is in no gate.** `newtown_godverse` is a shipping town cache that boots fine (F
verified: 18 real shops, real roads) but the town-matrix covers `newtown_real`, not it — so the one
town the whole epoch is built on is ungated. F's matrix, F's gap; it lands with the R24 re-gate rather
than a held round.
- **A's spacing warn earns immediately:** with toowoomba retired, **`ballarat_real` now warns at median
255 m > 150 m**. The thin tail has a second member — A's warn is doing exactly its job (warn, not error;
E curates). Filed for E's next curation pass.
- **`?noassets=1` + `&town=<key>` silently boots a *different town*** (the fixture, not the cache) — F's
own R17 wiring gates the cache fetch on `!NOASSETS`, correctly (noassets = zero network), but the tier
ladder's *"same shop under `?noassets`"* can't mean the same shop for a real town. Tier 0 for **stock**
is provable on the default boot; tier 0 for the **town** is a fixture fallback by design. Wording, not a bug.

File diff suppressed because it is too large Load Diff

View File

@ -54,7 +54,16 @@ TOWNS = {
"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},
"toowoomba_real": {"town": "Toowoomba", "state": "QLD", "center": (-27.5600, 151.9540), "span_km": 3.0},
# ── 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
# -27.5610,151.9535): it FIXED the named metric (median NN 395.7 → 89.4 m raw — normal for the
# pack) but made the real thing WORSE — hub density 3/12 → **2/12 within 160 m, the worst in the
# pack** (D's alive-darwin is 7/12). Toowoomba's shops are PAIRS strung along a road, not a high
# street; its densest 300 m cluster is only 3 shops, below MIN_TOWN_SHOPS, so no tighter box
# exists. Shipping it would have gamed the spacing metric while leaving a town nobody shops in.
# Retired rather than padded. (QLD keeps westend_real, 11 heroes / 44 shops.)
# "toowoomba_real": {"town": "Toowoomba", "state": "QLD", "center": (-27.5600, 151.9540), "span_km": 3.0},
"northbridge_real": {"town": "Northbridge", "state": "WA", "center": (-31.9490, 115.8570), "span_km": 2.4},
"adelaide_real": {"town": "Adelaide", "state": "SA", "center": (-34.9240, 138.6050), "span_km": 2.8},
"hobart_real": {"town": "Hobart", "state": "TAS", "center": (-42.8820, 147.3270), "span_km": 2.8},

245
pipeline/godverse_stock.py Normal file
View File

@ -0,0 +1,245 @@
#!/usr/bin/env python3
"""Lane G — G2a: per-shop REAL stock atlases (tier 1) for `?stock=real`.
THE FIRST REAL CRATE. One real shop's real crate — real sleeves, real titles, real prices —
baked to a static atlas keyed by godverse shop id. No server, no runtime query: tier 1 is files.
Two stages, split at THE SNAPSHOT BOUNDARY (the charter's determinism rule, and the same
discipline build_towns.py applies to Overpass raw):
snapshot POS DB + image map -> pipeline/_godverse/<shop>_<crate>.snapshot.json
pack snapshot + images -> web/assets/stock_godverse/<shop>/stock_record_*.{json,webp}
The snapshot is the artifact of record: it runs where the databases are, pins every source row
AND every cover's sha256, and is committed. The pack stage is pure — same snapshot + same image
bytes => byte-identical atlas, on any machine, with no DB. That split is what lets Lane E's
validate_atlas gate G's output on m3 without the dealgod DB.
python3 pipeline/godverse_stock.py snapshot --crate 550 --image-map <tsv>
~/Documents/MODELBEAST/venvs/mflux/bin/python pipeline/godverse_stock.py pack \
pipeline/_godverse/monsterrobot_550.snapshot.json # needs PIL (house convention)
THE SHOP (John's ruling, R23): `monsterrobot` — a real, trading record shop at 147 Musgrave Road,
Red Hill, Brisbane QLD. It is the same shop in three datasets, which is what makes tier 1 honest:
recordgod its POS: real crates, real slot numbers, real prices, real grading
monsterrobot.party its own website the covers are the SHOP'S OWN product shots
dealgod store 3962749 the scrape of that site; the cover cache + the godverse shop id
It is NOT in thriftgod's OSM census (see G-progress R23), so its godverse id is its dealgod store
id 3962749 real, stable, and clear of the census id space (thriftgod max shop id = 2992).
DETERMINISM: ordered queries, no RNG, no sampling, no clock in the output. Prices/titles/artists
are FACTS copied from the POS, not minted (thriftgod's mint() is TABLESAMPLE + unseeded random —
it cannot produce tier 1; see G3_ECONOMY_DESIGN.md §2).
LICENCE (charter law #3): covers are the shop's own product shots from the shop's own domain,
cached by dealgod. In-house 🟢. FLAG BEFORE ANY PUBLIC/COMMERCIAL RELEASE. Real titles/artists
ship at tier 1 per John's R23 amendment to the no-real-trademarks law (in-house green, same
flag). Provenance rides in every index. PII FENCE: recordgod holds real customer/staff/sales
tables this pipeline reads ONLY inventory/crate/disc_cache and must never read the others.
"""
import argparse, hashlib, json, os, subprocess, sys
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
SNAP_DIR = os.path.join(ROOT, "pipeline", "_godverse")
STOCK_DIR = os.path.join(ROOT, "web", "assets", "stock_godverse")
IMG_CACHE = os.environ.get("DEALGOD_IMG_CACHE", "/Volumes/johnking/dealgod_backup/images")
# The shop, per John's R23 ruling. `godverse_id` = dealgod store id (see module docstring).
SHOP = {
"godverse_id": 3962749, "slug": "monsterrobot", "name": "Monster Robot Party",
"address": "147 Musgrave Road", "suburb": "Red Hill", "city": "Brisbane",
"state": "QLD", "country": "AU", "type": "record",
}
OWN_PHOTO_HOST = "monsterrobot.party" # the licence test: the shop's own domain, nothing else
CELL_W, CELL_H, GRID, ATLAS_PX = 256, 256, 8, 2048 # Lane E's record contract (build_stock_pack.py)
PER_ATLAS = GRID * (ATLAS_PX // CELL_H) # 64
def price_band(price):
"""Lane E's record bands, verbatim — C's dig UI reads this (grail => `rare`)."""
p = price if price is not None else 4
return "bargain" if p < 8 else "standard" if p < 25 else "collector" if p < 60 else "grail"
def psql(db, sql):
"""Read-only query -> list of field-lists. psql CLI, not psycopg2: the m3 has no psycopg2, and
a snapshot stage that shells out has one less dep between the DB and the frozen file."""
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()]
def _n(v):
return None if v in ("", "\\N") else v
# ── stage 1: snapshot ────────────────────────────────────────────────────────────────────
def load_image_map(path):
"""release_id -> local_image, restricted to the SHOP'S OWN photos (the licence test).
On ultra (dealgod present) this is a query. On the m3 dealgod is a 2 GB dump and never
restored, so the map is streamed out of it once the command is recorded in the snapshot's
provenance so the extraction is reproducible:
gzcat dealgod-<date>.sql.gz | awk -F'\\t' '/^COPY public.products \\(/{c=1;next}
c && /^\\\\.$/{c=0;next} c && $50!="\\\\N"{print $50"\\t"$5"\\t"$34"\\t"$52"\\t"$56}'
(fields: 50=release_id 5=image_url 34=local_image 52=current_price 56=discogs_artist_name)
"""
m = {}
with open(path) as fh:
for ln in fh:
f = ln.rstrip("\n").split("\t")
if len(f) < 3:
continue
rel, url, local = f[0], f[1], f[2]
if OWN_PHOTO_HOST not in url or not local.startswith("/img/"):
continue
m.setdefault(rel, local) # first wins; ids are sorted upstream => stable
return m
def snapshot(args):
crate = psql("recordgod", f"SELECT id,name,COALESCE(label_text,'') FROM crate WHERE id={args.crate}")
if not crate:
sys.exit(f"crate {args.crate} not found in recordgod")
cid, cname, clabel = crate[0][0], crate[0][1], crate[0][2]
# ONLY inventory/crate/disc_cache — the PII fence (see module docstring).
rows = psql("recordgod", f"""
SELECT i.sku, COALESCE(i.slot_number::text,''), i.release_id,
COALESCE(d.artist,''), COALESCE(d.title, COALESCE(i.title,'')),
i.price, COALESCE(i.condition,''), COALESCE(i.sleeve_cond,'')
FROM inventory i
JOIN crate c ON c.id = i.crate_id
LEFT JOIN disc_cache d USING (release_id)
WHERE i.in_stock AND c.id = {args.crate} AND i.release_id IS NOT NULL
ORDER BY i.slot_number NULLS LAST, i.sku""")
imgs = load_image_map(args.image_map)
items, dropped = [], {"no_own_photo": 0, "file_missing": 0, "no_price": 0}
for sku, slot, rel, artist, title, price, cond, sleeve in rows:
local = imgs.get(rel)
if not local:
dropped["no_own_photo"] += 1
continue
path = os.path.join(IMG_CACHE, local[len("/img/"):])
if not os.path.isfile(path):
dropped["file_missing"] += 1
continue
if not _n(price):
dropped["no_price"] += 1
continue
with open(path, "rb") as fh:
sha = hashlib.sha256(fh.read()).hexdigest()
items.append({"sku": sku, "slot": int(slot) if slot else None, "release_id": int(rel),
"artist": artist, "title": title, "price": float(price),
"condition": cond, "sleeve_cond": sleeve,
"cover": local, "cover_sha256": sha})
if args.max_items:
items = items[:args.max_items]
snap = {
"version": 1, "kind": "record", "tier": 1,
"shop": SHOP,
"crate": {"id": int(cid), "name": cname, "label": clabel},
"counts": {"in_crate": len(rows), "baked": len(items), "dropped": dropped},
"source": {
"pos_db": "recordgod", "pos_dump": args.pos_dump,
"image_map_dump": args.image_map_dump, "image_cache": IMG_CACHE,
"captured": args.captured,
"extraction": "see load_image_map() docstring — the products projection command",
},
"licence": {
"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",
"pii": "none — inventory/crate/disc_cache only; customer/staff/sales never read",
},
"items": items,
}
os.makedirs(SNAP_DIR, exist_ok=True)
dst = os.path.join(SNAP_DIR, f"{SHOP['slug']}_{cid}.snapshot.json")
with open(dst, "w") as fh:
json.dump(snap, fh, indent=1, sort_keys=False)
fh.write("\n")
print(f"snapshot: crate {cid} '{clabel or cname}'{len(items)} real records "
f"(of {len(rows)} in crate; dropped {dropped}) -> {dst}")
# ── stage 2: pack (pure: snapshot + image bytes -> atlas) ─────────────────────────────────
def pack(args):
from PIL import Image
snap = json.load(open(args.snapshot))
shop, items = snap["shop"], snap["items"]
out_dir = os.path.join(STOCK_DIR, str(shop["godverse_id"]))
os.makedirs(out_dir, exist_ok=True)
idx_items, atlases = [], []
for ai in range((len(items) + PER_ATLAS - 1) // PER_ATLAS):
chunk = items[ai * PER_ATLAS:(ai + 1) * PER_ATLAS]
W, H = GRID * CELL_W, (ATLAS_PX // CELL_H) * CELL_H
sheet = Image.new("RGB", (W, H), (28, 28, 32))
aname = f"stock_record_atlas_{ai:02d}.webp"
for j, it in enumerate(chunk):
gx, gy = j % GRID, j // GRID
path = os.path.join(IMG_CACHE, it["cover"][len("/img/"):])
with open(path, "rb") as fh:
raw = fh.read()
if hashlib.sha256(raw).hexdigest() != it["cover_sha256"]:
sys.exit(f"cover drift: {it['sku']} {it['cover']} — snapshot sha mismatch, re-snapshot")
import io
cov = Image.open(io.BytesIO(raw)).convert("RGB").resize((CELL_W, CELL_H), Image.LANCZOS)
sheet.paste(cov, (gx * CELL_W, gy * CELL_H))
u0, v0 = gx * CELL_W / W, gy * CELL_H / H
idx_items.append({
"id": f"rec_{len(idx_items):04d}", "title": it["title"], "artist": it["artist"],
"price": int(round(it["price"])), "price_band": price_band(it["price"]),
"atlas": aname, "uv": [round(u0, 4), round(v0, 4),
round(u0 + CELL_W / W, 4), round(v0 + CELL_H / H, 4)],
})
sheet.save(os.path.join(out_dir, aname), "WEBP", quality=88, method=6)
atlases.append(aname)
with open(args.snapshot, "rb") as fh:
snap_sha = hashlib.sha256(fh.read()).hexdigest()
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,
"shop": {k: shop[k] for k in ("godverse_id", "slug", "name", "suburb", "state")},
"crate": snap["crate"],
"provenance": {
"generator": "pipeline/godverse_stock.py",
"source": snap["source"], "licence": snap["licence"],
"snapshot": os.path.basename(args.snapshot), "snapshot_sha256": snap_sha,
"determinism": "pure bake — same snapshot + same cover bytes => byte-identical atlas; "
"covers pinned by sha256, no RNG, no clock",
},
"items": idx_items,
}
dst = os.path.join(out_dir, "stock_record_index.json")
with open(dst, "w") as fh:
json.dump(index, fh, indent=1)
fh.write("\n")
print(f"pack: {len(idx_items)} real sleeves -> {len(atlases)} atlas(es) in {out_dir}")
print(f" shop {shop['godverse_id']} ({shop['slug']}) · crate "
f"{snap['crate']['id']} '{snap['crate']['label'] or snap['crate']['name']}'")
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
sub = ap.add_subparsers(dest="cmd", required=True)
s1 = sub.add_parser("snapshot", help="POS + image map -> frozen snapshot (runs where the DBs are)")
s1.add_argument("--crate", type=int, required=True)
s1.add_argument("--image-map", required=True, help="TSV: release_id, image_url, local_image, ...")
s1.add_argument("--max-items", type=int, default=0, help="cap (atlas budget — charter risk #2)")
s1.add_argument("--pos-dump", default="", help="recordgod dump filename, for provenance")
s1.add_argument("--image-map-dump", default="", help="dealgod dump filename, for provenance")
s1.add_argument("--captured", default="", help="snapshot date (YYYY-MM-DD), for provenance")
s1.set_defaults(func=snapshot)
s2 = sub.add_parser("pack", help="snapshot -> atlas + index (pure; needs PIL, no DB)")
s2.add_argument("snapshot")
s2.set_defaults(func=pack)
args = ap.parse_args()
args.func(args)

172
pipeline/validate_atlas.py Normal file
View File

@ -0,0 +1,172 @@
#!/usr/bin/env python3
"""Validate per-shop real-stock atlases — the G2a format gate (v5.0-alpha, ROUND23 ledger #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.
python3 pipeline/validate_atlas.py # exit 0 = every atlas valid (or none yet), 1 = a bad one
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).
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.
"""
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)
BANDS = {"bargain", "standard", "collector", "grail"}
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.
FORBIDDEN_ITEM_KEYS = {"seller", "seller_name", "owner", "owner_name", "customer", "user", "username",
"email", "phone", "mobile", "address", "member", "account", "consignor"}
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":
w, h = struct.unpack(">II", head[16:24])
return w, h
if head[:4] == b"RIFF" and head[8:12] == b"WEBP":
fmt = head[12:16]
if fmt == b"VP8X": # extended: 24-bit minus-one
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
w, h = struct.unpack("<HH", head[26:30])
return w & 0x3FFF, h & 0x3FFF
if fmt == b"VP8L": # lossless: packed 14-bit
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.basename(path)
try:
idx = json.load(open(path))
except Exception as e:
return [f"{name}: does not parse: {e}"], []
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)")
# ── C's draw ceiling (LANE_C_PUB §7.1) ──
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", [])
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) ──
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}")
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}²")
# ── the index itself (schema confirmed verbatim in LANE_C_PUB §7.3) ──
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): # UV origin top-left; C 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))
if not idxs:
print("atlas-QA: no per-shop atlases yet (Lane G's G2a) — nothing to validate")
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} "
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']}")
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())

View File

@ -152,6 +152,15 @@ def main():
except Exception as e:
warn(f"pack-QA skipped: {e}")
# per-shop atlas QA (v5 G2a, ROUND23 E #4): Lane G's tier-1 atlases are gate-checked from the
# committed files alone — no dealgod DB, no network. No atlases yet ⇒ clean pass.
try:
import validate_atlas
if validate_atlas.main() != 0:
err("per-shop atlas validation failed (see atlas-QA errors above)")
except Exception as e:
warn(f"atlas-QA skipped: {e}")
print(f"manifest v{m.get('version')} — facades {len(facade)}, "
f"skins {sum(len(v) if isinstance(v, list) else (len(v) if isinstance(v, dict) else 0) for v in sk.values())} groups, "
f"GLBs {n_glb}, audio {n_audio}")

Binary file not shown.

After

Width:  |  Height:  |  Size: 644 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 580 KiB

File diff suppressed because it is too large Load Diff

View File

@ -20,20 +20,26 @@ 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 v4.0 pack (R21): 22 towns · 1192 shops · 298 heroes**, 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.
**The pack: 21 real towns · 1180 shops · 295 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.
| state | towns |
|---|---|
| NSW (6) | katoomba · newtown · glebe · marrickville · newcastle (Hamilton strip) · bowral |
| VIC (7) | fitzroy · brunswick · geelong · bendigo · ballarat · castlemaine · daylesford |
| WA (2) | fremantle · northbridge |
| QLD (2) | westend · toowoomba |
| QLD (1) | westend |
| TAS (2) | hobart · launceston |
| SA (1) | adelaide |
| ACT (1) | braddon |
| NT (1) | darwin |
**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.
## 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

View File

@ -172,14 +172,6 @@
"roads": true,
"source": "osm"
},
{
"key": "toowoomba_real",
"town": "Toowoomba",
"state": "QLD",
"shops": 12,
"roads": true,
"source": "osm"
},
{
"key": "westend_real",
"town": "West End",

File diff suppressed because it is too large Load Diff

View File

@ -375,20 +375,53 @@ export class CitizenSim {
return base;
}
// nearest OPEN shop the ped is currently passing (current chunk, ≤PATRON_RANGE), else null.
// Hours-aware — at night only the openLate video shop qualifies, so its block draws the night crowd.
// nearest OPEN shop the ped is currently passing, else null. Hours-aware — at night only the openLate
// video shop qualifies, so its block draws the night crowd.
//
// [R23] The carried v4.x fix — GIG_RANGE (34 m) is a RADIUS, but this read only the ped's own 64 m chunk,
// so a gig venue genuinely in range, just across a chunk edge, was invisible. A's R21 cluster bias made
// that WORSE, not better: venues now sit INSIDE the retail cluster where peds converge from every side,
// so 42.6% of in-gig-range peds were chunk-blind (up from the 8.7% I filed at the old isolated pub).
//
// The fix is scoped to the GIG path on purpose. Extending the neighbour scan to ORDINARY shops as well
// was the literal brief, but measured it breaks the same brief's "byte-identical flags-off" clause: on
// the synthetic town (what ?classic=1 boots) 127 of 855 finds — 14.9% — would newly come from a
// neighbour chunk, i.e. the frozen v2 crowd would start shopping ~15% more. So:
// · ORDINARY shops: own chunk only — v2 semantics, frozen. A shopfront is noticed from its own block.
// · GIG venues: the full 34 m radius, honest across chunk edges — the gig is the "follow the sound"
// pull that is SUPPOSED to reach further; that is the whole point of GIG_RANGE > PATRON_RANGE.
// With no gig on, `_gigVenues` is empty and the neighbour loop is skipped entirely ⇒ ?classic and every
// flags-off boot take the identical code path AND identical cost to v2. Byte-identical by construction.
// Both ranges are < CHUNK (64), so a 3×3 sweep is exact, not an approximation. Fixed dz/dx order ⇒
// exact-distance ties resolve deterministically, as the old single-list scan did.
// Carried: PATRON_RANGE keeps v2's own chunk-blindness. Revisit only if the covenant is ever relaxed.
_nearestOpenShop(c) {
if (!this.shopsByChunk) return null;
const list = this.shopsByChunk.get(this.chunkKeyAt(c.x, c.z));
if (!list || !list.length) return null;
const cx = chunkCoord(c.x), cz = chunkCoord(c.z);
let best = null, bd = PATRON_RANGE;
let gigBest = null, gbd = GIG_RANGE; // R13: the nearest gig venue in range — it wins over ordinary
for (const s of list) { // shops (follow the sound), and across concurrent venues.
const own = this.shopsByChunk.get(chunkKey(cx, cz)); // shops (follow the sound), and across venues.
if (own) for (const s of own) {
if (!this._openAt(s.hours)) continue;
const d = Math.hypot(s.x - c.x, s.z - c.z);
if (this._gigVenues.has(s.shopId) && d < gbd) { gbd = d; gigBest = s; }
if (d < bd) { bd = d; best = s; }
}
if (this._gigVenues.size) { // gig on → the 34 m pull reaches across chunk edges (gigs only)
for (let dz = -1; dz <= 1; dz++) {
for (let dx = -1; dx <= 1; dx++) {
if (dx === 0 && dz === 0) continue; // own chunk already scanned above
const list = this.shopsByChunk.get(chunkKey(cx + dx, cz + dz));
if (!list) continue;
for (const s of list) {
if (!this._gigVenues.has(s.shopId)) continue; // neighbours contribute gig venues ONLY
if (!this._openAt(s.hours)) continue;
const d = Math.hypot(s.x - c.x, s.z - c.z);
if (d < gbd) { gbd = d; gigBest = s; }
}
}
}
}
return gigBest || best;
}
_beginVisit(c, shop) {

View File

@ -9,7 +9,8 @@
export { generatePlan, chunkIndex, chunkKey, CHUNK, lotCorners, obbOverlap, isOpen } from './plan.js';
export { shopName, townName, bandName } from './names.js';
export { generatePlanOSM, osmTownKeys, registerTownCache, validateTownCache, MIN_TOWN_SHOPS } from './plan_osm.js';
export { generatePlanOSM, osmTownKeys, registerTownCache, validateTownCache, MIN_TOWN_SHOPS,
medianShopSpacing, MAX_MEDIAN_SPACING_M } from './plan_osm.js';
export { withGigs, gigKeyFor, POSTER_CLEAR } from './gigs.js';
// The street-corridor law + venue vocabulary, re-exported so consumers get the whole Lane A contract
// from one import (ROUND13). `roadWidth`/`vergeBand`/`poleOffset` are the road-vs-verge split of

View File

@ -54,6 +54,46 @@ const ACCEPTED_SCHEMAS = new Set(['procity-town-cache/1', 'procity-town-cache/2'
export const MIN_TOWN_SHOPS = 6; // functional floor: up to 4 venues + the one openLate landmark + a spare
export const MAX_TOWN_SPAN_M = 5000; // a cache should be ONE compact town, not a region (see R17 risk note)
// ── the spacing floor (ROUND23 ledger #5 — D's R22 thin-tail finding, made law) ──────────────────
// MIN_TOWN_SHOPS counts shops; what makes a town READ ALIVE is clustering. D measured both 12-shop
// towns in the pack and they disagree — that disagreement is the whole finding:
// darwin_real ALIVE — shops sit on one strip; 1455 patronage checks → 18 visits.
// toowoomba_real DEAD — shops scattered over a 4.6 km road network; 1417 checks → 0 visits, 0 finds.
// "Not a shopping town — a road network with occasional shops."
// Both pass MIN_TOWN_SHOPS. One has a high street; the other doesn't. So: WARN, never error — E curates
// (re-bbox or retire, counted either way), the validator only flags. A flagged cache still boots a valid
// district, which is why this can't be an error: toowoomba shipped green in the whole v4.0 pack.
export const MAX_MEDIAN_SPACING_M = 150;
// D's metric: for each shop, the distance to its NEAREST OTHER shop; take the median. Exported so every
// lane measures the same thing (the R13-debt-#5 pattern — publish the law as a function, not a paragraph
// each lane re-implements). Returns null for a cache with < 2 usable shops (nothing to space).
//
// SPACE WARNING — this is CACHE space (raw lat/lon), not PLAN space (post-lift metres), and the two do
// NOT agree. The lift marches shops onto real edges at a regularised cadence, so it COMPRESSES dense
// towns (darwin: 68.3 m cache → 27.2 m plan) while leaving sparse ones alone (toowoomba: 396.1 → 386.8).
// D's notes quote PLAN figures (27 / 388); this function on the same towns returns 68.3 / 396.1. Both are
// right — they measure different spaces. Cache space is the correct one HERE: the validator's job is to
// flag a bad bbox at ingest, before any lift exists, and it is faithful exactly where the warn lives
// (the sparse end: ballarat plan/cache 1.01, toowoomba 0.98). See LANE_A_NOTES §R23 for the full table.
export function medianShopSpacing(cache) {
const shops = (cache && Array.isArray(cache.shops) ? cache.shops : []).filter(s => s && isNum(s.lat) && isNum(s.lon));
if (shops.length < 2) return null;
const lat0 = (cache.center && isNum(cache.center.lat)) ? cache.center.lat : shops[0].lat;
const kx = 111320 * Math.cos(lat0 * Math.PI / 180), kz = 111320; // same equirectangular convention as the span check
const nn = shops.map(s => {
let best = Infinity;
for (const o of shops) {
if (o === s) continue;
best = Math.min(best, Math.hypot((s.lon - o.lon) * kx, (s.lat - o.lat) * kz));
}
return best;
});
nn.sort((a, b) => a - b);
const m = nn.length >> 1;
return nn.length % 2 ? nn[m] : (nn[m - 1] + nn[m]) / 2;
}
// ── 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'.
@ -118,6 +158,13 @@ export function validateTownCache(cache) {
const latM = (mxLat - mnLat) * 111320, lonM = (mxLon - mnLon) * 111320 * Math.cos((c && c.lat || 0) * Math.PI / 180);
if (Math.max(latM, lonM) > MAX_TOWN_SPAN_M) warnings.push(`shops span ${(Math.max(latM, lonM) / 1000).toFixed(1)} km — likely more than one town (bound the query tighter)`);
}
// thin-tail warning (ROUND23 ledger #5): the span check catches a town that's too WIDE; this catches
// one that's too SPARSE to read as a town at all — D's finding. Metadata only, like the span warn:
// plan_osm still boots a valid district, it just has no high street to walk.
const spacing = medianShopSpacing(cache);
if (spacing !== null && spacing > MAX_MEDIAN_SPACING_M) {
warnings.push(`median shop spacing ${Math.round(spacing)} m > ${MAX_MEDIAN_SPACING_M} m — shops are scattered, not a high street (re-bbox onto the retail strip, or retire the town)`);
}
}
// v2: optional roads[] — simplified OSM ways. Each is `{ kind, pts:[[lat,lon],…] }` (id/name optional);
// `kind` is the OSM highway=* class. Absent (or a v1 cache) ⇒ the marched-avenue fallback, so nothing

View File

@ -8,7 +8,8 @@ import { readFileSync, existsSync, readdirSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
import { generatePlan, chunkIndex, lotCorners, obbOverlap, CHUNK } from './plan.js';
import { generatePlanOSM, osmTownKeys, validateTownCache, registerTownCache, MIN_TOWN_SHOPS } from './plan_osm.js';
import { generatePlanOSM, osmTownKeys, validateTownCache, registerTownCache, MIN_TOWN_SHOPS,
medianShopSpacing, MAX_MEDIAN_SPACING_M } from './plan_osm.js';
import { generatePlanFor, gigKeyFor, POSTER_CLEAR } from './index.js';
import { allFacadeSkins, SHOP_TYPES, VENUE_KINDS, genreForVenueKind, roadWidth } from '../core/registry.js';
import { xmur3 } from '../core/prng.js';
@ -400,6 +401,40 @@ for (const [label, cache, needle] of [
const v = validateTownCache(wide);
ok(v.ok && v.warnings.some(w => /span|town/.test(w)), `wide-spread cache: valid but warns (${v.warnings.find(w => /span/.test(w)) || 'NO WARN'})`);
}
// (c2) the SPACING warn (ROUND23 ledger #5 — D's thin-tail finding as law). The span warn catches a town
// that's too WIDE; this catches one too SPARSE to read as a town. Warn, never error — and the town must
// still boot green, because that's exactly what toowoomba did through the entire v4.0 pack.
{
// a scattered town: 12 shops ~400 m apart (toowoomba's shape) — inside the 5 km span law, so the span
// warn does NOT fire; this is the gap D found and the reason the metric exists.
const scattered = mkCache(Array.from({ length: 12 }, (_, i) => ({
id: 600 + i, name: `Scattered ${i}`, type: 'opshop',
lat: -33.7 + (i % 4) * 0.0036, lon: 150.3 + Math.floor(i / 4) * 0.0036, suburb: 'Testville',
})));
const v = validateTownCache(scattered);
ok(v.ok, `sparse cache: still VALID (warn, not error) — a flagged town boots a real district`);
ok(v.warnings.some(w => /median shop spacing/.test(w)), `sparse cache: warns (${v.warnings.find(w => /spacing/.test(w)) || 'NO WARN'})`);
ok(!v.warnings.some(w => /span/.test(w)), `sparse cache: the SPAN warn stays silent — spacing catches what span cannot`);
structuralSuite(generatePlanOSM(20261990, 'sparse', { cache: scattered }), `sparse-warn town`);
districtInvariants(generatePlanFor(20261990, 'osm', { gigs: true, town: 'sparse', cache: scattered }), `sparse-warn town`);
// a compact town must NOT warn (no false positive on the shape 20 of 23 pack towns have)
const tight = validateTownCache(mkCache(cluster(14)));
ok(!tight.warnings.some(w => /median shop spacing/.test(w)), `compact cache: no spacing warn (no false positive)`);
// the metric itself — D's definition: per shop, distance to the NEAREST OTHER shop, then the median.
ok(medianShopSpacing(mkCache(cluster(14))) < MAX_MEDIAN_SPACING_M, `medianShopSpacing: compact town under the floor`);
ok(medianShopSpacing(mkCache(cluster(14, { samePoint: true }))) === 0, `medianShopSpacing: coincident shops → 0 m`);
ok(medianShopSpacing(mkCache(cluster(1))) === null, `medianShopSpacing: < 2 shops → null (nothing to space)`);
ok(medianShopSpacing({ shops: [] }) === null, `medianShopSpacing: no shops → null, no throw`);
{
// exact-value check: two shops 100 m apart in latitude → both nearest-neighbours are 100 m → median 100.
const pair = mkCache([{ id: 1, name: 'A', type: 'opshop', lat: -33.7, lon: 150.3 },
{ id: 2, name: 'B', type: 'opshop', lat: -33.7 + 100 / 111320, lon: 150.3 }]);
const got = medianShopSpacing(pair);
ok(Math.abs(got - 100) < 0.5, `medianShopSpacing: 100 m pair measures ${got.toFixed(1)} m (projection sane)`);
}
}
// (d) the registry seam: register → osmTownKeys() sees it → generatePlanOSM resolves it
{
registerTownCache('regtest', mkCache(cluster(14)));