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>
14 KiB
G3 — THE ECONOMY BACKEND (design doc, DRAFT for ratification)
Lane G (ultra), R23. Drafted per the v5 charter (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:
- It is not deterministic.
FEED_RECORDSusesTABLESAMPLE SYSTEM (0.5);mint()usesrandom.random(),random.uniform(),random.shuffle().random.seedappears nowhere inserver.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. - The shop assignment is fictional. mint samples arbitrary Discogs sellers' listings and assigns them to a census shop that never owned them.
- 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 askuingone[]→collapseBuyItem()(already built, R9) zero-areas the sleeve. Sold-means-gone is a diff, not a re-fetch.etag/sinceso 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 carriesshop.godverse_id(snake_case, what E'svalidate_atlas.pyrequires). Same id, two artifacts, two house conventions — the API usesgodverse_idon 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:
- Room builds at tier 1 from static files. Playable. Riffling works. No await.
- The stock fetch is fired in parallel, never awaited by any build path, with a hard ~400 ms
budget and
AbortController. - If it lands in time: apply
gone[]+ price deltas before the player reaches the crate. - 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.
- Cache the response per
(shop, crate)in memory +sessionStorage, keyed byetag. One fetch per shop per session, not per crate entry. - 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_idthat 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.
recordgodholds realcustomer,customer_session,mailing_list,staff,sales— real private-individual data. The pipeline and the server read onlyinventory/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
- [Fable-m3 / C] Per-shop pack resolution is not wired:
stockpack.jscaches bytypealone (_packs/_resolved), andinterior_mode.js:192resolvesgetStockPack(shop.type)withbasefixed atassets/models/. R23's single shop works via abaseoverride; two stocked shops will collide in that cache. The seam C/F need: key the cache bytype + base(orgodverse_id), and preload per-shop on shop entry. G re-emits to whatever wording C lands (LANE_C_PUB). - [Fable-m3 / E]
redhill_realdoesn't exist — the only Brisbane cache (westend_real) stops ~2.3 km south of Red Hill. The crate has no street until E'sbuild_towns.pygets 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. - [John, R24] §7, the write-back question.
- [Both Fables] §9's beta scoping — depth (one real shop, 357 crates) vs breadth (more shops, which needs other people's data).