PROCITY/docs/G3_ECONOMY_DESIGN.md
m3ultra 0608f9c8c1 Lane G R24: G3 §7 RATIFIED — John's write-back ruling, A then B (charter law #4 satisfied)
John ruled at the R24 kickoff: v5.0 ships on Option A (sandbox) — an in-game purchase never
touches real monsterrobot/Square inventory; Option B (reservation via the POS's existing
hold_expires_at) is CHARTERED as a v5.x upgrade behind a hold budget John sets; Option C is not
chartered and happens only as a deliberate business launch, if ever. Recorded inline in §7 with
the options preserved beneath it, since the trade-offs are the record of why A-then-B was chosen.

Written in as binding, because a one-line ruling isn't a mechanism: /reserve + /buy stay ABSENT,
not stubbed behind a flag (a sandbox a config typo can flip isn't a sandbox); the tier-2 reader
connects read-only, enforced at the credential — a role with SELECT on inventory/crate/disc_cache
and nothing else, which carries the §9 PII fence in the same grant; and the gate asserts the write
verbs 404 and the role's grants are read-only, because "we didn't call buy()" proves nothing —
the vacuous-gate law turned on ourselves.

Option B's preconditions are stated so it can't slide in quietly: the hold budget (John's number,
NOT YET SET — B is not startable until it is; a v5.x item, not a v5.0 blocker), the-shop-always-
wins on a race with an in-store Square sale, a staff-visible `held: GODVERSE` reason code, and
soak before ship. Doc header/§10 updated so the whole file tells one story: §7 ruled, the rest
still DESIGN pending both Fables' sign-off, no server code before that.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 01:33:27 +10:00

293 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 01, 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 — **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).