Compare commits

...

5 Commits

Author SHA1 Message Date
m3ultra
bee7563a76 Lane E R27 (v5.0): provenance v5-final — and the roster gap the freeze caught (ledger #6)
THE BUG THE FREEZE FOUND: redhill_godverse was ON DISK but NOT IN THE ROSTER — for three rounds. The v5
provenance table needed real numbers, so I measured instead of carrying the v4 row forward. The count
didn't match: 23 caches on disk, 22 rostered. The missing one was redhill_godverse — THE TOWN THE ENTIRE
v5 EPOCH WALKS TO.

Mine, squarely. index.json is Lane E's file (write_index()). G's redhill_godverse landed in R24 (86e2985)
AFTER my last index run (5815dfb) and nobody re-rostered it. An addition isn't done when the cache is
written — it's done when the roster is regenerated. That is the exact mirror of my own R24 lesson ("a
retirement isn't done when the cache is deleted; it's done when the consumers are swept"): I wrote the
lesson for deletions and missed its reflection for additions.

WHY FOUR GREEN ROUNDS NEVER CAUGHT IT — a gate-shape lesson. Nothing was broken for the gates: selfcheck
enumerates web/assets/towns/*.json DIRECTLY (so it saw the town; A even pinned its goldens) and F's crate
smoke boots BY KEY (?town=redhill_godverse). The roster's only consumer is B's HUD selector (hud.js, my
R20 ledger #2 design). So the single symptom was that A PLAYER COULD NOT CHOOSE THE HEADLINE TOWN FROM THE
DROPDOWN — invisible to every automated check by construction. A gate that reads around the artifact under
test cannot test it: the vacuous-gate species, one layer out.
Fixed: re-ran write_index() -> 23 rostered, 23 on disk, 0 missing. Only index.json moved (every cache
byte-identical again). selfcheck ALL GREEN 156,212, golden 0x3fa36874 untouched. qa.sh --strict --matrix
7 passed / 0 failed / 0 warn.

-> F (#5, the tour): the money shot walks Musgrave Road into Monster Robot Party. If any tour frame reaches
that town THROUGH B's SELECTOR rather than by key, it could not have found it before this commit; by key it
always worked. Worth one glance at your framing path.
-> B: the dropdown now carries both godverse towns. Your `${t.state ? ', ' + t.state : ''}` handles their
empty state gracefully — nothing needed, and the defensive write is why this cost nothing cosmetically.
-> G: your godverse caches carry no `state`, so they roster as state:''. Harmless (B degrades cleanly). For
"Red Hill, QLD" in the picker, emit `state` — write_index() already prefers the cache's own field.

PROVENANCE v5-FINAL. The ⟨v5.0-FINAL⟩ rows are measured, not inherited — the town-pack row still claimed
v4's "22 towns / 1192 shops", stale since my R24 ballarat retirement and redhill build (now 21 real / 1196
shops / 299 heroes, + both godverse towns = 23 rostered). Added the two v5 art rows: the real-stock atlas
(1 crate, 120 items, Monster Robot's own POS, sku_ ids) and the mint atlases (14 crates, 224 items, seeded
from real dealgod listings, mint_ ids). These are the table's FIRST AMBER ROWS — everything before v5 was
green web-ok; real product photography is the first asset class this project ships that a human must clear
before public release.

I VERIFIED MY OWN PROSE AND IT CAUGHT ME: I first wrote the mint crates as "6 record · 6 book · 3 toy" —
that is the breakdown of all FIFTEEN crates, which includes Monster Robot's REAL record crate. The 14 mint
crates are 5 record · 6 book · 3 toy. A count attributed to the wrong population, one line from being
frozen into the release record. Every other claim in the table re-measured true (21/1196/299/8 states/23
rostered/roster==disk/72/37/120 items/224 items/both zones in-house-green).

THE LICENCE LINE'S STORY, recorded as a mechanism. The amber rows are guarded by exactly one printed line,
so the freeze says why it took two rounds to make honest: R23's glob matched 0 files and returned 0; R25's
permissive `or` fallback let the print go silent while the check still passed — the flag stopped reaching a
human on real product photos while the gate said OK. Now: one spelling, one lookup with one owner, and the
line is COUNTED (judged N => N lines, or FAIL). Both zones print — mint carries the same in-house-green
flag, because minting changes whose stock a crate CLAIMS to be, never whose photograph it IS. Frozen today
at: atlas-QA OK — 15 per-shop atlas(es) valid, 15 licence line(s) shown, 0 warning(s).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 13:59:49 +10:00
m3ultra
cedc5872fb Lane G R27 (v5.0): tier 2 — the two read endpoints, the three fences, and the gate's missing subject
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>
2026-07-17 13:56:02 +10:00
m3ultra
6960bdd279 Lane C R27 (v5.0): the tier-2 seam ruling + LANE_C_PUB v5-FROZEN (ledgers #2, #6)
SEAM RULING (§8): bless the direct wire — NO lifecycle hook. F already holds currentAdapter
(interior_mode.js:67) and passes it to dig.open() PER DIG (:111), so the live read lands, F
updates the pack, the next riffle picks it up; not-landed/slow/dead => the adapter is simply the
tier-1 pack => the dig never blocks (charter risk #1). No C lifecycle change.

BUT THE QUESTION UNDERNEATH IT IS C'S, AND THE OBVIOUS WIRING IS WRONG. pickRealOffers indexes
pack.items[(rr()*len)|0], so FILTERING THE ARRAY RESHUFFLES THE CRATE. Measured, 5 crates: one
record selling changed 21 OTHER records (6/2/2/6/5 collateral). Pick-then-omit (draw from the
stable full list, drop gone after) = 0 collateral: a crate with 3 sold shows 13, not a different
16. Selling one record must remove one record, not re-stock the shop.

THE TRAP: both forms hide the sold record — so F's planned assertion ("a real gone item never
renders") GOES GREEN ON THE WRONG WIRING. It cannot see the 21. -> F: assert on COLLATERAL (the
survivors are unchanged across a gone delta), not absence. Vacuous-gate law, third application,
and the cheapest to miss because it passes.

LANDED (dig.js): pack.gone (Set|array) omitted at PICK time. F's invariant published: update item
FIELDS in place (live price) freely; NEVER add/remove/reorder items[] — that identity+order IS the
seeded pick's stability (R25's positional-id lesson, one layer up).

DETERMINISM BOUNDARY sits exactly here: server down => byte-identical tier 1 (gate-able); server
up => the crate legitimately changes (tier 2 is non-deterministic BY DESIGN), so no gate may
assert dig determinism with the server up, and pack.gone never enters a seeded stream or golden.

SCOPED: the bin fan is decorative (a different seeded pick from the dig's offers) — a sold cover
may linger there until rebuild. That's when I'd publish a post-build hook; the epoch's claim
doesn't need it, because the crate you RIFFLE is true.

VERIFIED: pure-function measurement for the collateral claim, then end-to-end through the real
dig's public surface (pull -> Esc -> scroll -> pull): sold record gone, survivors 4/4, collateral
0, items[] untouched at 120, live in-place price shows on the next pull. (My first harness
returned the same title 4x — I never un-pulled — so its number was an artifact; re-ran it properly
rather than report a metric I'd just watched lie.)

DOCS FREEZE (§6): LANE_C_PUB is v5.0-FROZEN, whole document, with the case law at the top: three
amendments here exist because a rule of mine met a case written AFTER it, and both times the lane
building to my rule took the red. Header now states it: when a line meets a case it didn't
foresee, the line is the stale thing — amend it, don't bend the case.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 13:53:02 +10:00
m3ultra
01f398960b Lane B R27 (on-call): the R26 handshake verified against G's real manifest
R27 owes B nothing ("B/D: notes current; nothing else owed"; "B/D on-call").
This is on-call work on F's #5: the tour is specified as "Monster Robot Party's
frontage (B's mark visible)" — a claim R26 could not test, because G's manifest
did not exist and B proved the treatment against a temp manifest it scaffolded
and deleted. G shipped the real one (36d10ca). Now measured, not assumed.

Monster Robot Party is in redhill_godverse — NOT newtown_godverse, the only
town B ever tested the mark in. F's money shot is posed in a town this
treatment had never been verified against. It works.

The handshake held unmodified: G's {version, shops:[{godverseShopId, types,
sourcing}]} lands inside B's tolerant reader with zero coordination. The
tier→sourcing rename did not touch B — the mark never reads that field, so a
rename of it cannot break the footpath. A cue that reads no field cannot break
when the field moves.

Both arms proven live, the discriminating arm for the first time (R26's temp
manifest marked all 18 keyed shops, so "the manifest is the authority, not the
key" was never exercised). Redhill: 9 keyed, only 4 stocked.
  Monster Robot Party #3962749 (real)  keyed + in manifest → MARKED
  Silky Oak Furnature #31              keyed, not in manifest → BARE
  Thrift Shop and Eternity Boutique    keyed, not in manifest → BARE
Two keyed shops stand metres from a stocked one wearing no mark, because they
have no crate. The street advertises exactly the crates that exist. (#31 is
Fable's R25 id-namespace shop; the mark reads godverseShopId only, unaffected.)
Frontage 135 draws / 74,354 tris, 0 page errors, manifest prefetched at boot.

Intel for F's #5, no action owed: the novelty_record landmark (B's furniture —
a giant vinyl disc marking record shops) stands 2.5m off the frontage and is
prominent head-on. Ray-grid occlusion so F can frame without guessing:
  mark+sign   head-on 4% blocked   off-axis +4m 0%
  window      head-on 22% blocked  off-axis +4m 44%
Shoot HEAD-ON: the vinyl sits low (y~1.3), the mark rides high (y~3.3), and
stepping off-axis doubles window occlusion instead of helping. I first read the
black disc as a render bug — it isn't, it's the landmark working; recorded so
the next reader doesn't re-open it.

Notes-only; no code changed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 13:52:57 +10:00
m3ultra
839575b28d Lane A round 27 (v5.0): the docs freeze — CITY_SPEC v5 + the risk list judged (ledger #6)
Docs only. No code, no golden moved — correct: tier 2 touches no plan, no cache, no atlas bytes. selfcheck
ALL GREEN 156,212/156,212; scaffold + consistency green; classic 0x3fa36874 and gig 0xb1d48ea1 frozen. The
whole stock epoch moved zero plan goldens, which was the determinism boundary's entire promise.

CITY_SPEC v5 SECTION — written from the ARTIFACTS, not the briefs (this epoch's own lesson):
- The tier ladder: 0 parody / 1 static real / 2 live GODVERSE, each degrading softly to the rung below.
- `sourcing` is NOT `tier`, and C's ruling is the precise one — both ship in every index. `tier: 1` is the
  LADDER RUNG (a mint atlas is still a static file, so still rung 1); `sourcing: "real"|"mint"` is WHERE
  THE CONTENTS CAME FROM. Different axes. Collapsing them is exactly how a mint crate comes to read as
  real. Measured: 15 keyed atlases = 1 real + 14 mint.
- The three id-space fences, verified rather than restated (below).
- The manifest: existence DECLARED, never probed — the root fix for fail-soft-by-404 colliding with the
  zero-console-errors law. No entry, no fetch, zero 404s, no attribution exceptions. 15 entries / 15 dirs.
- Ruling 2 as law (mixed caches are the design; no gate may assert it away — mine did).
- Lane A's seam is exactly one field: godverseShopId, and `'godverseShopId' in shop` is the tier test.

THE ID FENCES ARE VERIFIED, NOT ASSERTED. The plan-id/godverse-id trap is STILL LIVE in the data and I
reproduced it: in redhill_godverse, bare `31` is "Wholefood Cafe" as a plan id and "Silky Oak Furnature" as
a godverse id — the exact ambiguity that made F's enterShop(31) enter the wrong shop and pass a soft-fall
assertion vacuously on the wrong subject. So it goes in the spec as a STANDING RULE, not a fixed bug: no
code or gate may compare a bare number across the two spaces. (Within the godverse space, G's two halves —
census <=2992, dealgod store ids — are numerically disjoint by construction, which is what makes the opaque
key safe.)

THE RISK LIST, JUDGED LINE BY LINE — AND I DID NOT PRE-RETIRE ANYONE'S GATE:
1. CARRIED. Tier-2 latency/stutter is this round's defining gate and I'm writing in wave 2 — F's reader and
   kill-the-server gate do not exist yet. NOTHING retires this line except that gate going green. Retiring
   it from a doc while the code is unwritten would be the precise species this epoch spent four holds
   catching: a rule meeting a case that doesn't exist yet. The offline law is a promise until #3 makes it
   falsifiable; that's the whole point of #3.
2. RETIRED — measured, and the remedy the risk NAMED is the remedy that shipped ("crate rotation rather
   than bigger atlases"). Max 2 atlases/shop vs C's hard cap of 2; the 14 mint shops sit at 16 items / 1
   atlas / 1024²; only the one real POS shop earns 2048² (120 items, 2 atlases, 1.2 MB) under the 60+ rule.
3. RETIRED — DESIGNED OUT, not solved. The risk assumed a fuzzy-matching problem ("names drift, shops
   move"). There is no matcher: identity is an explicit opaque key G emits. The predicted failure cannot
   occur in the predicted form — and the proof is the headline shop: Monster Robot is in NEITHER the census
   NOR OSM, so no name-matching strategy could ever have found it. Mis-stocking is fenced structurally
   instead (duplicate ids error, uniqueness over defined ids, the orphaned-atlas gate).
4. SPLIT. Write-back RETIRED by John's ruling recorded as MECHANISM, not config: /reserve and /buy are
   ABSENT, not stubbed — races and reservation semantics cannot occur because the verbs don't exist, and
   "prove the absence" beats any sandbox flag a config typo could flip. (Option B re-opens this line
   deliberately at v5.x, behind a hold budget.) The `gone` read half is CARRIED to F's #4.
5. Unchanged, extended: static-atlas determinism proved in practice (G's index-only sku re-emit moved no
   .webp bytes); the fail-soft ladder proved on every rung; the id fences now have case law behind them.
6. NEW, CARRIED — the orphaned atlas, with a live near-miss on the books.
7. NEW, CARRIED — "real" is one shop wide: 14 of 15 keyed atlases are mint, because exactly one shop has a
   POS. Honest and marked, not a defect — but the epoch's headline claim rests on ONE shop, and any future
   claim of scale needs either siblings for monsterrobot or continued care that `sourcing` stays legible to
   humans and not just to gates.

THE GATE THAT ARMED ITSELF (the law working unattended, worth recording). My R25 orphaned-atlas gate SKIPped
on newtown_godverse all through R25 — "no committed atlas keys to this town", subject absent, so it said so
instead of passing quietly. The moment G's 14 mint atlases landed it STARTED BINDING ON ITS OWN and passed:
156,211 -> 156,212, and that +1 IS the gate arming itself. Nobody edited it. That is what the vacuous-gate
law buys — a gate that waits, in the open, for its subject to exist.

G: redhill_godverse's lift drops keyed shop 2286 "Empire Revival"; you stocked its numeric neighbour 2287.
Nothing is orphaned and the gate is green — but stocking 2286 would orphan it today, and the hazard grows
with every shop stocked. Carried as risk #6.

F: nothing of mine gates your tag.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 13:52:47 +10:00
14 changed files with 862 additions and 21 deletions

View File

@ -7,6 +7,51 @@ Last updated: 2026-07-17 (round 26) · owner: PROCITY-C · reviewer: Fable
---
## Update 2026-07-17 (round 27, v5.0 THE LIVE CRATE) — the tier-2 seam + LANE_C_PUB v5-FROZEN
R27 §Lane C (ledgers #2 + #6). Wave 1 — ran after F tagged `v5.0-beta` (`78848ed`, 7/0/0).
### The seam ruling (§8): bless the direct wire — but the *form* was the real question
Fable asked: publish a post-build hook, or bless the direct wire? **Answer: bless it — no lifecycle hook.**
F already holds `currentAdapter` (`interior_mode.js:67`) and passes it to `dig.open()` **per dig** (`:111`),
so the live read lands, F updates the pack, and the next riffle picks it up. Read not landed / slow / dead ⇒
the adapter is simply the tier-1 pack ⇒ the dig never blocks. No C lifecycle change.
**But the question underneath it — *how* `gone` applies — is C's, and the obvious wiring is wrong.**
`pickRealOffers` indexes `pack.items[(rr()*len)|0]`, so **filtering the array reshuffles the crate**.
Measured across 5 crates: **one record selling changed 21 OTHER records** (6/2/2/6/5 collateral). Pick-then-
omit — draw from the stable full list, drop gone *after* — has **0 collateral**: a crate with 3 sold shows
13, not a different 16. Selling one record must remove one record, not re-stock the shop.
**And the trap: both forms hide the sold record.** So F's planned sandbox assertion — "a real `gone` item
never renders" — **goes green on the wrong wiring**. It cannot see the 21. → **F: assert on collateral (the
survivors are unchanged across a `gone` delta), not absence.** That's the vacuous-gate law's third
application, and the cheapest to miss, because it passes.
**Landed** (`dig.js`): `pack.gone` (Set|array) omitted at pick time; **F's invariant published** — update
item *fields* in place (live price) freely, **never add/remove/reorder `items[]`**; that identity+order IS
the seeded pick's stability (the same lesson as R25's positional ids, one layer up).
**Determinism boundary sits exactly here:** server down ⇒ byte-identical tier 1 (gate-able); server up ⇒ the
crate legitimately changes — tier 2 is non-deterministic *by design*, so **no gate may assert dig determinism
with the server up**, and `pack.gone` never enters a seeded stream or a golden.
**Scoped honestly:** the bin fan is decorative (a different seeded pick from the dig's offers), so a sold
cover may linger there until rebuild — that's when I'd publish a post-build hook, and the epoch's claim
doesn't need it, because *the crate you riffle is true*.
**Verified** — pure-function measurement for the collateral claim, then end-to-end through the real dig's
public surface (pull → Esc → scroll → pull): sold record gone · **survivors 4/4 · collateral 0** · `items[]`
untouched at 120 · live in-place price shows on the next pull. *My first dig harness returned the same title
4× (I never un-pulled), so its "survivors" number was a harness artifact — I re-ran it properly rather than
report a metric I'd just watched lie.*
### Docs freeze (§6): LANE_C_PUB is **v5.0-FROZEN**, whole document
Marked, with the case law kept at the top rather than buried: three of this doc's amendments exist because a
rule of mine met a case written *after* it (§7's naming vs its own loader; §7.2a's `sku_` before mint) — and
both times the lane building to my rule took the red. The header now says it plainly: **when a line here
meets a case it didn't foresee, the line is the stale thing — amend it, don't bend the case.**
---
## Update 2026-07-17 (round 26 wave 4) — the mint-id fence: my rule was the stale thing
F's fourth hold traced to **my** §7.2a. I wrote "`sku_`-prefixed for real-stock packs" in R25 — **before mint

View File

@ -3,6 +3,63 @@
*Lane brief: `docs/LANES/LANE_G_GODVERSE.md`. This file follows your progress-file convention —
newest round first. Fable-on-ultra writing.*
## Round 27 (2026-07-17) — tier 2: the two read endpoints (ledger #1)
**Landed:** `pipeline/godverse_server.py``/health`, `/shop/<gid>/stock`, `/shop/<gid>/crates`,
plus `start|stop|status` (F's kill-the-server lifecycle), `setup-role`, and `make-fixture`.
Stdlib + `psql`; no server code existed before this round, per the ratified G3.
**Live now, against the real POS:** crate 550 — `{baked:120, live:120, gone:0}`, real prices, real
`VG+` grading, etag stable across reads 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 (measured, not reviewed):**
| fence | proof |
|---|---|
| the credential — `godverse_ro` | `UPDATE inventory`**permission denied** · `SELECT FROM customer`**permission denied** |
| write verbs absent (§7 ruling) | `POST /reserve`**501** · `POST /buy`**501** · `GET` either → 404 |
| metadata only | no `cover`, no atlas bytes on the wire — covers stay static tier-1 files |
| tier 2 = real shops only | a `mint` shop's `/stock`**404** (no POS to be live about), not an empty crate |
| the id fence | `gone[]` carries the atlas `id` verbatim → `gone.includes(item.id)`, zero transform |
**One grant carries both the sandbox and the PII fence** — that's why §7 said enforce at the
credential, not in a code review. `godverse_ro` holds SELECT on inventory/crate/disc_cache and
nothing else, so customer/staff/sales are unreachable *by the process*, not by our good intentions.
**⚠ 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** (400800/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 (I 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.
An assert over it would pass **without touching its subject** — the exact species that has held
four tags. So I built the honest way out: **`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 the server at it
via `GODVERSE_POS_DB` and gets a deterministic gate; production reads the real POS untouched.
**A fixture is not a lie; a green assert over an absent subject is.**
**Lifecycle for the kill-the-server gate (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,
no response at all) → `status: DOWN``start` → live again. F drives it; no admin verb was added
to the API, because a server whose whole point is "no write verbs" shouldn't grow a remote kill.
**Three corrections this round forced into my own G3 §4 (design → as-built):**
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. 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, and this time it was mine, in the doc I wrote.
2. `items[]` drops `release_id` and `cover`: `cover` edges the API toward serving pixels (it never
should), and `release_id` is the *pressing* while 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* absence proof: a 404 means a handler ran and declined; 501 means there is no
write handler in the process. **→ F: assert 501.**
**→ Lane F, the four asserts I can hand you pre-measured:** `POST /reserve|/buy` → 501 · a game
buy cannot reach the server (no verb *and* no grant — prove both) · `gone` fixture arm as above ·
server dead → connection refused, so the reader must treat it as tier 1 in silence.
## Round 26 (2026-07-17) — the mint baker, the crate menu, the manifest (ledgers #2, #6)
**Landed (main, pathspec-atomic):**

View File

@ -1,11 +1,42 @@
# LANE A — CITYGEN · progress (PROCITY-A)
*Status: **all deliverables landed; self-check ALL GREEN (156,211/156,211); rounds 225 closed** (see below).
R25 (v5.0-alpha): the sweep reconciled with **Ruling 2** (keyed asserts are per-layer), the uniqueness check
rebuilt so it can actually fail, the **orphaned-atlas gate** added, and **six goldens pinned** — F's tag is
unblocked from my side. classic `0x3fa36874` / gig `0xb1d48ea1` frozen.*
*Status: **all deliverables landed; self-check ALL GREEN (156,212/156,212); rounds 227 closed** (see below).
R27 (v5.0 close): **CITY_SPEC v5 section** (tier ladder · `sourcing` vs `tier` · the three id-space fences ·
the manifest · Ruling 2) + the **charter risk list judged line by line** — risk #1 deliberately CARRIED, not
pre-retired, because F's kill-the-server gate is what retires it. classic `0x3fa36874` / gig `0xb1d48ea1`
frozen; **no golden moved this epoch**.*
*Date: 2026-07-17 · owner: PROCITY-A (Opus 4.8)*
## Round 27 (2026-07-17) — v5.0 THE LIVE CRATE: the docs freeze (ledger #6)
Lane A's half of the epoch close. Docs only; **no code, no golden moved** — correct, since tier 2 touches no
plan, no cache, no atlas bytes. selfcheck **ALL GREEN 156,212/156,212**.
- **CITY_SPEC v5 section**, written from the artifacts rather than the briefs (the epoch's own lesson):
the **tier ladder** (0 parody / 1 static real / 2 live, each degrading softly); **`sourcing` is not
`tier`** — both ship in every index, `tier: 1` is the rung and a mint atlas is *still* rung 1, while
`sourcing: "real"|"mint"` is provenance (15 keyed atlases = **1 real + 14 mint**); the **three id-space
fences**; the **manifest** (existence declared, never probed — the root fix for fail-soft-404 vs the
zero-console-errors law; 15 entries, 15 dirs); **Ruling 2**; and A's single seam, `godverseShopId`.
- **The id fences, verified not restated.** The plan-id/godverse-id trap is still live in the data and I
reproduced it: in `redhill_godverse`, bare **`31`** is "Wholefood Cafe" as a plan id and "Silky Oak
Furnature" as a godverse id — the exact ambiguity that made F's `enterShop(31)` assert vacuously on the
wrong shop. It's a standing rule, not a fixed bug, and the spec says so.
- **The risk list judged, without pre-retiring anyone's gate.** #1 (tier-2 latency) **CARRIED** — written in
wave 2, F's reader doesn't exist yet, and *nothing retires that line except the gate going green*;
retiring it from a doc while the code is unwritten is the precise species this epoch spent four holds
catching. #2 **retired** (measured: max 2 atlases/shop vs C's cap of 2; mint at 16 items/1024², only the
real POS shop at 2048²; the mecca case served by crate rotation — the remedy the risk itself named).
#3 **retired — designed out, not solved**: it assumed fuzzy name-matching, and there is no matcher;
Monster Robot is in *neither* census nor OSM, so no matcher could ever have found it. #4 **split**:
write-back retired by John's ruling-as-mechanism (`/reserve`+`/buy` absent, not stubbed — you can't race a
verb that doesn't exist), the `gone` read carried to F's #4. Two new carried: **#6 orphaned atlas**,
**#7 "real" is one shop wide** (14/15 are mint — honest and marked, but the headline rests on one shop).
- **The gate that armed itself.** My R25 orphan gate SKIPped on `newtown_godverse` while no atlas keyed
there; G's mint atlases landed and it **began binding unattended** — 156,211 → **156,212**, and that +1 is
the gate arming itself. Near-miss carried: redhill's lift drops keyed shop **2286 "Empire Revival"**;
G stocked neighbour **2287**. Stocking 2286 today would orphan it.
## Round 25 (2026-07-17) — v5.0-alpha: the sweep vs Ruling 2 + the six pins (ledger #1)
All 8 tree reds cleared — **6 were my own gate asserting Ruling 2 away**, 2 were goldens awaiting my pin.

View File

@ -516,6 +516,79 @@ nothing outside the cache-schema path).
> count beats teleport.** The frozen set is unmoved by the entire epoch: synthetic `0x3fa36874`, gig
> `0xb1d48ea1`, marched fixtures, classic.
## The stock ladder (v5 — THE REAL SHOP; frozen at `v5.0`, ROUND27 ledger #6)
> **v4 made the towns real; v5 makes the shops real.** The map-level contract above is unchanged by this
> epoch: **stock is DATA, not world.** The charter's determinism boundary is the load-bearing line —
> *world = seeded, frozen, golden-gated; stock = data tiers riding the asset law.* Nothing in this section
> may move a plan golden, and nothing in it did: the whole epoch left synthetic `0x3fa36874`, gig
> `0xb1d48ea1`, the marched fixtures and classic untouched.
**The tier ladder.** Each rung is fully playable and degrades **softly** to the one below — the fail-soft
pattern proven since `?noassets` on day one, made structural:
| tier | what | flag | deterministic? |
|---|---|---|---|
| **0 · parody** | seeded canvas sleeves (v1) | any boot | yes — the QA baseline |
| **1 · static real** | per-shop atlases baked from dealgod/POS | `?stock=real`, **no server** | yes — atlases are files |
| **2 · live GODVERSE** | thriftgod read-only endpoints; sold-means-gone | server reachable | **no — by design** |
**The offline law (charter law #1) is the epoch's gate, not a footnote:** the server *enriches, never
gates*. No boot path, gate, or tag may depend on a reachable server. Tier 2 is therefore **never inside a
golden or a byte-identical assert**; it gets its own smoke class whose server lifecycle the gate itself
controls, so the *assertion* is deterministic even though the *stock* isn't.
**`sourcing` is not `tier`** (C's ruling, R26 — and it's the distinction that keeps the ladder honest). The
rung and the provenance are different axes, and both ship in every atlas index:
- **`tier: 1`** — the ladder rung. A minted atlas is *still* a static file, so it is *still* rung 1.
- **`sourcing: "real" | "mint"`** — where the contents came from. **`real`** = a POS snapshot (Monster Robot
Party's actual crates). **`mint`** = a seeded `random.Random(shop_id)` sample of real dealgod listings —
*plausible, not that shop's stock*. Only one shop in the world has a POS; minting is how the other keyed
shops get distinct crates **without pretending to data we don't hold**. Current pack: **15 keyed atlases =
1 real + 14 mint**. No gate, doc, or human may read a mint crate as real — that is the whole point of the
field, and gates assert on it.
**The id-space fences.** Three namespaces meet at the crate, and **every collision this epoch came from
comparing across them**. They are fences, not decoration:
| space | form | scope | fenced from |
|---|---|---|---|
| **plan `shop.id`** | small int, `0..N` per town | one CityPlan, local | `godverseShopId`**numerically overlapping** |
| **`godverseShopId`** | census id (**≤2992**) dealgod store id (e.g. **3962749**) | the stock namespace | its two halves are numerically **disjoint by construction** (G) |
| **atlas slot id** | **`sku_<POS id>`** / **`mint_<listing id>`** | one atlas index | each other, by **prefix** |
- **plan id vs `godverseShopId` — never compare a bare number across them.** They are *semantically*
disjoint namespaces that *numerically overlap*, which is the trap: in `redhill_godverse` today, bare
**`31`** is **"Wholefood Cafe"** as a plan id and **"Silky Oak Furnature"** as a godverse id. That exact
ambiguity made F's `enterShop(31)` enter the wrong shop and a soft-fall assertion pass **vacuously on the
wrong subject**. Verified still live in the data — so this is a standing rule, not a fixed bug.
- **`sku_` vs `mint_` is a sourcing-scoped namespace fence** (C §7.2a as amended, R26 wave 4). A crate
wearing the other sourcing's prefix **fails**. The fence is *stronger* than the rule it replaced: the easy
green was deleting the prefix check; instead the prefix now proves its sourcing.
- **Positional ids are banned** (R25): `rec_0000…` numbering collides across packs by construction, which
is what made the integrator's own id-equality gate vacuous. Slot ids are sku-derived and unique across
packs, stable across re-emits.
**The manifest — `web/assets/stock_godverse/index.json`** (G emits, F consumes, E validates, C's §7.2d).
It declares **which `godverseShopId`s carry atlases** (`types[]` + `sourcing` per entry; mint included).
**Existence is declared, never probed.** This is the root fix for a real collision between two laws:
fail-soft-by-404 vs the zero-console-errors law — the runtime used to probe blind and eat 404 noise,
tolerated by attribution, which is a workaround, not a design. No manifest entry ⇒ no fetch ⇒ **zero 404s,
no exceptions**. Consistency is gated both ways (an atlas on disk absent from the manifest **fails**, and
vice versa — vacuous-gate law shape). Today: **15 entries, 15 dirs on disk.**
**Lane A's seam to all of it** is exactly one field — **`godverseShopId`** (documented above): it rides the
town cache, survives the lift onto `plan.shops[]`, and is what the runtime resolves `stock_godverse/<id>/`
from. **`'godverseShopId' in shop` is the tier test**: present ⇒ tier-1+ candidate, absent ⇒ tier 0. The
key is *absent, not undefined*, on every unkeyed shop — which is why the entire stock epoch moved **no plan
golden**. `selfcheck` gates the seam per-layer (Ruling 2), including the **orphaned-atlas** check: a
committed atlas whose shop the lift dropped is real stock keyed to a shop that isn't in the town.
**Ruling 2 (law):** a godverse cache is **deliberately mixed** — a keyed GODVERSE layer + an inherited OSM
texture layer that is legitimately unkeyed. G measured it load-bearing: census-only Red Hill was
unshippable (306 m spacing warn, a poster inside a kerb). **No gate may assert it away** — mine did, and
called two healthy towns broken.
## Shop types v1 (registry lives in `web/js/core/registry.js`, Lane A writes it, all lanes read)
> **`web/js/core/registry.js` is the machine-readable source of truth** — import `SHOP_TYPES` for

View File

@ -93,28 +93,64 @@ boot path, gate, or tag that may touch the network.
---
## 4. API surface [DESIGN]
## 4. API surface [AS-BUILT — R27, `pipeline/godverse_server.py`]
Read-only in v5.0; the write verbs are drafted here but **gated on §7**.
Read-only, per §7's ruling. **The write verbs are not "gated" — they do not exist.**
```
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]
GET /godverse/v1/health -> { ok, tier, pos_db, shops:[godverse_id] }
GET /godverse/v1/shop/<godverse_id>/stock?since=<etag>
-> { 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/<godverse_id>/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_<POS 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 a `sku` in `gone[]``collapseBuyItem()` (already
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** (400800/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);

View File

@ -2,6 +2,40 @@
Durable answers to questions other lanes raised against Lane A. Full status in `A-progress.md`.
## Round 27 (2026-07-17) — v5.0: the docs freeze — CITY_SPEC v5 + the risk list judged (ledger #6)
### → F + Fable: **A's docs half is done and the tree is green; nothing of mine gates your tag**
selfcheck **ALL GREEN 156,212/156,212**, classic `0x3fa36874` / gig `0xb1d48ea1` frozen, **no golden moved**
(none should — tier 2 touches no plan, no cache, no atlas bytes). CITY_SPEC has its **v5 section** (the tier
ladder · `sourcing` vs `tier` · the three id-space fences · the manifest · Ruling 2 · A's one seam), and the
charter risk list is judged line by line in `V5_REAL_SHOP.md`.
### The risk list: I did NOT pre-retire your gate
Written in wave 2, so **risk #1 (latency/stutter at tier 2) is CARRIED, not retired** — your reader and
kill-the-server gate don't exist yet, and **nothing retires that line except the gate going green**. Retiring
it from a doc while the code is unwritten would be the exact species this epoch spent four holds catching: a
rule meeting a case that doesn't exist yet. #2 retired (measured), #3 retired (designed out), #4 **split**
write-back retired by John's ruling-as-mechanism, the `gone` read carried to your #4. Two new carried lines
(#6 orphaned atlas, #7 "real" is one shop wide).
### The gate that armed itself — worth seeing, since it's the law working unattended
My R25 orphaned-atlas gate **SKIPped** on `newtown_godverse` all through R25 ("no committed atlas keys to
this town" — subject absent, so it said so instead of passing). The moment G's 14 mint atlases landed, it
**started binding on its own** and passed: selfcheck went 156,211 → **156,212**, and that +1 is the gate
arming itself. Nobody edited it. That's what the vacuous-gate law buys — a gate that waits, in the open, for
its subject.
**Live near-miss on the books → G:** `redhill_godverse`'s lift drops keyed shop **2286 "Empire Revival"**;
you stocked its numeric neighbour **2287**. Nothing is orphaned and the gate is green — but **stocking 2286
would orphan it today**, and the hazard grows with every shop stocked. Carried as risk #6.
### One correction to the round doc's framing, for the record
The brief lists "the manifest" and "sourcing" as if `tier` were superseded. The artifacts say otherwise, and
**C's ruling is the precise one**: every atlas index carries **both**`tier: 1` (the ladder rung; a mint
atlas is *still* a static file, so still rung 1) **and** `sourcing: "real"|"mint"` (where the contents came
from). They are different axes. The spec documents both, because collapsing them is exactly how a mint crate
would come to read as real.
## Round 25 (2026-07-17) — v5.0-alpha: the sweep reconciled with Ruling 2, six goldens pinned (ledger #1)
### → F: **the tree is green — all 8 reds are gone, nothing of mine is pending** (the handshake)

View File

@ -1,5 +1,57 @@
# LANE B — NOTES (measured)
## Round 27 (on-call) — the R26 handshake **verified against G's real manifest**; intel for F's #5 money shot
R27 owes B nothing (*"B/D: notes current; nothing else owed"*, *"B/D on-call"*). This is on-call work: F's
tour (#5) is specified as *Monster Robot Party's frontage, **B's mark visible*** — a claim my R26 verification
could not actually test, because G's manifest didn't exist yet and I proved the treatment against a **temp
manifest I scaffolded and deleted**. G has since shipped the real one (`36d10ca`). Claim now measured, not
assumed.
**Monster Robot Party is in `redhill_godverse`, not `newtown_godverse`** — the only town B ever tested the
mark in. F's money shot is posed in a town this treatment had never been verified against. It works.
### The R26 handshake held, unmodified
G's real emission is `{version:1, shops:[{godverseShopId, types, sourcing}]}` — 15 entries, lands inside B's
tolerant reader (`.shops`, objects carrying `godverseShopId`) with **zero coordination**, exactly as the R26
note promised. **The `tier` → `sourcing` rename (Fable's waves-1-2 ruling) did not touch B**: the mark
deliberately never reads that field, so a rename of it cannot break the footpath. *The "don't leak tier"
call paid a dividend it wasn't designed for — a cue that reads no field cannot break when the field moves.*
### Both arms proven live — the discriminating arm for the first time
R26's temp manifest marked **all 18** keyed shops, so "manifest is the authority, not the key" was never
actually exercised. G's real manifest makes it load-bearing: redhill has **9 keyed, only 4 stocked**.
Measured at Monster Robot's frontage (live chunk set), ground truth vs render:
| shop | keyed | in manifest | rendered |
|---|---|---|---|
| **Monster Robot Party #3962749** (`real`) | ✓ | ✓ | **marked** ✓ |
| Silky Oak Furnature #31 | ✓ | ✗ | **bare** ✓ |
| Thrift Shop and Eternity Boutique #908 | ✓ | ✗ | **bare** ✓ |
Two keyed shops stand metres from a stocked one wearing no mark, because they have no crate. **The street
advertises exactly the crates that exist.** (#31 is the same shop as Fable's R25 id-namespace bug — its
godverse id collides with a plan id; the mark reads `godverseShopId` only, so it is unaffected.)
Frontage: **135 draws / 74,354 tris**, 0 page errors, manifest prefetched at boot.
### Intel for F's #5 (no action needed from B)
The **`novelty_record` landmark** (B's furniture — a giant 0x1a1a1a vinyl disc on a post, marking record
shops) stands 2.5 m off Monster Robot's frontage and is **prominent in the head-on frame**. Measured
occlusion by ray-grid, so F can frame without guessing:
| band | head-on (8 m) | off-axis (+4 m) |
|---|---|---|
| **mark + sign** | **4%** blocked | 0% |
| window glass | 22% blocked | **44%** blocked |
**Shoot head-on.** The vinyl sits low (y≈1.3) and the mark rides high (y≈3.3), so it never meaningfully
occludes the mark; stepping off-axis *doubles* the window occlusion instead of helping. A giant vinyl record
in the frame of a record-shop money shot is a feature, not a blemish. *(I first read the black disc as a
render bug — it isn't; it's the landmark doing its job. Recorded so the next reader doesn't re-open it.)*
No code changed this round; notes-only.
---
## Round 26 (Fable's ROUND26 → Lane B) — #5 the polish pick: **the stocked shopfront reads from the footpath**
**Shipped. A stocked godverse shop now tells you it's diggable before you reach the door — a crate of

View File

@ -1,4 +1,11 @@
# LANE C — interiors contract (venues + real-town class mapping) → Lanes A/D/E/F
# LANE C — interiors contract (venues · real-town mapping · real stock) → Lanes A/D/E/F/G
> **v5.0-FROZEN · 2026-07-17 · the whole document.** Every section below is the shipped, measured state of
> Lane C's contracts across five epochs. Changes from here are version-bumped amendments, recorded in the
> marker list — the same discipline §0§5 have carried since v3.0. **Case law worth keeping:** three of this
> doc's amendments exist because a rule of mine met a case written *after* it (§7's naming vs the loader,
> §7.2a's `sku_` before mint existed) — the lane building to the rule took the red both times. When a line
> here meets a case it didn't foresee, **the line is the stale thing.** Amend it; don't bend the case.
> **v3.0-FROZEN** · 2026-07-16 · durable contract for Lane D + Lane F. §0§5 describe the *shipped*
> venue state; changes require a version bump (CITY_SPEC amendment law). Verified R13 (build) + R14 (audit).
@ -413,3 +420,45 @@ Fresh context, against G's merged `stock_godverse/3962749/` (120 real records):
- **Collision fixed:** shop pack (120 items) and town pack (350) coexist as distinct objects.
- **Fail-soft with a POPULATED cache:** missing base ⇒ `null` ⇒ parody (the R23 bug is gone).
- **Ceilings:** 98 draws with real covers via `opts.stockBase`; `pathOK`; 0 carves; 0 console errors.
---
## 8. Tier-2 live enrichment — the seam (v5.0, R27 ledger #2) → Lane F
**Ruling: bless the direct wire — no new lifecycle hook. But the `gone` *application form* is C's, and it's
landed in `dig.js`.** The question wasn't hook-vs-no-hook; it was *how* `gone` applies.
**1. Lifecycle — no hook needed.** F already holds `currentAdapter` (`interior_mode.js:67`) and passes it to
`dig.open()` **per dig** (`:111`) — a fresh read every time a crate is opened. When the live read lands, F
updates the pack; the next dig picks it up. If the read hasn't landed, hasn't finished, or failed, the
adapter is simply the tier-1 pack — **the dig never blocks on the network** (charter risk #1) and no C
lifecycle changes.
**2. The form — pick-then-omit, NEVER filter-the-array.**
`pack.gone` = a `Set`/array of ids the live read reports sold. C picks from the **full, stable `items[]`**
and omits gone ids **after** the draw.
- **Why (measured, R27, 5 crates):** filtering `items[]` first **reshuffles the crate** — every pick indexes
by position, so **one sale changed 21 OTHER records**. Pick-then-omit: **0 collateral**. A crate with 3
sold shows 13, not a different 16.
- **Both forms hide the sold record** — so **“the gone item never renders” cannot tell them apart.** → **F:
the sandbox proof (#4) must assert on *collateral* (the survivors are unchanged across a `gone` delta),
not just absence. The obvious assertion is vacuous here.** (Vacuous-gate law, third application — and the
cheapest one to get wrong, because it goes green.)
- **Proven in the real dig:** sold record gone · survivors **4/4** kept · **collateral 0** · `items[]`
untouched (120).
**3. F's invariant — `items[]` identity and order ARE the pick's stability.**
- **Do:** update item *fields* in place (live `price`) — free, no reshuffle; the card reads `it.price` at
pull time, so a live price shows on the next pull.
- **Never:** add, remove, or reorder `items[]`. That is what `pack.gone` exists for.
**4. The determinism boundary sits exactly here.** Server down ⇒ no `gone`, no live fields ⇒ the dig is
tier 1, deterministic and gate-able. Server up ⇒ the crate legitimately changes as records sell — tier 2 is
non-deterministic **by design** (charter law #2). So **no gate may assert dig determinism with the server
up**, and no live value may enter a seeded stream or a golden: `pack.gone` is consumed at pick time and
never persisted.
**5. The bin fan is decorative, not inventory.** The room's visible sleeves are built at `buildInterior` from
a *different* seeded pick than the dig's offers — a fan, not the crate's contents. A sold record's cover may
linger in the fan until the room rebuilds. If we ever want the fan to honour `gone`, **that** is when I'd
publish a post-build hook; it is not needed for the epoch's claim, because the crate you *riffle* is true.

View File

@ -1,5 +1,53 @@
# LANE E — cross-lane notes
## Round 27 — provenance v5-final, and the roster gap the docs freeze caught ⟨v5.0⟩
**1. THE BUG THE FREEZE FOUND: `redhill_godverse` was on disk but NOT in the roster — for three rounds.**
The v5 provenance table needed real numbers, so I measured instead of carrying the v4 row forward. The
count didn't match: **23 caches on disk, 22 rostered.** The missing one was **`redhill_godverse` — the
town the entire v5 epoch walks to.**
**Mine, squarely.** `index.json` is Lane E's file (`write_index()`). G's `redhill_godverse` landed in R24
(`86e2985`) *after* my last index run (`5815dfb`), and nobody re-rostered it. **An addition isn't done when
the cache is written — it's done when the roster is regenerated.** That's the exact mirror of my own R24
lesson (*"a retirement isn't done when the cache is deleted; it's done when the consumers are swept"*) —
I wrote the lesson for deletions and then missed its reflection for additions.
**Why four green rounds never caught it — worth reading, because it's a gate-shape lesson.** Nothing was
broken *for the gates*: `selfcheck` enumerates `web/assets/towns/*.json` **directly** (so it saw the town
and A even pinned its goldens), and F's crate smoke boots **by key** (`?town=redhill_godverse`). The
roster's *only* consumer is **B's HUD selector** (`hud.js`, my R20 ledger #2 design). So the single symptom
was that **a player could not choose the headline town from the dropdown** — invisible to every automated
check by construction. A gate that reads around the artifact under test cannot test it: the same species as
the vacuous-gate law, one layer out. **Fixed:** re-ran `write_index()` → **23 rostered, 23 on disk, 0
missing**; only `index.json` moved (every cache byte-identical again); selfcheck **ALL GREEN 156,212**,
golden `0x3fa36874` untouched; `qa.sh --strict --matrix` **7/0/0**.
**→ F: this matters for #5, the tour.** The money shot walks Musgrave Road into Monster Robot Party. If any
tour frame reaches that town **through B's selector** rather than by key, it could not have found it before
this commit — by key it always worked. Worth one glance at your framing path.
**→ B:** the dropdown now carries both godverse towns. Your `${t.state ? ', ' + t.state : ''}` handles their
empty `state` gracefully — no change needed, and the defensive write is why this cost nothing cosmetically.
**→ G:** your godverse caches carry no `state`, so they roster as `state:''`. Harmless today (B degrades
cleanly). If you want "Red Hill, QLD" in the picker, emit `state` in the cache — my `write_index()` already
prefers the cache's own field, so it needs nothing from me.
**2. Provenance v5-final (ledger #6).** The ⟨v5.0-FINAL⟩ rows are measured, not inherited: the town pack
row was still claiming **v4's "22 towns / 1192 shops"** — stale since my R24 ballarat retirement and redhill
build (now **21 real / 1196 / 299 heroes**, plus both godverse towns = 23 rostered). Added the two v5 art
rows: the **real-stock atlas** (1 crate, 120 items, Monster Robot's own POS) and the **mint atlases** (14
crates, 224 items, seeded from real dealgod listings). **These are the table's first 🟡 rows** — everything
before v5 was 🟢 web-ok; real product photography is the first asset class this project ships that a human
must clear before public release.
**3. The licence line's story, written down as a mechanism (ledger #6).** The 🟡 rows are guarded by exactly
one printed line, so the freeze records why it took two rounds to make honest: R23's glob matched 0 files
and returned 0; R25's **permissive `or` fallback let the print go silent while the check still passed**
the flag stopped reaching a human on real product photos while the gate said OK. Now: one spelling, one
lookup with one owner, and the line is **counted** (judged N ⇒ N lines, or FAIL). **Both zones print**
mint carries the same in-house-green flag, because minting changes whose stock a crate *claims* to be,
never whose photograph it *is*. Frozen today at **15 crates, 15 flags, every one in front of a human.**
## Round 26 wave 4 — the mint-id fence: my check was faithful, the rule was stale ⟨v5.0-beta⟩
**The red was mine and the fix is C's ruling.** My R25 slot-id check demanded `sku_` on every real-stock
@ -702,12 +750,50 @@ loader wiring (`spawnRig`/fleet). Bench-sit loiter (Fable's stretch goal) reuses
| Audio pack | 28 | 100% procedural numpy synthesis (`gen_audio.py`) | oscillator/noise render → ffmpeg OGG+M4A | 🟢 CC0-equiv, parody-safe |
| Stock packs | 3 (350/311/273) | GODVERSE `dealgod` Postgres — **real** cover art | `build_stock_pack.py`: parody-transform metadata + atlas | 🟢 real-art / parody-metadata |
| Ped clip **⟨v3.1⟩** | `sit.glb` (E R16; + pre-existing `walk`/`idle` from D/90sDJsim) | Adobe **Mixamo** `Sitting_Idle.fbx` | `fbx_to_clip.py` (Blender, mesh-strip) | 🟢 Mixamo royalty-free (motion only, mesh stripped) |
| Town pack **⟨v4.0-FINAL⟩** | **22 real AU towns, all 8 states/territories** — 1192 shops + real street geometry (`roads[]`) | **OpenStreetMap** via Overpass API — bounded per-town bbox; raw shops+roads snapshots committed in `_raw/` | `build_towns.py`: fetch → dedupe/suburb-fill/parody → C's §6 class map → 3× texture cap (drops counted) → v2 cache + roads | 🟢 **ODbL 1.0** (© OSM contributors; `web/assets/towns/SOURCES.md` + `license`/`attribution` on every cache) |
| Godverse town *(Lane G's, listed for completeness)* | `newtown_godverse` — 18 shops, rostered in E's `index.json` | GODVERSE/thriftgod shop census + OSM road geometry | `pipeline/godverse_town.py` (**Lane G owns this provenance**) | 🟢 ODbL 1.0 + GODVERSE census (G's own `license`/`attribution` fields) |
| Town pack **⟨v5.0-FINAL⟩** | **21 real AU towns, all 8 states/territories****1196 shops** (299 heroes) + real street geometry (`roads[]`) | **OpenStreetMap** via Overpass API — bounded per-town bbox; raw shops+roads snapshots committed in `_raw/` | `build_towns.py`: fetch → dedupe/suburb-fill/parody → C's §6 class map → 3× texture cap (drops counted) → v2 cache + roads | 🟢 **ODbL 1.0** (© OSM contributors; `web/assets/towns/SOURCES.md` + `license`/`attribution` on every cache) |
| Godverse towns *(Lane G's, listed for completeness)* **⟨v5.0-FINAL⟩** | `newtown_godverse` (72 shops) · **`redhill_godverse` (37)** — the town whose record shop is THE shop. Both rostered in E's `index.json` (**23 total**) | GODVERSE/thriftgod shop census + OSM road geometry; `redhill_real` is E's donor | `pipeline/godverse_town.py` (**Lane G owns this provenance**) | 🟢 ODbL 1.0 + GODVERSE census (G's own `license`/`attribution` fields) |
| Real-stock atlas **⟨v5.0-FINAL⟩** | **1 crate, 120 items**, 2×2048² webp — Monster Robot Party (`3962749`) | **the real POS** (`recordgod`) — crate 550 "DEEP HOUSE"; covers are **the shop's own product shots**, cached by dealgod | `pipeline/godverse_stock.py` (**Lane G owns**) — `snapshot`(DB-side, sha256-pinned) → `pack`(pure) | 🟡 **in-house-green — FLAG BEFORE ANY PUBLIC/COMMERCIAL RELEASE** · `sourcing:"real"` · ids `sku_<POS id>` |
| Mint atlases **⟨v5.0-FINAL⟩** | **14 crates, 224 items**, 14×1024² webp — keyed hero shops with **no POS** (5 record · 6 book · 3 toy across both godverse towns; the 6th record crate is the real one above) | **real dealgod listings** (the sellers' own product shots) — a *seeded sample*, `random.Random(shop_id)`, deterministic | `godverse_stock.py` mint baker (**Lane G owns**) — same bake path, sampled source | 🟡 **in-house-green — same flag** · `sourcing:"mint"` · ids `mint_<listing id>` · **never claimed as that shop's stock** |
**GLB fleet = 33 manifest (23 fit + 10 furniture) + 5 `_hi` = 38 published.** Only external spend ever:
~$0.64 (fal-Hunyuan, R5). Everything else is $0 / on-device (electricity only).
### ⟨v5.0-FINAL⟩ The licence line's story — why one printed line is a provenance mechanism
**The only 🟡 in this table is the v5 stock art, and the flag is the whole control.** Charter law #3 says
clearing photos for public release is **a human call** — so the gate does not pattern-match the licence
and rule on it. It **prints it**, every run, for every atlas, and a human decides. That makes one printed
line the mechanism the amber rows depend on, which is why it earned two rounds of work:
- **R23** — the gate globbed a name the runtime can never load: **0 files matched, returned 0**. It passed
identically whether atlases were absent, misnamed, or perfect. (The vacuous-gate law came from this.)
- **R25 — the line went silent and the gate still said OK.** Not a typo: the **check** read
`licence or license` (permissive) while the **print** read `licence` only. G's re-emit moved to US
`license`; the check passed, the print resolved to `{}`, and `FLAG BEFORE ANY PUBLIC/COMMERCIAL RELEASE`
**stopped reaching a human on a real shop's product photos**. *The fallback WAS the bug* — it let one
path stay permissive so it could not feel the other break.
- **The fix is structural, not a spelling.** One spelling, no fallback (British `licence` now ERRORS,
naming C's §7.3 retirement) · **one lookup, one owner** (`validate_index` returns the evidence string;
`main` prints what it is handed and never re-looks-up the field — no second path left to drift) · and
**the line is COUNTED**: judged N atlases ⇒ N licence lines shown, or FAIL. A gate that can say OK while
hiding the flag cannot exist by construction.
**Today, frozen:** `atlas-QA OK — 15 per-shop atlas(es) valid, 15 licence line(s) shown, 0 warning(s)`.
Fifteen crates, fifteen flags, every one in front of a human. **Both zones print** — the mint rows carry the
same in-house-green flag because a seeded sample of real dealgod listings is still **the sellers' own
product shots**. Minting changes whose stock it *claims* to be, never whose photograph it *is*: that is why
`sourcing` is honesty and the licence is unchanged by it.
### ⟨v5.0-FINAL⟩ The id-space fences — provenance carried in the id itself
Two disjoint namespaces, blessed as design (C §7.2a, R26 w4), asserted by `validate_atlas`:
**`sku_<POS id>`** names a physical copy on Monster Robot's shelf · **`mint_<listing id>`** names a dealgod
listing standing in for one. A crate wearing the other's prefix **FAILS**. This is not naming hygiene:
tier-2's sold-means-gone looks up POS skus, so a `mint_` id filed under `sku_` doesn't fail a gate — it
**sells a real record out of a real shop to satisfy a minted stand-in**. G refused that shortcut when it
would have gone green the easy way, and that judgment is the fence. *(Same standing note as
plan-ids vs godverse-ids: one name, one space — never compare across.)*
### Audio inventory — 28 assets (`web/assets/audio/`, committed; contract = `manifest.audio`)
All procedural, seeded, $0, parody-safe; OGG/Opus + M4A/AAC; beds+music loudnorm 16 LUFS, SFX peak-norm.

View File

@ -81,3 +81,52 @@ plus this epoch's own:
5. **Settled non-risks:** the dig/wallet/stockAdapter machinery (shipped v2, soaked 16 rounds);
static-atlas determinism (E's byte-identical pipeline discipline); the fail-soft ladder
(proven pattern from `?noassets` day one).
### Retired-or-carried at the v5.0 close (ROUND27 ledger #6 — Lane A, line by line)
Verdicts against what shipped, not against the plan. Written in **wave 2**, so tier-2 items that F's reader
and kill-the-server gate will decide are **carried, not pre-retired** — the gate retires them, not this doc.
1. **CARRIED — and it is this round's defining gate, not a footnote.** The only risk here the epoch has not
yet measured. The reader (#2) and the kill-the-server gate (#3) don't exist as I write this; **nothing
retires this line except that gate going green** — boot server-up (live fields consumed), kill mid-session
(degrade to tier 1, zero console errors, frame-time measured across the failure), boot server-down (pure
tier 1). Until then the charter's offline law is a promise, and the whole point of #3 is that a promise
isn't a gate. *(If tier 2 proves bigger than one round, F holds and this line simply carries — four holds
have proven nobody forces an epoch close.)*
2. **RETIRED — the remedy the risk named is the remedy that shipped.** It predicted "pagination/crate
rotation rather than bigger atlases", and that is exactly what happened: the mecca case is served by the
**crate-rotation menu**, not by growing the sheet. Measured across all **15 keyed atlases**: max **2
atlases/shop** against C's hard cap of 2; the 14 mint shops sit at **16 items / 1 atlas / 1024²** and only
the one real POS shop earns 2048² (**120 items, 2 atlases, 1.2 MB**) under C §7.2's 60+ item rule. E's
validator enforces the ceilings from the committed files alone. Total: 344 items.
3. **RETIRED — the risk was designed out, not solved.** It assumed a **fuzzy-matching** problem ("names
drift, shops move") and demanded mismatches be counted. There is no matcher: identity is an **explicit
opaque key** (`godverseShopId`) that G emits, so there is nothing to mis-match. The predicted failure
couldn't occur in the predicted form — and the case that would have broken any name-matcher proves it:
**Monster Robot Party is in neither the census nor OSM**, so no matching strategy could ever have found
it; it is keyed by injection. Mis-stocking is fenced structurally instead: duplicate ids **error**,
uniqueness runs over defined ids, and the **orphaned-atlas gate** fails if a stocked shop isn't seated.
4. **SPLIT — the write half RETIRED by ruling, the read half CARRIED to F's #4.** *Write-back:* retired, and
retired properly — John's **A-then-B** ruling is recorded as **mechanism, not config**: `/reserve` and
`/buy` are **ABSENT, not stubbed**. Race conditions and reservation semantics cannot occur because the
verbs do not exist; "prove the absence" is a stronger guarantee than any sandbox flag, since a sandbox a
config typo can flip isn't a sandbox. *(Option B's reservation upgrade is chartered as v5.x, behind a
hold budget — it re-opens this line deliberately, when it is built.)* *Read (`gone`, real→game):* carried
— F's #4 asserts both directions this round.
5. **Unchanged, and extended by what the epoch settled:** static-atlas determinism proved in practice
(byte-identical re-bakes, including G's index-only sku re-emit that moved no `.webp` bytes); the
fail-soft ladder proved on every rung; and the **id-space fences** (`sku_`/`mint_`, plan-id vs
godverse-id) now have case law behind them rather than good intentions.
6. **NEW, CARRIED — the orphaned atlas.** The lift legitimately drops shops (overlap-resolve, counted since
R19); dropping one that carries a **committed atlas** means real stock keyed to a shop that isn't in the
town. Gated in `selfcheck` since R25 and **currently green** — and the gate armed itself this round
without anyone touching it: it SKIPped on `newtown_godverse` while no atlas keyed there, and **began
binding the moment G's mint atlases landed**. Live near-miss on the books: `redhill_godverse`'s lift drops
keyed shop **2286 "Empire Revival"**; G stocked its numeric neighbour **2287** instead. Carried because
the hazard grows with every shop stocked — **stocking 2286 would orphan it today.**
7. **NEW, CARRIED — "real" is one shop wide.** 14 of 15 keyed atlases are **mint**; exactly one is a POS
snapshot, because exactly one shop has a POS. That is honest and marked (`sourcing`), not a defect — but
the epoch's headline claim rests on **one** shop, and any future claim of scale needs either more POS
shops (v5.x: "if monsterrobot ever gets siblings") or continued care that `sourcing` stays legible to
humans, not just gates.

302
pipeline/godverse_server.py Normal file
View File

@ -0,0 +1,302 @@
#!/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)

View File

@ -20,9 +20,17 @@ the "© OpenStreetMap contributors (ODbL)" credit visible.
**`index.json` is the machine-readable roster** (key/town/state/shops/roads) — read it rather than
hardcoding a list. Every cache carries the same terms; per-town numbers live in each cache's `counts`.
**The pack: 21 real towns · 1196 shops · 299 heroes** (+ Lane G's `newtown_godverse` = 22 rostered in
`index.json`), every one fetched from OSM Overpass with a bounded per-town bbox, ODbL 1.0, raw snapshots
in `_raw/` (shops + roads per town), fetched 2026-07-16 (`redhill_real` 2026-07-17).
**The pack: 21 real towns · 1196 shops · 299 heroes** (+ Lane G's `newtown_godverse` and `redhill_godverse`
= **23 rostered** in `index.json`), every one fetched from OSM Overpass with a bounded per-town bbox, ODbL
1.0, raw snapshots in `_raw/` (shops + roads per town), fetched 2026-07-16 (`redhill_real` 2026-07-17).
> **⟨v5.0-FINAL⟩ roster fix (R27):** `redhill_godverse` — the town the whole v5 epoch walks to — was **on
> disk but missing from `index.json` for three rounds**. G's cache landed *after* my last `write_index()`
> run and nobody re-rostered it. Every gate stayed green because `selfcheck` reads the directory directly
> and F's smoke boots the town **by key**; only B's HUD selector reads this roster, so the sole symptom was
> that **a player could not choose it from the dropdown**. Mine to own: the index is Lane E's, and an
> addition isn't done when the cache is written — it's done when the roster is regenerated. (The mirror of
> R24's lesson, where a *retirement* wasn't done until the consumers were swept.) See LANE_E_NOTES R27.
| state | towns |
|---|---|

View File

@ -144,7 +144,7 @@
"key": "newtown_godverse",
"town": "Newtown",
"state": "",
"shops": 18,
"shops": 72,
"roads": true,
"source": "godverse+osm"
},
@ -164,6 +164,14 @@
"roads": true,
"source": "osm"
},
{
"key": "redhill_godverse",
"town": "Red Hill",
"state": "",
"shops": 37,
"roads": true,
"source": "godverse+osm"
},
{
"key": "redhill_real",
"town": "Red Hill",

View File

@ -286,10 +286,21 @@ export function createDig(THREE, renderer) {
document.addEventListener('keydown', onKey, true);
// ?stock=real: seeded pick of real pack items (same bin seed → same crate contents).
// TIER-2 SEAM (v5.0): `pack.gone` = ids the live read says sold IN THE REAL SHOP. We pick from the
// FULL, stable `pack.items` and omit gone ids AFTER the draw — never from a filtered array.
// Measured (R27, 5 crates): filtering the array first reshuffles the crate — one sale changed 21 OTHER
// records, because every pick indexes by position. Pick-then-omit: 0 collateral, the sold record simply
// leaves and the survivors keep their slots (a crate with 3 sold shows 13, not a different 16).
// Both forms hide the sold record, so "the gone item never renders" cannot tell them apart — the
// collateral is what a gate must assert on. F: update item FIELDS in place (live price) freely; do NOT
// add, remove, or reorder `pack.items` — that identity/order IS the seeded pick's stability.
function pickRealOffers(pack, seed, n) {
const rr = mulberry32(seed >>> 0), out = [];
const gone = pack.gone instanceof Set ? pack.gone
: (Array.isArray(pack.gone) ? new Set(pack.gone) : null);
for (let i = 0; i < n; i++) {
const it = pack.items[(rr() * pack.items.length) | 0];
if (gone && gone.has(it.id)) continue; // sold in the real shop → this slot is simply empty
// Real fields straight off the atlas index (§7.2c): condition/sleeve_cond are OPTIONAL — present on
// POS-sourced items, absent on mint ones that had no genuine grade. `y` (year) is never in the index,
// so it stays empty and the card simply omits it. No invented values: an unstated grade stays unstated.