THE SHOP HAS A STREET. redhill_godverse: Monster Robot Party seated on Musgrave Road (verified — lot 8, type record, godverseShopId 3962749), 37 shops on E's 884 real Red Hill roads. Its coordinate is John-supplied (Google Maps place /g/11g6rlbbc0 -> -27.4553791, 153.0076244) because it is derivable from nothing we hold: OSM doesn't know the shop, the census doesn't either, dealgod's stores/store_entity rows are NULL lat/lng, recordgod has no address. Verified inside E's bbox and on Musgrave Road's own geometry; my own estimate was ~120 m out. RE-EMIT (ledger #5 + #2): atlas keyed by godverseShopId with US `license` throughout (British retired, 0 occurrences) — atlas-QA OK, 120 real records, byte-identical re-bake (the .webp bytes did not move; only the index). newtown_godverse re-emitted with ids (18 keyed). godverse_town.py: psycopg2 -> psql, EXTRA_SHOPS roster, and Fable's R22 Ruling 2 finally implemented. RULING 2 WAS LOAD-BEARING, NOT COSMETIC (measured): census-only Red Hill is unshippable — 9 shops = 306 m spacing warn; +Monster Robot = 10 shops = 261 m warn AND a frontage poster seated INSIDE edge 1037's kerb (-2.95 m); + the donor's texture layer (37) = no warn, poster passes. 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". 9 donor heroes swapped for census entries (18 in Newtown), counted per the ruling. CONFLICT #1 — C §7.3 vs E's validator. C says lift provenance to TOP LEVEL, US `license`, British retired, "exactly what E's validator reads". Measured: it is not. validate_atlas.py:98 reads NESTED `provenance`; :105 reads `licence` British-first. Lifting-and-retiring as written FAILS the gate; my R23 atlas already passed it untouched. Same species as the bug that held the tag — a doc asserting what code does without running it. Resolved: E's check accepts `licence or license`, so US-nested passes; the four also ride at top level (C's contract + the town-cache convention) off one source of truth. -> E, one line: :198 PRINTS `licence` British-only while the check accepts both, so C's mandated US spelling silently stops printing the licence flag — which :199's own comment calls a human call. Gate passes; evidence line vanished. Fix: .get("licence") or .get("license"). CONFLICT #2 — Ruling 2 vs A's identity gate. NEEDS A RULING; it is why the tree is red (6 of 8). Ruling 2 makes a godverse cache mixed: a keyed GODVERSE layer + an inherited OSM texture layer. A's sweep asserts EVERY shop is keyed -> partial FAILs. Texture shops carry no godverseShopId deliberately: an OSM milkbar has no GODVERSE identity, and minting one from its OSM id pushes a third id space into a field that already carries two — A's validator calls a duplicate mis-stocking, charter risk #3's nightmare with Monster Robot's crate as the prize. And A's own two checks disagree: the validator says "Missing = tier 0 (soft). Duplicate = mis-stocked (hard). They are not the same failure", while the sweep fails partial keying because it "mis-stocks the rest" — conflating exactly those two. Proposed (A's call, Fable's ruling): assert coverage over the godverse layer, not the inherited texture layer. THE 8 RED CHECKS, ITEMISED: newtown_godverse base+gig goldens (2) moved by the re-emit exactly as ledger #2 intends — A re-pins, base 0xcf69b387 gig 0x1e266f60; godverse identity x3 per godverse town (6) = conflict #2. Nothing else. Classic covenant 0x3fa36874 intact. redhill_godverse pins: base 0xb2224939. Verified live: the shop's 120-record pack and the town-wide 350 pack now COEXIST through C's type+base fix. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
253 lines
14 KiB
Markdown
253 lines
14 KiB
Markdown
# G3 — THE ECONOMY BACKEND (design doc, DRAFT for ratification)
|
||
|
||
*Lane G (ultra), R23. Drafted per the v5 charter ([V5_REAL_SHOP.md](V5_REAL_SHOP.md)) ledger #2.
|
||
Ratification: both Fables + John, at R24 kickoff. **Nothing server-side ships before that.** This
|
||
round is paper — plus the tier-1 artifact that made the paper honest (G2a, merged this round).*
|
||
|
||
Status of each section: **[MEASURED]** = verified against the real databases this round.
|
||
**[DESIGN]** = proposed, not built. **[JOHN]** = a decision reserved to John by charter law.
|
||
|
||
---
|
||
|
||
## 1. What the data actually is [MEASURED]
|
||
|
||
The charter assumed one join: thriftgod's census of real shops → dealgod's real items. **That join
|
||
does not exist.** The three datasets are three different populations:
|
||
|
||
| dataset | what it really holds | inventory? |
|
||
|---|---|---|
|
||
| `thriftgod.shop` | 2,928 real AU secondhand shops, OSM-anchored (the census) | **no** |
|
||
| `dealgod.stores` | 9,285 online sellers — **8,754 are Discogs accounts**, not physical shops | 1.77M listings |
|
||
| `recordgod` | **one real trading record shop's POS** — crates, slots, prices, grading | 28,624 live |
|
||
|
||
Measured consequences:
|
||
|
||
- **No Newtown record shop has any stock, in any database.** Egg Records, Papa Disquo, Haze
|
||
Guitars, The Vintage Record, Downtown Music: 0 items each. dealgod has zero Newtown stores and
|
||
zero census name matches.
|
||
- `thriftgod.item` (the minted table) holds 2,221 items across **29 shops — all Brisbane/
|
||
Newcastle/Alice charity shops**. Zero record shops. It is a demo seed, not a stock system.
|
||
- This is **not** charter risk #3 ("fuzzy matching"). It is a population mismatch. Fuzzy matching
|
||
cannot fix it, because there is nothing on the other side of the join to match *to*.
|
||
|
||
### 1a. The one shop where all three meet [MEASURED]
|
||
|
||
`monsterrobot` — **147 Musgrave Road, Red Hill, Brisbane QLD** (John, R23) — is the same real shop
|
||
in three forms, and that is the whole reason tier 1 can be honest:
|
||
|
||
```
|
||
recordgod (its POS) ──┐
|
||
monsterrobot.party ├── one real shop: real crates · real slots · real prices · its OWN photos
|
||
dealgod store 3962749 ─┘
|
||
```
|
||
|
||
- 28,624 live vinyl; **24,914 physically placed in 357 real crates** with real labels
|
||
("DEEP HOUSE", "$12–$15 TECHNO") and real slot numbers — with gaps where records actually sold.
|
||
- Covers are the shop's own product shots on the shop's own domain, cached by dealgod
|
||
(23,392 releases). **99.3%** of its crate stock joins to a cached cover by Discogs `release_id`.
|
||
- It is **not in thriftgod's OSM census**, so its godverse id is its dealgod store id `3962749`
|
||
(real, stable, clear of the census space — thriftgod max shop id = 2992).
|
||
|
||
**Shipped this round (G2a):** crate 550 "DEEP HOUSE" → 120 real records → `web/assets/
|
||
stock_godverse/3962749/`. Byte-identical re-bake verified.
|
||
|
||
---
|
||
|
||
## 2. Why `mint()` cannot be the source of any tier [MEASURED]
|
||
|
||
thriftgod's `mint()` is the obvious candidate for stocking shops. It disqualifies itself three ways:
|
||
|
||
1. **It is not deterministic.** `FEED_RECORDS` uses `TABLESAMPLE SYSTEM (0.5)`; `mint()` uses
|
||
`random.random()`, `random.uniform()`, `random.shuffle()`. **`random.seed` appears nowhere in
|
||
`server.py`.** Re-running mint gives different stock at different prices. That is fatal to tier
|
||
1 (charter law #2), and it is *why* tier 1 reads the POS instead: a crate is a table of facts.
|
||
2. **The shop assignment is fictional.** mint samples arbitrary Discogs sellers' listings and
|
||
assigns them to a census shop that never owned them.
|
||
3. **Therefore it breaks two laws downstream.** The licence law's green rests on "the *stores'
|
||
own* product shots" — a Discogs seller's photo shown as Egg Records' stock isn't that. And the
|
||
write-back question (§7) is **meaningless** for minted stock: you cannot mark real inventory
|
||
sold for an item the shop never had.
|
||
|
||
mint stays what it is: a fun demo seeder for parody tier 0. It is not an inventory system.
|
||
*(If a future round wants seeded mint for tier 0 variety, seeding it is a small, safe change —
|
||
`random.Random(shop_id)` + an ordered `ORDER BY id` sample instead of `TABLESAMPLE`.)*
|
||
|
||
---
|
||
|
||
## 3. The tier ladder, as built [MEASURED for 0–1, DESIGN for 2]
|
||
|
||
| tier | source | deterministic | offline |
|
||
|---|---|---|---|
|
||
| 0 · parody | seeded canvas / E's parody packs | yes | yes |
|
||
| 1 · static real | **per-shop atlas baked from the POS snapshot** (G2a, shipped) | **yes — verified** | **yes — files** |
|
||
| 2 · live | thriftgod server over the POS (this doc) | **no, by design** | **enriches only** |
|
||
|
||
**The offline law is structural, not a promise.** `stockpack.js` already returns `null` when an
|
||
index or atlas is missing, and every caller falls back to parody canvas. Tier 2 rides the same
|
||
seam: the server is an *enrichment* that upgrades an already-playable tier-1 room. There is no
|
||
boot path, gate, or tag that may touch the network.
|
||
|
||
---
|
||
|
||
## 4. API surface [DESIGN]
|
||
|
||
Read-only in v5.0; the write verbs are drafted here but **gated on §7**.
|
||
|
||
```
|
||
GET /godverse/v1/shop/<godverse_id>/stock?crate=<id>&since=<iso>
|
||
-> { shop_id, crate, tier, items:[{sku, slot, release_id, artist, title,
|
||
price, condition, sleeve_cond, cover, state}],
|
||
gone:[sku], etag, served_at }
|
||
GET /godverse/v1/shop/<godverse_id>/crates -> crate list + counts (the rotation menu)
|
||
POST /godverse/v1/reserve { sku, session } -> { ok, holds_until } [GATED — §7]
|
||
POST /godverse/v1/buy { sku, session, hold } -> { ok, receipt } [GATED — §7]
|
||
```
|
||
|
||
Design rules:
|
||
|
||
- **The atlas is never served by the API.** Covers stay static files (tier 1). The server sends
|
||
*facts* (what's still there, what it costs), never pixels. A tier-2 shop with a dead server is
|
||
visually identical to tier 1 — that's the fail-soft.
|
||
- **`gone[]` is the whole point of tier 2.** The client already holds the tier-1 crate; the server
|
||
says only what changed. A sold record is a `sku` in `gone[]` → `collapseBuyItem()` (already
|
||
built, R9) zero-areas the sleeve. Sold-means-gone is a *diff*, not a re-fetch.
|
||
- **`etag`/`since`** so a re-enter is a 304, not a payload.
|
||
- Shop id is the **godverse id** (§1a), never a POS internal id. **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.
|
||
|
||
---
|
||
|
||
## 5. Async enrichment + caching (risk #1) [DESIGN]
|
||
|
||
**Law: the dig never blocks on the network.** Not "usually" — never. The mechanism:
|
||
|
||
1. Room builds at tier 1 from static files. Playable. Riffling works. **No await.**
|
||
2. The stock fetch is fired in parallel, never awaited by any build path, with a **hard ~400 ms
|
||
budget** and `AbortController`.
|
||
3. If it lands in time: apply `gone[]` + price deltas before the player reaches the crate.
|
||
4. If it lands late: apply on arrival (a record vanishing mid-riffle is *correct* — that's a real
|
||
shop). If it never lands: the room stays tier 1, silently. **Zero console noise** — a dead
|
||
server is an expected state, not an error.
|
||
5. Cache the response per `(shop, crate)` in memory + `sessionStorage`, keyed by `etag`. One
|
||
fetch per shop per session, not per crate entry.
|
||
6. **One flight at a time per shop**; re-entering a shop reuses the cache. No polling loop.
|
||
|
||
The v1 machinery makes this cheap: the pack is a shared material + per-item UVs, so applying
|
||
`gone[]` is a vertex collapse on an existing merged mesh — no rebuild, no re-upload, no stutter.
|
||
|
||
---
|
||
|
||
## 6. Identity: census ↔ lot ↔ POS (risk #3) [DESIGN, informed by §1]
|
||
|
||
The R22 contract said "atlas per shop keyed by godverse shop id". §1 shows the id must come from
|
||
whichever dataset actually *has* the shop, and mismatches must fail soft, never mis-stock:
|
||
|
||
- **A shop is stocked only by explicit, recorded identity** — a `godverse_id` that resolves to a
|
||
real POS. There is no fuzzy name matching, ever. A shop we cannot identify with certainty gets
|
||
tier 0. **Mis-stocking is worse than no stock**: it puts one shop's records in another's crate,
|
||
which is a lie the whole epoch exists to avoid.
|
||
- **Every unmatched census shop is counted, not guessed** (the drops law). R23's count: 2,928
|
||
census shops, 1 identified real POS.
|
||
- **The census is not a stock oracle.** Being in the census means the shop exists, not that we
|
||
know what's in it. v5.0-beta's "every hero shop stocked" is therefore bounded by *real POS
|
||
access*, not by census coverage — see §9.
|
||
|
||
---
|
||
|
||
## 7. THE WRITE-BACK QUESTION [JOHN — charter law #4]
|
||
|
||
**Does an in-game purchase ever touch real thriftgod/recordgod inventory?**
|
||
|
||
This is now a live question, not a hypothetical: `monsterrobot` is a **real trading shop** whose
|
||
POS syncs to **Square**, with real customers buying these exact records off the real shelf. Stated
|
||
per charter law #4 with trade-offs and **no default**.
|
||
|
||
Relevant measured facts: the POS already has `in_stock`, `sold_date`, **`hold_expires_at`**, `qty`
|
||
and `square_item_id`. A reservation primitive therefore already exists — the question is whether
|
||
the game is allowed to pull it.
|
||
|
||
### Option A — Sandbox (read-live, sell-local)
|
||
Game reads real stock; a purchase never leaves the game. Sold-means-gone is per-session.
|
||
- **For:** zero real-world risk; no payment, tax, or fulfilment surface; a bug can't cost real
|
||
stock; ships fastest.
|
||
- **Against:** the one-of-a-kind thrill is fiction — the real crate never changes, and two players
|
||
(or the same player, next boot) buy the same record. Tier 2 becomes "live prices" more than
|
||
"live stock".
|
||
|
||
### Option B — Reservation (a real, expiring hold)
|
||
A game purchase places a real hold on the real SKU via the existing `hold_expires_at`. The record
|
||
genuinely leaves the real crate for N hours; unclaimed, it returns.
|
||
- **For:** genuinely one-of-a-kind, and **reversible** — the strongest truth-per-unit-risk; uses a
|
||
primitive the POS already has.
|
||
- **Against:** a game action removes real sellable stock from real in-store customers. Needs a
|
||
hold cap/budget, a staff-visible reason code ("held: GODVERSE"), and a race policy against
|
||
Square in-store sales. Someone must decide what happens when a held record sells in the shop
|
||
anyway (the game must lose, gracefully).
|
||
|
||
### Option C — Full write-back (game sale = real sale)
|
||
A purchase marks the real SKU sold and syncs to Square.
|
||
- **For:** total truth — buy it in the game and it is gone from the real shop, forever.
|
||
- **Against:** irreversible real-world inventory and revenue consequences; the game becomes a
|
||
sales channel (real payment, tax, postage, fulfilment, refunds, chargebacks); a bug destroys
|
||
real stock; a game wallet is not money. This is a business decision, not an engineering one.
|
||
|
||
**Lane G asks nothing and recommends nothing here.** Until ratified: **tier 2 is read-live,
|
||
sell-sandboxed** (the charter's standing state, Option A by default *only* because law #4 says so).
|
||
|
||
---
|
||
|
||
## 8. Failure modes [DESIGN]
|
||
|
||
| failure | behaviour | gate class |
|
||
|---|---|---|
|
||
| server unreachable / DNS dead | tier 1, silent, zero console noise | tier-2 smoke |
|
||
| server dies **mid-session** | room keeps last-good state; no re-fetch storm; no stutter | tier-2 smoke |
|
||
| server slow (> budget) | request aborted; tier 1 stands; late arrival applies or is dropped | tier-2 smoke |
|
||
| 5xx / malformed JSON | treated as unreachable — never a partial apply | tier-2 smoke |
|
||
| atlas missing but server up | **tier 0** — no stock without covers; never text-only sleeves | tier-1 gate |
|
||
| `gone[]` names an unknown sku | ignored + counted, never an error | tier-2 smoke |
|
||
| clock skew on holds | server time is authoritative; client never computes expiry | tier-2 smoke |
|
||
|
||
**No tier-2 failure may fail a gate or block a tag** (charter law #1). Tier 2 gets its own smoke
|
||
class: reachability, fail-soft, latency. Kill the server mid-session and the game must not stutter
|
||
— that is v5.0's release gate, and it is a *client* test, not a server test.
|
||
|
||
---
|
||
|
||
## 9. Fences (non-negotiable, both already load-bearing)
|
||
|
||
- **PII.** `recordgod` holds real `customer`, `customer_session`, `mailing_list`, `staff`,
|
||
`sales` — real private-individual data. The pipeline and the server read **only**
|
||
`inventory` / `crate` / `disc_cache`. No exceptions, no joins, no "just for debugging".
|
||
Charter law #3: no private-individual data, ever.
|
||
- **Licence.** Covers are the shop's own product shots → in-house 🟢, **flagged before any
|
||
public/commercial release**. Real titles/artists ship at tier 1 per John's R23 amendment to the
|
||
no-real-trademarks law, under the same flag. Every atlas carries its provenance.
|
||
- **Scope reality check for v5.0-beta.** "Every hero shop stocked" needs a real POS per shop. We
|
||
have exactly one (John's own). Stocking a *second* real shop means a second real shopkeeper's
|
||
data and consent — a licence and relationship question, not a pipeline question. Beta should be
|
||
scoped to **monsterrobot's 357 crates** (24,646 real records — the whole shop, crate rotation
|
||
per risk #2) rather than to more shops, unless John says otherwise.
|
||
|
||
---
|
||
|
||
## 10. Open items for the reviewers
|
||
|
||
1. **[Fable-m3 / C]** Per-shop pack resolution is not wired: `stockpack.js` caches by `type`
|
||
alone (`_packs`/`_resolved`), and `interior_mode.js:192` resolves `getStockPack(shop.type)`
|
||
with `base` fixed at `assets/models/`. R23's single shop works via a `base` override; **two
|
||
stocked shops will collide in that cache.** The seam C/F need: key the cache by
|
||
`type + base` (or `godverse_id`), and preload per-shop on shop entry. G re-emits to whatever
|
||
wording C lands (LANE_C_PUB).
|
||
2. **[Fable-m3 / E]** `redhill_real` doesn't exist — the only Brisbane cache (`westend_real`)
|
||
stops ~2.3 km south of Red Hill. The crate has no street until E's `build_towns.py` gets a
|
||
Red Hill entry (~`center: (-27.4553, 153.0064), span_km: 2.4`) and one Overpass fetch. That's
|
||
E's file and an outward call — **G has not touched either.**
|
||
3. **[John, R24]** §7, the write-back question.
|
||
4. **[Both Fables]** §9's beta scoping — depth (one real shop, 357 crates) vs breadth (more
|
||
shops, which needs other people's data).
|