Compare commits
5 Commits
2cc6c32c73
...
bee7563a76
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bee7563a76 | ||
|
|
cedc5872fb | ||
|
|
6960bdd279 | ||
|
|
01f398960b | ||
|
|
839575b28d |
@ -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
|
||||
|
||||
@ -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** (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 (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):**
|
||||
|
||||
@ -1,11 +1,42 @@
|
||||
# LANE A — CITYGEN · progress (PROCITY-A)
|
||||
|
||||
*Status: **all deliverables landed; self-check ALL GREEN (156,211/156,211); rounds 2–25 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 2–27 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.
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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** (400–800/month, Dec 2025 → Jun 2026) but **26 in July and none
|
||||
> since 2026-07-01**; the POS hasn't been *written* since **2026-07-09** (the 07-16 dump is fresh;
|
||||
> its data is stale). And **0 of crate 550's 120 records have ever sold**. So against the real POS
|
||||
> `gone` is `[]`, permanently, until the shop trades again.
|
||||
>
|
||||
> **Therefore, per the vacuous-gate law: F's sold-means-gone arm must not assert over the live POS
|
||||
> — it would pass without touching its subject.** Two honest routes, and G built the second:
|
||||
> 1. Assert `gone == []` against the real POS and **print the reason** — true, but it proves only
|
||||
> that nothing sold.
|
||||
> 2. **Point the server at a labelled fixture** (`godverse_server.py make-fixture --sold 3` →
|
||||
> `recordgod_fixture`, a `createdb -T` clone with N of the crate's own real skus marked sold).
|
||||
> Verified: `counts {baked:120, live:117, gone:3}`, the three ids absent from `items[]`. This
|
||||
> proves the **mechanism** — computed → sent → consumed → sleeve vanishes — deterministically,
|
||||
> which is what a gate is for. Production still reads the real POS, untouched.
|
||||
>
|
||||
> **A fixture is not a lie; a green assert over an absent subject is.** The epoch has held four
|
||||
> tags on exactly that distinction.
|
||||
- **`etag`/`since`** so a re-enter is a 304, not a payload.
|
||||
- Shop id is the **godverse id** (§1a), never a POS internal id. **R24 naming, as landed:** the
|
||||
town cache carries `shop.godverseShopId` (A's schema field, camelCase, read by the JS runtime);
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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
302
pipeline/godverse_server.py
Normal 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)
|
||||
@ -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 |
|
||||
|---|---|
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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.
|
||||
|
||||
Loading…
Reference in New Issue
Block a user