Compare commits
7 Commits
f14e4d1974
...
57973e0540
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
57973e0540 | ||
|
|
43b035aa2e | ||
|
|
d2eb9df03f | ||
|
|
84f3641791 | ||
|
|
84e0a1b764 | ||
|
|
47f05ecaa8 | ||
|
|
292b07aa91 |
@ -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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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:**
|
||||
|
||||
@ -1,11 +1,37 @@
|
||||
# LANE A — CITYGEN · progress (PROCITY-A)
|
||||
|
||||
*Status: **all deliverables landed; self-check all-green (161,300/161,300); rounds 2–22 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 2–23 closed** (see below).
|
||||
R23 (v5.0-alpha): D's thin-tail finding is law — `validateTownCache` warns above **150 m median shop
|
||||
spacing**; flags 2 of 23 towns (toowoomba 396 m, **ballarat 255 m — beyond the brief**). Warn, never error.
|
||||
R22: the pack absorbed — 23 towns, 36 goldens in one commit. classic `0x3fa36874` / gig `0xb1d48ea1` frozen.*
|
||||
*Date: 2026-07-16 · owner: PROCITY-A (Opus 4.8)*
|
||||
|
||||
## 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
|
||||
|
||||
@ -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
247
docs/G3_ECONOMY_DESIGN.md
Normal 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 0–1, 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).
|
||||
@ -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)
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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.3–2.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 2–4× 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 0–19 % 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
|
||||
|
||||
@ -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 ≈ 38–75% (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.**
|
||||
|
||||
@ -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.
|
||||
|
||||
1487
pipeline/_godverse/monsterrobot_550.snapshot.json
Normal file
1487
pipeline/_godverse/monsterrobot_550.snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -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
245
pipeline/godverse_stock.py
Normal 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
172
pipeline/validate_atlas.py
Normal 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())
|
||||
@ -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}")
|
||||
|
||||
BIN
web/assets/stock_godverse/3962749/stock_record_atlas_00.webp
Normal file
BIN
web/assets/stock_godverse/3962749/stock_record_atlas_00.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 644 KiB |
BIN
web/assets/stock_godverse/3962749/stock_record_atlas_01.webp
Normal file
BIN
web/assets/stock_godverse/3962749/stock_record_atlas_01.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 580 KiB |
1728
web/assets/stock_godverse/3962749/stock_record_index.json
Normal file
1728
web/assets/stock_godverse/3962749/stock_record_index.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -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
@ -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
@ -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) {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)));
|
||||
|
||||
Loading…
Reference in New Issue
Block a user