Verified G's stock_godverse/3962749/ (120 real records, Monster Robot Party crate 550, 2 x 2048^2, cell 256) in a fresh context — deliberately BEFORE the merge, so a contract failure would block it. It didn't. PASS — the contract holds; G re-emits nothing: - Resolves through the existing stockAdapter path via a base override (no C code change for the single-shop alpha). 120 items / 2 atlases / 8-of-8 stock meshes atlas-backed (real covers: peperecordings, DEEPCHILD, "39", mff FREAKS...). - Ceilings hold: 118 draws with real covers (<=350 law), pathOK, 0 carves. - Fail-soft holds (clean cache): missing atlas -> null -> parody (20 meshes, 110 draws); no adapter (?noassets shape) -> parody, zero fetch. 0 console errors. - G's index compliant: items[] sorted by id (my determinism ask honoured), provenance, uv in [0,1], 2 atlases = exactly the cap, cell_w/h harmless (C reads uv, never cell). CORRECTED MY OWN CEILING (LANE_C_PUB §7.2): I wrote "<=8 MB decoded" from an assumed ~16-item "typical shop". A real record shop is 120 records => 2 x 2048^2 ~= 32 MB, 4x my number. The structural rules held (<=2 atlases; 2048^2 for 60+ items — G landed on the cap); only the MB figure was wrong, because it described a shop that doesn't exist. New: <=2 atlases @<=2048^2, LRU = 1 shop's atlas set resident => <=32 MB peak (bounded; vs E's 181 MB unbounded today). Do NOT shrink the cell below 256 to save VRAM — the dig renders covers close up. G's ask #1 CONFIRMED and worse than reported (LANE_C_PUB §8 — C's spec, C's fix): stockpack keys _packs/_resolved by type alone, so base is ignored on a cache hit (measured: the 2nd preload returned the identical object). (1) two stocked shops collide; (2) it BREAKS FAIL-SOFT — an unstocked shop of a cached type inherits that crate instead of parody. Seam: key by type+base, per-shop preload on entry, LRU=1 + dispose; C consumes whatever identity field lands, C does not invent it. Two gaps block F's #7a, neither C's: the crate has no street (redhill_real absent; monsterrobot not in any shops[] — G's ask #2 to E/Fable), and interior_mode.js:192 pins base to assets/models/ (needs §8 + the identity field). Committed from a temp git worktree on main — the shared checkout stayed on lane-G-godverse (Fable's review) and G's branch keeps its disjoint namespace. Docs-only. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
24 KiB
LANE C — interiors contract (venues + real-town class mapping) → Lanes A/D/E/F
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 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.
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. G2a — per-shop stock atlases: C's interior ceilings (v4.0, ledger #3) → Lane G + Lane E
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.
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.
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:
⚠️ 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:
- 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, nevercell(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=realis 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.
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.
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.
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:
- Two stocked shops collide — the 2nd silently serves the 1st shop's crate (blocks v5.0-beta).
- 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 apackKey; a godverse shop's key is itsstock_godverse/<godverse_id>/base).getStockPacktakes 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'sshops[]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
stockAdapterpath 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); nostockAdapter(the?noassetsshape) → 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_hharmless (C readsuv) ✓. - Verdict: the contract holds. G re-emits nothing. The only C-side work is §8's seam.