Lane C R24: the contract fix — the runtime is the authority (ledger #1, the root cause)
F held the tag because my published atlas contract and my own loader were mutually
incompatible. That was mine. Closed, and published FIRST so G and E re-emit against it.
WHAT WAS WRONG: my R22 §7 invented `stock_shop_<godverseShopId>_index.json`; the loader
(stockpack.js:25) fetches `${base}stock_${type}_index.json`. An index named per the contract
can never be fetched by the code meant to load it. G built runtime-compatible (correct), E
built contract-compatible (also correct — the contract said so), and E's validator globbed
stock_shop_*_index.json -> zero files -> vacuous pass. Nobody was wrong; the contract was.
§7 REWRITTEN AGAINST THE CODE (LANE_C_PUB):
- Authority rule: stockpack.js IS the contract; doc vs loader disagreement = doc bug.
- Layout ratified as G shipped it: assets/stock_godverse/<godverseShopId>/stock_<type>_index.json
(`type` is load-bearing — SLOT_FOR picks the slot). stock_shop_* RETIRED.
-> E: the glob becomes stock_godverse/**/stock_*_index.json (the vacuous gate's actual cause).
- Runtime-read vs ignored stated exactly: reads atlases[] + items[{atlas,uv}] (uv top-left, the
loader flips V); ignores version/atlas_px/cell*/count/kind/tier/shop/crate/provenance.
- Provenance drift dies: runtime reads none of it, so the authority is E's validator + the 23
shipped town caches -> TOP-LEVEL `license` (US), attribution, generator, snapshot. British
`licence` retired; G lifts+renames those four, keeps its rich nested provenance{} as colour.
atlas_px stays REQUIRED (checked against real dims). cell* informational — C reads uv, never
cell; do NOT shrink the cell for VRAM (the dig renders covers close-up).
CODE (F's filed item): stockpack cache identity is now `${base}|${type}`;
getStockPack(type, base = DEFAULT_STOCK_BASE); new opts.stockBase on buildInterior = the seam F
wires. Omitted => town-wide packs, byte-identical to today. Pre-v5 callers untouched.
VERIFIED FRESH — and I fixed my own metric first: R23's "8/8 atlas-backed" only checked that a
map exists, but a parody canvas has a map too (same species as the vacuous gate). Re-asserted
falsifiably: every sleeve's texture source IS stock_godverse/3962749/stock_record_atlas_00/01.webp,
pack items == index items by id; negative control — the parody room's textures are (canvas, no src)
and touch the atlas zero times, so the metric discriminates. Collision FIXED (shop 120 + town 350
coexist). Fail-soft FIXED with a populated cache (missing base -> null -> parody). 98 draws with
real covers, pathOK, 0 carves, 0 console errors.
SCOPED NOT SLID: the LRU+dispose (§7.6) lands at v5.0-beta — one stocked shop can't accrete, and
safe eviction needs a room-exit hook (a live room holds the pack's shared material) that arrives
with F's per-shop preload. Written into the contract so it can't slide silently.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
0740b4697d
commit
ba901d4a75
@ -3,7 +3,63 @@
|
||||
*Status: **v1 complete & verified**. Standalone interiors library + test page. Every shop door opens
|
||||
into a unique, seeded, themed interior, generated on demand in ~4ms, byte-identical every revisit.*
|
||||
|
||||
Last updated: 2026-07-16 (round 23) · owner: PROCITY-C · reviewer: Fable
|
||||
Last updated: 2026-07-17 (round 24) · owner: PROCITY-C · reviewer: Fable
|
||||
|
||||
---
|
||||
|
||||
## Update 2026-07-17 (round 24, THE ALPHA COMPLETION) — the contract fix (ledger #1, the root cause)
|
||||
|
||||
R24 §Lane C. **The root cause was mine and it's closed.** F held the tag because my published atlas
|
||||
contract and my own loader were mutually incompatible; the ruling — **the runtime is the authority** — is
|
||||
now the section's first line. Published FIRST (half-day rule; G and E re-emit against it).
|
||||
|
||||
**What was actually wrong.** My R22 §7 invented `stock_shop_<godverseShopId>_index.json`. The loader
|
||||
(`stockpack.js:25`) fetches `` `${base}stock_${type}_index.json` ``. **An index named per my contract can
|
||||
never be fetched by my own code.** So G built runtime-compatible (correct), E built contract-compatible
|
||||
(also correct — the contract said so), and E's validator globbed `stock_shop_*_index.json` → **zero files
|
||||
→ vacuous pass**. Nobody was wrong; the contract was. I wrote it, so I own it.
|
||||
|
||||
**§7 rewritten against the code (LANE_C_PUB):**
|
||||
- **Authority rule:** `stockpack.js` IS the contract; if the doc and the loader disagree, the doc is a bug.
|
||||
- **Layout ratified as G shipped it:** `assets/stock_godverse/<godverseShopId>/stock_<type>_index.json`.
|
||||
`type` is load-bearing (it picks the slot via `SLOT_FOR`: record→sleeve). `stock_shop_*` is **retired**
|
||||
→ **E: the validator glob becomes `stock_godverse/**/stock_*_index.json`** (that's the vacuous gate's
|
||||
actual cause).
|
||||
- **Runtime-read vs runtime-ignored, stated exactly.** Reads `atlases[]` + `items[{atlas, uv}]` (uv origin
|
||||
**top-left**; the loader flips V — don't pre-flip). Ignores `version/atlas_px/cell*/count/kind/tier/shop/
|
||||
crate/provenance` entirely.
|
||||
- **The provenance drift dies (§7.3):** since the runtime reads none of it, the authority is E's validator
|
||||
+ the 23 shipped town caches — both use **top-level `license` (US), `attribution`, `generator`,
|
||||
`snapshot`**. British `licence` retired; **G lifts+renames those four**, keeps its rich nested
|
||||
`provenance{}` (source/sha256/determinism) as colour. `atlas_px` stays REQUIRED (E checks it against real
|
||||
dims — a genuine falsifiable check). `cell*` informational: **C reads `uv`, never `cell`** — and don't
|
||||
shrink the cell for VRAM, the dig renders covers close-up.
|
||||
|
||||
**Code landed (the `type+base` keying, F's filed item):**
|
||||
- `stockpack.js`: cache identity is now `` `${base}|${type}` ``; `getStockPack(type, base = DEFAULT_STOCK_BASE)`.
|
||||
- `interiors.js`: new **`opts.stockBase`** — the seam F wires (`buildInterior(shop, THREE, { stock:'real',
|
||||
stockBase: 'assets/stock_godverse/<id>/' })`). Omitted ⇒ town-wide packs, byte-identical to today.
|
||||
- Pre-v5 callers untouched (`getStockPack('record')` still means the town pack).
|
||||
|
||||
**Verified fresh — and I had to fix my own metric first.** R23 I claimed "8/8 atlas-backed"; that checked
|
||||
`material.map` exists — **but a parody canvas has a map too**. That's the same species as the vacuous gate
|
||||
I'd just been handed. Re-asserted falsifiably:
|
||||
- **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**. Titles are real
|
||||
(*Yoshitoshi Artists – In House We Trust / Deep Dish – Penetrate Deeper*).
|
||||
- **Negative control (what makes it a gate):** 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) + town pack (350) coexist as distinct objects.
|
||||
- **Fail-soft FIXED with a populated cache:** missing base ⇒ `null` ⇒ parody (the R23 correctness bug —
|
||||
an unstocked shop inheriting a cached crate — is gone).
|
||||
- Ceilings: **98 draws** with real covers via `opts.stockBase`; `pathOK`; 0 carves; **0 console errors**.
|
||||
|
||||
**Scoped, not slid:** the **LRU + dispose** (§7.6) lands at **v5.0-beta**, not here — with one stocked shop
|
||||
there's no accretion, and safe eviction needs a room-exit hook (a live room holds the pack's shared
|
||||
material), which arrives with F's per-shop preload. Written into the contract so it can't slide silently.
|
||||
|
||||
**→ F:** the seam is `opts.stockBase`; §7.5 has the call. **→ G:** layout ratified, re-emit provenance field
|
||||
names only (§7.3). **→ E:** the glob (§7.1) is the vacuous gate's cause.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@ -7,9 +7,11 @@
|
||||
> (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 amendment (2026-07-16):** **§7.2's VRAM number corrected + §7.8 added** — G's first real
|
||||
> crate (120 records) proved my "≤8 MB" figure was derived from a wrong assumption, and the per-shop cache
|
||||
> seam is now specified for G+F. **G's R23 atlas needs no re-emit.** §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 what `stockpack.js` actually implements, extended per-shop, and the
|
||||
> `type+base` cache keying is landed. **G's R23 atlas layout is ratified** (G re-emits provenance field
|
||||
> names only, §7.3). §0–§6 unchanged.
|
||||
|
||||
*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
|
||||
@ -226,99 +228,84 @@ mapping for the caches is this table.)
|
||||
|
||||
---
|
||||
|
||||
## 7. G2a — per-shop stock atlases: C's interior ceilings (v4.0, ledger #3) → Lane G + Lane E
|
||||
## 7. Real-stock packs — THE RUNTIME IS THE AUTHORITY (v5.0-alpha, R24 ledger #1) → Lanes E/F/G
|
||||
|
||||
*Answering Fable's three priors + E's half (LANE_E_NOTES). **Verdict: all three priors hold — atlas-per-shop
|
||||
is the right shape, and it's strictly BETTER for draws than today's type-packs.** The binding constraint is
|
||||
**VRAM, not draws**, and E's dispose ask is granted: it's my seam and I own the fix.*
|
||||
> **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.js` is 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).**
|
||||
|
||||
**1. Draw ceiling — ONE atlas per shop (hard cap 2).**
|
||||
Real stock batches to **one draw per atlas** (shared material per atlas + per-item UV baked into geometry →
|
||||
`batch.js` merges). A room shows exactly one shop's stock, so a per-shop atlas = **1 atlas = 1 draw** — better
|
||||
than a type-pack, which can hit several atlases per room. **Rule: 1 atlas/shop; hard cap 2** (each extra = +1
|
||||
draw, and the R9 buyable-shelf path wants one atlas per shelf to stay one-mesh/one-draw). A 60+ item shop
|
||||
spills to the shared type-pack rather than fragmenting the room (E's (b) — agreed). *Headroom: `?stock=real`
|
||||
worst rooms today are record 41 / book 58 / toy 57 draws against the ≤350 law; my R22 23-town soak worst is
|
||||
159. Draws are not the constraint.*
|
||||
### 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: `type` picks the stock slot
|
||||
(`SLOT_FOR`: record→`sleeve`, book→`spine`, toy→`box`). **A record shop's per-shop pack must be
|
||||
type `record`**, 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 become `stock_godverse/**/stock_*_index.json`
|
||||
(the current `stock_shop_*_index.json` is why it passed on 0 files).
|
||||
|
||||
**2. Texture/VRAM ceiling — the real limit. E's dispose ask: GRANTED, and it's mine to land.**
|
||||
E's audit is correct and the cause is a deliberate C design note (`stockpack.js:11`): atlas textures are
|
||||
"shared/cached in the pack (**NOT per-room**), like depot GLBs". That is **right for 3 bounded type-packs**
|
||||
and **wrong for unbounded per-shop atlases** (23 towns × N shops). The ceiling:
|
||||
### 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 of `atlases` (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`**.
|
||||
|
||||
> **⚠️ CORRECTED v5.0-alpha — measurement beat my assumption.** I wrote "≤8 MB decoded" by assuming a
|
||||
> "typical shop" is ~16 items @1024². **G's first real crate is 120 records** (Monster Robot Party, crate
|
||||
> 550) — a real record shop is nothing like 16 items. At the contract's own 256² cell that's 2 × 2048²
|
||||
> = **~32 MB decoded**, 4× my stated figure. The *structural* rules were right (≤2 atlases; 2048² for a
|
||||
> 60+ item shop) — only the MB number was wrong, because it was derived from a shop that doesn't exist.
|
||||
> **G's R23 atlas is compliant; no re-emit.** The corrected ceiling:
|
||||
### 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 spelling
|
||||
`license`, exactly what E's validator reads and what every town cache already uses.
|
||||
**British `licence` is retired. → Lane G: rename + lift those four to top level.**
|
||||
- Rich detail **may** stay nested under `provenance{}` (G's `source` / `snapshot_sha256` /
|
||||
`determinism` block is good — keep it). The four gate fields are the contract; the block is colour.
|
||||
- **`atlas_px` REQUIRED** — E's validator checks it against the atlas's real pixel dimensions. That is
|
||||
a genuine falsifiable check; keep it.
|
||||
- **`cell` / `cell_w` / `cell_h` are informational.** **C reads `uv`, never `cell`** — 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.
|
||||
|
||||
- **Per shop: ≤2 atlases, ≤2048² each** (unchanged — G's 120-item crate lands exactly on the cap).
|
||||
- **Resident: ONE shop's atlas set** (LRU = 1 shop, not 2) ⇒ **≤32 MB decoded peak**. Still bounded, and
|
||||
still far better than the **181 MB unbounded** accretion E measured today.
|
||||
- **≤1024² whenever the crate fits it** (≤64 items at 256²) — don't hand a 6-item shop a 2048².
|
||||
- Cell size is E/G's call: **C reads `uv`, never `cell`** (so 256², 176×256 portrait, etc. are all fine).
|
||||
Do **not** shrink the cell below 256² to save VRAM — the dig renders a cover close-up; resolution is the
|
||||
one thing the player actually sees.
|
||||
- **C lands the LRU cap + dispose in `stockpack.js`** (evicted shop's atlas texture + material disposed).
|
||||
Type-packs keep today's shared cache (bounded, reused across rooms of a type) — unchanged.
|
||||
- **Scheduling:** today's 181 MB accretion is **pre-existing**, and `?stock=real` is **opt-in**
|
||||
(`index.html:196` — `params.get('stock')==='real' && !NOASSETS`), so it cannot touch the default boot and is
|
||||
**not a v4.0-tag blocker**. I land the cap as **C's first v4.x item — before G2a ships**. G: build to the
|
||||
ceiling; the loader will honour it.
|
||||
### 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:
|
||||
1. two stocked shops collide — the 2nd serves the 1st's crate; **and**
|
||||
2. **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.
|
||||
|
||||
**3. Index schema — E's (c) confirmed VERBATIM, keyed per shop.**
|
||||
`stock_shop_<godverseShopId>_index.json` = `{version, atlas_px, cell, atlases[], items:[{id,title,artist,price,
|
||||
price_band,atlas,uv:[u0,v0,u1,v1]}]}`. `stockpack.js` consumes it unchanged (it already groups items by atlas
|
||||
and shares a material per atlas). **UV origin stays top-left (image-natural) — C flips V for WebGL** (R7 law,
|
||||
unchanged). Book cell 176×256 portrait is fine: **C reads `uv`, never `cell`**.
|
||||
### 7.5 The seam (→ Lane F, ledger #6a)
|
||||
```js
|
||||
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.**
|
||||
|
||||
**4. Fail-soft + `?noassets` — unchanged law, confirmed.**
|
||||
Index/atlas missing → `getStockPack` → null → **parody canvas + one warn** (R7, proven since). `?noassets`
|
||||
fetches zero. A godverse town under `?noassets` stays silent-and-fine. `?stock=real` opt-in ⇒ G2a cannot
|
||||
regress the default or classic boots.
|
||||
### 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.
|
||||
|
||||
**5. Determinism — one ask back to G.**
|
||||
The seeded pick per bin is C's and stays: same `shop.seed` → same crate contents, byte-identical revisits.
|
||||
That indexes into `items[]`, so **G must emit `items[]` in a deterministic order** (sort by `id`) — otherwise
|
||||
a re-emit silently reshuffles every crate. Same discipline as E's byte-identical cache re-runs.
|
||||
|
||||
**6. Buy-anywhere (R9) — compatible, no change.**
|
||||
`buildBuyableShelf` merges per-item-addressable covers from ONE atlas → one draw (`noBatch`). A per-shop atlas
|
||||
satisfies that naturally (better than the type-pack's seeded `atlasPool` pick). Parody = not buyable stays.
|
||||
|
||||
**7. Parody law — E's (d), unchanged and not C's to relax.** Metadata deterministically parody-transformed;
|
||||
cover art real; prices real-or-seeded. *(v5.0-alpha: John amended this at tier 1 — real titles/artists ship
|
||||
in-house-green, flagged before public. Recorded in G's provenance; not C's to relax either way.)*
|
||||
|
||||
**8. The per-shop cache seam (v5.0-alpha — C's spec; G+F build to this). CONFIRMED BROKEN in R23.**
|
||||
G's ask #1 is real and I reproduced it. `stockpack.js` keys `_packs`/`_resolved` by **`type` alone**, and
|
||||
`preloadStockPack(type, {base})` **ignores `base` on a cache hit**. Measured (R23, fresh context):
|
||||
- `preloadStockPack('record', {base:'assets/stock_godverse/3962749/'})` → the 120-record godverse pack ✓
|
||||
- then `preloadStockPack('record', {base:'assets/models/'})` → **returned the identical object** ✗
|
||||
|
||||
Two consequences, the second worse than G's report:
|
||||
1. **Two stocked shops collide** — the 2nd silently serves the 1st shop's crate (blocks v5.0-beta).
|
||||
2. **It breaks the fail-soft law** — an *unstocked* shop of a cached type inherits that crate instead of
|
||||
falling back to parody. (On a clean cache fail-soft is intact: missing atlas → `null` → parody, 0 errors.
|
||||
The collision is what defeats it, so this is a correctness bug, not just a mix-up.)
|
||||
|
||||
**The seam (C owns it, C lands it):**
|
||||
- **Cache key = `type + base`** (equivalently a `packKey`; a godverse shop's key is its `stock_godverse/<godverse_id>/` base). `getStockPack` takes the same key.
|
||||
- **Resolution order** stays: explicit per-shop base → the type-pack base → `null` → parody.
|
||||
- **Per-shop preload on entry** (interior build/enter), **not** a boot-time preload of every shop.
|
||||
- **LRU = 1 shop's atlas set, disposed on eviction** (§7.2) — the load-bearing half of the §7 promise.
|
||||
- **Identity link:** a stocked shop must carry its pack pointer on the shop record (G's `godverse_id` →
|
||||
`stock_godverse/<id>/`). Not in the town cache today — G flagged it ("monsterrobot must be added to the
|
||||
godverse cache's `shops[]` as id 3962749"). C consumes whatever field lands; **C does not invent it.**
|
||||
|
||||
**9. R23 verification — the first real crate, in a room (ledger #3). PASS.**
|
||||
Measured against G's `3962749` atlas (120 real records, 2 × 2048², cell 256²) in a fresh context:
|
||||
- **Resolves through the existing `stockAdapter` path** via a base override — no C code change needed for
|
||||
the single-shop alpha. 120 items, 2 atlases, **8/8 stock meshes atlas-backed** (real covers, not parody).
|
||||
- **Ceilings hold:** **118 draws** with real covers (≤350 law); room `pathOK`, 0 carves.
|
||||
- **Fail-soft holds** (clean cache): missing atlas → `null` → parody (20 parody meshes, 110 draws, pathOK);
|
||||
no `stockAdapter` (the `?noassets` shape) → parody, zero fetch. **0 console errors** throughout.
|
||||
- **G's index is contract-compliant:** `items[]` sorted by id (my determinism ask honoured ✓), provenance
|
||||
present ✓, `uv ⊂ [0,1]` ✓, 2 atlases = the cap ✓, `cell_w/cell_h` harmless (C reads `uv`) ✓.
|
||||
- **Verdict: the contract holds. G re-emits nothing.** The only C-side work is §8's seam.
|
||||
### 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.
|
||||
|
||||
@ -42,7 +42,10 @@
|
||||
// • type : any CITY_SPEC type or thriftgod alias (see theme.js); defaults to 'opshop'
|
||||
// • lot : { w, d } metres — room adapts to it; if absent, archetype ranges decide the size
|
||||
// • storeys: ceiling height driver (1 → 3.2m … up to 4.5m)
|
||||
// opts: { archetype?: 'cosy'|'gallery'|'wide'|'hall'|'pokey', stockAdapter? }
|
||||
// opts: { archetype?: 'cosy'|'gallery'|'wide'|'hall'|'pokey', stockAdapter?, stock?: 'real', stockBase? }
|
||||
// • stockBase (v5.0): which real-stock pack `stock:'real'` resolves. Omit → the town-wide packs
|
||||
// (`assets/models/`, today's behaviour). Pass `assets/stock_godverse/<godverseShopId>/` for a shop
|
||||
// with its own real crate. The pack cache is keyed by (type, base) — see stockpack.js + LANE_C_PUB §7.
|
||||
//
|
||||
// Registry override (Lane F): call mergeRegistry(registry) ONCE at init to let authored registry data
|
||||
// override recipe fields. It is deliberately NOT a per-build option — a per-call mutation of the shared
|
||||
@ -138,13 +141,16 @@ export function buildInterior(shop, THREE, opts) {
|
||||
const dims = computeDims(ctx, recipe, archetype, norm.lot, norm.storeys);
|
||||
const shell = buildShell(ctx, { recipe, archetype, dims });
|
||||
|
||||
// fittings + wall decor + stock + walkable-path guarantee. ?stock=real feeds Lane E's GODVERSE pack
|
||||
// through the stockAdapter seam (preload it first); missing pack → parody canvas (warn once).
|
||||
// fittings + wall decor + stock + walkable-path guarantee. ?stock=real feeds a real-stock pack through
|
||||
// the stockAdapter seam (preload it first); missing pack → parody canvas (warn once).
|
||||
// `opts.stockBase` (v5.0) selects WHICH pack: omit it for the town-wide packs, or pass a per-shop base
|
||||
// (`assets/stock_godverse/<godverseShopId>/`) for a shop with its own real crate. The pack cache is keyed
|
||||
// by (type, base), so a per-shop pack and the town-wide pack coexist — see stockpack.js.
|
||||
let adapter = opts.stockAdapter || null;
|
||||
if (!adapter && opts.stock === 'real') {
|
||||
const pack = getStockPack(recipe.key);
|
||||
const pack = getStockPack(recipe.key, opts.stockBase);
|
||||
if (pack) adapter = makeStockAdapter(pack);
|
||||
else warnOnce(`stock=real: no pack for "${recipe.key}" (preload it) — using parody canvas`);
|
||||
else warnOnce(`stock=real: no pack for "${recipe.key}" @${opts.stockBase || 'default base'} (preload it) — using parody canvas`);
|
||||
}
|
||||
const lay = layout(ctx, { recipe, dims, shell, adapter, shop: norm });
|
||||
|
||||
|
||||
@ -16,23 +16,38 @@ import * as THREE_NS from 'three';
|
||||
import { mergeGeometries } from 'three/addons/utils/BufferGeometryUtils.js';
|
||||
|
||||
const _texLoader = new THREE_NS.TextureLoader();
|
||||
const _packs = new Map(); // type → Promise<pack|null>
|
||||
const _resolved = new Map(); // type → pack|null (sync accessor for buildInterior)
|
||||
|
||||
// Preload a pack (fetch index + atlas textures). Cached. Call once before building with ?stock=real.
|
||||
export function preloadStockPack(type, { base = 'assets/models/', THREE = THREE_NS } = {}) {
|
||||
if (_packs.has(type)) return _packs.get(type);
|
||||
// Where the TOWN-WIDE packs live. A per-shop (godverse) pack lives under its own base instead —
|
||||
// `assets/stock_godverse/<godverseShopId>/` — and carries the SAME `stock_<type>_index.json` name,
|
||||
// because `type` is what picks the stock slot (SLOT_FOR: record→sleeve, book→spine, toy→box).
|
||||
export const DEFAULT_STOCK_BASE = 'assets/models/';
|
||||
|
||||
// CACHE IDENTITY IS (type, base) — never `type` alone. A per-shop atlas and the town-wide pack are
|
||||
// different packs that share a type; keying by type alone made `base` a no-op on a cache hit, so the
|
||||
// 2nd caller silently received the 1st caller's pack (R23, measured). That wasn't only a mix-up: it
|
||||
// defeated the fail-soft law — an UNSTOCKED shop of an already-cached type inherited that crate
|
||||
// instead of falling back to parody. (R24 ledger #1; the runtime is the contract's authority.)
|
||||
const packKey = (type, base) => `${base}|${type}`;
|
||||
const _packs = new Map(); // packKey → Promise<pack|null>
|
||||
const _resolved = new Map(); // packKey → pack|null (sync accessor for buildInterior)
|
||||
|
||||
// Preload a pack (fetch index + atlas textures). Cached per (type, base). Call once before building
|
||||
// with ?stock=real — for a per-shop pack, pass that shop's base.
|
||||
export function preloadStockPack(type, { base = DEFAULT_STOCK_BASE, THREE = THREE_NS } = {}) {
|
||||
const key = packKey(type, base);
|
||||
if (_packs.has(key)) return _packs.get(key);
|
||||
const p = fetch(`${base}stock_${type}_index.json`)
|
||||
.then(r => (r.ok ? r.json() : null))
|
||||
.then(idx => (idx && idx.items && idx.items.length ? buildPack(type, idx, base, THREE) : null))
|
||||
.catch(() => null)
|
||||
.then(pack => { _resolved.set(type, pack); return pack; });
|
||||
_packs.set(type, p);
|
||||
.then(pack => { _resolved.set(key, pack); return pack; });
|
||||
_packs.set(key, p);
|
||||
return p;
|
||||
}
|
||||
|
||||
// Synchronous accessor — the resolved pack if preloaded, else null (buildInterior uses this).
|
||||
export function getStockPack(type) { return _resolved.get(type) || null; }
|
||||
// Synchronous accessor — the resolved pack for (type, base), else null (buildInterior uses this).
|
||||
// `base` defaults to the town-wide packs, so every pre-v5 caller keeps its exact behaviour.
|
||||
export function getStockPack(type, base = DEFAULT_STOCK_BASE) { return _resolved.get(packKey(type, base)) || null; }
|
||||
|
||||
async function buildPack(type, idx, base, THREE) {
|
||||
const names = idx.atlases || [];
|
||||
|
||||
Loading…
Reference in New Issue
Block a user