THE LIVE CRATE READS. godverse_server.py: /health, /shop/<gid>/stock, /shop/<gid>/crates, plus
start|stop|status (F's kill-the-server lifecycle), setup-role, make-fixture. Stdlib + psql. First
server code of the epoch, per the ratified G3.
Live against the real POS: crate 550 -> {baked:120, live:120, gone:0}, real prices, real VG+
grading, etag stable and ?since= -> 304. /crates returns 315 REAL crates with the shop's own
labels: TECHNO (57), $12-$15 TECHNO (46), DRUM N BASS (66).
THE THREE FENCES, each enforced by the machine, each measured:
- The credential: godverse_ro holds SELECT on inventory/crate/disc_cache and nothing else.
UPDATE inventory -> permission denied. SELECT FROM customer -> permission denied. ONE grant
carries both the §7 sandbox and the §9 PII fence — which is why §7 said enforce it at the
credential, not in a code review.
- Write verbs absent: POST /reserve -> 501, POST /buy -> 501, GET either -> 404.
- Metadata only: no cover, no pixels on the wire; a mint shop's /stock is 404 (no POS to be live
about), not an empty crate.
THE FINDING F NEEDS BEFORE IT WRITES THE GATE — `gone[]` has no subject in production. The
mechanism is right; the data has no movement. Monster Robot Party's POS records 5,567 sales ever
(400-800/month Dec->Jun) but 26 in July and NONE SINCE 2026-07-01, and hasn't been written since
2026-07-09 — the 07-16 dump is fresh, its data is stale (restored it to check: crate 550 holds 122
on both 07-15 and 07-16, identical). 0 of the crate's 120 records have ever sold. So `gone` is []
against the real POS, permanently, until the shop trades again — and an assert over it would pass
WITHOUT TOUCHING ITS SUBJECT, the exact species that has held four tags. So: make-fixture --sold 3
clones the POS (createdb -T) and marks 3 of the crate's OWN REAL SKUS sold -> verified
{baked:120, live:117, gone:3}, the three ids absent from items[]. F points at it via
GODVERSE_POS_DB for a deterministic gate; production reads the real POS untouched. A fixture is
not a lie; a green assert over an absent subject is.
LIFECYCLE (ledger #3): stop is a SIGKILL, not a graceful drain — the gate tests a death, not a
goodbye. Verified: stop -> connection refused (curl rc=7) -> status DOWN -> start -> live. No
admin verb was added to the API: a server whose whole point is "no write verbs" shouldn't grow a
remote kill.
THREE CORRECTIONS TO MY OWN G3 §4 (design -> as-built), all measured not reasoned:
1. gone:[sku] -> gone:[id]. §4 AS I RATIFIED IT said bare skus — wording that predates R25's sku
ids and R26's fence law, and a bare number crossing two id spaces is the precise bug that law
exists to stop. My doc was the stale rule meeting a case written after it: the epoch's
signature failure, this time in the doc I wrote.
2. items[] drops release_id and cover — cover edges the API toward serving pixels; release_id is
the pressing, and tier 2 speaks only in copies.
3. Write verbs answer 501, not the 404 my own docstring claimed — caught by running it. 501 is the
STRONGER proof: 404 means a handler declined; 501 means no write handler exists in the process.
-> F: assert 501.
Scoped: pathspec commit. web/assets/towns/index.json is another lane's live edit — left untouched.
Runtime pidfile moved out of the repo (it is not an artifact).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
303 lines
15 KiB
Python
303 lines
15 KiB
Python
#!/usr/bin/env python3
|
|
"""Lane G — GODVERSE tier-2: the two READ endpoints (R27 ledger #1).
|
|
|
|
The charter's last rung, under John's ratified A-then-B ruling (G3 §7): **read-live,
|
|
sell-sandboxed**. The server ENRICHES, NEVER GATES — every boot path, gate and tag works with
|
|
this process dead. Nothing here can write to the shop.
|
|
|
|
python3 pipeline/godverse_server.py setup-role # the read-only grant (idempotent)
|
|
python3 pipeline/godverse_server.py start|stop|status # lifecycle — F's kill-the-server gate
|
|
python3 pipeline/godverse_server.py serve # foreground
|
|
python3 pipeline/godverse_server.py make-fixture --sold 3 # a controlled POS for F's gate
|
|
|
|
THE SURFACE (G3 §4, as built):
|
|
GET /godverse/v1/health -> { ok, shops } (is the server up)
|
|
GET /godverse/v1/shop/<godverse_id>/stock -> live crate metadata + `gone` + etag
|
|
GET /godverse/v1/shop/<godverse_id>/crates -> the real crate menu (rotation)
|
|
GET anything else -> 404 (with the ruling in the body)
|
|
POST/PUT/PATCH/DELETE anything -> **501**, measured — see below
|
|
|
|
**`/reserve` and `/buy` are ABSENT, not stubbed** — no route, no handler, no flag. A sandbox a
|
|
config typo can flip isn't a sandbox (G3 §7).
|
|
|
|
**→ Lane F, assert 501 and not 404 for the write verbs.** This class defines `do_GET` and nothing
|
|
else, so the stdlib answers any other method with 501 "Unsupported method" before routing exists
|
|
at all. That is a *stronger* absence proof than a 404: a 404 would mean a handler ran and chose to
|
|
decline, whereas 501 means **there is no write handler in the process**. Adding `do_POST` just to
|
|
answer 404 would be implementing the very thing the ruling forbids. (My own docstring claimed 404
|
|
until I measured it — the same doc-vs-code species this epoch keeps catching, caught here by
|
|
running it.)
|
|
|
|
THREE FENCES, each enforced by the machine and not by a code review:
|
|
1. **The credential.** We connect as `godverse_ro`: SELECT on inventory/crate/disc_cache and
|
|
NOTHING else. Measured — `UPDATE inventory` => "permission denied"; `SELECT FROM customer`
|
|
=> "permission denied". That one grant carries the sandbox AND G3 §9's PII fence.
|
|
2. **Metadata only.** The API serves facts, never pixels. Covers stay static tier-1 files. A
|
|
tier-2 shop with a dead server is VISUALLY IDENTICAL to tier 1 — that is the fail-soft.
|
|
3. **The id-namespace fence** (R26 law). `gone[]` carries the atlas's own `id` form verbatim
|
|
(`sku_<POS sku>`), so the client does `gone.includes(item.id)` with ZERO transformation.
|
|
*G3 §4 as ratified said `gone:[sku]` — bare numbers. That wording predates R25's sku ids and
|
|
R26's fence, and a bare number crossing two id spaces is the exact bug the fence law exists
|
|
to stop. The design doc was the stale thing; §4 is updated as-built.*
|
|
|
|
TIER 2 IS FOR REAL SHOPS ONLY. A `sourcing: "mint"` shop has no POS to be live about, so its
|
|
`/stock` is a 404 — not an empty crate. Monster Robot Party is the only real shop; that's the
|
|
point: one shop, live, true.
|
|
"""
|
|
import argparse, hashlib, json, os, signal, subprocess, sys, tempfile, time
|
|
from datetime import datetime, timezone
|
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
from urllib.parse import urlparse, parse_qs
|
|
|
|
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
STOCK = os.path.join(ROOT, "web", "assets", "stock_godverse")
|
|
PORT = int(os.environ.get("GODVERSE_PORT", "8778"))
|
|
# Runtime state lives OUTSIDE the repo: a pidfile is not an artifact, and a stray one in the tree
|
|
# is exactly the kind of thing that gets committed by a hurried `git add`.
|
|
PIDFILE = os.path.join(tempfile.gettempdir(), f"godverse-{PORT}.pid")
|
|
# The POS. Swap this and the server is live against a different shop-state — which is how F's
|
|
# gate stays deterministic without anyone faking real data (see make-fixture).
|
|
POS_DB = os.environ.get("GODVERSE_POS_DB", "recordgod")
|
|
POS_USER = os.environ.get("GODVERSE_POS_USER", "godverse_ro")
|
|
POS_HOST = os.environ.get("GODVERSE_POS_HOST", "127.0.0.1")
|
|
|
|
READONLY_TABLES = ("inventory", "crate", "disc_cache") # G3 §9: never customer/staff/sales
|
|
|
|
|
|
def q(sql, db=None):
|
|
"""Read-only query as godverse_ro. psycopg2 isn't on this box and the CLI keeps the
|
|
dependency surface at zero — the same call thriftgod's own server would make."""
|
|
dsn = f"host={POS_HOST} dbname={db or POS_DB} user={POS_USER}"
|
|
r = subprocess.run(["psql", dsn, "-tAF\t", "-c", sql], capture_output=True, text=True)
|
|
if r.returncode:
|
|
raise RuntimeError(r.stderr.strip().splitlines()[-1] if r.stderr.strip() else "psql failed")
|
|
return [ln.split("\t") for ln in r.stdout.splitlines() if ln.strip()]
|
|
|
|
|
|
def manifest():
|
|
with open(os.path.join(STOCK, "index.json")) as fh:
|
|
return json.load(fh)
|
|
|
|
|
|
def real_shops():
|
|
"""Real-sourced (POS-backed) shops, per the manifest. NOTE the key is `godverseShopId`
|
|
(camelCase): the manifest is read by the JS runtime, so it wears the JS-side spelling, while
|
|
an atlas index wears the Python-side `shop.godverse_id`. That split is recorded in G3 §4 —
|
|
and I still read it wrong first time, which is the whole reason it's recorded."""
|
|
return [s for s in manifest().get("shops", []) if s.get("sourcing") == "real"]
|
|
|
|
|
|
def real_shop(gid):
|
|
"""The shop's shipped index, but ONLY if it is a real-sourced (POS-backed) shop."""
|
|
for s in real_shops():
|
|
if str(s.get("godverseShopId")) == str(gid):
|
|
for kind in s.get("types", ["record"]):
|
|
p = os.path.join(STOCK, str(gid), f"stock_{kind}_index.json")
|
|
if os.path.isfile(p):
|
|
with open(p) as fh:
|
|
return json.load(fh)
|
|
return None
|
|
|
|
|
|
def etag_of(payload):
|
|
"""Deterministic: same POS state => same etag. `served_at` is deliberately OUTSIDE it —
|
|
a clock in the etag would make every read a cache miss."""
|
|
return hashlib.sha256(json.dumps(payload, sort_keys=True).encode()).hexdigest()[:16]
|
|
|
|
|
|
def stock(gid):
|
|
idx = real_shop(gid)
|
|
if not idx:
|
|
return None
|
|
baked = {it["id"]: it["id"][4:] for it in idx["items"] if it["id"].startswith("sku_")}
|
|
if not baked:
|
|
return None
|
|
skus = ",".join("'" + s.replace("'", "''") + "'" for s in baked.values())
|
|
rows = q(f"""SELECT i.sku, COALESCE(i.slot_number::text,''), i.in_stock,
|
|
COALESCE(d.artist,''), COALESCE(d.title, COALESCE(i.title,'')),
|
|
i.price, COALESCE(i.condition,''), COALESCE(i.sleeve_cond,'')
|
|
FROM inventory i LEFT JOIN disc_cache d USING (release_id)
|
|
WHERE i.sku IN ({skus}) ORDER BY i.slot_number NULLS LAST, i.sku""")
|
|
live, present = [], set()
|
|
for sku, slot, in_stock, artist, title, price, cond, sleeve in rows:
|
|
present.add(sku)
|
|
if in_stock != "t":
|
|
continue
|
|
live.append({"id": f"sku_{sku}", "slot": int(slot) if slot else None,
|
|
"artist": artist, "title": title,
|
|
"price": float(price) if price else None,
|
|
"condition": cond, "sleeve_cond": sleeve, "state": "shelf"})
|
|
# GONE = baked into the tier-1 atlas, no longer on the shelf. Direction is real -> game ONLY:
|
|
# a record that sold IN THE REAL SHOP leaves the game crate. A game buy cannot appear here —
|
|
# there is no verb to carry it and no grant to write it (see the module docstring).
|
|
gone = sorted(i for i, sku in baked.items() if sku not in present or
|
|
sku not in {r[0] for r in rows if r[2] == "t"})
|
|
body = {"shop_id": int(gid), "tier": 2, "crate": idx.get("crate"),
|
|
"items": live, "gone": gone,
|
|
"counts": {"baked": len(baked), "live": len(live), "gone": len(gone)}}
|
|
body["etag"] = etag_of(body)
|
|
return body
|
|
|
|
|
|
def crates(gid):
|
|
if not real_shop(gid):
|
|
return None
|
|
rows = q("""SELECT c.id, c.name, COALESCE(c.label_text,''), count(i.sku)
|
|
FROM crate c LEFT JOIN inventory i ON i.crate_id = c.id AND i.in_stock
|
|
GROUP BY c.id, c.name, c.label_text HAVING count(i.sku) > 0
|
|
ORDER BY c.id""")
|
|
body = {"shop_id": int(gid), "tier": 2,
|
|
"crates": [{"id": int(a), "name": b, "label": c, "count": int(n)} for a, b, c, n in rows]}
|
|
body["etag"] = etag_of(body)
|
|
return body
|
|
|
|
|
|
class H(BaseHTTPRequestHandler):
|
|
protocol_version = "HTTP/1.1"
|
|
|
|
def log_message(self, *a):
|
|
pass # the gate reads assertions, not our chatter
|
|
|
|
def _send(self, code, obj, etag=None):
|
|
raw = json.dumps(obj).encode()
|
|
self.send_response(code)
|
|
self.send_header("Content-Type", "application/json")
|
|
self.send_header("Content-Length", str(len(raw)))
|
|
self.send_header("Access-Control-Allow-Origin", "*") # the game is served from :8130
|
|
if etag:
|
|
self.send_header("ETag", etag)
|
|
self.end_headers()
|
|
self.wfile.write(raw)
|
|
|
|
def _404(self):
|
|
self._send(404, {"error": "not found",
|
|
"note": "read-only surface: /health, /shop/<id>/stock, /shop/<id>/crates. "
|
|
"Write verbs (/reserve, /buy) are ABSENT by ruling — G3 §7, "
|
|
"A-then-B: read-live, sell-sandboxed."})
|
|
|
|
def do_GET(self):
|
|
u = urlparse(self.path)
|
|
parts = [p for p in u.path.strip("/").split("/") if p]
|
|
try:
|
|
if parts == ["godverse", "v1", "health"]:
|
|
return self._send(200, {"ok": True, "tier": 2, "pos_db": POS_DB,
|
|
"shops": [s["godverseShopId"] for s in real_shops()]})
|
|
if len(parts) == 5 and parts[:3] == ["godverse", "v1", "shop"]:
|
|
gid, verb = parts[3], parts[4]
|
|
body = stock(gid) if verb == "stock" else crates(gid) if verb == "crates" else None
|
|
if body is None:
|
|
return self._404()
|
|
if parse_qs(u.query).get("since", [None])[0] == body["etag"]:
|
|
self.send_response(304); self.send_header("ETag", body["etag"])
|
|
self.send_header("Content-Length", "0"); self.end_headers()
|
|
return
|
|
body["served_at"] = datetime.now(timezone.utc).isoformat()
|
|
return self._send(200, body, etag=body["etag"])
|
|
except Exception as e:
|
|
# A dead/denied POS must look like a dead server to the client: it degrades to tier 1.
|
|
return self._send(503, {"error": "pos unavailable", "detail": str(e)[:200]})
|
|
self._404()
|
|
|
|
# No do_POST / do_PUT / do_PATCH / do_DELETE. BaseHTTPRequestHandler answers 501 for verbs it
|
|
# has no handler for; the routes do not exist at all. Absent, not stubbed.
|
|
|
|
|
|
def serve():
|
|
print(f"godverse tier-2 :{PORT} · POS={POS_DB} as {POS_USER} (read-only) · write verbs ABSENT")
|
|
ThreadingHTTPServer(("0.0.0.0", PORT), H).serve_forever()
|
|
|
|
|
|
def setup_role(_):
|
|
"""The grant that IS the sandbox (G3 §7). Idempotent."""
|
|
subprocess.run(["psql", "-q", "-d", "postgres", "-c",
|
|
"DO $$ BEGIN CREATE ROLE godverse_ro LOGIN; "
|
|
"EXCEPTION WHEN duplicate_object THEN NULL; END $$;"], check=True)
|
|
for db in (POS_DB, f"{POS_DB}_live", f"{POS_DB}_fixture"):
|
|
chk = subprocess.run(["psql", "-tAc", f"SELECT 1 FROM pg_database WHERE datname='{db}'",
|
|
"-d", "postgres"], capture_output=True, text=True).stdout.strip()
|
|
if not chk:
|
|
continue
|
|
subprocess.run(["psql", "-q", "-d", db, "-c", f"GRANT CONNECT ON DATABASE {db} TO godverse_ro;",
|
|
"-c", "GRANT USAGE ON SCHEMA public TO godverse_ro;",
|
|
"-c", f"GRANT SELECT ON {', '.join('public.' + t for t in READONLY_TABLES)} "
|
|
"TO godverse_ro;"], check=True)
|
|
print(f" {db}: SELECT on {', '.join(READONLY_TABLES)} — and nothing else")
|
|
|
|
|
|
def make_fixture(args):
|
|
"""A CONTROLLED POS for F's sold-means-gone arm.
|
|
|
|
Why this exists: the real POS has no movement in our window — last sale 2026-07-01, last write
|
|
2026-07-09, and 0 of crate 550's 120 records have ever sold, so a real `gone` is EMPTY and an
|
|
assertion over it would have no subject (the vacuous-gate law). This is the honest way out: a
|
|
labelled fixture proves the MECHANISM deterministically (gone computed -> sent -> consumed ->
|
|
sleeve vanishes), while production reads the real POS unchanged. A fixture is not a lie; a
|
|
green assert over an absent subject is.
|
|
"""
|
|
fx = f"{POS_DB}_fixture"
|
|
subprocess.run(["dropdb", "--if-exists", fx], check=True)
|
|
subprocess.run(["createdb", "-T", POS_DB, fx], check=True)
|
|
idx = real_shop(args.shop)
|
|
skus = [it["id"][4:] for it in idx["items"] if it["id"].startswith("sku_")][:args.sold]
|
|
lst = ",".join(f"'{s}'" for s in skus)
|
|
subprocess.run(["psql", "-q", "-d", fx, "-c",
|
|
f"UPDATE inventory SET in_stock=false, sold_date=now() WHERE sku IN ({lst});"],
|
|
check=True)
|
|
setup_role(args)
|
|
print(f"fixture {fx}: {len(skus)} record(s) marked SOLD -> they must appear in `gone` and "
|
|
f"must never render:\n " + "\n ".join("sku_" + s for s in skus))
|
|
|
|
|
|
def start(_):
|
|
if status(None, quiet=True):
|
|
print("already running"); return
|
|
os.makedirs(os.path.dirname(PIDFILE), exist_ok=True)
|
|
p = subprocess.Popen([sys.executable, os.path.abspath(__file__), "serve"],
|
|
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
|
start_new_session=True)
|
|
open(PIDFILE, "w").write(str(p.pid))
|
|
for _ in range(50):
|
|
time.sleep(0.1)
|
|
if status(None, quiet=True):
|
|
print(f"started pid={p.pid} :{PORT}"); return
|
|
print("failed to start"); sys.exit(1)
|
|
|
|
|
|
def stop(_):
|
|
"""F's kill-the-server gate calls this mid-session. SIGKILL, not a graceful drain: the gate is
|
|
testing what happens when the server DIES, not when it says goodbye."""
|
|
try:
|
|
pid = int(open(PIDFILE).read().strip())
|
|
os.kill(pid, signal.SIGKILL)
|
|
os.remove(PIDFILE)
|
|
print(f"killed pid={pid}")
|
|
except Exception as e:
|
|
print(f"not running ({e})")
|
|
|
|
|
|
def status(_, quiet=False):
|
|
import urllib.request
|
|
try:
|
|
with urllib.request.urlopen(f"http://127.0.0.1:{PORT}/godverse/v1/health", timeout=1) as r:
|
|
up = json.loads(r.read())
|
|
if not quiet:
|
|
print(f"UP :{PORT} · POS={up['pos_db']} · real shops={up['shops']}")
|
|
return True
|
|
except Exception:
|
|
if not quiet:
|
|
print(f"DOWN :{PORT}")
|
|
return False
|
|
|
|
|
|
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
sub = ap.add_subparsers(dest="cmd", required=True)
|
|
for name, fn in (("serve", lambda a: serve()), ("start", start), ("stop", stop),
|
|
("status", status), ("setup-role", setup_role)):
|
|
sub.add_parser(name).set_defaults(func=fn)
|
|
mf = sub.add_parser("make-fixture", help="a controlled POS so F's gate has a subject")
|
|
mf.add_argument("--shop", default="3962749")
|
|
mf.add_argument("--sold", type=int, default=3)
|
|
mf.set_defaults(func=make_fixture)
|
|
a = ap.parse_args()
|
|
a.func(a)
|