# 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//stock?crate=&since= -> { 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//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).