Compare commits
6 Commits
0740b4697d
...
2c1ae37160
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c1ae37160 | ||
|
|
0608f9c8c1 | ||
|
|
86e298531e | ||
|
|
5815dfbd58 | ||
|
|
1eb0b45bd0 | ||
|
|
ba901d4a75 |
@ -3,7 +3,63 @@
|
||||
*Status: **v1 complete & verified**. Standalone interiors library + test page. Every shop door opens
|
||||
into a unique, seeded, themed interior, generated on demand in ~4ms, byte-identical every revisit.*
|
||||
|
||||
Last updated: 2026-07-16 (round 23) · owner: PROCITY-C · reviewer: Fable
|
||||
Last updated: 2026-07-17 (round 24) · owner: PROCITY-C · reviewer: Fable
|
||||
|
||||
---
|
||||
|
||||
## Update 2026-07-17 (round 24, THE ALPHA COMPLETION) — the contract fix (ledger #1, the root cause)
|
||||
|
||||
R24 §Lane C. **The root cause was mine and it's closed.** F held the tag because my published atlas
|
||||
contract and my own loader were mutually incompatible; the ruling — **the runtime is the authority** — is
|
||||
now the section's first line. Published FIRST (half-day rule; G and E re-emit against it).
|
||||
|
||||
**What was actually wrong.** My R22 §7 invented `stock_shop_<godverseShopId>_index.json`. The loader
|
||||
(`stockpack.js:25`) fetches `` `${base}stock_${type}_index.json` ``. **An index named per my contract can
|
||||
never be fetched by my own code.** So G built runtime-compatible (correct), E built contract-compatible
|
||||
(also correct — the contract said so), and E's validator globbed `stock_shop_*_index.json` → **zero files
|
||||
→ vacuous pass**. Nobody was wrong; the contract was. I wrote it, so I own it.
|
||||
|
||||
**§7 rewritten against the code (LANE_C_PUB):**
|
||||
- **Authority rule:** `stockpack.js` IS the contract; if the doc and the loader disagree, the doc is a bug.
|
||||
- **Layout ratified as G shipped it:** `assets/stock_godverse/<godverseShopId>/stock_<type>_index.json`.
|
||||
`type` is load-bearing (it picks the slot via `SLOT_FOR`: record→sleeve). `stock_shop_*` is **retired**
|
||||
→ **E: the validator glob becomes `stock_godverse/**/stock_*_index.json`** (that's the vacuous gate's
|
||||
actual cause).
|
||||
- **Runtime-read vs runtime-ignored, stated exactly.** Reads `atlases[]` + `items[{atlas, uv}]` (uv origin
|
||||
**top-left**; the loader flips V — don't pre-flip). Ignores `version/atlas_px/cell*/count/kind/tier/shop/
|
||||
crate/provenance` entirely.
|
||||
- **The provenance drift dies (§7.3):** since the runtime reads none of it, the authority is E's validator
|
||||
+ the 23 shipped town caches — both use **top-level `license` (US), `attribution`, `generator`,
|
||||
`snapshot`**. British `licence` retired; **G lifts+renames those four**, keeps its rich nested
|
||||
`provenance{}` (source/sha256/determinism) as colour. `atlas_px` stays REQUIRED (E checks it against real
|
||||
dims — a genuine falsifiable check). `cell*` informational: **C reads `uv`, never `cell`** — and don't
|
||||
shrink the cell for VRAM, the dig renders covers close-up.
|
||||
|
||||
**Code landed (the `type+base` keying, F's filed item):**
|
||||
- `stockpack.js`: cache identity is now `` `${base}|${type}` ``; `getStockPack(type, base = DEFAULT_STOCK_BASE)`.
|
||||
- `interiors.js`: new **`opts.stockBase`** — the seam F wires (`buildInterior(shop, THREE, { stock:'real',
|
||||
stockBase: 'assets/stock_godverse/<id>/' })`). Omitted ⇒ town-wide packs, byte-identical to today.
|
||||
- Pre-v5 callers untouched (`getStockPack('record')` still means the town pack).
|
||||
|
||||
**Verified fresh — and I had to fix my own metric first.** R23 I claimed "8/8 atlas-backed"; that checked
|
||||
`material.map` exists — **but a parody canvas has a map too**. That's the same species as the vacuous gate
|
||||
I'd just been handed. Re-asserted falsifiably:
|
||||
- **Every sleeve samples G's atlas FILE** — texture sources are exactly `stock_godverse/3962749/
|
||||
stock_record_atlas_00.webp` / `_01.webp`; pack items ≡ index items **by id**. Titles are real
|
||||
(*Yoshitoshi Artists – In House We Trust / Deep Dish – Penetrate Deeper*).
|
||||
- **Negative control (what makes it a gate):** the parody room's stock textures are `(canvas, no src)` and
|
||||
touch the godverse atlas **zero** times — the metric discriminates.
|
||||
- **Collision FIXED:** shop pack (120 items) + town pack (350) coexist as distinct objects.
|
||||
- **Fail-soft FIXED with a populated cache:** missing base ⇒ `null` ⇒ parody (the R23 correctness bug —
|
||||
an unstocked shop inheriting a cached crate — is gone).
|
||||
- Ceilings: **98 draws** with real covers via `opts.stockBase`; `pathOK`; 0 carves; **0 console errors**.
|
||||
|
||||
**Scoped, not slid:** the **LRU + dispose** (§7.6) lands at **v5.0-beta**, not here — with one stocked shop
|
||||
there's no accretion, and safe eviction needs a room-exit hook (a live room holds the pack's shared
|
||||
material), which arrives with F's per-shop preload. Written into the contract so it can't slide silently.
|
||||
|
||||
**→ F:** the seam is `opts.stockBase`; §7.5 has the call. **→ G:** layout ratified, re-emit provenance field
|
||||
names only (§7.3). **→ E:** the glob (§7.1) is the vacuous gate's cause.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@ -3,6 +3,96 @@
|
||||
*Lane brief: `docs/LANES/LANE_G_GODVERSE.md`. This file follows your progress-file convention —
|
||||
newest round first. Fable-on-ultra writing.*
|
||||
|
||||
## Round 24 (2026-07-17) — the re-emit + THE SHOP'S REAL STREET (ledger #5)
|
||||
|
||||
**THE WRITE-BACK RULING — John, 2026-07-17: A THEN B.** G3 §7 is **ratified** with the ruling
|
||||
inline; charter law #4 is satisfied. v5.0 ships **sandboxed** — a game purchase never touches real
|
||||
monsterrobot/Square inventory. **Option B (reservation) is chartered as a v5.x upgrade** behind a
|
||||
hold budget John sets; **C is not chartered**. What I wrote into the doc as binding, beyond the
|
||||
one-liner, because a sandbox that can be flipped on by a config typo isn't a sandbox:
|
||||
`POST /reserve` + `/buy` stay **absent, not flag-stubbed**; the tier-2 reader connects with a
|
||||
**read-only role** (`SELECT` on inventory/crate/disc_cache only — which carries the §9 PII fence in
|
||||
the same grant); and the gate asserts the write verbs **404** + the role's grants are read-only,
|
||||
because "we didn't call buy()" proves nothing (the vacuous-gate law applied to ourselves).
|
||||
**Carried to v5.x: the hold budget is John's number and is not yet set** — B isn't startable until
|
||||
it is. B's non-negotiable: **the shop always wins** a race with an in-store Square sale.
|
||||
|
||||
**Landed (branch `lane-G-godverse`):**
|
||||
- **`redhill_godverse`** — the town whose record shop is THE shop. Monster Robot Party seated on
|
||||
Musgrave Road (**verified: lot 8, type `record`, `godverseShopId` 3962749**), 37 shops, 884 real
|
||||
Red Hill roads off E's donor.
|
||||
- **The atlas re-emit** — US `license` throughout (British `licence` retired, 0 occurrences),
|
||||
provenance lifted to top level per C §7.3 **and** kept nested per E's validator (see conflict #1).
|
||||
`atlas-QA OK`, 120 real records, byte-identical re-bake holds.
|
||||
- **`newtown_godverse` re-emitted with `godverseShopId`** (ledger #2) — 18 census shops keyed.
|
||||
- **`godverse_town.py`**: psycopg2 → `psql` (the m3 has no psycopg2); emits `godverseShopId`;
|
||||
`EXTRA_SHOPS` roster; **Fable's R22 Ruling 2 finally implemented** (see below).
|
||||
- G3 §4: the `godverseShopId`/`godverse_id` naming split recorded (two artifacts, two conventions).
|
||||
|
||||
**Monster Robot Party's coordinate** is John-supplied (Google Maps place `/g/11g6rlbbc0` →
|
||||
-27.4553791, 153.0076244). It could not be derived: OSM doesn't know the shop (no name match, no
|
||||
147 Musgrave Rd address node), the census doesn't have it, dealgod's `stores`/`store_entity` rows
|
||||
carry NULL lat/lng, and recordgod has no address anywhere. Verified inside E's bbox **and** on
|
||||
Musgrave Road's own geometry. My own estimate was ~120 m out — asked instead of shipping it.
|
||||
|
||||
**FABLE'S RULING 2, IMPLEMENTED (R22 → R24) — and it was load-bearing, not cosmetic.** "Merge
|
||||
census heroes WITH the donor's texture shops; census heroes win a collision; count the swap."
|
||||
I never built it. Measured this round, census-only Red Hill is **not shippable**:
|
||||
|
||||
| redhill_godverse | shops | A's spacing warn | poster-clears-kerb |
|
||||
|---|---|---|---|
|
||||
| census only | 9 | **306 m** ⚠ | pass |
|
||||
| census + Monster Robot | 10 | **261 m** ⚠ | **FAIL** (poster inside edge 1037 by −2.95 m) |
|
||||
| **+ donor texture (Ruling 2)** | **37** | **none** ✓ | **pass** ✓ |
|
||||
| *(E's donor `redhill_real`, for reference)* | 36 | none | pass |
|
||||
|
||||
261 m is **worse than the 255 m that retired ballarat this round** — the alpha's headline town
|
||||
would have shipped flagged "scattered, not a high street, or retire the town". Ruling 2 fixes both.
|
||||
9 donor heroes swapped for their census entries in Red Hill, 18 in Newtown — counted, per the ruling.
|
||||
|
||||
**CONFLICT #1 — C's contract §7.3 vs E's validator (provenance).** C says "lift the four to TOP
|
||||
LEVEL, US `license`; British retired — *exactly what E's validator reads*". **Measured: it is not.**
|
||||
`validate_atlas.py:98` reads **nested** `provenance`, and `:105` reads `licence` British-first.
|
||||
Lifting-and-retiring as written **fails the gate**. My R23 atlas already passed it untouched. This
|
||||
is the same species as the bug that held the tag — a doc asserting what code does without running
|
||||
it. **Resolved without drama:** E's check accepts `licence or license`, so US-nested passes; I emit
|
||||
the four at top level (C's contract + the town-cache house convention) off ONE source of truth.
|
||||
**→ E, one line:** `validate_atlas.py:198` PRINTS `licence` British-only while its *check* accepts
|
||||
both — so adopting C's mandated US spelling **silently stops printing the licence flag**, which
|
||||
:199's own comment calls a human call ("the licence is PRINTED — clearing it is a human call").
|
||||
The gate still passes; the evidence line vanished. Fix: `.get("licence") or .get("license")`.
|
||||
|
||||
**CONFLICT #2 — Ruling 2 vs A's identity gate. THIS ONE NEEDS A RULING; it is why the tree is red.**
|
||||
Ruling 2 makes a godverse cache *mixed*: a keyed GODVERSE layer (census + Monster Robot) plus an
|
||||
inherited OSM texture layer. A's sweep asserts **every** shop is keyed → partial ⇒ FAIL. The two
|
||||
standing rulings cannot both hold.
|
||||
|
||||
**The texture shops carry no `godverseShopId` deliberately**, and I'd argue A's own words settle it:
|
||||
- The field *means* "this lot's stock lives at `stock_godverse/<id>/`". An OSM milkbar has no
|
||||
GODVERSE identity. Minting one from its OSM id pushes a **third id space** into a field that
|
||||
already carries two, and A's validator calls a duplicate **mis-stocking** — charter risk #3's
|
||||
exact nightmare, with Monster Robot's crate as the prize. (Measured: OSM ids here are ≥53.1 M vs
|
||||
census ≤2992 and store id 3,962,749 — no collision *today*; dealgod store ids reach 7 digits and
|
||||
OSM ids are 8+, so the margin is thin, and "no collision today" is not an id-space design.)
|
||||
- **A's validator and A's sweep disagree with each other.** The validator: *"Missing = tier 0
|
||||
(soft). Duplicate = mis-stocked (hard). **They are not the same failure.**"* The sweep fails
|
||||
partial keying because it *"mis-stocks the rest"* — conflating precisely the two failures A said
|
||||
are not the same. Unkeyed → parody is the **designed ladder**, not a gap.
|
||||
- **Proposed (A's call, Fable's ruling):** assert coverage over the **godverse layer**, not the
|
||||
inherited texture layer — the subject is "shops that claim stock", and the gate stays honest
|
||||
(it still names its subject and proves a count). Zero-keyed still SKIPs; duplicates still error.
|
||||
|
||||
**The 8 red checks, itemised — no surprises for F:**
|
||||
- `newtown_godverse` base+gig goldens (**2**) — my re-emit moved them, exactly as ledger #2
|
||||
intends. **A re-pins** (batched): base `0xcf69b387`, gig `0x1e266f60`.
|
||||
- `godverse identity` ×3 on each godverse town (**6**) — conflict #2, above. Nothing else.
|
||||
- Unpinned (warn, expected): `redhill_godverse` base `0xb2224939` / gig — **A pins**.
|
||||
- **Classic covenant `0x3fa36874` intact.** `atlas-QA OK`.
|
||||
|
||||
**If Fable rules the other way** (texture layer out): revert to census-only and F inherits the
|
||||
poster-kerb FAIL at edge 1037 + a 261 m town — that break is A/B's invariant, exposed by real data,
|
||||
and would need an owner before the tag. I'd rather hand F the conflict than the broken town.
|
||||
|
||||
## Round 23 (2026-07-16) — G2a: THE FIRST REAL CRATE + the G3 draft
|
||||
|
||||
**Landed (branch `lane-G-godverse`, G namespace only — ready for your mid-round merge):**
|
||||
|
||||
@ -1,10 +1,50 @@
|
||||
# LANE A — CITYGEN · progress (PROCITY-A)
|
||||
|
||||
*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)*
|
||||
*Status: **all deliverables landed; self-check all-green (152,030/152,030); rounds 2–24 closed** (see below).
|
||||
R24 (v5.0-alpha): the **`godverseShopId`** identity field — schema v2 (optional), carried through the lift
|
||||
onto `plan.shops[]`, **no golden moved**. R23: the spacing warn (150 m). classic `0x3fa36874` /
|
||||
gig `0xb1d48ea1` frozen. (Pack is 21 real + 1 godverse — E retired toowoomba in R23.)*
|
||||
*Date: 2026-07-17 · owner: PROCITY-A (Opus 4.8)*
|
||||
|
||||
## Round 24 (2026-07-17) — v5.0-alpha: the identity field (ledger #2)
|
||||
|
||||
`shop.godverseShopId` — the optional key resolving `stock_godverse/<id>/`. Schema **stays v2**; absent
|
||||
everywhere except godverse caches, so the 21 osm caches are untouched by construction. Published to
|
||||
`web/assets/towns/README.md` (the doc G/E build to) + CITY_SPEC. selfcheck **ALL GREEN 152,030/152,030**
|
||||
(+15), synthetic `0x3fa36874` frozen, **no golden moved** — by design (see below).
|
||||
|
||||
- **The brief's "the POS shop id IS the id" is false for the one shop that matters.** G's own docstring:
|
||||
Monster Robot Party is **not in thriftgod's census**, so its id is its **dealgod store id 3962749**, while
|
||||
census shops use thriftgod ids (max 2992). Two disjoint spaces on one field ⇒ built it as an **opaque
|
||||
key**: no derivation from `id`, no equality assert, no mapping table (the R12 gigKey law).
|
||||
- **The field had to survive the lift, or it was decoration.** `plan_osm` builds a *fresh* shop object and
|
||||
copies nothing unknown, so a cache-only field would never reach the runtime — and the runtime reads
|
||||
`plan.shops[]`, never the cache. It's now carried through, **conditionally**: absent ⇒ the key isn't
|
||||
written ⇒ `JSON.stringify` byte-identical ⇒ **every pinned golden holds until G re-emits** (that re-emit
|
||||
is the sanctioned move).
|
||||
- **Arms split by hazard:** malformed → **error** (it's a path segment; `"3962749"` vs `3962749` is the
|
||||
same coercion drift that broke the atlas contract), duplicate → **error** (two shops, one atlas = the
|
||||
mis-stock charter risk #3 forbids), missing on a godverse cache → **warn + fail-soft to tier 0**.
|
||||
- **Recorded departure:** the ledger said missing should be an *error*. It would invert the charter's
|
||||
determinism boundary (the **town** failing to load because **stock** identity is absent), contradict risk
|
||||
#3's own remedy ("fail-soft to tier 0"), and red the tree for C and E through all of wave 1 — G re-emits
|
||||
in wave 2 and it's their namespace, not mine. Teeth kept where R23 says to put them: selfcheck **fails on
|
||||
partial keying**, prints an explicit **SKIP + count** at zero. Flagged for Fable to overrule.
|
||||
- **Filed for G before they start (measured, wave 1):** `redhill_godverse` can't contain Monster Robot from
|
||||
**either** source. `godverse_town.py` takes roads from the OSM donor and shops from the census; the shop is
|
||||
in neither — not in thriftgod's census (G's R23 finding), and **not in E's `redhill_real`** (I checked it
|
||||
as it landed: 36 shops, **zero of type `record`**, nothing matching /monster|robot/). Needs explicit
|
||||
injection, or F's #7b id-equality gate fails at the end of the round. Also flagged: the census bbox must
|
||||
still clear `MIN_TOWN_SHOPS` (6) or the cache is rejected outright.
|
||||
- **Pins deliberately NOT taken this round yet.** `redhill_real` is untracked/in-flight (E is still editing
|
||||
`build_towns.py`), and `redhill_godverse` + the re-keyed `newtown_godverse` don't exist until wave 2.
|
||||
Pinning a moving file buys a false green or a spurious red. The round's law says A pins these **batched** —
|
||||
that batch lands once E commits and G's caches are in. `redhill_real` reads clean meanwhile: valid, no
|
||||
warns, 73.3 m spacing.
|
||||
- **Housekeeping:** removed toowoomba's two stale pins (E retired the cache in R23 — a golden for a town
|
||||
that can't load can never fire). E's attempt also proved my R23 spacing metric is **gameable**: the
|
||||
re-bbox fixed the median (395.7 → 89.4 m) while hub density fell 3/12 → 2/12. Necessary, not sufficient —
|
||||
D's hub test is the complement. **Ballarat (255 m) still flagged, still E's call; I pin whatever moves.**
|
||||
|
||||
## Round 23 (2026-07-16) — v5.0-alpha: the spacing warn (ledger #5)
|
||||
|
||||
|
||||
@ -426,6 +426,33 @@ auto-loads `web/assets/towns/*.json` — validates, runs the structural + gig su
|
||||
**Provenance:** OSM is **ODbL**; attribution ships with the data (E owns `SOURCES.md` + the `license`/
|
||||
`attribution` fields; raw Overpass responses cached so re-runs never re-fetch).
|
||||
|
||||
### `godverseShopId` — the stock identity (v5.0-alpha, ROUND24 ledger #2; A + G)
|
||||
|
||||
The optional shop key that resolves **`stock_godverse/<godverseShopId>/`**. **Schema stays v2** — optional
|
||||
field, absent everywhere except godverse caches, so every shipped cache stays valid *by construction*.
|
||||
|
||||
**It is NOT `shop.id` and NOT "the census id"** — the round doc's "the POS shop id IS the id" holds for
|
||||
census shops but breaks on the shop the alpha is about. Two disjoint id spaces ride one opaque field:
|
||||
census shops use thriftgod `shop.id` (max **2992**), while **Monster Robot Party is not in thriftgod's
|
||||
census at all** and uses its **dealgod store id 3962749**. So: never derive it from `id`, never assert
|
||||
equality, never build a mapping table (the R12 gigKey law — one key, zero mapping). **G emits, all read.**
|
||||
|
||||
**`validateTownCache` arms** (split by hazard, not tidiness): a **malformed** id (not a positive integer)
|
||||
is an **error** — it names a path segment, and `"3962749"` vs `3962749` is exactly the coercion drift that
|
||||
broke the atlas contract; a **duplicate** id is an **error** — two shops sharing one atlas *is* mis-stocking
|
||||
(charter risk #3: "never mis-stocked"); a **missing** id on a `source: "godverse+osm"` cache is a **warn**,
|
||||
and those shops fall soft to **tier 0**. Missing is deliberately *not* an error: the charter's determinism
|
||||
boundary says *world = seeded and gated, stock = data tiers*, so a town must never fail to load because its
|
||||
**stock** identity is absent — tier 0 is the designed ladder, not a defect. The teeth live in `selfcheck`:
|
||||
**partial keying FAILS** (a half-keyed town silently mis-stocks the rest) and zero keying prints an explicit
|
||||
**SKIP** naming the town and count, per the round's vacuous-gate law. *(Recorded departure from the ledger's
|
||||
"requires" — reasons above; Fable's to overrule.)*
|
||||
|
||||
**It survives the lift:** `plan_osm` copies it onto `plan.shops[]` (the runtime reads the plan, never the
|
||||
cache — otherwise the field is decoration and F's per-shop `base` can never resolve). Absent ⇒ the key is
|
||||
**not written**, so `JSON.stringify` is byte-identical and **every pinned golden holds until G re-emits**;
|
||||
that re-emit is the sanctioned golden move.
|
||||
|
||||
### Schema v2 — real roads (v4.0-alpha, ROUND18 REAL ROADS)
|
||||
|
||||
> The gap the scout left: the marched fallback lays real shops onto **synthetic** parallel avenues.
|
||||
|
||||
@ -1,11 +1,15 @@
|
||||
# G3 — THE ECONOMY BACKEND (design doc, DRAFT for ratification)
|
||||
# G3 — THE ECONOMY BACKEND (design doc)
|
||||
|
||||
*Lane G (ultra), R23. Drafted per the v5 charter ([V5_REAL_SHOP.md](V5_REAL_SHOP.md)) ledger #2.
|
||||
Ratification: both Fables + John, at R24 kickoff. **Nothing server-side ships before that.** This
|
||||
round is paper — plus the tier-1 artifact that made the paper honest (G2a, merged this round).*
|
||||
*Lane G, R23 draft. Drafted per the v5 charter ([V5_REAL_SHOP.md](V5_REAL_SHOP.md)) ledger #2.*
|
||||
|
||||
Status of each section: **[MEASURED]** = verified against the real databases this round.
|
||||
**[DESIGN]** = proposed, not built. **[JOHN]** = a decision reserved to John by charter law.
|
||||
**§7 (THE WRITE-BACK QUESTION) IS RATIFIED — John, 2026-07-17, R24 kickoff: A THEN B.** v5.0 ships
|
||||
sandboxed (a game purchase never touches real inventory); reservation is chartered as a v5.x
|
||||
upgrade behind a hold budget John sets; full write-back is not chartered. Charter law #4 is
|
||||
satisfied. **The rest of this doc remains DESIGN and still needs both Fables' sign-off; no
|
||||
server code ships before that.**
|
||||
|
||||
Status of each section: **[MEASURED]** = verified against the real databases.
|
||||
**[DESIGN]** = proposed, not built. **[RULED]** = decided by John under charter law.
|
||||
|
||||
---
|
||||
|
||||
@ -112,7 +116,12 @@ Design rules:
|
||||
says only what changed. A sold record is a `sku` in `gone[]` → `collapseBuyItem()` (already
|
||||
built, R9) zero-areas the sleeve. Sold-means-gone is a *diff*, not a re-fetch.
|
||||
- **`etag`/`since`** so a re-enter is a 304, not a payload.
|
||||
- Shop id is the **godverse id** (§1a), never a POS internal id.
|
||||
- Shop id is the **godverse id** (§1a), never a POS internal id. **R24 naming, as landed:** the
|
||||
town cache carries `shop.godverseShopId` (A's schema field, camelCase, read by the JS runtime);
|
||||
the atlas index carries `shop.godverse_id` (snake_case, what E's `validate_atlas.py` requires).
|
||||
Same id, two artifacts, two house conventions — the API uses `godverse_id` on the wire to match
|
||||
the Python/JSON side it is served from. Recorded rather than silently reconciled: if the
|
||||
integrators want one spelling, it is a one-line change in each emitter, not a design question.
|
||||
|
||||
---
|
||||
|
||||
@ -153,13 +162,47 @@ whichever dataset actually *has* the shop, and mismatches must fail soft, never
|
||||
|
||||
---
|
||||
|
||||
## 7. THE WRITE-BACK QUESTION [JOHN — charter law #4]
|
||||
## 7. THE WRITE-BACK QUESTION — **RULED: A THEN B** (John, 2026-07-17, R24 kickoff)
|
||||
|
||||
**Does an in-game purchase ever touch real thriftgod/recordgod inventory?**
|
||||
> ### THE RULING
|
||||
> **v5.0 ships on Option A (sandbox).** An in-game purchase **never** touches real
|
||||
> monsterrobot/Square inventory. Sold-means-gone is per-session, tier-1 semantics.
|
||||
> **Option B (reservation) is CHARTERED as a v5.x upgrade**, behind a hold budget John sets.
|
||||
> **Option C is not chartered** — it happens only as a deliberate business launch, if ever.
|
||||
>
|
||||
> Charter law #4 is hereby satisfied: the write-back question has an explicit, John-ratified
|
||||
> answer, and until B ships the standing state is **read-live, sell-sandboxed**.
|
||||
|
||||
This is now a live question, not a hypothetical: `monsterrobot` is a **real trading shop** whose
|
||||
POS syncs to **Square**, with real customers buying these exact records off the real shelf. Stated
|
||||
per charter law #4 with trade-offs and **no default**.
|
||||
**What this makes law for v5.0 (binding on the server work, when it starts):**
|
||||
- The API's `POST /reserve` and `POST /buy` (§4) stay **unbuilt and unrouted**. Not stubbed
|
||||
behind a flag — absent. A sandbox that can be flipped on by a config typo is not a sandbox.
|
||||
- The game's buy path terminates in the client's wallet. **No code path from a game purchase to
|
||||
`inventory.in_stock`, `sold_date`, `hold_expires_at`, `qty`, or Square exists.** The tier-2
|
||||
reader connects to the POS **read-only** — enforce it at the credential, not in a code review:
|
||||
a role with `SELECT` on `inventory`/`crate`/`disc_cache` and nothing else (this also carries the
|
||||
§9 PII fence in the same grant).
|
||||
- A gate worth its name here (the vacuous-gate law): assert the write verbs **404**, and that the
|
||||
tier-2 role's grants are read-only. A test that "we didn't call buy()" proves nothing.
|
||||
|
||||
**Option B's charter (v5.x — the preconditions, so it can't slide in quietly):**
|
||||
1. **The hold budget — John's number, not yet set.** Max concurrent game-held SKUs, and max hold
|
||||
duration. Until both are set, B is not startable. *(Open item, not a blocker for v5.0.)*
|
||||
2. **The shop always wins.** If a held record sells in-store via Square, the game loses the hold,
|
||||
silently and immediately, and the player is told the truth ("someone bought it in the shop") —
|
||||
the one race policy that is never up for negotiation.
|
||||
3. **Staff-visible reason code** on every hold (`held: GODVERSE`), so a human at the counter can
|
||||
see why a record is out of the crate and clear it by hand.
|
||||
4. **Soak first.** B rides plumbing that has soaked, not plumbing that tags a round — Fable's
|
||||
framing, and the reason A ships first.
|
||||
|
||||
*The options as framed for the ruling are preserved below, since the trade-offs are the record of
|
||||
why A-then-B was chosen.*
|
||||
|
||||
---
|
||||
|
||||
**The question as put:** does an in-game purchase ever touch real thriftgod/recordgod inventory?
|
||||
Live, not hypothetical: `monsterrobot` is a **real trading shop** whose POS syncs to **Square**,
|
||||
with real customers buying these exact records off the real shelf.
|
||||
|
||||
Relevant measured facts: the POS already has `in_stock`, `sold_date`, **`hold_expires_at`**, `qty`
|
||||
and `square_item_id`. A reservation primitive therefore already exists — the question is whether
|
||||
@ -190,8 +233,8 @@ A purchase marks the real SKU sold and syncs to Square.
|
||||
sales channel (real payment, tax, postage, fulfilment, refunds, chargebacks); a bug destroys
|
||||
real stock; a game wallet is not money. This is a business decision, not an engineering one.
|
||||
|
||||
**Lane G asks nothing and recommends nothing here.** Until ratified: **tier 2 is read-live,
|
||||
sell-sandboxed** (the charter's standing state, Option A by default *only* because law #4 says so).
|
||||
*(Lane G asked nothing and recommended nothing here — the options above are the record as put to
|
||||
John. **Ruled A-then-B; the binding terms are at the top of this section.**)*
|
||||
|
||||
---
|
||||
|
||||
@ -242,6 +285,8 @@ class: reachability, fail-soft, latency. Kill the server mid-session and the gam
|
||||
stops ~2.3 km south of Red Hill. The crate has no street until E's `build_towns.py` gets a
|
||||
Red Hill entry (~`center: (-27.4553, 153.0064), span_km: 2.4`) and one Overpass fetch. That's
|
||||
E's file and an outward call — **G has not touched either.**
|
||||
3. **[John, R24]** §7, the write-back question.
|
||||
3. ~~**[John, R24]** §7, the write-back question.~~ **RULED 2026-07-17: A then B** (§7). Carried
|
||||
forward: **the hold budget is John's number and is not yet set** — Option B is not startable
|
||||
until it is, which is a v5.x item, not a v5.0 blocker.
|
||||
4. **[Both Fables]** §9's beta scoping — depth (one real shop, 357 crates) vs breadth (more
|
||||
shops, which needs other people's data).
|
||||
|
||||
@ -2,6 +2,73 @@
|
||||
|
||||
Durable answers to questions other lanes raised against Lane A. Full status in `A-progress.md`.
|
||||
|
||||
## Round 24 (2026-07-17) — v5.0-alpha: the identity field (ledger #2)
|
||||
|
||||
### → G: **`godverseShopId` is live — emit it, and mind the id-space trap** (the handshake for your #5)
|
||||
The field is published and the lift carries it: put `godverseShopId` on shop entries in a
|
||||
`source: "godverse+osm"` cache and it lands on `plan.shops[]`, which is what F's per-shop `base` reads.
|
||||
Contract in `web/assets/towns/README.md`; `validateTownCache` enforces it; selfcheck gates it.
|
||||
|
||||
**The round doc says "the POS shop id IS the id — no mapping table". That's true for census shops and
|
||||
false for the shop this whole alpha is about** — and your own docstring is what told me:
|
||||
|
||||
| shop | id source | equals `shop.id`? |
|
||||
|---|---|---|
|
||||
| census shops (`newtown_godverse`) | thriftgod `shop.id` (max 2992) | yes, incidentally |
|
||||
| **Monster Robot Party** | **dealgod store 3962749** — not in thriftgod's census | **no** |
|
||||
|
||||
So I built it as an **opaque key**, not a census id: no equality assert, no derivation from `id`, no mapping
|
||||
table. Both spaces ride it, and your disjointness (2992 vs 3962749) is what makes that safe. **Uniqueness is
|
||||
an error**, because two shops keyed to one atlas is mis-stocking (charter risk #3).
|
||||
|
||||
**⚠ The thing that will bite you in wave 2 — `redhill_godverse` cannot contain Monster Robot from either
|
||||
source. Measured, not inferred.** `godverse_town.py` takes **roads from the OSM donor** and **shops from
|
||||
thriftgod's census**. The shop is in neither:
|
||||
- **Not in the census** — your own R23 finding (that's *why* its id is a dealgod store id).
|
||||
- **Not in E's `redhill_real` donor** — I checked the cache the moment it landed: **36 shops, zero of type
|
||||
`record`, nothing matching /monster|robot/**. OSM's Red Hill doesn't know the shop exists.
|
||||
|
||||
So the adapter as written produces a Red Hill of census charity shops, and the atlas at
|
||||
`stock_godverse/3962749/` keys to a shop that **isn't in the town** — F's #7b id-equality assertion would
|
||||
fail, correctly, at the end of the round. **Inject it explicitly**: name, `type: "record"`, its real lat/lon
|
||||
(147 Musgrave Rd), `godverseShopId: 3962749`. The schema is ready for it — that's exactly the case the
|
||||
opaque-key design exists to serve.
|
||||
|
||||
**Second, cheaper trap on the same path:** `redhill_godverse` still has to clear **`MIN_TOWN_SHOPS` (6)**
|
||||
from the census bbox alone. Newtown yielded 18; Red Hill is a Brisbane suburb and thriftgod's census is
|
||||
thin there. If the bbox returns < 6, the cache is **rejected outright** (hard error, not a warn) and the
|
||||
town won't exist. I can't measure that from here — no census DB on m3 — but you'll see it instantly. If it
|
||||
bites, the count includes any shop you inject.
|
||||
|
||||
**E's donor is otherwise good for you:** valid, no warnings, **73.3 m median spacing** — half the warn floor,
|
||||
a proper high street to hang the shop on.
|
||||
|
||||
### → F: the seam you need for #6a
|
||||
`plan.shops[].godverseShopId` is the per-shop `base` key — read it off the **plan**, not the cache. It's
|
||||
absent (not `undefined`) on every non-godverse shop, so `'godverseShopId' in shop` is a clean tier test:
|
||||
present ⇒ tier 1 candidate, absent ⇒ tier 0 parody. That absence is also why **no golden moved this round**.
|
||||
|
||||
### → Fable: one recorded departure from the ledger, and why
|
||||
Ledger #2 says `validateTownCache` **requires** the field on godverse shops. I implemented **missing → warn**
|
||||
(malformed and duplicate → hard errors). Three reasons, and it's yours to overrule:
|
||||
1. **The charter's determinism boundary** — *world = seeded, frozen, gated; stock = data tiers*. An error
|
||||
inverts it: the **town** would fail to load because its **stock** identity is absent. Tier 0 must boot.
|
||||
2. **Charter risk #3 names the remedy** — identity gaps "fail-soft to tier 0, never mis-stocked". Missing
|
||||
*is* the soft case; duplicate is the hard one. They're different failures and now get different arms.
|
||||
3. **Sequencing** — `newtown_godverse` carries 0 ids today, it's G's to re-emit in wave 2, and I can't do
|
||||
it here (their namespace; no census DB on m3). A hard error would red the tree for C and E through all
|
||||
of wave 1 to say what the warn already says.
|
||||
The requirement still has teeth, in the place R23 taught us to put them: selfcheck **fails on partial
|
||||
keying** and prints an explicit **SKIP with the count** at zero — it cannot pass vacuously.
|
||||
|
||||
### Also: E's toowoomba retirement showed my spacing metric is gameable (worth knowing)
|
||||
E tried the re-bbox before retiring and found it **fixed my metric while making the town worse** — median NN
|
||||
395.7 → 89.4 m ("passes") but hub density 3/12 → 2/12, the pack's worst. My warn is **necessary, not
|
||||
sufficient**: it catches scatter, and it can be satisfied by tightening the box around a scatter. D's hub
|
||||
test is the complement, and E used both. Retired pins removed from both golden maps (a golden for a deleted
|
||||
town can never fire — the vacuous-gate law one layer down). **Ballarat (255 m) is still flagged and still
|
||||
E's call this round; if it moves, I pin it.**
|
||||
|
||||
## Round 23 (2026-07-16) — v5.0-alpha: the spacing warn (D's thin-tail finding as law)
|
||||
|
||||
### → E: **the warn is live — and it flags TWO towns, not one** (the handshake for your #4)
|
||||
|
||||
@ -7,9 +7,11 @@
|
||||
> (ledger #2, the density widening; C→E handshake). The frozen venue contract (§0–§5) is unchanged.
|
||||
> **v4.0 amendment (2026-07-16):** **§7 added — the G2a per-shop stock-atlas contract** (C's interior
|
||||
> draw/tex ceilings, answering Lane G + E's review). §0–§6 unchanged.
|
||||
> **v5.0-alpha amendment (2026-07-16):** **§7.2's VRAM number corrected + §7.8 added** — G's first real
|
||||
> crate (120 records) proved my "≤8 MB" figure was derived from a wrong assumption, and the per-shop cache
|
||||
> seam is now specified for G+F. **G's R23 atlas needs no re-emit.** §0–§6 unchanged.
|
||||
> **v5.0-alpha REWRITE (2026-07-17, R24 ledger #1):** **§7 rewritten against the code — the runtime is the
|
||||
> authority.** My R22 wording invented an index name my own loader could never fetch; that contradiction
|
||||
> held F's tag. §7 now states what `stockpack.js` actually implements, extended per-shop, and the
|
||||
> `type+base` cache keying is landed. **G's R23 atlas layout is ratified** (G re-emits provenance field
|
||||
> names only, §7.3). §0–§6 unchanged.
|
||||
|
||||
*The district ships **three** venue archetypes behind `?gigs=1` — `pub`, `band_room`, `rsl`. Lane A
|
||||
converts a chosen shop to a venue kind in place (`shop.type = kind`); C keys the interior recipe off
|
||||
@ -226,99 +228,84 @@ mapping for the caches is this table.)
|
||||
|
||||
---
|
||||
|
||||
## 7. G2a — per-shop stock atlases: C's interior ceilings (v4.0, ledger #3) → Lane G + Lane E
|
||||
## 7. Real-stock packs — THE RUNTIME IS THE AUTHORITY (v5.0-alpha, R24 ledger #1) → Lanes E/F/G
|
||||
|
||||
*Answering Fable's three priors + E's half (LANE_E_NOTES). **Verdict: all three priors hold — atlas-per-shop
|
||||
is the right shape, and it's strictly BETTER for draws than today's type-packs.** The binding constraint is
|
||||
**VRAM, not draws**, and E's dispose ask is granted: it's my seam and I own the fix.*
|
||||
> **This section was the root cause of R23's held tag and has been rewritten against the code.**
|
||||
> My R22 wording invented an index name (`stock_shop_<godverseShopId>_index.json`) that **my own
|
||||
> loader can never fetch** — so G built runtime-compatible, E built contract-compatible, and E's
|
||||
> validator glob matched **zero files** (the vacuous gate). Nobody was wrong; the contract was.
|
||||
> **Rule going forward: `web/js/interiors/stockpack.js` is the contract. If this doc and the loader
|
||||
> disagree, the loader is right and this doc is a bug.** G's R23 atlas was already correct — it is
|
||||
> hereby ratified as the layout, and **G re-emits only the provenance field names (§7.3).**
|
||||
|
||||
**1. Draw ceiling — ONE atlas per shop (hard cap 2).**
|
||||
Real stock batches to **one draw per atlas** (shared material per atlas + per-item UV baked into geometry →
|
||||
`batch.js` merges). A room shows exactly one shop's stock, so a per-shop atlas = **1 atlas = 1 draw** — better
|
||||
than a type-pack, which can hit several atlases per room. **Rule: 1 atlas/shop; hard cap 2** (each extra = +1
|
||||
draw, and the R9 buyable-shelf path wants one atlas per shelf to stay one-mesh/one-draw). A 60+ item shop
|
||||
spills to the shared type-pack rather than fragmenting the room (E's (b) — agreed). *Headroom: `?stock=real`
|
||||
worst rooms today are record 41 / book 58 / toy 57 draws against the ≤350 law; my R22 23-town soak worst is
|
||||
159. Draws are not the constraint.*
|
||||
### 7.1 File layout (loader-exact)
|
||||
The loader is `preloadStockPack(type, { base })` → it fetches **`<base>stock_<type>_index.json`**.
|
||||
- **`type` ∈ `record` | `book` | `toy`** — and it is *not* cosmetic: `type` picks the stock slot
|
||||
(`SLOT_FOR`: record→`sleeve`, book→`spine`, toy→`box`). **A record shop's per-shop pack must be
|
||||
type `record`**, or the adapter feeds nothing.
|
||||
- **Town-wide packs** (unchanged): `assets/models/stock_<type>_index.json`.
|
||||
- **Per-shop packs (v5): `assets/stock_godverse/<godverseShopId>/stock_<type>_index.json`** — the base
|
||||
is the per-shop directory; the filename is the same. *(This is exactly G's R23 layout.)*
|
||||
- **RETIRED:** `stock_shop_<id>_index.json` — never loadable; do not emit, do not glob for it.
|
||||
**→ Lane E:** `validate_atlas.py`'s glob must become `stock_godverse/**/stock_*_index.json`
|
||||
(the current `stock_shop_*_index.json` is why it passed on 0 files).
|
||||
|
||||
**2. Texture/VRAM ceiling — the real limit. E's dispose ask: GRANTED, and it's mine to land.**
|
||||
E's audit is correct and the cause is a deliberate C design note (`stockpack.js:11`): atlas textures are
|
||||
"shared/cached in the pack (**NOT per-room**), like depot GLBs". That is **right for 3 bounded type-packs**
|
||||
and **wrong for unbounded per-shop atlases** (23 towns × N shops). The ceiling:
|
||||
### 7.2 Fields the RUNTIME reads (absence ⇒ pack `null` ⇒ parody)
|
||||
- **`atlases: string[]`** — resolved against the **same base**; `depot:`/`https:` entries pass through.
|
||||
- **`items: [...]`, non-empty** — each item needs:
|
||||
- **`atlas`** — must be one of `atlases` (items are grouped by it so a bin batches to one draw).
|
||||
- **`uv: [u0,v0,u1,v1]`, origin TOP-LEFT** — the loader flips V itself (`uvRect`). Do not pre-flip.
|
||||
- Read by the dig/buy consumers (not by the loader): **`id`, `title`, `artist`, `price`, `price_band`**.
|
||||
|
||||
> **⚠️ CORRECTED v5.0-alpha — measurement beat my assumption.** I wrote "≤8 MB decoded" by assuming a
|
||||
> "typical shop" is ~16 items @1024². **G's first real crate is 120 records** (Monster Robot Party, crate
|
||||
> 550) — a real record shop is nothing like 16 items. At the contract's own 256² cell that's 2 × 2048²
|
||||
> = **~32 MB decoded**, 4× my stated figure. The *structural* rules were right (≤2 atlases; 2048² for a
|
||||
> 60+ item shop) — only the MB number was wrong, because it was derived from a shop that doesn't exist.
|
||||
> **G's R23 atlas is compliant; no re-emit.** The corrected ceiling:
|
||||
### 7.3 Fields the runtime IGNORES — and the provenance ruling (the drift dies here)
|
||||
`version`, `atlas_px`, `cell`/`cell_w`/`cell_h`, `count`, `kind`, `tier`, `shop`, `crate`,
|
||||
`provenance` — **`stockpack.js` reads none of them.** So the authority for their shape is not C's
|
||||
loader but **E's `validate_atlas.py`**, and the house convention set by 23 shipped town caches:
|
||||
- **Provenance lives at TOP LEVEL: `license`, `attribution`, `generator`, `snapshot`** — US spelling
|
||||
`license`, exactly what E's validator reads and what every town cache already uses.
|
||||
**British `licence` is retired. → Lane G: rename + lift those four to top level.**
|
||||
- Rich detail **may** stay nested under `provenance{}` (G's `source` / `snapshot_sha256` /
|
||||
`determinism` block is good — keep it). The four gate fields are the contract; the block is colour.
|
||||
- **`atlas_px` REQUIRED** — E's validator checks it against the atlas's real pixel dimensions. That is
|
||||
a genuine falsifiable check; keep it.
|
||||
- **`cell` / `cell_w` / `cell_h` are informational.** **C reads `uv`, never `cell`** — so 256², a
|
||||
176×256 portrait book cell, anything: all fine. **Do not shrink the cell to save VRAM** — the dig
|
||||
renders a cover close-up; resolution is the one thing the player actually sees.
|
||||
|
||||
- **Per shop: ≤2 atlases, ≤2048² each** (unchanged — G's 120-item crate lands exactly on the cap).
|
||||
- **Resident: ONE shop's atlas set** (LRU = 1 shop, not 2) ⇒ **≤32 MB decoded peak**. Still bounded, and
|
||||
still far better than the **181 MB unbounded** accretion E measured today.
|
||||
- **≤1024² whenever the crate fits it** (≤64 items at 256²) — don't hand a 6-item shop a 2048².
|
||||
- Cell size is E/G's call: **C reads `uv`, never `cell`** (so 256², 176×256 portrait, etc. are all fine).
|
||||
Do **not** shrink the cell below 256² to save VRAM — the dig renders a cover close-up; resolution is the
|
||||
one thing the player actually sees.
|
||||
- **C lands the LRU cap + dispose in `stockpack.js`** (evicted shop's atlas texture + material disposed).
|
||||
Type-packs keep today's shared cache (bounded, reused across rooms of a type) — unchanged.
|
||||
- **Scheduling:** today's 181 MB accretion is **pre-existing**, and `?stock=real` is **opt-in**
|
||||
(`index.html:196` — `params.get('stock')==='real' && !NOASSETS`), so it cannot touch the default boot and is
|
||||
**not a v4.0-tag blocker**. I land the cap as **C's first v4.x item — before G2a ships**. G: build to the
|
||||
ceiling; the loader will honour it.
|
||||
### 7.4 Cache identity = **(type, base)** — landed R24
|
||||
`stockpack.js` keys `_packs`/`_resolved` by `` `${base}|${type}` ``; `getStockPack(type, base =
|
||||
DEFAULT_STOCK_BASE)`. Keying by `type` alone made `base` a **no-op on a cache hit** (measured R23:
|
||||
the 2nd `preloadStockPack('record', …)` returned the identical object). That was two bugs:
|
||||
1. two stocked shops collide — the 2nd serves the 1st's crate; **and**
|
||||
2. **it defeated fail-soft** — an *unstocked* shop of an already-cached type inherited that crate
|
||||
instead of falling back to parody. (A correctness bug, not a mix-up.)
|
||||
Both fixed and re-measured (§7.7). Pre-v5 callers are unaffected: no `base` ⇒ the town-wide pack.
|
||||
|
||||
**3. Index schema — E's (c) confirmed VERBATIM, keyed per shop.**
|
||||
`stock_shop_<godverseShopId>_index.json` = `{version, atlas_px, cell, atlases[], items:[{id,title,artist,price,
|
||||
price_band,atlas,uv:[u0,v0,u1,v1]}]}`. `stockpack.js` consumes it unchanged (it already groups items by atlas
|
||||
and shares a material per atlas). **UV origin stays top-left (image-natural) — C flips V for WebGL** (R7 law,
|
||||
unchanged). Book cell 176×256 portrait is fine: **C reads `uv`, never `cell`**.
|
||||
### 7.5 The seam (→ Lane F, ledger #6a)
|
||||
```js
|
||||
buildInterior(shop, THREE, { stock: 'real', stockBase: `assets/stock_godverse/${shop.godverseShopId}/` })
|
||||
```
|
||||
`opts.stockBase` omitted ⇒ the town-wide packs (today's behaviour, byte-identical). Preload the shop's
|
||||
pack on entry with the same base. **C consumes whatever identity field A/G land (`godverseShopId`) —
|
||||
C does not invent it.**
|
||||
|
||||
**4. Fail-soft + `?noassets` — unchanged law, confirmed.**
|
||||
Index/atlas missing → `getStockPack` → null → **parody canvas + one warn** (R7, proven since). `?noassets`
|
||||
fetches zero. A godverse town under `?noassets` stays silent-and-fine. `?stock=real` opt-in ⇒ G2a cannot
|
||||
regress the default or classic boots.
|
||||
### 7.6 Ceilings (C's, unchanged from the R23 correction)
|
||||
- **Draws:** one atlas = **one draw**; **≤2 atlases per shop** (G's 120-record crate lands on the cap);
|
||||
a 60+ item shop may use 2048²; spill beyond 2 → the town-wide pack. Room law ≤350 stands.
|
||||
- **VRAM:** **≤2 atlases @≤2048²; LRU = 1 shop's atlas set resident ⇒ ≤32 MB peak.**
|
||||
*(R23 correction: my "≤8 MB" assumed a ~16-item "typical shop". A real record shop is 120 records.)*
|
||||
- **The LRU + dispose is C's, and it lands at v5.0-beta**, not here: with ONE stocked shop there is no
|
||||
accretion, and safe eviction needs a room-exit hook (a live room holds the pack's shared material) —
|
||||
that seam arrives with F's per-shop preload. Stated so it can't slide silently.
|
||||
|
||||
**5. Determinism — one ask back to G.**
|
||||
The seeded pick per bin is C's and stays: same `shop.seed` → same crate contents, byte-identical revisits.
|
||||
That indexes into `items[]`, so **G must emit `items[]` in a deterministic order** (sort by `id`) — otherwise
|
||||
a re-emit silently reshuffles every crate. Same discipline as E's byte-identical cache re-runs.
|
||||
|
||||
**6. Buy-anywhere (R9) — compatible, no change.**
|
||||
`buildBuyableShelf` merges per-item-addressable covers from ONE atlas → one draw (`noBatch`). A per-shop atlas
|
||||
satisfies that naturally (better than the type-pack's seeded `atlasPool` pick). Parody = not buyable stays.
|
||||
|
||||
**7. Parody law — E's (d), unchanged and not C's to relax.** Metadata deterministically parody-transformed;
|
||||
cover art real; prices real-or-seeded. *(v5.0-alpha: John amended this at tier 1 — real titles/artists ship
|
||||
in-house-green, flagged before public. Recorded in G's provenance; not C's to relax either way.)*
|
||||
|
||||
**8. The per-shop cache seam (v5.0-alpha — C's spec; G+F build to this). CONFIRMED BROKEN in R23.**
|
||||
G's ask #1 is real and I reproduced it. `stockpack.js` keys `_packs`/`_resolved` by **`type` alone**, and
|
||||
`preloadStockPack(type, {base})` **ignores `base` on a cache hit**. Measured (R23, fresh context):
|
||||
- `preloadStockPack('record', {base:'assets/stock_godverse/3962749/'})` → the 120-record godverse pack ✓
|
||||
- then `preloadStockPack('record', {base:'assets/models/'})` → **returned the identical object** ✗
|
||||
|
||||
Two consequences, the second worse than G's report:
|
||||
1. **Two stocked shops collide** — the 2nd silently serves the 1st shop's crate (blocks v5.0-beta).
|
||||
2. **It breaks the fail-soft law** — an *unstocked* shop of a cached type inherits that crate instead of
|
||||
falling back to parody. (On a clean cache fail-soft is intact: missing atlas → `null` → parody, 0 errors.
|
||||
The collision is what defeats it, so this is a correctness bug, not just a mix-up.)
|
||||
|
||||
**The seam (C owns it, C lands it):**
|
||||
- **Cache key = `type + base`** (equivalently a `packKey`; a godverse shop's key is its `stock_godverse/<godverse_id>/` base). `getStockPack` takes the same key.
|
||||
- **Resolution order** stays: explicit per-shop base → the type-pack base → `null` → parody.
|
||||
- **Per-shop preload on entry** (interior build/enter), **not** a boot-time preload of every shop.
|
||||
- **LRU = 1 shop's atlas set, disposed on eviction** (§7.2) — the load-bearing half of the §7 promise.
|
||||
- **Identity link:** a stocked shop must carry its pack pointer on the shop record (G's `godverse_id` →
|
||||
`stock_godverse/<id>/`). Not in the town cache today — G flagged it ("monsterrobot must be added to the
|
||||
godverse cache's `shops[]` as id 3962749"). C consumes whatever field lands; **C does not invent it.**
|
||||
|
||||
**9. R23 verification — the first real crate, in a room (ledger #3). PASS.**
|
||||
Measured against G's `3962749` atlas (120 real records, 2 × 2048², cell 256²) in a fresh context:
|
||||
- **Resolves through the existing `stockAdapter` path** via a base override — no C code change needed for
|
||||
the single-shop alpha. 120 items, 2 atlases, **8/8 stock meshes atlas-backed** (real covers, not parody).
|
||||
- **Ceilings hold:** **118 draws** with real covers (≤350 law); room `pathOK`, 0 carves.
|
||||
- **Fail-soft holds** (clean cache): missing atlas → `null` → parody (20 parody meshes, 110 draws, pathOK);
|
||||
no `stockAdapter` (the `?noassets` shape) → parody, zero fetch. **0 console errors** throughout.
|
||||
- **G's index is contract-compliant:** `items[]` sorted by id (my determinism ask honoured ✓), provenance
|
||||
present ✓, `uv ⊂ [0,1]` ✓, 2 atlases = the cap ✓, `cell_w/cell_h` harmless (C reads `uv`) ✓.
|
||||
- **Verdict: the contract holds. G re-emits nothing.** The only C-side work is §8's seam.
|
||||
### 7.7 R24 verification — falsifiable, per the new vacuous-gate law
|
||||
Fresh context, against G's merged `stock_godverse/3962749/` (120 real records):
|
||||
- **Every sleeve samples G's atlas file** — texture sources are exactly
|
||||
`stock_godverse/3962749/stock_record_atlas_00.webp` / `_01.webp`; pack items ≡ index items **by id**.
|
||||
*(Not "covers render": a parody canvas also has a `.map`. The negative control below is what makes
|
||||
this a gate — I had to fix my own R23 metric to satisfy the law.)*
|
||||
- **Negative control:** the parody room's stock textures are `(canvas, no src)` and touch the godverse
|
||||
atlas **zero** times — the metric discriminates.
|
||||
- **Collision fixed:** shop pack (120 items) and town pack (350) coexist as distinct objects.
|
||||
- **Fail-soft with a POPULATED cache:** missing base ⇒ `null` ⇒ parody (the R23 bug is gone).
|
||||
- **Ceilings:** 98 draws with real covers via `opts.stockBase`; `pathOK`; 0 carves; 0 console errors.
|
||||
|
||||
@ -1,5 +1,146 @@
|
||||
# LANE E — cross-lane notes
|
||||
|
||||
## Round 24 — the alpha completion: redhill, the honest validator, ballarat ⟨v5.0-alpha⟩
|
||||
|
||||
**1. `redhill_real` — the crate's real street (ledger #3). BUILT.** One bounded Overpass fetch under the
|
||||
standing authorization. **Red Hill, Brisbane** · centre `-27.4553, 153.0064` · span 2.4 km · **36 shops
|
||||
(9 heroes + 27 texture, −29 subsampled) · 884 roads** · median NN 73 m, hub 5/36. **Musgrave Road is in
|
||||
`roads[]` — 41 ways, kinds `[primary, residential]`.** That was the round's point: Monster Robot Party's
|
||||
real shop now has its real street. Raw snapshots in `_raw/`, ODbL, C's mapping, same discipline as the
|
||||
pack. **→ Lane G: this is your donor** for `godverse_town.py redhill_real redhill_godverse`.
|
||||
|
||||
**→ G AND A: TWO HAZARDS ON THIS DONOR. Please read before you seat the shop.**
|
||||
|
||||
> **(a) The shop is NOT in OSM.** There is no `shop=music` node and no "Monster Robot"/"monsterrobot"
|
||||
> anywhere in the Red Hill bbox — I checked the raw snapshot, not just the emitted cache. It **cannot**
|
||||
> arrive via my fetch; **G must inject it from the census** (id `3962749`) into `shops[]`. If G assumes
|
||||
> the donor already carries it, the town ships without the one shop the release is about.
|
||||
>
|
||||
> **(b) THE SEATING HAZARD — from G's geocode, the shop seats on the WRONG STREET.** A's lift seats each
|
||||
> shop on its **nearest edge**. Measured against the real `roads[]` geometry, from G's geocode
|
||||
> `-27.4553, 153.0064`:
|
||||
>
|
||||
> | distance | street | kind |
|
||||
> |---|---|---|
|
||||
> | **38.6 m** | **Zig Zag Street** | residential ← **A would seat it HERE** |
|
||||
> | 46.1 m | Craig Street | residential |
|
||||
> | 51.6 m | **Musgrave Road** | primary ← the real address (147 Musgrave Rd) |
|
||||
>
|
||||
> Three streets are nearer than the addressed one, so Monster Robot Party lands on Zig Zag Street and the
|
||||
> round's headline quietly becomes false. A street-address geocode sitting 52 m off its own centreline is
|
||||
> itself the tell — the pin is roughly the block, not the shopfront.
|
||||
>
|
||||
> **The measured fix (E's recommendation, G's call):** the point on the real Musgrave Road centreline
|
||||
> nearest G's geocode is **`-27.455169, 153.006902`**. Seating there: **0 m to Musgrave Road, next
|
||||
> candidate 43 m away** — unambiguous, with margin. It moves the shop 51.6 m; the residual error *along*
|
||||
> the street is G's geocode error and is below the game's resolution. **Caveat, stated plainly:** this
|
||||
> snaps to the right street, it does not verify the house number — I have no address data to confirm
|
||||
> where #147 sits along Musgrave Rd.
|
||||
>
|
||||
> **The robust fix (A's call, likely v5.x):** a godverse shop knows its addressed street; A's lift could
|
||||
> prefer a name-matching edge over the merely-nearest one. That fixes the whole class, not this one shop.
|
||||
> The snap above is enough to ship the alpha honestly.
|
||||
|
||||
**2. `validate_atlas` — made honest (ledger #4). F's catch was right; there were FOUR bugs, not one.**
|
||||
The R23 gate could pass without ever touching its subject — the vacuous-gate law's exact species. Fixed:
|
||||
|
||||
| # | bug | why it mattered |
|
||||
|---|---|---|
|
||||
| 1 | globbed the **published-contract** name (`stock_shop_<id>_index.json`) — a name the runtime can *never* load | matched **0 files** and returned 0. It passed identically whether atlases were absent, misnamed, or perfect |
|
||||
| 2 | the **code read provenance top-level** while **my own docstring said nested** | G nested it (correctly). The gate would have failed G's real, correct atlas |
|
||||
| 3 | `license` vs **`licence`** | same — a spelling fork silently failing a valid artifact |
|
||||
| 4 | ceiling stale by one round | drifted from C's §7 |
|
||||
|
||||
Per the R24 **"the runtime is the authority"** ruling the gate now globs the path the loader fetches:
|
||||
`stock_godverse/<godverseShopId>/stock_<type>_index.json`, `type ∈ {record, book, toy}`. **The vacuous-gate
|
||||
law is implemented literally:** 0 matches **+ stray files** ⇒ **FAIL**, listing the unmatched paths; 0
|
||||
matches **+ a truly empty dir** ⇒ `atlas-QA SKIP: no atlases present (0 files matched glob …)`, exit 0 —
|
||||
it skips **loudly, with the glob printed**, so a silent pass can't hide. It also asserts identity
|
||||
(`shop.godverse_id` == the directory name) — a falsifiable id equality, per the law's "prove it touched
|
||||
its subject".
|
||||
|
||||
**Proved, not asserted** — four cases: empty ⇒ SKIP (loud, exit 0) · **misnamed (F's exact bug) ⇒ FAIL**
|
||||
· unparseable ⇒ FAIL · **G's real atlas ⇒ PASS**: `3962749/stock_record_index.json — Monster Robot Party
|
||||
— items=120 atlases=2 px=2048 OK`, licence printed, 0 warnings. **My own test found a fifth hole:** the
|
||||
glob still matched the old `stock_shop_*` name, so the misnamed case failed only *incidentally* — I added
|
||||
a `STOCK_TYPES` name check, and it now fails **for the right reason**. A gate that fails for the wrong
|
||||
reason is a gate that will pass for the wrong reason later.
|
||||
|
||||
**→ Lane C:** built to the runtime + G's real artifact per the ruling. `LANE_C_PUB §7` **line 271** still
|
||||
names the contradictory `stock_shop_<godverseShopId>_index.json` — when your corrected contract lands I'll
|
||||
reconcile; if it names anything other than the above, the gate and the loader disagree again.
|
||||
|
||||
**3. Ballarat — RETIRED (my call, counted). A different illness from Toowoomba.** Full rationale in
|
||||
`SOURCES.md` and the commented config entry. The short version, because the distinction matters:
|
||||
**Toowoomba was dead everywhere. Ballarat is alive exactly where the game isn't.** It has 103 POIs and a
|
||||
genuinely dense core (densest 400 m disc = **47** POIs; Toowoomba's was **3**) — but that disc is
|
||||
cafés/clothes/bakeries, while the op-shops sit 854 m / 1042 m / 1709 m out. Heroes and density don't
|
||||
coincide, so no bbox holds both:
|
||||
|
||||
| span | shops | heroes | median NN | hub ≤160 m | op-shops |
|
||||
|---|---|---|---|---|---|
|
||||
| 1.0 km (the live core) | 8 | 2 | 184 m | 4/8 (50%) | **0** ← a thrift town with no thrift shop |
|
||||
| 2.0 km | 12 | 3 | 186 m | 3/12 (25%) | 1 |
|
||||
| 3.0 km (as shipped) | 20 | 5 | 255 m | 4/20 (20%) | 2 |
|
||||
|
||||
Every row is worse than any shipped town (darwin 68 m, glebe 60 m, redhill 73 m, katoomba 22 m).
|
||||
|
||||
**→ A, D: the binding constraint was MINE, not OSM's — and it's a real finding.** With 2 heroes the 3×
|
||||
texture cap keeps 6 of 47 texture POIs, and **the subsample is spatially blind** (deterministic hash rank
|
||||
over osmId). So in any hero-poor/texture-rich town the survivors scatter *no matter how tight the box* —
|
||||
**part of the 255 m A's warn caught was my own cap's fingerprint, not Ballarat's.** Seeding texture near
|
||||
heroes would fix the class, but it moves **every** town's golden → I'm filing it as a **v5.x proposal**,
|
||||
not doing it in a release round.
|
||||
|
||||
**→ A: I OVERSTATED MY R23 ADVICE — please don't encode it as a threshold.** I told you the hub fraction
|
||||
is the discriminator. It isn't, on its own: **glebe — shipped and healthy — reads 7/52 (13%)**, and
|
||||
redhill reads 5/36 (14%). A hub threshold would false-flag both. What actually held up:
|
||||
- **median NN is the robust primary** (dead: toowoomba 396 m, ballarat 255 m; alive: 22–77 m).
|
||||
- **hub is only comparable at equal shop counts** — toowoomba 2/12 vs darwin 7/12 was a fair comparison
|
||||
*because both had 12 shops*; hub% otherwise scales with town size and morphology.
|
||||
- The R23 calibration point still stands and still matters: **your `validateTownCache` sees raw cache
|
||||
lat/lon; D's numbers are seated (post-lift)** — darwin is 27 m seated but **68.3 m raw**, so a threshold
|
||||
read off D's table will false-warn darwin.
|
||||
|
||||
**4. A methodology note I'm recording because it nearly bit me.** I simulated the re-bbox offline to pick
|
||||
Ballarat's span; the sim predicted **68 m / hub 5-8** and I had already written those numbers into a
|
||||
config comment as fact. The real build returned **184 m / 4-8** — *same* counts (2 heroes + 6 texture),
|
||||
*different sample*: my re-implementation of the hash subsample picked a dense six where the pipeline picks
|
||||
a spread six. **Never trust a re-implementation of the pipeline's own selection — run the pipeline.** The
|
||||
sim's numbers would have shipped as a claim about a town nobody would have re-measured.
|
||||
|
||||
**5. Determinism receipt.** The ballarat retirement forced a full 21-town rebuild from the cached raw
|
||||
snapshots (no refetches). **Every existing cache came back byte-identical** — only ballarat (deleted),
|
||||
redhill (new) and `index.json` moved. Pack now **21 real towns · 1196 shops · 299 heroes**, 22 rostered
|
||||
with Lane G's `newtown_godverse`. Selfcheck **ALL GREEN, 149,696 checks**, synthetic golden `0x3fa36874`
|
||||
unchanged (the count moves with the roster: ballarat's 1144-edge graph out, redhill's 884 in).
|
||||
|
||||
**6. → A: THE GOLDEN PINS ARE YOURS AND MY ROUND MOVED THEM (the batched pin, ledger law).** Your own
|
||||
selfcheck printed the exact values — no round trip needed:
|
||||
|
||||
```
|
||||
REAL_TOWN_GOLDENS['redhill_real'] = 0x6046700f // ADD (new town, currently UNPINNED)
|
||||
REAL_TOWN_GIG_GOLDENS['redhill_real'] = 0x673785ee // ADD
|
||||
REAL_TOWN_GOLDENS['ballarat_real'] = 0xf0a9e8a1 // REMOVE (cache deleted — dead config)
|
||||
REAL_TOWN_GIG_GOLDENS['ballarat_real'] = 0xf097c331 // REMOVE
|
||||
```
|
||||
|
||||
The ballarat pins are now exactly what **your R23 note** called the toowoomba pin: *"a golden for a town
|
||||
that cannot load is dead config: it can never fire, so it can never fail — same species as the vacuous-gate
|
||||
law, one layer down."* Your words, my retirement, same fix. And note the mirror on the other side:
|
||||
`redhill_real` currently **warns** (`⚠ base UNPINNED`) rather than failing, so **the round's headline town
|
||||
is ungated until you pin it** — an unpinned town is the same vacuous species pointing the other way. Your
|
||||
warn-with-the-hash design is what made this a two-line handoff instead of a bug hunt; worth keeping.
|
||||
|
||||
**7. → F: an R23 orphan I created and owe you.** `tools/qa/tour_shots.py` (your v4.0 release tour) still
|
||||
shoots `plansrc=osm&town=toowoomba_real` for its `toowoomba_thintail` frame — I deleted that cache in R23
|
||||
and never swept the consumers, so **re-running the v4 tour now breaks on a missing town.** It's your file
|
||||
and it's a shipped historical artifact (the frame *was* the honest thin-tail proof), so I'm flagging
|
||||
rather than editing: either re-point the frame at a surviving thin town or retire it with a note. Same
|
||||
class, lower stakes: `web/js/citygen/gigs.js:134` names ballarat/toowoomba in a comment as thin-town
|
||||
examples — stale prose only, no behaviour. **The general lesson for me: a retirement isn't done when the
|
||||
cache is deleted; it's done when the consumers are swept.**
|
||||
|
||||
## Round 23 — v5.0-alpha: the atlas validator + the toowoomba call ⟨v5.0-alpha⟩
|
||||
|
||||
**1. `validate_atlas` — the G2a format gate (ledger #4).** `pipeline/validate_atlas.py`, wired into
|
||||
|
||||
98
docs/LANES/ROUND25_INSTRUCTIONS.md
Normal file
98
docs/LANES/ROUND25_INSTRUCTIONS.md
Normal file
@ -0,0 +1,98 @@
|
||||
# PROCITY — Round 25 lane instructions (from Fable, integrator)
|
||||
|
||||
Date: 2026-07-17 · **R24 verdict: THE CRATE WALKS — and the tag correctly holds a second round.**
|
||||
F proved the epoch's claim falsifiably: Monster Robot Party resolves its own per-shop base,
|
||||
**120/120 rendered titles from its atlas and 0/120 from the generic pack, the GPU samples
|
||||
`stock_godverse/3962749/*.webp`**, a real record debits $15 exactly, the ladder re-proved on
|
||||
every rung, and the wire is a verified no-op off the godverse path. G gave the shop its street
|
||||
(Red Hill, 884 real roads, the shop's coordinate John-supplied because **no dataset we hold
|
||||
derives it** — honest sourcing), proved **Ruling 2 was load-bearing not cosmetic** (census-only
|
||||
Red Hill = 306 m spacing warn + a poster inside a kerb; with the texture layer it passes — the
|
||||
headline town was one design decision from shipping flagged), and recorded John's **A-then-B
|
||||
write-back ruling as mechanism**: `/reserve` + `/buy` stay ABSENT, not stubbed — a sandbox a
|
||||
config typo can flip isn't a sandbox. C owned the R23 root cause squarely. E built the street
|
||||
and the honest validator. A published the identity field first, as briefed.
|
||||
|
||||
**Why the tag holds:** the tree carries 8 selfcheck reds F cannot fix — 2 are re-emitted
|
||||
`newtown_godverse` goldens awaiting A's pin (A ran before G; a handoff gap, not a defect), and
|
||||
**6 are A's sweep asserting what Fable's own Ruling 2 makes impossible by design** (every-shop-
|
||||
keyed on deliberately-mixed caches). Plus R24's finding chain, in escalating cost: the brief's
|
||||
own honest-gate spec was vacuous (positional `rec_0000…` ids collide across packs — id equality
|
||||
can't discriminate; **the vacuous-gate law's first application broke the law**, and the fix —
|
||||
titles + texture URLs — is F's, not the brief's); A's uniqueness check collapses `undefined`s
|
||||
into one Set entry (fails structurally on mixed caches AND can't catch a real duplicate — F
|
||||
measured the guarded hazard at zero); E's validator prints the licence flag from British
|
||||
`licence` while the contract mandates US `license`, so **"FLAG BEFORE ANY PUBLIC/COMMERCIAL
|
||||
RELEASE" silently never prints on real product photos**; and F's own classic gate cried wolf
|
||||
(fixed by F, two assertions two verdicts). Round 25 closes all of it. Tag **`v5.0-alpha`** at
|
||||
close — third time, earned.
|
||||
|
||||
## Process rulings (from R24's events)
|
||||
|
||||
1. **Lane G is a full lane on main.** G committed straight to main and it was right: the
|
||||
namespace is disjoint, the commits were pathspec-atomic, and the branch ceremony has served
|
||||
its purpose. Same treaty as A–F from here: own files only, atomic pathspec commits;
|
||||
anything cross-namespace still needs a ruling first. (The R23 merge amendment is thereby
|
||||
retired along with the branch.)
|
||||
2. **Ruling 2 is confirmed as law** — G measured it: mixed caches (keyed GODVERSE layer +
|
||||
inherited texture layer) are the *design*, not an accident A's gates may assert away.
|
||||
3. **The brief-was-wrong entry for the record:** my #7a spec (id equality) was itself vacuous.
|
||||
F's replacement (title sets + texture-URL provenance) is the canonical honest-gate pattern
|
||||
for stock — measurement over brief applies to the integrator's gate specs too.
|
||||
|
||||
## The ledger
|
||||
|
||||
1. **A — reconcile the sweep with Ruling 2, pin the six (the round's spine).**
|
||||
- The sweep's godverse arm goes per-layer: keyed asserts apply ONLY to shops carrying
|
||||
`godverseShopId` (the GODVERSE layer); texture-layer shops are legitimately unkeyed.
|
||||
- Replace the uniqueness check with one that works: uniqueness over **defined** ids only
|
||||
(F measured zero real duplicates — the check should be able to catch one if it ever
|
||||
appears, which the Set-collapse version cannot).
|
||||
- Pin the six goldens: `newtown_godverse` base+gig (moved by G's keyed re-emit — values are
|
||||
in G's commit), `redhill_real` base+gig, `redhill_godverse` base+gig.
|
||||
- Acceptance: **selfcheck ALL GREEN, zero warns pending** except any E leaves deliberately.
|
||||
2. **E — the licence line, made unmissable (conflict #1).** Align the validator with C's §7.3
|
||||
(top-level provenance, US `license` — one truth; your own docstring/check disagreement dies
|
||||
in the same commit). Per the vacuous-gate law: when provenance exists, the licence evidence
|
||||
line is a REQUIRED printed output and its absence is a FAIL — the flag exists for a human to
|
||||
read, on real product photos; a gate that can say OK while hiding it is the R23 species.
|
||||
3. **C — one contract line; G — the sku re-emit (F's finding #1, made structural).** Positional
|
||||
slot ids collide by construction. C: amend §7 — atlas-pack slot ids MUST be unique across
|
||||
packs (sku-derived: `sku_<POS id>` or similar — your wording). G: re-emit the two atlases
|
||||
with sku slot ids (index-only change — no town cache touched, no golden moves; the .webp
|
||||
bytes shouldn't move either, same as your R24 re-bake). E's validator asserts the form.
|
||||
This makes id-equality gates discriminating forever, before there are 50 atlases to migrate.
|
||||
4. **F — land your R24 work, then the tag.** Your wire, the DBG interior handoff, the crate
|
||||
smoke, the classic-gate fix, and the notes are staged/uncommitted as of the R24 close —
|
||||
commit them (F-pathspec) if the harness verdict hasn't already. Then, after A/E/G land:
|
||||
full `qa.sh --strict --matrix` with the honest validator AND your crate smoke in the gate
|
||||
set, the README v5 teaser ships WITH the tag (one claim, as you ruled), tag **`v5.0-alpha`**,
|
||||
push. Your hold streak is the system working — end it only on green.
|
||||
5. **B / D — on-call.** B: the standing first-pick-of-polish offer moves to the beta round,
|
||||
where the dig UX for real items will want street-side support anyway.
|
||||
|
||||
## Laws
|
||||
|
||||
Charter v5 + the vacuous-gate law (now with its own first counterexample on record) + the
|
||||
process rulings above. Golden moves this round: A's six pins only. No server code; the ratified
|
||||
G3 stays paper until the beta charters tier-2 read-live.
|
||||
|
||||
## THE EXACT RUN ORDER
|
||||
|
||||
1. **Start together: A (#1) + E (#2) + C (#3's one line).**
|
||||
2. **When C's line lands: G (#3) re-emits.** (G needs nothing else — A's pins don't gate the
|
||||
re-emit since atlas indexes aren't in goldens.)
|
||||
3. **When A + E + G are in: F (#4)** — commit your R24 work if not already, full gate, tag,
|
||||
push.
|
||||
|
||||
Waves: **[A, E, C] → [G] → [F]**. B/D on-call.
|
||||
|
||||
## Standing
|
||||
|
||||
- **John — nothing needs you.** Your A-then-B ruling is recorded as binding mechanism in the
|
||||
G3 doc (§7); the alpha tags this round on green.
|
||||
- **v5.0-beta preview (next round, do not start):** G2a at scale — every hero shop in
|
||||
newtown_godverse + redhill_godverse stocked (the census inventory question: most census shops
|
||||
aren't on the POS — the beta decides what a real-but-un-inventoried shop's crate holds) ·
|
||||
mint()-seeded tier-0 variety per the G3 doc's note · the dig UX for real items (price cards
|
||||
are real data) · B's polish pick.
|
||||
@ -35,7 +35,7 @@
|
||||
"captured": "2026-07-15",
|
||||
"extraction": "see load_image_map() docstring \u2014 the products projection command"
|
||||
},
|
||||
"licence": {
|
||||
"license": {
|
||||
"covers": "the shop's own product shots (monsterrobot.party), cached by dealgod",
|
||||
"zone": "in-house-green",
|
||||
"flag": "FLAG BEFORE ANY PUBLIC/COMMERCIAL RELEASE",
|
||||
|
||||
@ -50,10 +50,30 @@ TOWNS = {
|
||||
"bowral_real": {"town": "Bowral", "state": "NSW", "center": (-34.4780, 150.4180), "span_km": 2.2},
|
||||
"fitzroy_real": {"town": "Fitzroy", "state": "VIC", "center": (-37.7990, 144.9790), "span_km": 2.2},
|
||||
"brunswick_real": {"town": "Brunswick", "state": "VIC", "center": (-37.7700, 144.9600), "span_km": 2.6},
|
||||
"ballarat_real": {"town": "Ballarat", "state": "VIC", "center": (-37.5620, 143.8500), "span_km": 3.0},
|
||||
# R24 — RETIRED (counted), and for a DIFFERENT illness than Toowoomba. A's spacing warn caught
|
||||
# it at median NN 255 m. Ballarat is NOT dead: its raw bbox holds 103 POIs and its densest 400 m
|
||||
# disc is 47 of them (Toowoomba's densest was 3). But its retail core is alive exactly where the
|
||||
# GAME isn't — that disc is cafes/clothes/bakeries, while the op-shops sit 854 m, 1042 m and
|
||||
# 1709 m out. The heroes and the density don't coincide, so no bbox holds both. Measured, every
|
||||
# span (real builds, not simulated):
|
||||
# span shops heroes medNN hub160 op-shops
|
||||
# 1.0 km 8 2 184 m 4/8 (50%) 0 <- the live core: a thrift town with NO thrift shop
|
||||
# 2.0 km 12 3 186 m 3/12 (25%) 1
|
||||
# 3.0 km 20 5 255 m 4/20 (20%) 2 <- as shipped
|
||||
# Every row is worse than any shipped town (darwin 68 m/58%, glebe 60 m, redhill 73 m). The
|
||||
# binding constraint is mine, not OSM's: with 2 heroes the 3x cap keeps 6 of 47 texture POIs and
|
||||
# the subsample is spatially blind, so the survivors scatter no matter how tight the box. Fixing
|
||||
# THAT (seeding texture near heroes) is a pipeline change that would move every town's golden —
|
||||
# a v5.x proposal, not a curation move. Cache + raw snapshots removed. See LANE_E_NOTES R24.
|
||||
# "ballarat_real": {"town": "Ballarat", "state": "VIC", "center": (-37.5620, 143.8500), "span_km": 3.0},
|
||||
"daylesford_real": {"town": "Daylesford", "state": "VIC", "center": (-37.3430, 144.1430), "span_km": 2.0},
|
||||
"geelong_real": {"town": "Geelong", "state": "VIC", "center": (-38.1490, 144.3600), "span_km": 3.0},
|
||||
"westend_real": {"town": "West End", "state": "QLD", "center": (-27.4830, 153.0100), "span_km": 2.2},
|
||||
# R24 ledger #3 — the v5 crate's real street: **Monster Robot Party, 147 Musgrave Rd, Red Hill,
|
||||
# Brisbane** (John's ruling: the crate ships as the shop it really is, not Newtown). Centre/span
|
||||
# per G's ask (they geocoded the address); westend_real is the only other Brisbane cache and stops
|
||||
# ~2.3 km south. This is the DONOR G runs godverse_town.py against → redhill_godverse.
|
||||
"redhill_real": {"town": "Red Hill", "state": "QLD", "center": (-27.4553, 153.0064), "span_km": 2.4},
|
||||
# ── RETIRED R23 (E's curation call, counted) — toowoomba_real ────────────────────────────────
|
||||
# D's R22 finding: DEAD — median NN 388 m, 3/12 shops near the hub, **0 patronage finds in 1417
|
||||
# checks**. I tried the re-bbox first (3.0 km CBD-wide → 1.6 km on the Ruthven St heart, centre
|
||||
|
||||
@ -151,7 +151,9 @@ def snapshot(args):
|
||||
"captured": args.captured,
|
||||
"extraction": "see load_image_map() docstring — the products projection command",
|
||||
},
|
||||
"licence": {
|
||||
# US `license` throughout (R24: British `licence` retired per C's contract §7.3; E's
|
||||
# validator reads `provenance.licence or provenance.license`, so US passes the gate).
|
||||
"license": {
|
||||
"covers": "the shop's own product shots (monsterrobot.party), cached by dealgod",
|
||||
"zone": "in-house-green", "flag": "FLAG BEFORE ANY PUBLIC/COMMERCIAL RELEASE",
|
||||
"metadata": "real titles/artists — John's R23 amendment to the no-real-trademarks law",
|
||||
@ -204,16 +206,31 @@ def pack(args):
|
||||
|
||||
with open(args.snapshot, "rb") as fh:
|
||||
snap_sha = hashlib.sha256(fh.read()).hexdigest()
|
||||
lic = snap["license"]
|
||||
generator, snapshot_name = "pipeline/godverse_stock.py", os.path.basename(args.snapshot)
|
||||
attribution = (f"Stock, prices + covers: {shop['name']} ({OWN_PHOTO_HOST}) — the shop's own "
|
||||
f"product shots · cover cache: GODVERSE/dealgod · POS: recordgod")
|
||||
index = {
|
||||
"version": 1, "kind": "record", "tier": 1,
|
||||
"atlas_px": ATLAS_PX, "cell_w": CELL_W, "cell_h": CELL_H,
|
||||
"count": len(idx_items), "atlases": atlases,
|
||||
# TOP LEVEL, per C's contract §7.3 + the house convention of the shipped town caches
|
||||
# (`license` is a short US-spelled string there, `attribution` its companion).
|
||||
"license": f"{lic['zone']} — {lic['covers']}; {lic['flag']}",
|
||||
"attribution": attribution,
|
||||
"generator": generator,
|
||||
"snapshot": snapshot_name,
|
||||
"shop": {k: shop[k] for k in ("godverse_id", "slug", "name", "suburb", "state")},
|
||||
"crate": snap["crate"],
|
||||
# NESTED, per E's validate_atlas.py — which is the authority C's §7.3 defers to for these
|
||||
# fields, and which reads provenance.{generator,snapshot,licence|license{zone,flag,covers}}.
|
||||
# C's doc asserts top-level is "exactly what E's validator reads"; measured, it is not (see
|
||||
# G-progress R24). Emitting both keeps C's contract and E's gate green off ONE source of
|
||||
# truth (`lic`/`generator`/`snapshot` above) — flagged for F/Fable to collapse to one.
|
||||
"provenance": {
|
||||
"generator": "pipeline/godverse_stock.py",
|
||||
"source": snap["source"], "licence": snap["licence"],
|
||||
"snapshot": os.path.basename(args.snapshot), "snapshot_sha256": snap_sha,
|
||||
"generator": generator,
|
||||
"source": snap["source"], "license": lic,
|
||||
"snapshot": snapshot_name, "snapshot_sha256": snap_sha,
|
||||
"determinism": "pure bake — same snapshot + same cover bytes => byte-identical atlas; "
|
||||
"covers pinned by sha256, no RNG, no clock",
|
||||
},
|
||||
|
||||
@ -6,12 +6,22 @@ shops[] come from thriftgod's Postgres (the real-business census DealGod scraped
|
||||
walk the town's actual streets past its actual secondhand shops.
|
||||
|
||||
python3 pipeline/godverse_town.py newtown_real newtown_godverse # roads-donor key, out key
|
||||
python3 pipeline/godverse_town.py redhill_real redhill_godverse # THE shop's real street
|
||||
|
||||
Runs on ultra (needs the local `thriftgod` PG + psycopg2 — the thriftgod server's own deps).
|
||||
Reads thriftgod via the `psql` CLI (R24): psycopg2 isn't installed on the m3, and one less dep
|
||||
between the census and the cache. Works unchanged wherever thriftgod lives.
|
||||
|
||||
`godverseShopId` (R24 ledger #2, with Lane A) — the OPAQUE key that resolves
|
||||
`stock_godverse/<godverseShopId>/`. **It is not `shop.id` and not "the census id"**: census shops
|
||||
carry their thriftgod id (max 2992), while Monster Robot Party isn't in the census at all and
|
||||
carries its dealgod store id (3962749). Two disjoint id spaces, one opaque field — G emits it,
|
||||
everyone else reads it, nobody derives it. A's `validateTownCache` wants a positive integer,
|
||||
unique per cache; **missing is a WARN, not an error** (measured — A's rule is deliberate: absent
|
||||
stock identity must never fail a town's boot, per the charter's determinism boundary). So the
|
||||
GODVERSE layer is keyed and the inherited texture layer is not — see Ruling 2 below for why that
|
||||
is the honest shape rather than a gap.
|
||||
"""
|
||||
import json, os, sys
|
||||
|
||||
import psycopg2, psycopg2.extras
|
||||
import json, math, os, subprocess, sys
|
||||
|
||||
DONOR, OUT = sys.argv[1], sys.argv[2]
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
@ -22,17 +32,98 @@ TYPE_MAP = {'music': 'record', 'charity': 'opshop', 'second_hand': 'opshop', 'an
|
||||
'books': 'book', 'toys': 'toy', 'video_games': 'video', 'games': 'toy',
|
||||
'pawnbroker': 'pawn', 'market': 'dept'}
|
||||
|
||||
# REAL shops that exist in neither OSM nor thriftgod's census, added by explicit, sourced ruling.
|
||||
# This is not a mapping table (the R12 gigKey law) — it's a hand-verified roster of the shops the
|
||||
# epoch is actually about. Every entry states where its identity and its coordinates came from.
|
||||
#
|
||||
# Monster Robot Party: the v5 alpha's shop — its POS *is* `recordgod`, its own site hosts the
|
||||
# covers, and dealgod scrapes it as store 3962749 (which is therefore its godverseShopId). OSM
|
||||
# doesn't know it (no name match, no 147 Musgrave Rd address node) and the census doesn't either,
|
||||
# so the coordinate cannot be derived from any dataset we hold: John supplied the shop's Google
|
||||
# Maps place (place `/g/11g6rlbbc0`), whose point is -27.4553791, 153.0076244 — inside
|
||||
# `redhill_real`'s bbox and on Musgrave Road's own geometry. Verified, not estimated.
|
||||
EXTRA_SHOPS = {
|
||||
'redhill_godverse': [
|
||||
{'id': 3962749, 'godverseShopId': 3962749, 'name': 'Monster Robot Party', 'type': 'record',
|
||||
'lat': -27.4553791, 'lon': 153.0076244, 'suburb': 'Red Hill',
|
||||
'source': 'GODVERSE/dealgod store 3962749 · POS recordgod · coords: Google Maps place '
|
||||
'/g/11g6rlbbc0 (147 Musgrave Road, Red Hill QLD), John-supplied R24'},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def psql(db, sql):
|
||||
out = subprocess.run(['psql', '-d', db, '-tAF\t', '-c', sql],
|
||||
capture_output=True, text=True, check=True).stdout
|
||||
return [ln.split('\t') for ln in out.splitlines() if ln.strip()]
|
||||
|
||||
|
||||
donor = json.load(open(os.path.join(TOWNS, f'{DONOR}.json')))
|
||||
bb = donor['bbox']
|
||||
db = psycopg2.connect(os.environ.get('THRIFTGOD_DSN', 'dbname=thriftgod'))
|
||||
c = db.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
|
||||
c.execute("""SELECT id, name, shop_type, lat, lng, suburb FROM shop
|
||||
WHERE lat BETWEEN %s AND %s AND lng BETWEEN %s AND %s ORDER BY id""",
|
||||
(bb['minLat'], bb['maxLat'], bb['minLon'], bb['maxLon']))
|
||||
shops = [{'id': r['id'], 'name': r['name'],
|
||||
'type': TYPE_MAP.get(r['shop_type'], 'opshop'),
|
||||
'lat': float(r['lat']), 'lon': float(r['lng']),
|
||||
'suburb': (r['suburb'] or '').split('|')[0]} for r in c.fetchall()]
|
||||
rows = psql(os.environ.get('THRIFTGOD_DB', 'thriftgod'), f"""
|
||||
SELECT id, COALESCE(name,''), COALESCE(shop_type,''), lat, lng, COALESCE(suburb,'')
|
||||
FROM shop
|
||||
WHERE lat BETWEEN {bb['minLat']} AND {bb['maxLat']}
|
||||
AND lng BETWEEN {bb['minLon']} AND {bb['maxLon']}
|
||||
ORDER BY id""")
|
||||
|
||||
shops = [{'id': int(r[0]),
|
||||
'godverseShopId': int(r[0]), # census shops: the thriftgod id IS the key (not derived
|
||||
# from `id` by law — they coincide, see the docstring)
|
||||
'name': r[1],
|
||||
'type': TYPE_MAP.get(r[2], 'opshop'),
|
||||
'lat': float(r[3]), 'lon': float(r[4]),
|
||||
'suburb': r[5].split('|')[0]} for r in rows]
|
||||
census_n = len(shops)
|
||||
|
||||
extras = EXTRA_SHOPS.get(OUT, [])
|
||||
known = {s['id'] for s in shops}
|
||||
for e in sorted(extras, key=lambda s: s['id']):
|
||||
if e['id'] in known: # census wins a collision, and we count the swap
|
||||
continue
|
||||
shops.append({k: e[k] for k in ('id', 'godverseShopId', 'name', 'type', 'lat', 'lon', 'suburb')})
|
||||
extra_n = len(shops) - census_n
|
||||
|
||||
|
||||
# ── Fable's Ruling 2 (R22, implemented R24): INHERIT THE DONOR'S TEXTURE LAYER ────────────────
|
||||
# "Merge census heroes WITH the donor's texture shops; census heroes always win a collision
|
||||
# (dedupe by proximity + name), drop the donor hero, keep yours, count the swap. Result: real
|
||||
# census shops on a street that also feels alive."
|
||||
#
|
||||
# Not cosmetic — MEASURED (R24): the census layer alone is 9 shops across Red Hill's 2.4 km bbox →
|
||||
# A's spacing warn at 261 m ("scattered, not a high street"), *worse than the 255 m that retired
|
||||
# ballarat this round*, plus a frontage poster seated inside edge 1037's kerb. With the donor's
|
||||
# texture layer merged (37 shops) both go away and E's donor's own density is preserved.
|
||||
#
|
||||
# THE MERGED SHOPS CARRY NO `godverseShopId`, DELIBERATELY. The field is the GODVERSE stock
|
||||
# identity; an OSM cafe has none, and minting one from its OSM id would push a third id space into
|
||||
# a field that already carries two (thriftgod census ≤2992 · dealgod store ids). OSM ids are the
|
||||
# same order of magnitude as dealgod store ids, so a collision is *possible* — and A's validator
|
||||
# calls a duplicate godverseShopId mis-stocking (charter risk #3: two shops keyed to one atlas,
|
||||
# one of them Monster Robot's crate). Unkeyed is both true and safe: they fall soft to tier 0,
|
||||
# which is exactly what a milkbar with no stock should do. A's coverage warn states this honestly.
|
||||
def norm(n):
|
||||
return ' '.join((n or '').lower().split())
|
||||
|
||||
|
||||
def near(a, b, m=150.0): # metres, flat-earth at suburb scale — plenty at 150 m
|
||||
dy = (a['lat'] - b['lat']) * 111_320.0
|
||||
dx = (a['lon'] - b['lon']) * 111_320.0 * math.cos(math.radians(a['lat']))
|
||||
return math.hypot(dx, dy) <= m
|
||||
|
||||
|
||||
swaps = 0
|
||||
for d in sorted(donor.get('shops', []), key=lambda s: s['id']):
|
||||
hit = next((s for s in shops if norm(s['name']) == norm(d['name']) and near(s, d)), None)
|
||||
if hit: # the donor's own entry for a shop we already hold
|
||||
swaps += 1
|
||||
continue
|
||||
if d['id'] in {s['id'] for s in shops}:
|
||||
continue
|
||||
shops.append({'id': d['id'], 'name': d['name'], 'type': d['type'],
|
||||
'lat': d['lat'], 'lon': d['lon'], 'suburb': d.get('suburb', '')})
|
||||
texture_n = len(shops) - census_n - extra_n
|
||||
shops.sort(key=lambda s: s['id']) # deterministic order, extras + texture included
|
||||
|
||||
out = dict(donor)
|
||||
out.update({
|
||||
@ -42,10 +133,20 @@ out.update({
|
||||
'generator': 'pipeline/godverse_town.py',
|
||||
'attribution': donor['attribution'] + ' · shop census: GODVERSE/thriftgod',
|
||||
'shops': shops,
|
||||
'counts': {**donor.get('counts', {}), 'shops': len(shops)},
|
||||
# The donor's shop-side counts (raw/heroes/texture/textureDropped) describe the shop layer we
|
||||
# REPLACE, so carrying them would state a census of shops this cache doesn't contain. Keep the
|
||||
# donor's road count; recount our own shops.
|
||||
'counts': {**{k: v for k, v in donor.get('counts', {}).items() if k == 'roads'},
|
||||
'shops': len(shops), 'census': census_n, 'extra': extra_n,
|
||||
'texture': texture_n, 'donorSwaps': swaps},
|
||||
})
|
||||
dst = os.path.join(TOWNS, f'{OUT}.json')
|
||||
json.dump(out, open(dst, 'w'), indent=1)
|
||||
with open(dst, 'w') as fh:
|
||||
json.dump(out, fh, indent=1)
|
||||
types = {}
|
||||
for s in shops: types[s['type']] = types.get(s['type'], 0) + 1
|
||||
print(f"{OUT}: {len(shops)} REAL shops ({types}) + {len(out.get('roads', []))} OSM roads -> {dst}")
|
||||
for s in shops:
|
||||
types[s['type']] = types.get(s['type'], 0) + 1
|
||||
keyed = sum(1 for s in shops if 'godverseShopId' in s)
|
||||
print(f"{OUT}: {len(shops)} shops = {census_n} census + {extra_n} extra + {texture_n} donor-texture "
|
||||
f"({swaps} donor heroes swapped for census) · {keyed} keyed w/ godverseShopId")
|
||||
print(f" {types} + {len(out.get('roads', []))} OSM roads -> {dst}")
|
||||
|
||||
@ -1,37 +1,49 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Validate per-shop real-stock atlases — the G2a format gate (v5.0-alpha, ROUND23 ledger #4).
|
||||
"""Validate per-shop real-stock atlases — the G2a format gate (v5.0-alpha, R23 #4 · REBUILT R24 #4).
|
||||
|
||||
Lane G bakes one atlas + index per real shop from dealgod inventory (tier 1 of the charter's stock
|
||||
ladder). This gate is the FORMAT side: it proves G's output is well-formed, inside C's ceilings, and
|
||||
carries its provenance — **on m3, from the files alone, with no dealgod DB and no network**. That's the
|
||||
point: the gate must run anywhere, on the committed artefacts.
|
||||
Judges Lane G's tier-1 atlases **from the committed files alone** — no dealgod DB, no network — so the
|
||||
gate runs on m3. Wired into `validate_manifest` (qa gate 3).
|
||||
|
||||
python3 pipeline/validate_atlas.py # exit 0 = every atlas valid (or none yet), 1 = a bad one
|
||||
R24 REBUILD — F held the v5.0-alpha tag partly on this file, and was right. What was wrong (all four
|
||||
real, all mine):
|
||||
1. It globbed `stock_shop_<id>_index.json` — the PUBLISHED contract's name, which **the runtime can
|
||||
never load**. G shipped what the loader reads. So the gate matched 0 files, returned 0, and passed
|
||||
whether the atlas was absent, misnamed or perfect. *A gate that returns 0 on an empty set is not a
|
||||
gate.* → now globs the RUNTIME path, and an empty set SKIPs loudly while a present-but-unmatched
|
||||
file FAILS (the vacuous-gate law).
|
||||
2. Provenance: my code read it TOP-LEVEL while my own docstring said NESTED — G nested it. My doc and
|
||||
my code contradicted each other and G's atlas failed either way. → nested, per the artifact.
|
||||
3. `licence` vs `license` — G uses British; I demanded American. → British (the emitter's, and it's
|
||||
the artifact of record).
|
||||
4. A ceiling stale by one round (I warned on 2 atlases; C's §7.2 allows ≤2 × ≤2048², and G's 120-item
|
||||
crate lands exactly on the cap). → C's current numbers.
|
||||
|
||||
Contract (published R22 → LANE_E_NOTES E's half · LANE_C_PUB §7 C's ceilings · charter law #3 licence):
|
||||
index `web/assets/stock_godverse/**/stock_shop_<godverseShopId>_index.json`
|
||||
{version, atlas_px, cell, atlases[], items:[{id,title,artist,price,price_band,atlas,uv}]}
|
||||
+ provenance: {license, attribution, generator, snapshot}
|
||||
C's ceilings — 1 atlas/shop (hard cap 2); ≤1024² typical, 2048² only for a 60+ item shop.
|
||||
charter law #3 — every atlas carries provenance; no private-individual data, ever.
|
||||
determinism — `snapshot` pins the inventory query result (G snapshots it like E snapshots Overpass raw).
|
||||
THE AUTHORITY IS THE RUNTIME (R24 ruling). `stockpack.js` states it: a per-shop pack lives under
|
||||
`assets/stock_godverse/<godverseShopId>/` and carries the SAME `stock_<type>_index.json` name (type picks
|
||||
the stock slot); cache identity is `(type, base)`. This gate reads that, not a doc that disagrees with it.
|
||||
|
||||
Fail-soft is NOT this gate's business: a missing atlas is legal at runtime (parody canvas, R7 law). This
|
||||
gate only judges atlases that EXIST — a malformed one must fail loudly rather than render garbled sleeves.
|
||||
python3 pipeline/validate_atlas.py # 0 = valid (or an honest SKIP), 1 = a bad/misnamed atlas
|
||||
"""
|
||||
import json, os, sys, glob, struct
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
STOCK_G = os.path.join(ROOT, "web", "assets", "stock_godverse") # Lane G's namespace (charter law #5)
|
||||
STOCK_G = os.path.join(ROOT, "web", "assets", "stock_godverse") # Lane G's namespace (charter law #5)
|
||||
GLOB = os.path.join(STOCK_G, "*", "stock_*_index.json") # the RUNTIME path — what the loader fetches
|
||||
BANDS = {"bargain", "standard", "collector", "grail"}
|
||||
# The loader fetches `<base>stock_<type>_index.json` where `type` is the shop's registry type — it picks
|
||||
# the stock slot (stockpack SLOT_FOR: record→sleeve, book→spine, toy→box). Any other `<type>` is a name
|
||||
# the runtime can never fetch. The glob alone can't say that (`stock_*_index.json` also matches the old
|
||||
# contract's `stock_shop_<id>_index.json`), so the name is checked, not just matched — presence is not
|
||||
# the-right-thing (the vacuous-gate law's actual lesson).
|
||||
STOCK_TYPES = {"record", "book", "toy"}
|
||||
REQUIRED_ITEM = ("id", "title", "artist", "price", "price_band", "atlas", "uv")
|
||||
REQUIRED_PROV = ("license", "attribution", "generator", "snapshot") # charter law #3 + determinism
|
||||
MAX_ATLASES = 2 # C: 1 atlas/shop, hard cap 2 (each extra = +1 draw)
|
||||
PX_TYPICAL, PX_MAX = 1024, 2048
|
||||
BIG_SHOP_ITEMS = 60 # C: 2048² only for a 60+ item shop (else spill to the type-pack)
|
||||
# charter law #3: "No private-individual data, ever" — a real shop's stock row must never carry a person.
|
||||
MAX_ATLASES = 2 # C §7.2: ≤2 atlases per shop
|
||||
PX_MAX = 2048 # C §7.2: ≤2048² each
|
||||
PX_TYPICAL = 1024 # C §7.2: ≤1024² whenever the crate fits it (≤64 items at 256²)
|
||||
FITS_1024 = 64
|
||||
# charter law #3: no private-individual data, ever — a stock row must never carry a person.
|
||||
FORBIDDEN_ITEM_KEYS = {"seller", "seller_name", "owner", "owner_name", "customer", "user", "username",
|
||||
"email", "phone", "mobile", "address", "member", "account", "consignor"}
|
||||
"email", "phone", "mobile", "address", "member", "account", "consignor", "staff"}
|
||||
|
||||
|
||||
def image_size(path):
|
||||
@ -39,17 +51,16 @@ def image_size(path):
|
||||
with open(path, "rb") as f:
|
||||
head = f.read(32)
|
||||
if head[:8] == b"\x89PNG\r\n\x1a\n":
|
||||
w, h = struct.unpack(">II", head[16:24])
|
||||
return w, h
|
||||
return struct.unpack(">II", head[16:24])
|
||||
if head[:4] == b"RIFF" and head[8:12] == b"WEBP":
|
||||
fmt = head[12:16]
|
||||
if fmt == b"VP8X": # extended: 24-bit minus-one
|
||||
if fmt == b"VP8X":
|
||||
b = head[24:30]
|
||||
return (b[0] | b[1] << 8 | b[2] << 16) + 1, (b[3] | b[4] << 8 | b[5] << 16) + 1
|
||||
if fmt == b"VP8 ": # lossy: 14-bit each
|
||||
if fmt == b"VP8 ":
|
||||
w, h = struct.unpack("<HH", head[26:30])
|
||||
return w & 0x3FFF, h & 0x3FFF
|
||||
if fmt == b"VP8L": # lossless: packed 14-bit
|
||||
if fmt == b"VP8L":
|
||||
b = struct.unpack("<I", head[21:25])[0]
|
||||
return (b & 0x3FFF) + 1, ((b >> 14) & 0x3FFF) + 1
|
||||
raise ValueError("unrecognised image header (want PNG or WebP)")
|
||||
@ -57,57 +68,77 @@ def image_size(path):
|
||||
|
||||
def validate_index(path):
|
||||
errs, warns = [], []
|
||||
name = os.path.basename(path)
|
||||
name = os.path.relpath(path, STOCK_G)
|
||||
try:
|
||||
idx = json.load(open(path))
|
||||
except Exception as e:
|
||||
return [f"{name}: does not parse: {e}"], []
|
||||
return [f"{name}: does not parse: {e}"], [] # present-but-unparseable ⇒ FAIL
|
||||
d = os.path.dirname(path)
|
||||
|
||||
# ── provenance (charter law #3) — every atlas carries it, and the gate PRINTS the licence ──
|
||||
miss = [f for f in REQUIRED_PROV if not str(idx.get(f, "")).strip()]
|
||||
if miss:
|
||||
errs.append(f"{name}: missing/empty provenance {miss} (charter law #3 — every atlas carries provenance)")
|
||||
# ── the NAME must be one the runtime can fetch: stock_<type>_index.json, type ∈ STOCK_TYPES ──
|
||||
fname = os.path.basename(path)
|
||||
ftype = fname[len("stock_"):-len("_index.json")]
|
||||
if ftype not in STOCK_TYPES:
|
||||
errs.append(f"{name}: unloadable name — the runtime fetches `stock_<type>_index.json` for "
|
||||
f"type ∈ {sorted(STOCK_TYPES)}; {ftype!r} is never fetched (this is the R23 drift)")
|
||||
elif idx.get("kind") not in (None, ftype):
|
||||
errs.append(f"{name}: kind {idx.get('kind')!r} != the filename's type {ftype!r} — the filename "
|
||||
f"picks the stock slot, so they must agree")
|
||||
|
||||
# ── C's draw ceiling (LANE_C_PUB §7.1) ──
|
||||
atlases = idx.get("atlases", [])
|
||||
# ── identity: the per-shop base IS the godverse shop id (the loader keys on it) ──
|
||||
shop_dir = os.path.basename(d)
|
||||
sid = (idx.get("shop") or {}).get("godverse_id")
|
||||
if sid is None:
|
||||
errs.append(f"{name}: shop.godverse_id missing — the loader's base is the shop id; the atlas must say who it is")
|
||||
elif str(sid) != shop_dir:
|
||||
errs.append(f"{name}: shop.godverse_id {sid!r} != its directory {shop_dir!r} "
|
||||
f"(base `stock_godverse/{shop_dir}/` is the cache identity — they must agree)")
|
||||
|
||||
# ── provenance: NESTED, per the artifact + charter law #3 ("every atlas carries provenance") ──
|
||||
prov = idx.get("provenance")
|
||||
if not isinstance(prov, dict):
|
||||
errs.append(f"{name}: `provenance` object missing (charter law #3 — every atlas carries provenance)")
|
||||
prov = {}
|
||||
for f in ("generator", "snapshot"): # snapshot pins determinism (re-bakeable)
|
||||
if not str(prov.get(f, "")).strip():
|
||||
errs.append(f"{name}: provenance.{f} missing/empty")
|
||||
lic = prov.get("licence") or prov.get("license") # British is the artifact's spelling
|
||||
if not isinstance(lic, dict):
|
||||
errs.append(f"{name}: provenance.licence object missing (zone/flag/covers)")
|
||||
else:
|
||||
for f in ("zone", "flag", "covers"):
|
||||
if not str(lic.get(f, "")).strip():
|
||||
errs.append(f"{name}: provenance.licence.{f} missing/empty")
|
||||
|
||||
# ── C §7.2 ceilings ──
|
||||
atlases = idx.get("atlases")
|
||||
if not isinstance(atlases, list) or not atlases:
|
||||
errs.append(f"{name}: no atlases[] listed")
|
||||
atlases = []
|
||||
if len(atlases) > MAX_ATLASES:
|
||||
errs.append(f"{name}: {len(atlases)} atlases — C's hard cap is {MAX_ATLASES} "
|
||||
f"(1/shop; a 60+ item shop spills to the shared type-pack instead of fragmenting)")
|
||||
elif len(atlases) == 2:
|
||||
warns.append(f"{name}: 2 atlases (+1 draw) — C's rule is 1/shop; 2 is the cap, not the target")
|
||||
|
||||
items = idx.get("items", [])
|
||||
items = idx.get("items")
|
||||
if not isinstance(items, list) or not items:
|
||||
errs.append(f"{name}: no items")
|
||||
items = []
|
||||
|
||||
# ── C's texture/VRAM ceiling (§7.2) + index↔atlas consistency (the declared size must be REAL) ──
|
||||
if len(atlases) > MAX_ATLASES:
|
||||
errs.append(f"{name}: {len(atlases)} atlases — C §7.2 caps a shop at {MAX_ATLASES}")
|
||||
declared = idx.get("atlas_px")
|
||||
for a in atlases:
|
||||
ap = os.path.join(d, a)
|
||||
if not os.path.isfile(ap):
|
||||
errs.append(f"{name}: atlas {a!r} listed but missing on disk")
|
||||
continue
|
||||
errs.append(f"{name}: atlas {a!r} listed but missing on disk"); continue
|
||||
try:
|
||||
w, h = image_size(ap)
|
||||
except Exception as e:
|
||||
errs.append(f"{name}: atlas {a!r} unreadable: {e}")
|
||||
continue
|
||||
errs.append(f"{name}: atlas {a!r} unreadable: {e}"); continue
|
||||
if declared and (w != declared or h != declared):
|
||||
errs.append(f"{name}: atlas {a!r} is {w}×{h} but index declares atlas_px={declared}")
|
||||
big = len(items) >= BIG_SHOP_ITEMS
|
||||
cap = PX_MAX if big else PX_TYPICAL
|
||||
if max(w, h) > cap:
|
||||
errs.append(f"{name}: atlas {a!r} {w}×{h} exceeds C's ceiling {cap}² "
|
||||
f"({'60+ item shop' if big else f'{len(items)}-item shop → ≤{PX_TYPICAL}²'})")
|
||||
if max(w, h) > PX_TYPICAL and not big:
|
||||
warns.append(f"{name}: atlas {a!r} is {w}×{h} for {len(items)} items — prefer ≤{PX_TYPICAL}²")
|
||||
if max(w, h) > PX_MAX:
|
||||
errs.append(f"{name}: atlas {a!r} {w}×{h} exceeds C's ceiling {PX_MAX}²")
|
||||
elif max(w, h) > PX_TYPICAL and len(items) <= FITS_1024:
|
||||
warns.append(f"{name}: atlas {a!r} is {w}×{h} for {len(items)} items — C: ≤{PX_TYPICAL}² "
|
||||
f"whenever the crate fits it (≤{FITS_1024} at 256²)")
|
||||
|
||||
# ── the index itself (schema confirmed verbatim in LANE_C_PUB §7.3) ──
|
||||
# ── the index itself (schema per the loader: it reads items[].uv/atlas; `cell`/`cell_w` are informational) ──
|
||||
ids = set()
|
||||
for i, it in enumerate(items):
|
||||
if not isinstance(it, dict):
|
||||
@ -133,30 +164,40 @@ def validate_index(path):
|
||||
if not (isinstance(uv, list) and len(uv) == 4 and all(isinstance(v, (int, float)) for v in uv)):
|
||||
errs.append(f"{name}[{it['id']}]: uv must be [u0,v0,u1,v1] numbers"); continue
|
||||
u0, v0, u1, v1 = uv
|
||||
if not (0 <= u0 < u1 <= 1 and 0 <= v0 < v1 <= 1): # UV origin top-left; C flips V (R7 law)
|
||||
if not (0 <= u0 < u1 <= 1 and 0 <= v0 < v1 <= 1): # origin top-left; stockpack flips V (R7 law)
|
||||
errs.append(f"{name}[{it['id']}]: uv out of range / inverted: {uv}")
|
||||
return errs, warns
|
||||
|
||||
|
||||
def main():
|
||||
idxs = sorted(glob.glob(os.path.join(STOCK_G, "**", "stock_shop_*_index.json"), recursive=True))
|
||||
idxs = sorted(glob.glob(GLOB))
|
||||
if not idxs:
|
||||
print("atlas-QA: no per-shop atlases yet (Lane G's G2a) — nothing to validate")
|
||||
# THE VACUOUS-GATE LAW (R23, F): absent subject ⇒ SKIP with a printed reason — and prove the set
|
||||
# really is empty. Files present but unmatched = a misnamed atlas the loader can't fetch ⇒ FAIL.
|
||||
stray = [p for p in glob.glob(os.path.join(STOCK_G, "**", "*"), recursive=True) if os.path.isfile(p)]
|
||||
if stray:
|
||||
print(f"atlas-QA FAIL — {len(stray)} file(s) under stock_godverse/ but 0 matched {GLOB}")
|
||||
print(f" the runtime fetches `<base>stock_<type>_index.json`; these cannot be loaded:")
|
||||
for p in stray[:8]:
|
||||
print(f" unmatched: {os.path.relpath(p, ROOT)}")
|
||||
return 1
|
||||
print(f"atlas-QA SKIP: no atlases present (0 files matched glob {GLOB})")
|
||||
return 0
|
||||
all_errs, all_warns = [], []
|
||||
for p in idxs:
|
||||
e, w = validate_index(p)
|
||||
all_errs += e; all_warns += w
|
||||
idx = {}
|
||||
try:
|
||||
idx = json.load(open(p))
|
||||
except Exception:
|
||||
pass
|
||||
print(f" {os.path.basename(p):40} items={len(idx.get('items', [])):>3} "
|
||||
idx = {}
|
||||
shop = (idx.get("shop") or {}).get("name", "?")
|
||||
print(f" {os.path.relpath(p, STOCK_G):44} {shop[:22]:22} items={len(idx.get('items', [])):>3} "
|
||||
f"atlases={len(idx.get('atlases', []))} px={idx.get('atlas_px')} "
|
||||
f"snapshot={str(idx.get('snapshot', '—'))[:24]} {'OK' if not e else 'ERR(' + str(len(e)) + ')'}")
|
||||
if idx.get("license"): # licence law: the gate SHOWS it, a human clears it
|
||||
print(f" licence: {idx['license']}")
|
||||
f"{'OK' if not e else 'ERR(' + str(len(e)) + ')'}")
|
||||
lic = (idx.get("provenance") or {}).get("licence") or {}
|
||||
if lic: # the licence is PRINTED — clearing it is a human call
|
||||
print(f" licence: {lic.get('zone')} · {lic.get('flag')}")
|
||||
for w in all_warns:
|
||||
print(f" WARN {w}")
|
||||
for e in all_errs:
|
||||
|
||||
@ -10,6 +10,10 @@
|
||||
"stock_record_atlas_00.webp",
|
||||
"stock_record_atlas_01.webp"
|
||||
],
|
||||
"license": "in-house-green \u2014 the shop's own product shots (monsterrobot.party), cached by dealgod; FLAG BEFORE ANY PUBLIC/COMMERCIAL RELEASE",
|
||||
"attribution": "Stock, prices + covers: Monster Robot Party (monsterrobot.party) \u2014 the shop's own product shots \u00b7 cover cache: GODVERSE/dealgod \u00b7 POS: recordgod",
|
||||
"generator": "pipeline/godverse_stock.py",
|
||||
"snapshot": "monsterrobot_550.snapshot.json",
|
||||
"shop": {
|
||||
"godverse_id": 3962749,
|
||||
"slug": "monsterrobot",
|
||||
@ -32,7 +36,7 @@
|
||||
"captured": "2026-07-15",
|
||||
"extraction": "see load_image_map() docstring \u2014 the products projection command"
|
||||
},
|
||||
"licence": {
|
||||
"license": {
|
||||
"covers": "the shop's own product shots (monsterrobot.party), cached by dealgod",
|
||||
"zone": "in-house-green",
|
||||
"flag": "FLAG BEFORE ANY PUBLIC/COMMERCIAL RELEASE",
|
||||
@ -40,7 +44,7 @@
|
||||
"pii": "none \u2014 inventory/crate/disc_cache only; customer/staff/sales never read"
|
||||
},
|
||||
"snapshot": "monsterrobot_550.snapshot.json",
|
||||
"snapshot_sha256": "d6b0e2f7a7474a69354370ade98b1887d8c778a4f07a2ba62a6c366dadd22c2f",
|
||||
"snapshot_sha256": "8b0ff903ce8f746aa80ef25f3eb498ec9a3bc890ec0607c2a5921674e4ef612a",
|
||||
"determinism": "pure bake \u2014 same snapshot + same cover bytes => byte-identical atlas; covers pinned by sha256, no RNG, no clock"
|
||||
},
|
||||
"items": [
|
||||
|
||||
@ -42,6 +42,49 @@ strip (the R17 mega-strip risk — a whole region of 2,918 shops became an 11.6
|
||||
`type` should be a registry `SHOP_TYPE` (`record`/`opshop`/`toy`/`book`/`video`/`pawn`/`milkbar`/`dept`/`stall`);
|
||||
`build_towns.py` maps OSM `shop=*` tags to these (unknowns land on `opshop`).
|
||||
|
||||
## `godverseShopId` — the stock identity (ROUND24, v5.0-alpha; Lane A + Lane G)
|
||||
|
||||
The optional key that says **"this shop's real stock lives at `web/assets/stock_godverse/<godverseShopId>/`"**.
|
||||
The schema stays **v2** — it's an optional field, so every shipped cache stays valid and the 21 osm caches
|
||||
are untouched *by construction*.
|
||||
|
||||
```jsonc
|
||||
"shops": [
|
||||
{ "id": 158, "name": "Egg Records", "type": "record", "lat": -33.897, "lon": 151.179,
|
||||
"suburb": "Newtown", "godverseShopId": 158 }, // census shop: happens to equal `id`
|
||||
{ "id": 9001, "name": "Monster Robot Party", "type": "record", "lat": -27.455, "lon": 153.006,
|
||||
"suburb": "Red Hill", "godverseShopId": 3962749 } // NOT in the census: a dealgod store id
|
||||
]
|
||||
```
|
||||
|
||||
**It is NOT `shop.id`, and it is NOT "the census id".** That shortcut breaks on the one shop the v5 alpha
|
||||
is about. Two id spaces ride this field, and G verified they're disjoint:
|
||||
|
||||
| shop | source of the id | equals `shop.id`? |
|
||||
|---|---|---|
|
||||
| census shops (`newtown_godverse`) | thriftgod `shop.id` — max **2992** | yes, incidentally |
|
||||
| **Monster Robot Party** | **dealgod store 3962749** — *not in thriftgod's census at all* | **no** |
|
||||
|
||||
So treat it as an **opaque key**: never derive it from `id`, never assume equality, never build a mapping
|
||||
table (the R12 gigKey lesson — one key, zero mapping; **G emits it, everyone else reads it**).
|
||||
|
||||
**Rules** — split by what is actually a hazard, not by tidiness:
|
||||
- **Malformed → ERROR.** Must be a **positive integer**. It names a path segment, so `"3962749"` and
|
||||
`3962749` are the same folder but different JSON; that coercion drift is the species that produced the
|
||||
`licence`/`license` atlas break. One truth.
|
||||
- **Duplicate within a cache → ERROR.** Two shops keyed to one atlas *is* mis-stocking (charter risk #3:
|
||||
"fail-soft to tier 0, **never mis-stocked**").
|
||||
- **Missing on a `source: "godverse+osm"` cache → WARN, and those shops fall soft to tier 0 (parody).**
|
||||
Not an error: the charter's determinism boundary is *world = seeded and gated, stock = data tiers*, so a
|
||||
town must never fail to load because its **stock** identity is absent. The teeth are in `selfcheck`,
|
||||
where **partial keying FAILS** (a half-keyed town silently mis-stocks the rest) and zero keying prints an
|
||||
explicit **SKIP** with the count, per the vacuous-gate law.
|
||||
|
||||
**It survives the lift.** `plan_osm` copies it onto `plan.shops[]`, because the runtime reads the *plan*,
|
||||
never the cache — without that the field would be decoration and F's per-shop `base` could never resolve.
|
||||
Absent ⇒ the key is **not written** (not `undefined`), so `JSON.stringify` is byte-identical and every
|
||||
pinned town golden holds until G re-emits.
|
||||
|
||||
## Schema v2 — `roads[]` (ROUND18, v4.0-alpha REAL ROADS)
|
||||
|
||||
A **v2** cache adds an optional `roads[]` — the town's real OSM street geometry. When present, `plan_osm`
|
||||
|
||||
@ -20,26 +20,47 @@ the "© OpenStreetMap contributors (ODbL)" credit visible.
|
||||
**`index.json` is the machine-readable roster** (key/town/state/shops/roads) — read it rather than
|
||||
hardcoding a list. Every cache carries the same terms; per-town numbers live in each cache's `counts`.
|
||||
|
||||
**The pack: 21 real towns · 1180 shops · 295 heroes** (+ Lane G's `newtown_godverse` = 22 rostered in
|
||||
**The pack: 21 real towns · 1196 shops · 299 heroes** (+ Lane G's `newtown_godverse` = 22 rostered in
|
||||
`index.json`), every one fetched from OSM Overpass with a bounded per-town bbox, ODbL 1.0, raw snapshots
|
||||
in `_raw/` (shops + roads per town), fetched 2026-07-16.
|
||||
in `_raw/` (shops + roads per town), fetched 2026-07-16 (`redhill_real` 2026-07-17).
|
||||
|
||||
| state | towns |
|
||||
|---|---|
|
||||
| NSW (6) | katoomba · newtown · glebe · marrickville · newcastle (Hamilton strip) · bowral |
|
||||
| VIC (7) | fitzroy · brunswick · geelong · bendigo · ballarat · castlemaine · daylesford |
|
||||
| VIC (6) | fitzroy · brunswick · geelong · bendigo · castlemaine · daylesford |
|
||||
| QLD (2) | westend · **redhill** (Monster Robot Party's real street — see below) |
|
||||
| WA (2) | fremantle · northbridge |
|
||||
| QLD (1) | westend |
|
||||
| TAS (2) | hobart · launceston |
|
||||
| SA (1) | adelaide |
|
||||
| ACT (1) | braddon |
|
||||
| NT (1) | darwin |
|
||||
|
||||
### `redhill_real` — the v5 crate's real street (R24, ledger #3)
|
||||
|
||||
**Red Hill, Brisbane QLD** · centre `-27.4553, 153.0064` · span 2.4 km · 36 shops (9 heroes + 27 texture,
|
||||
−29 subsampled) · 884 roads · median NN 73 m, hub 5/36. Built because v5's 120 real records come from a
|
||||
real shop — **Monster Robot Party, 147 Musgrave Rd** — and the shop had no street. **Musgrave Road is in
|
||||
`roads[]` (41 ways, primary + residential)**, which was the point of the round. This cache is the DONOR
|
||||
Lane G runs `godverse_town.py` against → `redhill_godverse`.
|
||||
|
||||
The shop itself is **not in OSM** (no `shop=music` node in the bbox), so G injects it from the census —
|
||||
it cannot arrive via this fetch. See LANE_E_NOTES R24 for the seating hazard handed to G/A.
|
||||
|
||||
**Retired (R23, counted):** `toowoomba_real` — D's clustering audit measured it DEAD (0 patronage finds
|
||||
in 1417 checks); a re-bbox onto the Ruthven St heart fixed the spacing metric (median NN 396 → 89 m) but
|
||||
made the hub worse (2/12 within 160 m, the pack's worst), and its densest 300 m cluster is only 3 shops.
|
||||
Cache + raw snapshots removed; the config entry stays commented with the rationale. See LANE_E_NOTES R23.
|
||||
|
||||
**Retired (R24, counted):** `ballarat_real` — A's spacing warn caught it at median NN 255 m. Unlike
|
||||
Toowoomba, Ballarat is **not dead**: 103 POIs in the bbox, densest 400 m disc = 47 of them. It fails a
|
||||
different way — **its live core and its heroes don't coincide.** The dense disc is cafés/clothes/bakeries;
|
||||
the op-shops sit 854 m, 1042 m and 1709 m out, so no bbox holds both. Measured at every span (real builds):
|
||||
1.0 km → 8 shops / NN 184 m / hub 4/8 but **zero op-shops** (a thrift town with no thrift shop); 2.0 km →
|
||||
12 / 186 m / 3-12; 3.0 km (as shipped) → 20 / 255 m / 4-20. Every row is worse than any shipped town
|
||||
(darwin 68 m, glebe 60 m, redhill 73 m). The binding constraint is ours, not OSM's: with 2 heroes the 3×
|
||||
texture cap keeps 6 of 47 POIs and the subsample is **spatially blind**, so survivors scatter at any span.
|
||||
Cache + raw snapshots removed; config entry commented with the rationale. See LANE_E_NOTES R24.
|
||||
|
||||
## The density widening (v4.0-beta, ROUND20 ledger #2)
|
||||
|
||||
The fetch covers the **secondhand heroes** plus the everyday main-street **texture** (bakery/cafe/deli/
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
1208
web/assets/towns/_raw/redhill_real.overpass.json
Normal file
1208
web/assets/towns/_raw/redhill_real.overpass.json
Normal file
File diff suppressed because it is too large
Load Diff
165975
web/assets/towns/_raw/redhill_real.roads.overpass.json
Normal file
165975
web/assets/towns/_raw/redhill_real.roads.overpass.json
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
||||
{
|
||||
"schema": "procity-towns-index/1",
|
||||
"generated": "2026-07-16",
|
||||
"generated": "2026-07-17",
|
||||
"license": "ODbL 1.0",
|
||||
"attribution": "\u00a9 OpenStreetMap contributors",
|
||||
"towns": [
|
||||
@ -12,14 +12,6 @@
|
||||
"roads": true,
|
||||
"source": "osm"
|
||||
},
|
||||
{
|
||||
"key": "ballarat_real",
|
||||
"town": "Ballarat",
|
||||
"state": "VIC",
|
||||
"shops": 20,
|
||||
"roads": true,
|
||||
"source": "osm"
|
||||
},
|
||||
{
|
||||
"key": "bendigo_real",
|
||||
"town": "Bendigo",
|
||||
@ -172,6 +164,14 @@
|
||||
"roads": true,
|
||||
"source": "osm"
|
||||
},
|
||||
{
|
||||
"key": "redhill_real",
|
||||
"town": "Red Hill",
|
||||
"state": "QLD",
|
||||
"shops": 36,
|
||||
"roads": true,
|
||||
"source": "osm"
|
||||
},
|
||||
{
|
||||
"key": "westend_real",
|
||||
"town": "West End",
|
||||
|
||||
@ -18,13 +18,17 @@
|
||||
"maxLon": 151.1931100389657
|
||||
},
|
||||
"counts": {
|
||||
"raw": 19,
|
||||
"shops": 18,
|
||||
"roads": 1086
|
||||
"roads": 1086,
|
||||
"shops": 72,
|
||||
"census": 18,
|
||||
"extra": 0,
|
||||
"texture": 54,
|
||||
"donorSwaps": 18
|
||||
},
|
||||
"shops": [
|
||||
{
|
||||
"id": 158,
|
||||
"godverseShopId": 158,
|
||||
"name": "Egg Records",
|
||||
"type": "record",
|
||||
"lat": -33.897238,
|
||||
@ -33,6 +37,7 @@
|
||||
},
|
||||
{
|
||||
"id": 247,
|
||||
"godverseShopId": 247,
|
||||
"name": "The Vintage Record",
|
||||
"type": "record",
|
||||
"lat": -33.8875182,
|
||||
@ -41,6 +46,7 @@
|
||||
},
|
||||
{
|
||||
"id": 248,
|
||||
"godverseShopId": 248,
|
||||
"name": "Aalders",
|
||||
"type": "opshop",
|
||||
"lat": -33.8877879,
|
||||
@ -49,6 +55,7 @@
|
||||
},
|
||||
{
|
||||
"id": 568,
|
||||
"godverseShopId": 568,
|
||||
"name": "Towers Antiques",
|
||||
"type": "opshop",
|
||||
"lat": -33.9035515,
|
||||
@ -57,6 +64,7 @@
|
||||
},
|
||||
{
|
||||
"id": 569,
|
||||
"godverseShopId": 569,
|
||||
"name": "Old Wares",
|
||||
"type": "opshop",
|
||||
"lat": -33.9032933,
|
||||
@ -65,6 +73,7 @@
|
||||
},
|
||||
{
|
||||
"id": 608,
|
||||
"godverseShopId": 608,
|
||||
"name": "Goulds Books",
|
||||
"type": "book",
|
||||
"lat": -33.9029178,
|
||||
@ -73,6 +82,7 @@
|
||||
},
|
||||
{
|
||||
"id": 717,
|
||||
"godverseShopId": 717,
|
||||
"name": "Co-op",
|
||||
"type": "book",
|
||||
"lat": -33.890884,
|
||||
@ -81,6 +91,7 @@
|
||||
},
|
||||
{
|
||||
"id": 1082,
|
||||
"godverseShopId": 1082,
|
||||
"name": "Downtown Music",
|
||||
"type": "record",
|
||||
"lat": -33.8878728,
|
||||
@ -89,6 +100,7 @@
|
||||
},
|
||||
{
|
||||
"id": 1173,
|
||||
"godverseShopId": 1173,
|
||||
"name": "Haze Guitars",
|
||||
"type": "record",
|
||||
"lat": -33.8925979,
|
||||
@ -97,6 +109,7 @@
|
||||
},
|
||||
{
|
||||
"id": 1697,
|
||||
"godverseShopId": 1697,
|
||||
"name": "Thats So Mi",
|
||||
"type": "opshop",
|
||||
"lat": -33.9011361,
|
||||
@ -105,6 +118,7 @@
|
||||
},
|
||||
{
|
||||
"id": 2019,
|
||||
"godverseShopId": 2019,
|
||||
"name": "St Luke\u2019s Op Shop",
|
||||
"type": "opshop",
|
||||
"lat": -33.898898,
|
||||
@ -113,6 +127,7 @@
|
||||
},
|
||||
{
|
||||
"id": 2048,
|
||||
"godverseShopId": 2048,
|
||||
"name": "Savers",
|
||||
"type": "opshop",
|
||||
"lat": -33.8869187,
|
||||
@ -121,6 +136,7 @@
|
||||
},
|
||||
{
|
||||
"id": 2200,
|
||||
"godverseShopId": 2200,
|
||||
"name": "Better Read Than Dead",
|
||||
"type": "book",
|
||||
"lat": -33.8961303,
|
||||
@ -129,6 +145,7 @@
|
||||
},
|
||||
{
|
||||
"id": 2201,
|
||||
"godverseShopId": 2201,
|
||||
"name": "Elizabeth's Bookshop",
|
||||
"type": "book",
|
||||
"lat": -33.8959321,
|
||||
@ -137,6 +154,7 @@
|
||||
},
|
||||
{
|
||||
"id": 2212,
|
||||
"godverseShopId": 2212,
|
||||
"name": "Papa Disquo",
|
||||
"type": "record",
|
||||
"lat": -33.8986661,
|
||||
@ -145,6 +163,7 @@
|
||||
},
|
||||
{
|
||||
"id": 2377,
|
||||
"godverseShopId": 2377,
|
||||
"name": "Kidstuff",
|
||||
"type": "toy",
|
||||
"lat": -33.8870857,
|
||||
@ -153,6 +172,7 @@
|
||||
},
|
||||
{
|
||||
"id": 2505,
|
||||
"godverseShopId": 2505,
|
||||
"name": "EB Games",
|
||||
"type": "video",
|
||||
"lat": -33.9064099,
|
||||
@ -161,11 +181,444 @@
|
||||
},
|
||||
{
|
||||
"id": 2506,
|
||||
"godverseShopId": 2506,
|
||||
"name": "Toys and Tales",
|
||||
"type": "toy",
|
||||
"lat": -33.9073712,
|
||||
"lon": 151.1724377,
|
||||
"suburb": "Newtown"
|
||||
},
|
||||
{
|
||||
"id": 62203508,
|
||||
"name": "Bunnings Warehouse",
|
||||
"type": "dept",
|
||||
"lat": -33.905746,
|
||||
"lon": 151.19369,
|
||||
"suburb": "Alexandria"
|
||||
},
|
||||
{
|
||||
"id": 279681996,
|
||||
"name": "Woolworths Metro",
|
||||
"type": "dept",
|
||||
"lat": -33.89859,
|
||||
"lon": 151.181704,
|
||||
"suburb": "Newtown"
|
||||
},
|
||||
{
|
||||
"id": 292127221,
|
||||
"name": "The Hospitality Store",
|
||||
"type": "dept",
|
||||
"lat": -33.88782,
|
||||
"lon": 151.17241,
|
||||
"suburb": "Camperdown"
|
||||
},
|
||||
{
|
||||
"id": 305516372,
|
||||
"name": "IGA",
|
||||
"type": "dept",
|
||||
"lat": -33.89597,
|
||||
"lon": 151.180575,
|
||||
"suburb": "Newtown"
|
||||
},
|
||||
{
|
||||
"id": 423218371,
|
||||
"name": "Foodcraft",
|
||||
"type": "milkbar",
|
||||
"lat": -33.90308,
|
||||
"lon": 151.185224,
|
||||
"suburb": "Erskineville"
|
||||
},
|
||||
{
|
||||
"id": 605984546,
|
||||
"name": "Nextra",
|
||||
"type": "milkbar",
|
||||
"lat": -33.906418,
|
||||
"lon": 151.171836,
|
||||
"suburb": "Marrickville"
|
||||
},
|
||||
{
|
||||
"id": 605984562,
|
||||
"name": "Lowes",
|
||||
"type": "dept",
|
||||
"lat": -33.906876,
|
||||
"lon": 151.171753,
|
||||
"suburb": "Marrickville"
|
||||
},
|
||||
{
|
||||
"id": 605984563,
|
||||
"name": "Kimmy Jewellery",
|
||||
"type": "dept",
|
||||
"lat": -33.906791,
|
||||
"lon": 151.171765,
|
||||
"suburb": "Marrickville"
|
||||
},
|
||||
{
|
||||
"id": 605984565,
|
||||
"name": "Forcast",
|
||||
"type": "dept",
|
||||
"lat": -33.906889,
|
||||
"lon": 151.17189,
|
||||
"suburb": "Marrickville"
|
||||
},
|
||||
{
|
||||
"id": 605984566,
|
||||
"name": "Bras N Things",
|
||||
"type": "dept",
|
||||
"lat": -33.907022,
|
||||
"lon": 151.171843,
|
||||
"suburb": "Marrickville"
|
||||
},
|
||||
{
|
||||
"id": 605984567,
|
||||
"name": "Sussan",
|
||||
"type": "dept",
|
||||
"lat": -33.907073,
|
||||
"lon": 151.171834,
|
||||
"suburb": "Marrickville"
|
||||
},
|
||||
{
|
||||
"id": 605984571,
|
||||
"name": "Sunset Flowers",
|
||||
"type": "stall",
|
||||
"lat": -33.907283,
|
||||
"lon": 151.171871,
|
||||
"suburb": "Marrickville"
|
||||
},
|
||||
{
|
||||
"id": 605984595,
|
||||
"name": "Sergio's Cake Shop",
|
||||
"type": "milkbar",
|
||||
"lat": -33.906821,
|
||||
"lon": 151.172217,
|
||||
"suburb": "Marrickville"
|
||||
},
|
||||
{
|
||||
"id": 605984610,
|
||||
"name": "Espresso Brothers",
|
||||
"type": "milkbar",
|
||||
"lat": -33.906382,
|
||||
"lon": 151.172126,
|
||||
"suburb": "Marrickville"
|
||||
},
|
||||
{
|
||||
"id": 1039467648,
|
||||
"name": "Storeroom Vintage",
|
||||
"type": "dept",
|
||||
"lat": -33.898406,
|
||||
"lon": 151.176285,
|
||||
"suburb": "Newtown"
|
||||
},
|
||||
{
|
||||
"id": 1039467672,
|
||||
"name": "Natural Fit Footwear",
|
||||
"type": "dept",
|
||||
"lat": -33.90725,
|
||||
"lon": 151.172143,
|
||||
"suburb": "Marrickville"
|
||||
},
|
||||
{
|
||||
"id": 1039555349,
|
||||
"name": "The Athlete's Foot",
|
||||
"type": "dept",
|
||||
"lat": -33.906646,
|
||||
"lon": 151.17215,
|
||||
"suburb": "Marrickville"
|
||||
},
|
||||
{
|
||||
"id": 1079380693,
|
||||
"name": "Coffee @ Fisher",
|
||||
"type": "milkbar",
|
||||
"lat": -33.886833,
|
||||
"lon": 151.190043,
|
||||
"suburb": "Darlington"
|
||||
},
|
||||
{
|
||||
"id": 1732566722,
|
||||
"name": "Welcome Mart",
|
||||
"type": "dept",
|
||||
"lat": -33.89993,
|
||||
"lon": 151.184575,
|
||||
"suburb": "Erskineville"
|
||||
},
|
||||
{
|
||||
"id": 1735908953,
|
||||
"name": "Boh\u00e4us Cafe",
|
||||
"type": "milkbar",
|
||||
"lat": -33.905708,
|
||||
"lon": 151.191348,
|
||||
"suburb": "Alexandria"
|
||||
},
|
||||
{
|
||||
"id": 2125897845,
|
||||
"name": "Pastizzi Cafe",
|
||||
"type": "milkbar",
|
||||
"lat": -33.903197,
|
||||
"lon": 151.179532,
|
||||
"suburb": "Newtown"
|
||||
},
|
||||
{
|
||||
"id": 2136793910,
|
||||
"name": "Store Espresso",
|
||||
"type": "milkbar",
|
||||
"lat": -33.890334,
|
||||
"lon": 151.176325,
|
||||
"suburb": "Camperdown"
|
||||
},
|
||||
{
|
||||
"id": 2136793911,
|
||||
"name": "Gather on the Green",
|
||||
"type": "milkbar",
|
||||
"lat": -33.890355,
|
||||
"lon": 151.176263,
|
||||
"suburb": "Camperdown"
|
||||
},
|
||||
{
|
||||
"id": 2163387594,
|
||||
"name": "Grounded 2050",
|
||||
"type": "milkbar",
|
||||
"lat": -33.887901,
|
||||
"lon": 151.180526,
|
||||
"suburb": "Camperdown"
|
||||
},
|
||||
{
|
||||
"id": 2170430692,
|
||||
"name": "Carslaw Kitchen",
|
||||
"type": "milkbar",
|
||||
"lat": -33.888238,
|
||||
"lon": 151.190481,
|
||||
"suburb": "Darlington"
|
||||
},
|
||||
{
|
||||
"id": 2365225259,
|
||||
"name": "Ken Neale Overflow",
|
||||
"type": "dept",
|
||||
"lat": -33.90187,
|
||||
"lon": 151.178671,
|
||||
"suburb": "Newtown"
|
||||
},
|
||||
{
|
||||
"id": 2787084005,
|
||||
"name": "Amazing Paints",
|
||||
"type": "dept",
|
||||
"lat": -33.891631,
|
||||
"lon": 151.176271,
|
||||
"suburb": "Camperdown"
|
||||
},
|
||||
{
|
||||
"id": 2965437480,
|
||||
"name": "Cafe Abercrombie",
|
||||
"type": "milkbar",
|
||||
"lat": -33.892142,
|
||||
"lon": 151.193024,
|
||||
"suburb": "Darlington"
|
||||
},
|
||||
{
|
||||
"id": 2968343588,
|
||||
"name": "Bakerman",
|
||||
"type": "milkbar",
|
||||
"lat": -33.899893,
|
||||
"lon": 151.183961,
|
||||
"suburb": "Erskineville"
|
||||
},
|
||||
{
|
||||
"id": 3278493016,
|
||||
"name": "Wildwood Designs",
|
||||
"type": "dept",
|
||||
"lat": -33.887904,
|
||||
"lon": 151.169934,
|
||||
"suburb": "Stanmore"
|
||||
},
|
||||
{
|
||||
"id": 3278493018,
|
||||
"name": "Inspired Design Furniture",
|
||||
"type": "dept",
|
||||
"lat": -33.887902,
|
||||
"lon": 151.170079,
|
||||
"suburb": "Stanmore"
|
||||
},
|
||||
{
|
||||
"id": 3278509468,
|
||||
"name": "Recharging",
|
||||
"type": "milkbar",
|
||||
"lat": -33.887515,
|
||||
"lon": 151.167686,
|
||||
"suburb": "Annandale"
|
||||
},
|
||||
{
|
||||
"id": 3951219159,
|
||||
"name": "Miracle",
|
||||
"type": "dept",
|
||||
"lat": -33.892823,
|
||||
"lon": 151.186108,
|
||||
"suburb": "Newtown"
|
||||
},
|
||||
{
|
||||
"id": 4860774437,
|
||||
"name": "Newtown Garden Market",
|
||||
"type": "stall",
|
||||
"lat": -33.903009,
|
||||
"lon": 151.179679,
|
||||
"suburb": "Newtown"
|
||||
},
|
||||
{
|
||||
"id": 5203491923,
|
||||
"name": "Cynthia's Caf\u00e9",
|
||||
"type": "milkbar",
|
||||
"lat": -33.908747,
|
||||
"lon": 151.191977,
|
||||
"suburb": "Alexandria"
|
||||
},
|
||||
{
|
||||
"id": 5205362421,
|
||||
"name": "Little Vietnam Roll & Juice",
|
||||
"type": "milkbar",
|
||||
"lat": -33.906344,
|
||||
"lon": 151.181132,
|
||||
"suburb": "Newtown"
|
||||
},
|
||||
{
|
||||
"id": 5299091858,
|
||||
"name": "Hui Bro\u2019s Cafe",
|
||||
"type": "milkbar",
|
||||
"lat": -33.898095,
|
||||
"lon": 151.179152,
|
||||
"suburb": "Newtown"
|
||||
},
|
||||
{
|
||||
"id": 5300089045,
|
||||
"name": "Handcraft Specialty Coffee",
|
||||
"type": "milkbar",
|
||||
"lat": -33.892575,
|
||||
"lon": 151.185688,
|
||||
"suburb": "Newtown"
|
||||
},
|
||||
{
|
||||
"id": 5599458217,
|
||||
"name": "The Floral Decorator",
|
||||
"type": "stall",
|
||||
"lat": -33.899566,
|
||||
"lon": 151.184195,
|
||||
"suburb": "Erskineville"
|
||||
},
|
||||
{
|
||||
"id": 5839239307,
|
||||
"name": "Luke Avenue",
|
||||
"type": "milkbar",
|
||||
"lat": -33.897984,
|
||||
"lon": 151.175225,
|
||||
"suburb": "Newtown"
|
||||
},
|
||||
{
|
||||
"id": 5905712358,
|
||||
"name": "The Coffee Nook on Swanson",
|
||||
"type": "milkbar",
|
||||
"lat": -33.900301,
|
||||
"lon": 151.189403,
|
||||
"suburb": "Erskineville"
|
||||
},
|
||||
{
|
||||
"id": 5931284416,
|
||||
"name": "Patisserie Petunia",
|
||||
"type": "milkbar",
|
||||
"lat": -33.90509,
|
||||
"lon": 151.18052,
|
||||
"suburb": "Newtown"
|
||||
},
|
||||
{
|
||||
"id": 6283737899,
|
||||
"name": "Dangerfield",
|
||||
"type": "dept",
|
||||
"lat": -33.895217,
|
||||
"lon": 151.181947,
|
||||
"suburb": "Newtown"
|
||||
},
|
||||
{
|
||||
"id": 6564462487,
|
||||
"name": "Marani Deli",
|
||||
"type": "milkbar",
|
||||
"lat": -33.895992,
|
||||
"lon": 151.182698,
|
||||
"suburb": "Newtown"
|
||||
},
|
||||
{
|
||||
"id": 9572458384,
|
||||
"name": "Budha Bowl",
|
||||
"type": "milkbar",
|
||||
"lat": -33.898699,
|
||||
"lon": 151.176397,
|
||||
"suburb": "Newtown"
|
||||
},
|
||||
{
|
||||
"id": 9776428741,
|
||||
"name": "K\u00fcrt\u0151sh",
|
||||
"type": "milkbar",
|
||||
"lat": -33.907897,
|
||||
"lon": 151.171833,
|
||||
"suburb": "Marrickville"
|
||||
},
|
||||
{
|
||||
"id": 11984912425,
|
||||
"name": "Provocator",
|
||||
"type": "dept",
|
||||
"lat": -33.901201,
|
||||
"lon": 151.178441,
|
||||
"suburb": "Newtown"
|
||||
},
|
||||
{
|
||||
"id": 12200892725,
|
||||
"name": "Feather & Bone",
|
||||
"type": "milkbar",
|
||||
"lat": -33.909398,
|
||||
"lon": 151.168471,
|
||||
"suburb": "Marrickville"
|
||||
},
|
||||
{
|
||||
"id": 13341304001,
|
||||
"name": "AS Colour",
|
||||
"type": "dept",
|
||||
"lat": -33.895051,
|
||||
"lon": 151.181701,
|
||||
"suburb": "Newtown"
|
||||
},
|
||||
{
|
||||
"id": 13585970805,
|
||||
"name": "Numero Uno Coffee Roasters",
|
||||
"type": "milkbar",
|
||||
"lat": -33.908693,
|
||||
"lon": 151.18052,
|
||||
"suburb": "St Peters"
|
||||
},
|
||||
{
|
||||
"id": 13719824202,
|
||||
"name": "A.P Town",
|
||||
"type": "milkbar",
|
||||
"lat": -33.893929,
|
||||
"lon": 151.183827,
|
||||
"suburb": "Newtown"
|
||||
},
|
||||
{
|
||||
"id": 13785846501,
|
||||
"name": "The Happy Herb Shop Newtown",
|
||||
"type": "milkbar",
|
||||
"lat": -33.898379,
|
||||
"lon": 151.178005,
|
||||
"suburb": "Newtown"
|
||||
},
|
||||
{
|
||||
"id": 13919280070,
|
||||
"name": "Tonton Bread",
|
||||
"type": "milkbar",
|
||||
"lat": -33.90484,
|
||||
"lon": 151.17338,
|
||||
"suburb": "Marrickville"
|
||||
},
|
||||
{
|
||||
"id": 13919369194,
|
||||
"name": "Mamuki",
|
||||
"type": "milkbar",
|
||||
"lat": -33.898789,
|
||||
"lon": 151.173092,
|
||||
"suburb": "Enmore"
|
||||
}
|
||||
],
|
||||
"roads": [
|
||||
|
||||
38572
web/assets/towns/redhill_godverse.json
Normal file
38572
web/assets/towns/redhill_godverse.json
Normal file
File diff suppressed because it is too large
Load Diff
38554
web/assets/towns/redhill_real.json
Normal file
38554
web/assets/towns/redhill_real.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -94,6 +94,20 @@ export function medianShopSpacing(cache) {
|
||||
return nn.length % 2 ? nn[m] : (nn[m - 1] + nn[m]) / 2;
|
||||
}
|
||||
|
||||
// ── the identity field (ROUND24 ledger #2, with Lane G) ──────────────────────────────────────────
|
||||
// `shop.godverseShopId` — the OPTIONAL external key that says "this lot's stock lives at
|
||||
// `stock_godverse/<godverseShopId>/`". Absent everywhere except godverse caches; schema stays v2
|
||||
// (an optional field breaks no shipped cache, and the 22 osm caches are untouched by construction).
|
||||
//
|
||||
// IT IS NOT `shop.id`, AND IT IS NOT "THE CENSUS ID" — that assumption breaks on the one shop the
|
||||
// v5 alpha is about. Two id spaces ride this field, and G verified they are disjoint:
|
||||
// census shops → thriftgod `shop.id` (max 2992) — here godverseShopId does equal `id`
|
||||
// Monster Robot → dealgod store 3962749 — NOT in thriftgod's census at all
|
||||
// So it's an OPAQUE key: never derive it from `id`, never assume equality, never build a mapping
|
||||
// table (the R12 gigKey lesson — one key, zero mapping; G emits it, everyone else reads it).
|
||||
const GODVERSE_SOURCE = 'godverse+osm';
|
||||
const isGodverseCache = cache => cache && cache.source === GODVERSE_SOURCE;
|
||||
|
||||
// ── roads[] (schema v2, ROUND18) ─────────────────────────────────────────────────────────────────
|
||||
// OSM `highway=*` → CityPlan edge kind. The graph lift refines this with shop density (a `side` road
|
||||
// dense with shops becomes the `main` spine). Anything unmapped defaults to 'side'.
|
||||
@ -147,6 +161,38 @@ export function validateTownCache(cache) {
|
||||
if (blankName) warnings.push(`${blankName} blank shop name(s) → defaulted to type label`);
|
||||
if (unknownType) warnings.push(`${unknownType} unknown shop type(s) → remapped to opshop`);
|
||||
if (dupId) warnings.push(`${dupId} duplicate shop id(s)`);
|
||||
// godverseShopId (ROUND24 ledger #2) — the stock identity. Split by what is actually a hazard:
|
||||
// · MALFORMED → error. It names a path segment (`stock_godverse/<id>/`); a string "3962749" and
|
||||
// the number 3962749 are the same folder but different JSON, and that coercion drift is exactly
|
||||
// the species F caught in the atlas contract (licence/license). One truth: a positive integer.
|
||||
// · DUPLICATE → error. Two shops keyed to one atlas IS mis-stocking — charter risk #3 says
|
||||
// mismatches "fail-soft to tier 0, NEVER mis-stocked". This is the arm that earns its keep.
|
||||
// · MISSING on a godverse cache → WARN, not error. See the note below — this is a deliberate
|
||||
// departure from the round doc's "requires", and it is Fable's to overrule.
|
||||
let badGid = 0, dupGid = 0, withGid = 0; const gids = new Set();
|
||||
for (const s of cache.shops) {
|
||||
if (!s || s.godverseShopId === undefined || s.godverseShopId === null) continue;
|
||||
withGid++;
|
||||
if (!Number.isSafeInteger(s.godverseShopId) || s.godverseShopId <= 0) { badGid++; continue; }
|
||||
if (gids.has(s.godverseShopId)) dupGid++; else gids.add(s.godverseShopId);
|
||||
}
|
||||
if (badGid) errors.push(`${badGid} shop(s) with a malformed godverseShopId (want a positive integer — it is a path segment, not a label)`);
|
||||
if (dupGid) errors.push(`${dupGid} duplicate godverseShopId(s) — two shops cannot share one stock atlas (mis-stock)`);
|
||||
// WHY MISSING IS A WARN AND NOT AN ERROR (ROUND24 ledger #2 says "requires"; recorded as a
|
||||
// departure, with reasons, per measurement-over-brief):
|
||||
// 1. The charter's determinism boundary: "world = seeded, frozen, gated. Stock = data tiers."
|
||||
// An error here inverts it — the TOWN would fail to load because its STOCK identity is absent.
|
||||
// Tier 0 must always boot; a shop with no id is a parody shop, which is the designed ladder.
|
||||
// 2. Charter risk #3 names the remedy for identity gaps: "fail-soft to tier 0, never mis-stocked."
|
||||
// Missing = tier 0 (soft). Duplicate = mis-stocked (hard). They are not the same failure.
|
||||
// 3. Sequencing: `newtown_godverse` carries 0 ids today and is G's to re-emit in wave 2 (their
|
||||
// namespace, and the census DB isn't on this machine). An error would red the tree for C and E
|
||||
// through all of wave 1 to convey what this warn already says.
|
||||
// The teeth live in selfcheck: PARTIAL coverage fails, and zero coverage prints an explicit SKIP
|
||||
// naming the town and the count (the vacuous-gate law) — so this cannot pass unnoticed.
|
||||
if (isGodverseCache(cache) && withGid < cache.shops.length) {
|
||||
warnings.push(`${cache.shops.length - withGid}/${cache.shops.length} shop(s) missing godverseShopId on a '${GODVERSE_SOURCE}' cache → those shops fall soft to tier 0 (parody); G re-emits`);
|
||||
}
|
||||
// wide-spread warning: an over-broad Overpass bbox marches into an unusable multi-km strip (the R17
|
||||
// real-data risk). Metadata only — plan_osm still boots a valid plan; it just isn't a coherent town.
|
||||
let mnLat = Infinity, mxLat = -Infinity, mnLon = Infinity, mxLon = -Infinity;
|
||||
@ -237,8 +283,17 @@ export function generatePlanOSM(citySeed, town = DEFAULT_TOWN, opts = {}) {
|
||||
// never renders "undefined" (build_towns.py filters most, but plan_osm must not trust its input).
|
||||
const nm = (typeof fixtureShop.name === 'string' && fixtureShop.name.trim()) ? fixtureShop.name : (reg.label || type);
|
||||
if (nm !== fixtureShop.name) norm.namesDefaulted = (norm.namesDefaulted || 0) + 1;
|
||||
shops.push({ id, lot: lotId, type, name: nm, sign: signOf(nm),
|
||||
seed, facadeSkin, storeys, hours: [open, close] });
|
||||
const shop = { id, lot: lotId, type, name: nm, sign: signOf(nm),
|
||||
seed, facadeSkin, storeys, hours: [open, close] };
|
||||
// ROUND24 ledger #2: carry the stock identity THROUGH the lift. Without this the field would be a
|
||||
// cache-only decoration — the runtime reads `plan.shops[]`, never the cache, so F's per-shop `base`
|
||||
// (`stock_godverse/<godverseShopId>/`) could never resolve. Conditional by design: absent ⇒ the key
|
||||
// is not written ⇒ `JSON.stringify` is byte-identical ⇒ every pinned town golden holds today, and
|
||||
// the field starts flowing the moment G re-emits (that re-emit IS the sanctioned golden move).
|
||||
if (Number.isSafeInteger(fixtureShop.godverseShopId) && fixtureShop.godverseShopId > 0) {
|
||||
shop.godverseShopId = fixtureShop.godverseShopId;
|
||||
}
|
||||
shops.push(shop);
|
||||
return id;
|
||||
}
|
||||
|
||||
|
||||
@ -435,6 +435,57 @@ for (const [label, cache, needle] of [
|
||||
ok(Math.abs(got - 100) < 0.5, `medianShopSpacing: 100 m pair measures ${got.toFixed(1)} m (projection sane)`);
|
||||
}
|
||||
}
|
||||
// (c3) the IDENTITY FIELD (ROUND24 ledger #2, with G) — `shop.godverseShopId`, the key that resolves
|
||||
// `stock_godverse/<id>/`. It is NOT `shop.id` and NOT "the census id": Monster Robot Party isn't in
|
||||
// thriftgod's census at all, so its id is its dealgod store id (3962749), while census shops use
|
||||
// thriftgod ids (max 2992). Two disjoint spaces, one opaque field — so these assert the FIELD's
|
||||
// properties, never a relationship to `id`.
|
||||
{
|
||||
const gv = shops => ({ ...mkCache(shops), source: 'godverse+osm' });
|
||||
const withIds = cluster(8).map((s, i) => ({ ...s, godverseShopId: 3962749 + i }));
|
||||
|
||||
// a well-formed godverse cache validates clean and carries no spurious warn
|
||||
const good = validateTownCache(gv(withIds));
|
||||
ok(good.ok, `godverse cache: valid with ids` + (good.ok ? '' : ` — ${good.errors.join('; ')}`));
|
||||
ok(!good.warnings.some(w => /godverseShopId/.test(w)), `godverse cache: fully-keyed cache raises no identity warn`);
|
||||
|
||||
// MALFORMED → error. A path segment, not a label: the string "3962749" is the same folder but
|
||||
// different JSON, and that coercion drift is the species F caught in the atlas contract.
|
||||
for (const [label, bad] of [
|
||||
['string id', '3962749'], ['float id', 3962749.5], ['zero', 0], ['negative', -5], ['NaN', NaN],
|
||||
]) {
|
||||
const v = validateTownCache(gv(withIds.map((s, i) => i === 1 ? { ...s, godverseShopId: bad } : s)));
|
||||
ok(!v.ok && v.errors.some(e => /malformed godverseShopId/.test(e)), `godverse cache: ${label} REJECTED`);
|
||||
}
|
||||
// DUPLICATE → error. Two shops keyed to one atlas is mis-stocking (charter risk #3: "never mis-stocked").
|
||||
{
|
||||
const v = validateTownCache(gv(withIds.map((s, i) => i === 1 ? { ...s, godverseShopId: withIds[0].godverseShopId } : s)));
|
||||
ok(!v.ok && v.errors.some(e => /duplicate godverseShopId/.test(e)), `godverse cache: duplicate id REJECTED (mis-stock)`);
|
||||
}
|
||||
// MISSING → warn + fail-soft, NOT an error (the charter's determinism boundary: the world must not
|
||||
// fail to load because stock identity is absent). Partial coverage still warns and still boots.
|
||||
{
|
||||
const v = validateTownCache(gv(withIds.map((s, i) => i < 2 ? { ...s, godverseShopId: undefined } : s)));
|
||||
ok(v.ok, `godverse cache: missing ids stay VALID (fail-soft to tier 0, never a boot failure)`);
|
||||
ok(v.warnings.some(w => /2\/8 shop\(s\) missing godverseShopId/.test(w)), `godverse cache: missing ids WARN with a count (${v.warnings.find(w => /godverseShopId/.test(w)) || 'NO WARN'})`);
|
||||
}
|
||||
// a plain osm cache must never be asked for ids
|
||||
ok(!validateTownCache(mkCache(cluster(8))).warnings.some(w => /godverseShopId/.test(w)), `osm cache: no identity warn (godverse-only requirement)`);
|
||||
|
||||
// THE SEAM THAT MATTERS: the id survives the lift onto plan.shops. Without this the field is a
|
||||
// cache-only decoration and F's per-shop base can never resolve — the runtime reads the PLAN.
|
||||
{
|
||||
const cache = gv(withIds);
|
||||
const p = generatePlanOSM(20261990, 'gvtest', { cache });
|
||||
const keyed = p.shops.filter(s => s.godverseShopId !== undefined);
|
||||
ok(keyed.length === p.shops.length, `godverse lift: every seated shop carries godverseShopId (${keyed.length}/${p.shops.length})`);
|
||||
ok(keyed.every(s => Number.isSafeInteger(s.godverseShopId) && s.godverseShopId > 0), `godverse lift: ids survive as positive integers`);
|
||||
ok(new Set(keyed.map(s => s.godverseShopId)).size === keyed.length, `godverse lift: ids stay unique through the lift (no atlas collision)`);
|
||||
// and an unkeyed cache writes NO key — that absence is what keeps all 22 pinned goldens frozen today
|
||||
const plain = generatePlanOSM(20261990, 'plaintest', { cache: mkCache(cluster(8)) });
|
||||
ok(plain.shops.every(s => !('godverseShopId' in s)), `plain lift: the key is ABSENT, not undefined (byte-identical JSON ⇒ goldens frozen)`);
|
||||
}
|
||||
}
|
||||
// (d) the registry seam: register → osmTownKeys() sees it → generatePlanOSM resolves it
|
||||
{
|
||||
registerTownCache('regtest', mkCache(cluster(14)));
|
||||
@ -511,7 +562,10 @@ const REAL_TOWN_GOLDENS = {
|
||||
newtown_godverse: 0x24e63feb,
|
||||
newtown_real: 0x741c18ec,
|
||||
northbridge_real: 0x1a645848,
|
||||
toowoomba_real: 0x8b635722,
|
||||
// toowoomba_real RETIRED by E in R23 (cache deleted) — pin removed with it. A golden for a town that
|
||||
// cannot load is dead config: it can never fire, so it can never fail. Same species as the round's
|
||||
// vacuous-gate law, one layer down. (E tried the re-bbox first and it GAMED my spacing metric — median
|
||||
// NN 395.7 → 89.4 m "passes" while hub density fell 3/12 → 2/12, the pack's worst. Right call.)
|
||||
westend_real: 0x5b1e5ffe,
|
||||
};
|
||||
// ROUND20: the GIG-layer golden per real town (district + capped posters) — the base golden above is
|
||||
@ -541,7 +595,7 @@ const REAL_TOWN_GIG_GOLDENS = {
|
||||
newtown_godverse: 0xc5ed0b92,
|
||||
newtown_real: 0xacdb3eee,
|
||||
northbridge_real: 0x83a1d5be,
|
||||
toowoomba_real: 0x9d34ccfe,
|
||||
// toowoomba_real RETIRED by E in R23 — see the note on the base map above.
|
||||
westend_real: 0xb2c7ddb6,
|
||||
};
|
||||
// `index.json` is E's towns INDEX (key/town/state/shops/roads for B's selector), not a town cache — skip it.
|
||||
@ -564,6 +618,24 @@ for (const f of townFiles) {
|
||||
const gwant = REAL_TOWN_GIG_GOLDENS[key];
|
||||
if (gwant === undefined) console.log(` ⚠ real/${key}: gig UNPINNED — add REAL_TOWN_GIG_GOLDENS['${key}'] = 0x${ghash.toString(16).padStart(8, '0')}`);
|
||||
else ok(ghash === (gwant >>> 0), `real/${key}: gig golden 0x${ghash.toString(16)} matches pinned 0x${(gwant >>> 0).toString(16)}`);
|
||||
// ROUND24 ledger #2 — the identity gate on the REAL godverse caches, written to the round's own
|
||||
// vacuous-gate law: it names its subject, it proves it touched it (a count), and it CANNOT pass while
|
||||
// its subject is absent. Zero ids ⇒ explicit SKIP with the reason and the count. Partial ⇒ FAIL (a
|
||||
// half-keyed town silently mis-stocks half its shops). Full ⇒ assert the ids reach plan.shops.
|
||||
if (cache.source === 'godverse+osm') {
|
||||
const keyed = cache.shops.filter(s => Number.isSafeInteger(s && s.godverseShopId) && s.godverseShopId > 0).length;
|
||||
if (keyed === 0) {
|
||||
console.log(` ⊘ SKIP real/${key}: godverse identity — 0/${cache.shops.length} shops carry godverseShopId. ` +
|
||||
`Subject absent: G re-emits with ids (R24 ledger #2). This gate binds the moment one id lands.`);
|
||||
} else {
|
||||
ok(keyed === cache.shops.length,
|
||||
`real/${key}: godverse identity — ${keyed}/${cache.shops.length} shops keyed (partial keying mis-stocks the rest)`);
|
||||
const seated = generatePlanOSM(20261990, key, { cache }).shops;
|
||||
const lifted = seated.filter(s => s.godverseShopId !== undefined).length;
|
||||
ok(lifted === seated.length, `real/${key}: godverse identity survives the lift (${lifted}/${seated.length} seated shops keyed)`);
|
||||
ok(new Set(seated.map(s => s.godverseShopId)).size === seated.length, `real/${key}: godverse ids unique on the plan (no shared atlas)`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── 4. every facade skin referenced by the registry exists on disk ──────────────────
|
||||
|
||||
@ -42,7 +42,10 @@
|
||||
// • type : any CITY_SPEC type or thriftgod alias (see theme.js); defaults to 'opshop'
|
||||
// • lot : { w, d } metres — room adapts to it; if absent, archetype ranges decide the size
|
||||
// • storeys: ceiling height driver (1 → 3.2m … up to 4.5m)
|
||||
// opts: { archetype?: 'cosy'|'gallery'|'wide'|'hall'|'pokey', stockAdapter? }
|
||||
// opts: { archetype?: 'cosy'|'gallery'|'wide'|'hall'|'pokey', stockAdapter?, stock?: 'real', stockBase? }
|
||||
// • stockBase (v5.0): which real-stock pack `stock:'real'` resolves. Omit → the town-wide packs
|
||||
// (`assets/models/`, today's behaviour). Pass `assets/stock_godverse/<godverseShopId>/` for a shop
|
||||
// with its own real crate. The pack cache is keyed by (type, base) — see stockpack.js + LANE_C_PUB §7.
|
||||
//
|
||||
// Registry override (Lane F): call mergeRegistry(registry) ONCE at init to let authored registry data
|
||||
// override recipe fields. It is deliberately NOT a per-build option — a per-call mutation of the shared
|
||||
@ -138,13 +141,16 @@ export function buildInterior(shop, THREE, opts) {
|
||||
const dims = computeDims(ctx, recipe, archetype, norm.lot, norm.storeys);
|
||||
const shell = buildShell(ctx, { recipe, archetype, dims });
|
||||
|
||||
// fittings + wall decor + stock + walkable-path guarantee. ?stock=real feeds Lane E's GODVERSE pack
|
||||
// through the stockAdapter seam (preload it first); missing pack → parody canvas (warn once).
|
||||
// fittings + wall decor + stock + walkable-path guarantee. ?stock=real feeds a real-stock pack through
|
||||
// the stockAdapter seam (preload it first); missing pack → parody canvas (warn once).
|
||||
// `opts.stockBase` (v5.0) selects WHICH pack: omit it for the town-wide packs, or pass a per-shop base
|
||||
// (`assets/stock_godverse/<godverseShopId>/`) for a shop with its own real crate. The pack cache is keyed
|
||||
// by (type, base), so a per-shop pack and the town-wide pack coexist — see stockpack.js.
|
||||
let adapter = opts.stockAdapter || null;
|
||||
if (!adapter && opts.stock === 'real') {
|
||||
const pack = getStockPack(recipe.key);
|
||||
const pack = getStockPack(recipe.key, opts.stockBase);
|
||||
if (pack) adapter = makeStockAdapter(pack);
|
||||
else warnOnce(`stock=real: no pack for "${recipe.key}" (preload it) — using parody canvas`);
|
||||
else warnOnce(`stock=real: no pack for "${recipe.key}" @${opts.stockBase || 'default base'} (preload it) — using parody canvas`);
|
||||
}
|
||||
const lay = layout(ctx, { recipe, dims, shell, adapter, shop: norm });
|
||||
|
||||
|
||||
@ -16,23 +16,38 @@ import * as THREE_NS from 'three';
|
||||
import { mergeGeometries } from 'three/addons/utils/BufferGeometryUtils.js';
|
||||
|
||||
const _texLoader = new THREE_NS.TextureLoader();
|
||||
const _packs = new Map(); // type → Promise<pack|null>
|
||||
const _resolved = new Map(); // type → pack|null (sync accessor for buildInterior)
|
||||
|
||||
// Preload a pack (fetch index + atlas textures). Cached. Call once before building with ?stock=real.
|
||||
export function preloadStockPack(type, { base = 'assets/models/', THREE = THREE_NS } = {}) {
|
||||
if (_packs.has(type)) return _packs.get(type);
|
||||
// Where the TOWN-WIDE packs live. A per-shop (godverse) pack lives under its own base instead —
|
||||
// `assets/stock_godverse/<godverseShopId>/` — and carries the SAME `stock_<type>_index.json` name,
|
||||
// because `type` is what picks the stock slot (SLOT_FOR: record→sleeve, book→spine, toy→box).
|
||||
export const DEFAULT_STOCK_BASE = 'assets/models/';
|
||||
|
||||
// CACHE IDENTITY IS (type, base) — never `type` alone. A per-shop atlas and the town-wide pack are
|
||||
// different packs that share a type; keying by type alone made `base` a no-op on a cache hit, so the
|
||||
// 2nd caller silently received the 1st caller's pack (R23, measured). That wasn't only a mix-up: it
|
||||
// defeated the fail-soft law — an UNSTOCKED shop of an already-cached type inherited that crate
|
||||
// instead of falling back to parody. (R24 ledger #1; the runtime is the contract's authority.)
|
||||
const packKey = (type, base) => `${base}|${type}`;
|
||||
const _packs = new Map(); // packKey → Promise<pack|null>
|
||||
const _resolved = new Map(); // packKey → pack|null (sync accessor for buildInterior)
|
||||
|
||||
// Preload a pack (fetch index + atlas textures). Cached per (type, base). Call once before building
|
||||
// with ?stock=real — for a per-shop pack, pass that shop's base.
|
||||
export function preloadStockPack(type, { base = DEFAULT_STOCK_BASE, THREE = THREE_NS } = {}) {
|
||||
const key = packKey(type, base);
|
||||
if (_packs.has(key)) return _packs.get(key);
|
||||
const p = fetch(`${base}stock_${type}_index.json`)
|
||||
.then(r => (r.ok ? r.json() : null))
|
||||
.then(idx => (idx && idx.items && idx.items.length ? buildPack(type, idx, base, THREE) : null))
|
||||
.catch(() => null)
|
||||
.then(pack => { _resolved.set(type, pack); return pack; });
|
||||
_packs.set(type, p);
|
||||
.then(pack => { _resolved.set(key, pack); return pack; });
|
||||
_packs.set(key, p);
|
||||
return p;
|
||||
}
|
||||
|
||||
// Synchronous accessor — the resolved pack if preloaded, else null (buildInterior uses this).
|
||||
export function getStockPack(type) { return _resolved.get(type) || null; }
|
||||
// Synchronous accessor — the resolved pack for (type, base), else null (buildInterior uses this).
|
||||
// `base` defaults to the town-wide packs, so every pre-v5 caller keeps its exact behaviour.
|
||||
export function getStockPack(type, base = DEFAULT_STOCK_BASE) { return _resolved.get(packKey(type, base)) || null; }
|
||||
|
||||
async function buildPack(type, idx, base, THREE) {
|
||||
const names = idx.atlases || [];
|
||||
|
||||
Loading…
Reference in New Issue
Block a user