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>
34 KiB
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). v3.1 amendment: §3 became the
stage.backline[]contract (ledger #5 — the amp unification). v4.0-beta amendment (2026-07-16): §6 added — the real-town shop-class → archetype mapping (ledger #2, the density widening; C→E handshake). The frozen venue contract (§0–§5) is unchanged. v4.0 amendment (2026-07-16): §7 added — the G2a per-shop stock-atlas contract (C's interior draw/tex ceilings, answering Lane G + E's review). §0–§6 unchanged. v5.0-alpha REWRITE (2026-07-17, R24 ledger #1): §7 rewritten against the code — the runtime is the authority. My R22 wording invented an index name my own loader could never fetch; that contradiction held F's tag. §7 now states whatstockpack.jsactually implements, extended per-shop, and thetype+basecache keying is landed. G's R23 atlas layout is ratified (G re-emits provenance field names only, §7.3). §0–§6 unchanged. v5.0-alpha amendment (2026-07-17, R25 ledger #3): §7.2a added — slot ids must be sku-derived, unique across packs and stable across re-emits (F's id-collision finding, made structural). Index-only; no atlas bytes, no goldens. §0–§6 unchanged. v5.0-beta amendment (2026-07-17, R26 ledger #1/#4/#6): §7.2bsourcing(real|mint — NOTtier, which is already the ladder rung), §7.2cconditionfor the price card, §7.2d the atlas manifest (existence declared, never probed — kills the 404 attribution exception). All index/manifest-only: no atlas bytes, no town caches, no goldens. §0–§6 unchanged. v5.0-beta amendment (2026-07-17, R26 wave 4): §7.2a's id form is now SOURCING-SCOPED —sku_<POS id>for real,mint_<listing id>for mint. Prefixes are namespace fences, not decoration: my R25 line was written before mint existed and read as one space where there are two. Doc-only.
The district ships three venue archetypes behind ?gigs=1 — pub, band_room, rsl. Lane A
converts a chosen shop to a venue kind in place (shop.type = kind); C keys the interior recipe off
shop.type. All three return the SAME shape (below), so Lane D drops rigs in and Lane F reads state with
no per-kind branching. Non-venue rooms are byte-identical (venue code runs only for recipe.venue, on its
own seed sub-streams). Verified fresh (seeds 20261990/1990/7/42/101 + 90-build soak): 0 throws · 0 carves ·
0 path-fails · 0 determinism-fails · leak geo0/tex0 · worst build 8.3 ms. Budgets in §3.
0. The pose ry convention (pinned) — READ THIS FIRST
Canonical convention (every C pose uses it): a pose's ry is the yaw of a thing whose front /
outward normal is local −Z at ry=0, i.e. world-facing (−sin ry, −cos ry). counter.stand,
browsePoints, watchPoints and bandPoses ALL follow it, and the box placeholder (placeholder.js) is
built to it (toes at −Z). keepers.js greet math assumes it. Drop a rig in at a pose's ry and it faces
the right way — no per-consumer flip.
Why no flip (the R13 fix, for the record): the GLB fleet meshes ship facing local +Z at ry=0 — the
opposite of the −Z convention (verified: the anterior toe-bone vector toe − foot reads (≈0, +0.99)).
Before R13 this was compensated per-consumer, inconsistently: band.js flipped by π (band/crowd correct,
placeholders backward) while keepers.js did not (placeholders correct, keepers + browsers 180° backward
whenever the GLB fleet was loaded — they'd been shipping back-to-front). D fixed it at the source in
rigs.js buildFigure: the cloned inner is rotated π about Y so every GLB rig faces −Z, matching the
placeholder and every C pose. All consumer flips were then deleted. Net: keepers, browsers, band and crowd
all face correctly, GLB and placeholder modes are consistent, and C's pure-data poses never moved (no golden
touched). If you add a new rig consumer, plant it at the pose ry with no flip.
1. What buildInterior returns for a venue (shop.type ∈ pub | band_room | rsl)
Everything below is pure data on the return (room-local coords, the ry convention in §0). Lane D drops rigs straight in; Lane F reads state — no new lifecycle vs. the R9 keeper/browser seam.
room.stage — the band stage, a 4-piece (null for non-venues)
room.stage = {
x, z, // deck centre (against the back wall)
w, d, // deck size (m) — varies by kind (table below)
deckY, // deck top surface height (m) — the front line stands here
riserY, // drum riser top height (m) = deckY + 0.16 — the drummer sits here
frontZ, // z of the deck lip (the crowd stands in front of this, +Z)
bandPoses: [ { x, z, ry, role, y, seated? } ×4 ],
}
bandPoses[0..2]= the front line:role∈guitar/vocal/bass, across the deck lip,y = deckY.bandPoses[3]= the drummer:role: 'drums',seated: true, up-stage centre on the drum riser,y = riserY(=deckY + 0.16, thefittings.jsstage riser box).- Lane D: lift each member to its own
pose.y(front line =deckY, drummer =riserY).rolehints the instrument — E's GLBs viaopts.instrumentFor(role)(electric_guitar/bass_guitar/mic_stand/drum_kit), primitives otherwise (asset law).seated:true→ seated bob.ry = πalready faces the audience (+Z) — no flip (§0). - The band is a 4-piece for v3.0 (front-line trio + drummer). Reverting to a trio would be a version bump
(drop
bandPoses[3]), not a runtime toggle.
room.watchPoints — the audience, cap by kind (empty [] for non-venues)
room.watchPoints = [ { x, z, ry, dance, slotIndex } ] // seeded floor poses, all facing the stage centre
- Crowd cap by kind:
pub8 ·band_room8 ·rsl12 (the RSL is the crowd-cap stress case). The room fills what geometry allows up to the cap; the count IS the crowd cap (F asserts crowd ≤watchPoints.length). danceseeded ~⅓ true ("a bit of both"). D:dance:false→ stand-and-watch idle;dance:true→ bob/sway with a seeded phase offset (useslotIndex/coords). Every point walkable, reachable, clear of the keeper stand + browse points + stage/PA. Deterministic pershop.seed.
room.counter / keeper — unchanged
The bar is a normal counter; the barkeep is just a keeper at room.counter.stand (R9 seam). Faces the
customer correctly under §0.
room.audio.gigKey — the live bed
room.audio = { musicKey, toneKey, gigKey? }. The manifest key is the gigKey, canonical form
gig-<genreKey> — no mapping table. F passes gig state via opts.gig:
buildInterior(rslShop, THREE, { gig: { on: true } }) // → room.audio.gigKey === 'gig-covers'
- Genre resolves
opts.gig.genreKey→ the venue's ownshop.genreKey(A sets it) → a kind default (pub→pubrock,band_room→grunge,rsl→covers). Present only when a gig is on. - Prefer
gigKeyovermusicKeywhile the gig plays. E's beds aregig-pubrock/gig-grunge/gig-covers; a missing key → silence (audio law), band still visible. - C hand-builds
'gig-'+genreKey(byte-identical to citygen'sgigKeyFor, selfcheck-asserted) rather than importing it — deliberately, to keep the interiors lib standalone (it never imports citygen).
The venue table (C-owned tuning)
| kind | archetype bias | ceiling | deck w × d, deckY |
crowd cap | genre → gigKey | dressing |
|---|---|---|---|---|---|---|
pub |
wide / hall | 3.6 m | 0.58·W × 2.0, 0.32 |
8 | pubrock → gig-pubrock |
pub tables, sticky carpet, bar |
band_room |
cosy / pokey / wide | 3.4 m (low) | 0.62·W × 1.6, 0.20 (barely a riser) |
8 | grunge → gig-grunge |
near-empty standing room, tin-shed dingy |
rsl |
hall / wide | 4.0 m (tall) | 0.50·W × 2.4, 0.40 |
12 | covers → gig-covers |
flock walls, club carpet, bistro tables, members' bar |
2. Quiet-night variant
No opts.gig → a normal interior: stage + watchPoints are still returned as data, but there's no
gigKey (room-tone + maybe the seeded radio musicKey), and D spawns no crowd. A seeded dark night reads
true — the venue is just a quiet pub/club.
3. stage.backline[] — the amp slots (v3.1, ledger #5)
room.stage.backline = [ { x, z, ry, y } ×2 ] — seeded up-stage amp/spare-cab poses, pure data (C plants
no amp meshes as of v3.1). This is the unified backline: Lane D plants E's guitar_amp GLB here (its own
primitive fallback under ?noassets) — one amp system, GLB when present. C's old primitive ampStacks are
dropped from all three venue recipes.
- slot[0] = stage-right (the primary amp) at
(stage.x + stage.w·0.36, deckY, stage.z − stage.d·0.22)— identical to D's R15-verified plant, so wiring D's amp tobackline[0]moves nothing on screen. slot[1] = stage-left spare cab (mirror).ry = πfaces the audience (+Z, the §0 convention);y = deckY. - Guarantees (verified fresh, seed 20261990, pub/band_room/rsl): every slot is up-stage of the front
line, flanks the drum riser, clear of all 4 bandPoses (nearest-bass clearance 0.87 m pub / 0.70 m
band_room / 0.92 m rsl) and all watchPoints (≥1.95 m), and on-deck (within the deck footprint).
Deterministic per
shop.seed. Lane D: verify no interpenetration once wired (C's R15 clearance discipline). - Draw impact: dropping C's primitives shaved ~4 draws off each empty venue shell (drawSweep glb-off:
pub 122 · band_room 120 · rsl 131 ≤350); D's GLB/primitive at
backline[0]adds it back on the gig night. - v3.2 (parked): the drummer sit clip, instrument LOD, and C's
audioEmitteridea.
4. Budgets (measured R14, seed 20261990, fresh context)
- Empty shells (
drawSweepglb-off): band_room 124 · pub 126 · rsl 135 — town-wide worst 161 (opshop/hall). ≤ 350. ✓ (identical to R13 — C geometry unchanged). - Gig-night interior draws, D's
GigCrewwith the real instrument GLBs + amp (localdepot): pub 60 · band_room 52 · rsl 71. Asset-free (placeholder crowd, no GLBs) worst RSL 237 (16 rigs). ≤ 350. ✓ - Tris: fleet peds ~2.7k each; E's instrument GLBs 14k each (decimated to the ≤15k target), 5 on a stage = 70k — the instruments dominate. RSL gig-night ≈ 182k tris once the async GLBs settle. The interior gate is draws (passes); tris are the street budget (200k), so this is not an interior gate. Note for E/F: F's gig-night smoke reports ~34k because it measures before the async instrument GLBs load; the settled scene is ~180k. A camera-distance LOD on instruments is a v3.1 candidate.
- Determinism / release-law: C's interior code is unchanged since R13; D's continuity + RY-fix ride
disjoint rng namespaces (
ctx.stream(salt)vsrng(citySeed,kind,id)) and cannot perturb C's placement or any golden. 90-build venue soak: 0 determinism · 0 carve · 0 path-fail · worst 8.3 ms; leak geo0/tex0.
5. The archetypes (C-owned)
band_room = a tin shed: small (cosy/pokey), low ceiling, barely-a-riser deck, near-empty standing floor,
bar in the corner — the PA louder than the room. rsl = carpet-and-flock club: the biggest floor in town
(hall/wide), a proper raised stage at one end, bistro tables round the edge, members' bar, the biggest crowd
cap. Both reuse the pub venue machinery (theme.js venueSpec + layout.js placeStage/placeWatchPoints);
only the dressing + venueSpec differ. Stage/PA keep GLB slots for Lane E (primitive fallback holds); the
backline amp is D's, planted at stage.backline[0] (see §3).
6. Real-town shop-class → interior archetype mapping (v4.0-beta, ledger #2) → Lane E
Directive (John, kickoff — SUBTLE: texture, not takeover). The secondhand shops stay the heroes of the town (they are the game). The widening adds general retail as texture — the everyday main-street staples a real Australian town has — mapped onto existing archetypes (no new archetypes this round). The player should feel the town's more alive, not that the game changed.
How it's applied (E owns the emission). plan_osm keeps only registry types (SHOP_TYPES — unknowns
flatten to opshop). So E maps each fetched OSM shop=* / amenity=* value to a registry type below and
bakes it into the cache's shop.type. Every target is an existing type, so C's recipes, A's registry, and the
budgets are all unchanged. Unknown/unlisted classes degrade safely to opshop (R18-proven: 18 real OSM types
all built valid).
Heroes — secondhand (unchanged; NEVER subsampled):
| OSM class | → type |
|---|---|
charity, second_hand, antiques, vintage |
opshop |
books, bookshop |
book |
music |
record |
video, games |
video |
pawnbroker |
pawn |
toys |
toy |
Widened — everyday main-street texture (general retail):
| OSM class(es) | → type | wears it as |
|---|---|---|
bakery, cafe†, confectionery, pastry, chocolate, deli/delicatessen, butcher, cheese, coffee, tea |
milkbar |
the food counter |
convenience, kiosk, general, dairy |
milkbar |
the corner store |
newsagent, stationery |
milkbar |
papers + magazines |
chemist, pharmacy, cosmetics, perfumery |
milkbar |
dispensary counter |
clothes, fashion, boutique, shoes, bag, jewelry/jewellery |
dept |
the dressed retail floor |
department_store, variety_store, supermarket |
dept |
big general retail |
hardware, doityourself, trade, electronics, mobile_phone, furniture, houseware/homeware |
dept |
shelved goods |
greengrocer, florist, farm, garden_centre |
stall |
produce/plants on trestles |
† cafe/fast_food are amenity=*, not shop=* — fetch them from the amenity tag; everything else is shop=*.
Fallback: any fetched class not listed → opshop.
Copy-paste map for E (build_towns.py):
{
"charity":"opshop","second_hand":"opshop","antiques":"opshop","vintage":"opshop",
"books":"book","bookshop":"book","music":"record","video":"video","games":"video",
"pawnbroker":"pawn","toys":"toy",
"bakery":"milkbar","cafe":"milkbar","confectionery":"milkbar","pastry":"milkbar","chocolate":"milkbar",
"deli":"milkbar","delicatessen":"milkbar","butcher":"milkbar","cheese":"milkbar","coffee":"milkbar","tea":"milkbar",
"convenience":"milkbar","kiosk":"milkbar","general":"milkbar","dairy":"milkbar",
"newsagent":"milkbar","stationery":"milkbar",
"chemist":"milkbar","pharmacy":"milkbar","cosmetics":"milkbar","perfumery":"milkbar",
"clothes":"dept","fashion":"dept","boutique":"dept","shoes":"dept","bag":"dept","jewelry":"dept","jewellery":"dept",
"department_store":"dept","variety_store":"dept","supermarket":"dept",
"hardware":"dept","doityourself":"dept","trade":"dept","electronics":"dept","mobile_phone":"dept",
"furniture":"dept","houseware":"dept","homeware":"dept",
"greengrocer":"stall","florist":"stall","farm":"stall","garden_centre":"stall"
}
Default (not in the map) → opshop.
Subtlety guardrails (E):
- Fetch only these tasteful staples — not every OSM
shop=*. Skip vehicle/tyres/fuel, funeral, storage, industrial, and pure services the player never browses (hairdresser,beauty,travel_agency,bank,laundry,estate_agent). - Never subsample the secondhand heroes.
- If a town's general-retail count would visually drown the secondhand strip, seed-subsample the texture
classes (the milkbar/dept/stall targets) toward ~2–3× the secondhand count, and count the drops in
norm.droppedlike every other drop.
Why these targets: milkbar (counter + fridge + magazine rack + glass case) is the canonical Australian
counter shop — it wears cafe/bakery/deli/newsagent/chemist naturally; dept (cube shelves + clothes racks +
glass cases) wears clothes + hardware + big retail; stall (trestles + crates) wears produce + florist. Three
general-retail looks + the six secondhand heroes + the three venues = variety with no new archetypes.
Pre-verified (seed 20261990, real-town lot 8.5×14): milkbar/dept/stall/opshop all build valid (0 carves,
≤143 draws); dept reads as a clean dressed-retail floor (cube shelves + clothes racks + counter, the
escalator does not dominate the ~7.9×11.7 room), so clothes/hardware → dept is confirmed. C re-checks
the full result in the five-town door-soak (ledger #6) once E's caches land. (FYI: C's theme.js canonicalType/ALIAS already recognizes several
of these — deli/corner_store→milkbar etc. — for any direct buildInterior(rawClass); the authoritative
mapping for the caches is this table.)
7. Real-stock packs — THE RUNTIME IS THE AUTHORITY (v5.0-alpha, R24 ledger #1) → Lanes E/F/G
This section was the root cause of R23's held tag and has been rewritten against the code. My R22 wording invented an index name (
stock_shop_<godverseShopId>_index.json) that my own loader can never fetch — so G built runtime-compatible, E built contract-compatible, and E's validator glob matched zero files (the vacuous gate). Nobody was wrong; the contract was. Rule going forward:web/js/interiors/stockpack.jsis the contract. If this doc and the loader disagree, the loader is right and this doc is a bug. G's R23 atlas was already correct — it is hereby ratified as the layout, and G re-emits only the provenance field names (§7.3).
7.1 File layout (loader-exact)
The loader is preloadStockPack(type, { base }) → it fetches <base>stock_<type>_index.json.
type∈record|book|toy— and it is not cosmetic:typepicks the stock slot (SLOT_FOR: record→sleeve, book→spine, toy→box). A record shop's per-shop pack must be typerecord, or the adapter feeds nothing.- Town-wide packs (unchanged):
assets/models/stock_<type>_index.json. - Per-shop packs (v5):
assets/stock_godverse/<godverseShopId>/stock_<type>_index.json— the base is the per-shop directory; the filename is the same. (This is exactly G's R23 layout.) - RETIRED:
stock_shop_<id>_index.json— never loadable; do not emit, do not glob for it. → Lane E:validate_atlas.py's glob must becomestock_godverse/**/stock_*_index.json(the currentstock_shop_*_index.jsonis why it passed on 0 files).
7.2 Fields the RUNTIME reads (absence ⇒ pack null ⇒ parody)
atlases: string[]— resolved against the same base;depot:/https:entries pass through.items: [...], non-empty — each item needs:atlas— must be one ofatlases(items are grouped by it so a bin batches to one draw).uv: [u0,v0,u1,v1], origin TOP-LEFT — the loader flips V itself (uvRect). Do not pre-flip.
- Read by the dig/buy consumers (not by the loader):
id,title,artist,price,price_band.
7.2a Slot ids — unique across packs, stable across re-emits (v5.0-alpha, R25 ledger #3)
Rule: items[].id MUST derive from the source's own stable key — never from position — and MUST be
unique across ALL packs. The shipped generic packs keep rec_NNNN as their namespace — a real-stock
pack must not emit into it.
AMENDED v5.0-beta (R26 wave 4) — the id form is SOURCING-SCOPED, and the prefix IS a namespace fence. R25's line said "
sku_-prefixed for real-stock packs". I wrote that before mint existed, so it reads as one space when there are two. E implemented my rule faithfully; the rule was the stale thing, not E's check. The form now follows §7.2b'ssourcing:
sourcingid form the space it names "real"sku_<POS id>(e.g.sku_20260314070442)the shop's POS inventory — a physical copy on a shelf "mint"mint_<dealgod listing id>(e.g.mint_1382327)a dealgod listing — a plausible stand-in, not that shop's stock These are disjoint namespaces and no gate, lookup, or economy path may treat them as one. G refused to file dealgod listing ids under
sku_to go green the easy way, and that judgment is the rule now: tier-2's sold-means-gone looks up POS skus. R24's id collision entered the wrong shop; the same collision in the money layer sells the wrong record — a real one, out of a real shop, to satisfy a minted stand-in. A prefix that lies costs money, not a gate. (Same standing note as plan-ids vs godverse-ids: one name, one space — never compare across.)
Why — two reasons, both measured:
- Positional ids collide by construction. Both packs number from zero, so all 120 godverse ids are also generic-pack ids — 120/120 overlap (measured R25). An id-equality gate therefore cannot discriminate which pack a rendered item came from: that is what made the brief's own #7a spec vacuous. F caught it and replaced it with title-sets + texture-URL provenance (now the canonical honest-gate pattern for stock). Sku ids make id-equality discriminating again — permanently, and while there are two atlases to migrate instead of fifty.
- Position is not identity — and this one outlives the gate. A positional id renumbers on every
re-bake: sell one record out of crate 550 and
rec_0050silently becomes a different record. Anything holding an id across snapshots — a wallet purchase, a saved dig, a gate assertion, and above all tier-2's sold-means-gone — would then point at the wrong item. The POS sku is stable precisely because it is the shop's own identity for that record: the same principle that makesgodverseShopIdthe POS shop id. (release_ididentifies the pressing, not the copy — the sku is the copy.)
Scope: index-only. id is not read by the loader (§7.3) — the dig/buy consumers and the gates read it.
No atlas bytes, no town cache, no golden moves.
→ Lane E (amended R26 w4): assert the form against sourcing (you already read it at line 178) —
id present · unique within the pack · and the prefix matching its space: sourcing:"real" ⇒ sku_,
sourcing:"mint" ⇒ mint_. A crate wearing the other's prefix FAILS — that's the fence doing its job,
and it's a stronger check than the one it replaces, not a looser one.
7.2b sourcing — real vs mint, and why it is NOT tier (v5.0-beta, R26 ledger #1)
The beta needs every keyed crate marked as actual POS inventory or seeded plausibility, so no gate,
doc or human mistakes one for the other. The brief called that field tier. It can't be tier: that name
is already taken, twice over.
- G's atlases already emit
tier: 1= the charter's ladder rung (0 parody · 1 static-real · 2 live GODVERSE). A mint atlas is also rung 1 — same mechanism (a statically baked per-shop atlas); only the data's origin differs. Andkindis already the stock type (record). - So real-vs-mint is an orthogonal axis, not a rung. Overloading
tierwould put two meanings in one name — the same species as the plan-id/godverse-id collision now standing-noted, and as the positional ids in §7.2a. One name, one meaning.
Rule: top-level sourcing: "real" | "mint", REQUIRED on every real-stock atlas index. tier stays the
numeric ladder rung, unchanged.
"real"— baked from that shop's own POS snapshot (Monster Robot Party). The index carries itssnapshotprovenance (§7.3)."mint"— a deterministic seeded sample (random.Random(shop_id)) standing in for a keyed shop with no POS. Plausible, never claimed as that shop's stock. A mint index MUST NOT carry POS-snapshot fields (the separation IS the honesty — E asserts both directions).- Gates read
sourcing(vacuous-gate law: an index with nosourcingFAILS). A mint crate can never report as real. The dig card's treatment is C's call — see §7.2c.
7.2c Price-card fields — condition (v5.0-beta, R26 ledger #4)
The dig card is a record-shop price sticker, and a sticker's fields are artist / title / condition /
price. condition is not in the index today — G's snapshot carries condition + sleeve_cond per item
and drops them at emit, so pickRealOffers currently maps cond: '' and the card renders dangling
separators around a hole. That's the "database row" the ledger wants gone.
Rule: condition (and optionally sleeve_cond) are OPTIONAL index item fields; emit them when the source
has them. Index-only; the loader never reads them (§7.3). C's card renders whatever is present and omits
what isn't — a missing field must never leave a stray separator. Mint items: emit condition only if the
sampled listing genuinely carries one; a plausible-but-invented grade is exactly the kind of fiction
sourcing exists to prevent.
7.2d The atlas manifest — existence is declared, never probed (v5.0-beta, R26 ledger #6)
Fail-soft-by-404 collides with the zero-console-errors law: the runtime probes per-shop atlases blind and
eats the 404 noise (8 in F's R24 gate run), tolerated by attribution — a workaround, not a design.
Rule: web/assets/stock_godverse/index.json is the authority for which shops carry atlases. No entry ⇒ no
fetch. Shape:
{ "version": 1,
"shops": [ { "godverseShopId": 3962749, "types": ["record"], "sourcing": "real" },
{ "godverseShopId": 31, "types": ["record"], "sourcing": "mint" } ] }
godverseShopIdis a number in the GODVERSE namespace — never compare it to a plan id (standing note).- → F: consult the manifest before fetching; no entry ⇒ no probe ⇒ zero 404s, no attribution exception. A shop in the manifest whose fetch still fails keeps the existing fail-soft → parody.
- → G: emit it alongside the atlases (mint entries included).
- → E: manifest↔files consistency both ways — an atlas on disk absent from the manifest FAILS, and a manifest entry with no atlas FAILS.
- Manifest absent entirely (a tree with no godverse atlases): that is a legitimate zero-shop state — the wire probes nothing and stays silent. Not an error.
7.3 Fields the runtime IGNORES — and the provenance ruling (the drift dies here)
version, atlas_px, cell/cell_w/cell_h, count, kind, tier, shop, crate,
provenance — stockpack.js reads none of them. So the authority for their shape is not C's
loader but E's validate_atlas.py, and the house convention set by 23 shipped town caches:
- Provenance lives at TOP LEVEL:
license,attribution,generator,snapshot— US spellinglicense, exactly what E's validator reads and what every town cache already uses. Britishlicenceis retired. → Lane G: rename + lift those four to top level. - Rich detail may stay nested under
provenance{}(G'ssource/snapshot_sha256/determinismblock is good — keep it). The four gate fields are the contract; the block is colour. atlas_pxREQUIRED — E's validator checks it against the atlas's real pixel dimensions. That is a genuine falsifiable check; keep it.cell/cell_w/cell_hare informational. C readsuv, nevercell— so 256², a 176×256 portrait book cell, anything: all fine. Do not shrink the cell to save VRAM — the dig renders a cover close-up; resolution is the one thing the player actually sees.
7.4 Cache identity = (type, base) — landed R24
stockpack.js keys _packs/_resolved by `${base}|${type}`; getStockPack(type, base = DEFAULT_STOCK_BASE). Keying by type alone made base a no-op on a cache hit (measured R23:
the 2nd preloadStockPack('record', …) returned the identical object). That was two bugs:
- two stocked shops collide — the 2nd serves the 1st's crate; and
- it defeated fail-soft — an unstocked shop of an already-cached type inherited that crate
instead of falling back to parody. (A correctness bug, not a mix-up.)
Both fixed and re-measured (§7.7). Pre-v5 callers are unaffected: no
base⇒ the town-wide pack.
7.5 The seam (→ Lane F, ledger #6a)
buildInterior(shop, THREE, { stock: 'real', stockBase: `assets/stock_godverse/${shop.godverseShopId}/` })
opts.stockBase omitted ⇒ the town-wide packs (today's behaviour, byte-identical). Preload the shop's
pack on entry with the same base. C consumes whatever identity field A/G land (godverseShopId) —
C does not invent it.
7.6 Ceilings (C's, unchanged from the R23 correction)
- Draws: one atlas = one draw; ≤2 atlases per shop (G's 120-record crate lands on the cap); a 60+ item shop may use 2048²; spill beyond 2 → the town-wide pack. Room law ≤350 stands.
- VRAM: ≤2 atlases @≤2048²; LRU = 1 shop's atlas set resident ⇒ ≤32 MB peak. (R23 correction: my "≤8 MB" assumed a ~16-item "typical shop". A real record shop is 120 records.)
- The LRU + dispose is C's, and it lands at v5.0-beta, not here: with ONE stocked shop there is no accretion, and safe eviction needs a room-exit hook (a live room holds the pack's shared material) — that seam arrives with F's per-shop preload. Stated so it can't slide silently.
7.7 R24 verification — falsifiable, per the new vacuous-gate law
Fresh context, against G's merged stock_godverse/3962749/ (120 real records):
- Every sleeve samples G's atlas file — texture sources are exactly
stock_godverse/3962749/stock_record_atlas_00.webp/_01.webp; pack items ≡ index items by id. (Not "covers render": a parody canvas also has a.map. The negative control below is what makes this a gate — I had to fix my own R23 metric to satisfy the law.) - Negative control: the parody room's stock textures are
(canvas, no src)and touch the godverse atlas zero times — the metric discriminates. - 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
gonedelta), 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 readsit.priceat pull time, so a live price shows on the next pull. - Never: add, remove, or reorder
items[]. That is whatpack.goneexists 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.