# G3 — THE ECONOMY BACKEND (design doc) *Lane G, R23 draft. Drafted per the v5 charter ([V5_REAL_SHOP.md](V5_REAL_SHOP.md)) ledger #2.* **§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. --- ## 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 [AS-BUILT — R27, `pipeline/godverse_server.py`] Read-only, per §7's ruling. **The write verbs are not "gated" — they do not exist.** ``` GET /godverse/v1/health -> { ok, tier, pos_db, shops:[godverse_id] } GET /godverse/v1/shop//stock?since= -> { shop_id, tier, crate, items:[{id, slot, artist, title, price, condition, sleeve_cond, state}], gone:[id], counts, etag, served_at } · 304 when ?since= matches GET /godverse/v1/shop//crates -> { shop_id, tier, crates:[{id,name,label,count}], etag } GET anything else -> 404 (the ruling is in the body) POST/PUT/PATCH/DELETE anything -> 501 — no write handler exists in the process ``` **Three corrections this doc owes to the code (measured R27, not reasoned):** 1. **`gone:[sku]` → `gone:[id]`.** As ratified, §4 said bare skus. That wording predates R25's `sku_` ids and R26's **id-namespace fence law**, and a bare number crossing two id spaces is precisely the bug that law exists to stop. `gone[]` now carries the atlas's own `id` verbatim, so the client does `gone.includes(item.id)` with zero transformation. *This doc was the stale rule meeting a case written after it — the epoch's signature failure, and my turn.* 2. **`items[]` drops `release_id` and `cover`.** `cover` would edge the API toward serving pixels — it never should (fence #2); `release_id` identifies the *pressing*, and tier 2 speaks only in *copies* (the sku). Both were design-time noise. 3. **Write verbs answer 501, not 404.** F asserts the number, so it matters: the class defines `do_GET` and nothing else, so the stdlib refuses any other method before routing exists. **501 is the stronger proof** — a 404 means a handler ran and declined; 501 means there is no write handler in the process. Adding `do_POST` to answer 404 would implement the forbidden thing. 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 an `id` in `gone[]` → `collapseBuyItem()` (already built, R9) zero-areas the sleeve. Sold-means-gone is a *diff*, not a re-fetch. > ### ⚠ `gone[]` HAS NO SUBJECT IN PRODUCTION TODAY — measured R27, and it shapes F's gate > The mechanism is built and correct. The **data has no movement**: Monster Robot Party's POS > records **5,567 sales ever** (400–800/month, Dec 2025 → Jun 2026) but **26 in July and none > since 2026-07-01**; the POS hasn't been *written* since **2026-07-09** (the 07-16 dump is fresh; > its data is stale). And **0 of crate 550's 120 records have ever sold**. So against the real POS > `gone` is `[]`, permanently, until the shop trades again. > > **Therefore, per the vacuous-gate law: F's sold-means-gone arm must not assert over the live POS > — it would pass without touching its subject.** Two honest routes, and G built the second: > 1. Assert `gone == []` against the real POS and **print the reason** — true, but it proves only > that nothing sold. > 2. **Point the server at a labelled fixture** (`godverse_server.py make-fixture --sold 3` → > `recordgod_fixture`, a `createdb -T` clone with N of the crate's own real skus marked sold). > Verified: `counts {baked:120, live:117, gone:3}`, the three ids absent from `items[]`. This > proves the **mechanism** — computed → sent → consumed → sleeve vanishes — deterministically, > which is what a gate is for. Production still reads the real POS, untouched. > > **A fixture is not a lie; a green assert over an absent subject is.** The epoch has held four > tags on exactly that distinction. - **`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 — **RULED: A THEN B** (John, 2026-07-17, R24 kickoff) > ### 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**. **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 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 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.**)* --- ## 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.~~ **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).