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>
68 KiB
LANE C — INTERIORS · progress (PROCITY-C)
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-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.jsIS 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.typeis load-bearing (it picks the slot viaSLOT_FOR: record→sleeve).stock_shop_*is retired → E: the validator glob becomesstock_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). Ignoresversion/atlas_px/cell*/count/kind/tier/shop/ crate/provenanceentirely. - 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. Britishlicenceretired; G lifts+renames those four, keeps its rich nestedprovenance{}(source/sha256/determinism) as colour.atlas_pxstays REQUIRED (E checks it against real dims — a genuine falsifiable check).cell*informational: C readsuv, nevercell— and don't shrink the cell for VRAM, the dig renders covers close-up.
- the 23 shipped town caches — both use top-level
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: newopts.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.
Update 2026-07-16 (round 23, v5.0-alpha THE FIRST REAL CRATE) — the crate in the room (ledger #3)
R23 §Lane C. Verdict: the contract holds — G's atlas needs no re-emit. Verified against G's
stock_godverse/3962749/ (120 real records from Monster Robot Party's DEEP HOUSE crate 550, 2 × 2048²,
cell 256²) in a fresh context. Ran before Fable's merge deliberately — if the contract failed, that
blocks the merge. It didn't.
PASS — the first real crate renders in a PROCITY room:
- 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). The sleeves are legible on screen: peperecordings · DEEPCHILD · "39" · mff FREAKS · The Angels Instinct feat Charlie Ashcroft — a real deep-house crate. - Ceilings hold: 118 draws with real covers (≤350 law),
pathOK, 0 carves. - Fail-soft holds (clean cache): missing atlas →
null→ parody (20 parody meshes, 110 draws); no adapter (the?noassetsshape) → parody, zero fetch. 0 console errors. - G's index is compliant:
items[]sorted by id (my R22 determinism ask honoured), provenance present,uv ⊂ [0,1], 2 atlases = exactly my cap,cell_w/cell_hharmless (C readsuv, nevercell).
I got a number wrong, and the first real data caught it (LANE_C_PUB §7.2 CORRECTED). My R22 ceiling said "≤8 MB decoded" — derived from assuming a "typical shop" is ~16 items @1024². A real record shop is 120 records. At the contract's own 256² cell that's 2 × 2048² ≈ 32 MB, 4× my figure. The structural rules held (≤2 atlases; 2048² for 60+ items — G landed exactly on the cap); only the MB number was wrong, because it described a shop that doesn't exist. Corrected: ≤2 atlases/shop @≤2048², LRU = 1 shop's atlas set resident ⇒ ≤32 MB peak (still bounded; still far better than the 181 MB unbounded accretion E measured). And explicitly: don't shrink the cell below 256² to save VRAM — the dig shows a cover close-up; resolution is the one thing the player actually sees.
G's ask #1 CONFIRMED — and worse than reported (LANE_C_PUB §8: C's spec, C's fix). stockpack.js keys
_packs/_resolved by type alone, so base is ignored on a cache hit. Measured:
preloadStockPack('record', {base:'…/3962749/'}) then preloadStockPack('record', {base:'assets/models/'})
returned the identical object. Consequences: (1) two stocked shops collide — the 2nd serves the 1st's
crate (blocks v5.0-beta, as G said); (2) it breaks the fail-soft law — an unstocked shop of a cached
type inherits that crate instead of parody. The second is a correctness bug, not a mix-up. Seam spec
published (cache key = type + base; per-shop preload on entry; LRU=1 + dispose; C consumes whatever
identity field lands — C does not invent it). C owns and lands it — the §7 promise coming due.
Two gaps block F's gate #7a (neither is C's; both flagged): (1) the crate has no street —
redhill_real doesn't exist and monsterrobot isn't in any town's shops[] (G's ask #2 → E/Fable);
(2) the shell's interior_mode.js:192 calls getStockPack(shop.type) with base fixed to
assets/models/, so it can't reach a per-shop pack until §8's seam + the identity field land (C+F).
Process note: the shared tree was checked out on lane-G-godverse for Fable's review, so I committed
these C docs from a temporary git worktree on main — the shared checkout was never touched, and G's
branch keeps the disjoint namespace the two-studio merge law depends on.
Update 2026-07-16 (round 22, v4.0 THE EPOCH CLOSE) — 23-town door-soak (#5) + the G2a contract (#3)
R22 §Lane C, both items. Wave 3 — ran after A pinned the 36 goldens (3face97) and E's tri-diet fix
(d81e499), so this soaks the final v4.0 world.
Ledger #5 — sampled door-soak across all 23 towns
The five-town harness scaled: ~10 doors/town, sample takes one of every type present then stride-fills, so every archetype is covered town-set-wide. 238 doors · 1,133 in-plan shops · 23 towns.
| town | shops | doors | worst draws | town | shops | doors | worst draws | |
|---|---|---|---|---|---|---|---|---|
| adelaide | 110 | 11 | 158 | katoomba | 72 | 10 | 149 | |
| ballarat | 19 | 10 | 154 | launceston | 30 | 10 | 137 | |
| bendigo | 35 | 10 | 141 | marrickville | 26 | 10 | 149 | |
| bowral | 30 | 10 | 156 | newcastle | 34 | 10 | 152 | |
| braddon | 19 | 10 | 158 | newtown_godverse | 18 | 10 | 134 | |
| brunswick | 92 | 11 | 149 | newtown | 67 | 11 | 149 | |
| castlemaine | 23 | 10 | 150 | northbridge | 58 | 11 | 149 | |
| darwin | 12 | 10 | 154 | toowoomba | 12 | 10 | 138 | |
| daylesford | 26 | 10 | 153 | westend | 41 | 10 | 146 | |
| fitzroy (mecca) | 139 | 10 | 159 | fremantle | 79 | 11 | 152 | |
| geelong | 62 | 11 | 145 | glebe | 51 | 11 | 151 | |
| hobart | 78 | 11 | 157 |
Verdict: 23/23 PASS — 0 throws · 0 carves · 0 path-fails · 0 over-budget; worst 159 draws (fitzroy) ≤350.
- All 12 archetypes covered town-set-wide (band_room, book, dept, milkbar, opshop, pawn, pub, record, rsl, stall, toy, video) — the whole registry.
- 23/23 towns landed 3 venues — A's cluster bias + district placement hold from the 12-shop thin tail (darwin, toowoomba) to the 139-shop mecca (fitzroy). Fitzroy, 2× anything absorbed before, is the cheapest end of nothing — worst draw 159, same band as everywhere else: interiors don't care about town scale.
- Lane G's
newtown_godversepasses (18 shops, 8 types, worst 134) — the merged town needs nothing from C.
Ledger #3 — the G2a per-shop atlas contract (C's half) → published in LANE_C_PUB §7
Answered Fable's priors + E's half. All three priors hold; atlas-per-shop is 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.
- Draws: 1 atlas/shop = 1 draw (shared material + baked UV → batch.js merges). Rule: 1, hard cap 2; 60+ item shops spill to the type-pack. Headroom is enormous (stock rooms 41–58 draws vs the ≤350 law).
- VRAM (the real ceiling): E's 181 MB audit is right and the cause is my own design note
(
stockpack.js:11caches atlases "NOT per-room, like depot GLBs") — correct for 3 bounded type-packs, wrong for unbounded per-shop atlases. Ruling: ≤2 resident shop atlases, ≤8 MB decoded, ≤1024² typical (2048² only for 60+ items); C lands the LRU cap + dispose instockpack.js. Scheduling: the accretion is pre-existing and?stock=realis opt-in (index.html:196) so it can't touch the default boot — not a v4.0 blocker; I land it as C's first v4.x item, before G2a ships. - Schema: E's index confirmed verbatim, keyed per shop; UV origin stays top-left (C flips V); C reads
uvnevercell(so book's 176×256 portrait is fine). Fail-soft/parody/?noassets: unchanged law. - One ask back to G: emit
items[]in deterministic order (sort by id) — C's seeded per-bin pick indexes into it, so a re-emit would silently reshuffle every crate.
C→F: 23-town interior verdict GREEN (nothing gates the tag from C). Docs-only. Both C items closed for v4.0.
Update 2026-07-16 (round 21, v4.0-beta close) — the five-town density door-soak (ledger #3)
R21 §Lane C (wave 2 — ran after A's venue cluster-adjacency bias landed, 110c828, so the soak reads the
relocated world + A's re-pinned goldens). The carried R20 #6: every one of the five beta towns at its new
density, deterministically sampled ~25 doors/town — the sample takes one of every type present then
stride-fills, so the texture classes' first real appearance (milkbar/dept/stall on real shops) is always covered.
| town | shops (in plan) | sampled | venues | types (all sampled) | throws | carves | path-fails | over-budget | worst draws |
|---|---|---|---|---|---|---|---|---|---|
katoomba_real |
72 | 25 | 3 | 10 | 0 | 0 | 0 | 0 | 157 |
newtown_real |
67 | 25 | 3 | 11 | 0 | 0 | 0 | 0 | 155 |
fremantle_real |
79 | 25 | 3 | 11 | 0 | 0 | 0 | 0 | 153 |
bendigo_real |
35 | 25 | 3 | 10 | 0 | 0 | 0 | 0 | 154 |
castlemaine_real |
23 | 23 (all) | 3 | 8 | 0 | 0 | 0 | 0 | 156 |
123 doors · 0 throws · 0 carves · 0 path-fails · 0 over-budget · worst 157 draws (≤350). In every town
typesInTown === typesSampled — every archetype present was entered, including all three venue kinds
(pub/band_room/rsl) and the texture classes. All five towns landed 3 venues → A's cluster bias + district
placement hold at density.
§6 mapping verified live (John's SUBTLE directive, working by construction): katoomba's cache is 20
heroes + 60 texture — exactly E's 3× cap, heroes untouched. In-plan (after A's counted drops): 17 heroes
(opshop 10 · book 4 · record 2 · video 1) + 52 texture (milkbar 29 · dept 18 · stall 5) + 3 venues = 72.
The texture classes' first real appearance renders right: "Blue Mountains Chocolate Company" — a real
Katoomba shop=chocolate — now wears milkbar (70s geo wallpaper, cube shelves of price-tagged goods, counter,
wall rack): a believable confectionery counter shop. The widening reads as texture, not takeover.
(Cache→plan deltas — e.g. katoomba 80→72 — are A's overflow/fragmentation drops, counted by cause in A's R20 accounting; not an interior concern, and every surviving door builds clean.) C→F: density verdict GREEN. Docs-only. C otherwise on-call; my §6 mapping + this soak close both C items for the beta.
Update 2026-07-16 (round 20, v4.0-beta FIVE TOWNS) — the class→archetype mapping (ledger #2, C's half)
R20 §Lane C (I lead — E builds to it, half-day rule): published the OSM shop-class → interior archetype mapping in LANE_C_PUB.md §6 (new v4.0-beta amendment; the frozen venue contract §0–§5 untouched). This is the density widening — real towns fetch general retail, not just the thriftgod secondhand classes, so the main street holds its real bakery/cafe/chemist/clothes shops.
Design (John's directive: SUBTLE — texture, not takeover):
- No new archetypes. General retail wears existing recipes: food/grocer/newsagent/chemist →
milkbar(the canonical AU counter shop); clothes/hardware/big-retail →dept; produce/florist →stall. - Secondhand stays the hero — charity/antiques→opshop, books→book, music→record, video/games→video, pawn→pawn, toys→toy; never subsampled.
- Unknown/unlisted →
opshop(R18-proven safe). Included a copy-paste JSON map for E'sbuild_towns.py- a tasteful fetch list (skip vehicle/funeral/industrial/pure-services like hairdresser/bank).
- Guardrail for E: if a town's general retail would visually drown the secondhand strip, seed-subsample
the texture classes toward ~2–3× the secondhand count and count the drops in
norm.dropped.
Mechanic note: plan_osm keeps only registry types (unknowns flatten to opshop), so E bakes a registry
type per the map into each cache's shop.type — no C code change, all targets are existing recipes/registry
entries, budgets unchanged. C→E handshake: §6 is live.
Pre-validated the mapping (measurement over folklore, before E builds to it): milkbar/dept/stall/opshop all
build valid at the katoomba lot (8.5×14) — 0 carves, ≤143 draws, pathOK. Rendered dept (my one flagged case
— the escalator centrepiece): reads as a clean dressed-retail floor (cube shelves of goods + clothes rack +
counter + "GROUND FLOOR/HABERDASHERY" signage) in the 7.9×11.7 hall, escalator does not dominate — so
clothes/hardware → dept is confirmed, not a concern.
Pending (ledger #6, after E re-emits the 5 widened caches): the five-town sampled door-soak — every town,
sampled doors at the new density, all types resolve, no carve, budgets; one table, five rows. I'll watch that
dept reads right on small real lots (the escalator centrepiece). Docs-only this commit; door-soak follows E.
Update 2026-07-16 (round 19, v4.0-alpha CLOSE) — re-soak katoomba on REAL roads (against A's in-flight pass)
R19 §Lane C (on-call, ledger #5): E's roads landed (dbb52c2 — katoomba_real is now schema v2, 484 real
roads), so I re-ran the 20-door soak against the real street graph — closing the R18 "re-run when roads land"
caveat. Honest caveat: the working tree also carried Lane A's UNCOMMITTED R19 fragmentation pass (plan_osm.js
WIP — JOIN_TOL/BRIDGE_MAX, junction-protected Douglas–Peucker, shopless-island cull / shop-island bridge),
so the dev server served A's in-progress finalization, not a frozen state. Read this as "interiors survive
A's real-graph pipeline as it stands mid-round"; a clean confirm follows A's commit.
Robust result (holds whatever A's WIP does): every shop the plan produced builds a valid interior — 0 throws · 0 carves · 0 path-fails · 0 over-budget · worst 147 draws (min 115); 3 gig venues (pub/band_room/rsl — district placement holds on the real graph); plan deterministic (byte-identical re-gen). Lot dims unchanged (7.5–8.5 × 14) → interiors are dim-identical to R18 (road-agnostic confirmed; my R18 degenerate-lot proof already spans anything the real graph can seat).
Shop count observed = 19 (NOT filed as a defect). The plan yielded 19 shops here, but against A's in-flight
fragmentation code that's expected, not a bug — A's ruling explicitly culls shopless islands and bridges/
drops-with-count shop-bearing ones, so the final count is A's to pin (could land at 20 connected or an explicit
19 with a norm.dropped count). I'm not attributing it to A as a finding; I only confirm interiors build clean
whichever way it resolves. (If A's committed pass surprises with a shop that can't build an interior, that
would be the finding — none did in this run.)
Final C spot-confirm pending A's COMMITTED finalization + golden pin (per Fable's sequencing, C confirms
after A). When A pins katoomba_real's real-roads golden I'll re-run once against the frozen state. Docs-only
round for C.
Update 2026-07-16 (round 18, v4.0-alpha REAL ROADS) — interior spot-soak of katoomba_real (ledger #6)
R18 §Lane C (small): enter every katoomba_real shop and assert valid interiors + no carve + budgets.
Built the town plan the way the shell does — registerTownCache(katoomba_real) → generatePlanFor(seed, 'osm', {town, gigs:true}) (gigs on = the default boot) → buildInterior per shop (lot dims resolved from
plan.lots). All 20 doors PASS:
| metric | result |
|---|---|
| shops entered | 20 / 20 built |
| throws · carves · path-fails · over-budget | 0 · 0 · 0 · 0 |
| worst draws | 153 (shop 16, opshop/hall) ≤ 350 |
| types resolved | opshop · book · record · video + venues pub · rsl · band_room |
| gig venues (default boot) | 3 (ids 3/5/19) — all built valid gig interiors (stage + watchPoints) |
| draw range | 108–153 |
Degenerate-lot stress (preempts A's real-road re-seating): 32 combos (8 extreme lots {2×3, 1×1, 3×30,
40×4, 100×100, 0×0, 4.2×4.1, 2.5×18} × 4 types incl. venues) → 0 throws · 0 carves · 0 path-fails;
every room floors at ≥4×4 m (computeDims Math.max(4, lot−0.6)). So whatever frontage the real-graph
lift seats shops on, the interior contract absorbs it.
Caveat (honest): audited against the CURRENT katoomba_real.json — schema v1, roads: none, so it boots
the marched fallback (E's katoomba roads cache isn't committed yet; A's lift a78dc76 is in). Interiors
are road-agnostic (they consume lot dims + type + seed, never road geometry), and the degenerate-lot proof
covers any lot the real graph produces, so the conclusion is stable. Interior lane contributes 0 failures to
the alpha's failure list. A one-line re-run once E's roads[] cache lands (lot dims may shift with real
frontage) — I'll re-confirm if asked, but the interior contract already absorbs the range. C otherwise on-call.
Update 2026-07-16 (round 17, v3.2 THE TAIL) — backline handshake CLOSED + real-town type fallback
R17 §Lane C (ledger #3): close the R16-pending render-verify of D's wired amp. D landed the wiring
(ab7a8d0): band.js _addBackline reads stage.backline[0] with an immediate primitive ampPrim() fallback
(asset-free) + GLB upgrade. Handshake CLOSED — verified fresh (seed 20261990), 3 archetypes × 2 modes:
- amp plants at
backline[0]exactly (offset 0.00 GLB / 0.01 prim — rounding); 1 amp per stage (no more C primitive → the double-amp is gone). - no interpenetration — amp↔nearest-band AABB edge gaps all positive: pub 0.40/0.44 m, band_room 0.27/0.29 m (tightest; the bass extends downstage away from the up-stage amp), rsl 0.54/0.57 m (GLB/primitive). Rendered band_room gig confirms the read (amp up-stage-right behind the bassist, clear).
On-call (proactive, for A/E's real-map scout): confirmed C's interior type fallback eats real OSM shop
types. 18 real shop= values (hairdresser/bakery/convenience/supermarket/chemist/butcher/… + ''/null/
undefined/SHOP/Op_Shop/antiques) all build with 0 throws, all pathOK, resolving to opshop
(theme.js canonicalType) or a mapped alias — so a player entering any real-town shop gets a valid interior.
Venue types (pub/band_room/rsl) are recognized; everything else degrades to opshop. No C change needed for v4.
Docs-only round (verification confirmed shipped behavior). C otherwise on-call.
Update 2026-07-16 (round 16, v3.1 THE FLIP) — the backline handoff (C's half of ledger #5)
R16 §Lane C (ledger #5, C's half): unify the amp. room.stage.backline[] now exposes 2 seeded up-stage
amp slots {x,z,ry,y} (pure data, no meshes), and C's primitive ampStacks are dropped from placeStage —
Lane D plants E's guitar_amp GLB at backline[0] (its own primitive fallback under ?noassets), one amp
system. Shape published in LANE_C_PUB.md §3 (v3.1 amendment marker; rest of the
frozen doc unchanged). C→D handshake: backline shape is live.
backline[0]= stage-right(stage.x + stage.w·0.36, deckY, stage.z − stage.d·0.22)== D's R15-verified amp pose exactly, so wiring D's plant to it moves nothing on screen.backline[1]= stage-left spare (mirror). ry=π, y=deckY.- Verified fresh (port-isolated, seed 20261990): both slots up-stage, on-deck, clear of all 4 bandPoses (nearest-bass 0.87 m pub / 0.70 m band_room / 0.92 m rsl) and all watchPoints (≥1.95 m); determinism byte-identical (placement + stage + watchPoints); 0 leftover ampStack prop groups; drawSweep glb-off venues pub 122 · band_room 120 · rsl 131 (was 126/124/135 — the dropped primitives shaved ~4 draws each), worst 161 (opshop/hall, unchanged). ≤350 ✓.
- Determinism/golden safety: removing the ampStack loop only drops draws on the
stagerng sub-stream, which nothing downstream reads; theplacementsnapshot excludes ampStacks (never tracked), so it's byte-identical. Interiors aren't hashed into any golden; the?classic=1boot has no venues → untouched. - Disjoint from F's flip (interiors don't read flags) — landed early so D isn't blocked. Verify with D
once wired (ledger #5 D-half): re-render each archetype, confirm the GLB at
backline[0]doesn't interpenetrate (pending D's R16 commit).
Code: layout.js placeStage (backline[] + drop the amp loop), interiors.js API doc. theme.js untouched
(recipes carry no ampStack fitting — the primitives were placeStage-planted, not recipe fittings).
Update 2026-07-16 (round 15, v3.0: closing the release) — verified D's amp fix
R15 §Lane C (one item): verify D's guitar_amp reposition (ledger #4). D moved the plant (commit 66aec0f)
to C's §3 corrected pose exactly — band.js:236 amp.position.set(stage.x + stage.w·0.36, deckY, stage.z − stage.d·0.22). Verified (fresh port-isolated context, seed 20261990, D's real GigCrew + GLB backline):
matches §3 (both venues); bass clearance 0.87 m (pub) / 0.92 m (rsl) — the R14 interpenetration (0.29 m)
is gone; on-deck within footprint; flanks C's primitive ampStack (opposite side, 4.4 m pub / 3.5 m rsl) as a
clean up-stage backline; no clash with band/crowd/watchPoints. Rendered pub gig confirms the read (amp up-stage
behind the bassist, primitive up-stage-left, band clear). LANE_C_PUB §3 updated RESOLVED. Freeze otherwise
stands; no other C change this round. Nothing else blocks on C — F runs the release (continuity wiring + smoke,
alias deletion, week soak, tour, tag v3.0).
Update 2026-07-16 (round 14, v3.0: the release) — venue audit + docs freeze (light round)
R14 §Lane C: interior release audit, the guitar_amp ruling, and the docs freeze. C's interior code was
unchanged since R13, so venue geometry is stable by construction; the audit re-verified it against the
current tree (D's R13 continuity + RY-fix + real instrument GLBs landed) and froze the contracts. Ran an
adversarial audit workflow (10 agents: conformance + D-integration/amp + docs-freeze edit list, then a
verify pass) — it earned its keep (caught the amp defect I'd first waved through + two stale comments).
Empirical re-verify (fresh context, port-isolated server):
- Venue contract byte-identical to R13: caps 8/8/12, deckY 0.32/0.20/0.40, riserY 0.48/0.36/0.56, gigKeys gig-pubrock/grunge/covers, quiet-night = no gigKey, determinism true.
drawSweepglb-off: band_room 124 · pub 126 · rsl 135 (worst 161 opshop/hall) ≤350 — unchanged.- Gig-night draws with D's real
GigCrew+ GLB instruments (localdepot): pub 60 · band_room 52 · rsl 71 ≤350. Asset-free worst RSL 237. All 4 instruments load (guitar/bass/mic/drums). - Release-law confirmed (workflow, CONFIRMED): D's continuity + the RY fix ride a disjoint rng namespace
(
rng(citySeed,kind,id)) from C'sctx.stream(salt); the RY fix is a constant Y-rotation inbuildFigure(no rng). Cannot perturb C's placement or any golden.
guitar_amp ruling (debt for C) → Lane D: NOT blessed as-is — reposition up-stage. D plants the GLB
at (stage.x+stage.w·0.32, deckY, stage.z+0.15) — that +0.15 is down-stage, landing the 0.5 m amp 0.29 m
from the bass rig (interpenetrates the bassist, confirmed by render + 3 audit agents), and it's redundant
with C's own primitive ampStacks (placeStage plants 1–2 up-stage at (±stage.w·0.28, deckY, stage.z − stage.d·0.18)). Corrected pose in LANE_C_PUB.md §3: move up-stage-right ≈
(stage.w·0.36, deckY, stage.z − stage.d·0.22), clear of the bass and offset from C's primitives → a clean
flanking backline. Full amp unification (C exposes stage.backline[], GLB replaces primitive) = v3.1.
Cosmetic + ?gigs=1 opt-in → does not block the tag.
Tri observation → E/F (not a C gate): E's decimated instrument GLBs are 14k tris each (5 on a stage = 70k; peds ~2.7k each), so a settled RSL gig-night interior is ~182k tris. The interior gate is draws (passes at ≤71); the 200k tris is the street budget, so no violation. But F's gig-night smoke reports ~34k because it measures before the async instrument GLBs load — the real settled scene is ~5× heavier. A camera-distance instrument LOD is a v3.1 candidate.
Docs freeze (v3.0-FROZEN markers added):
LANE_C_PUB.md— retitled + frozen marker; §0 ry convention reframed past-tense (the +Z fleet was fixed at the source in D'srigs.js buildFigure, R13 — no per-consumer flips); 4-piece stated as shipped (veto language removed); the amp ruling added (§3); budgets updated to R14; in-flight handoffs removed.LANE_C_AUDIO.md— frozen marker + a scope note (this covers base interior audio; gig audiogigKeylives in LANE_C_PUB); the deferredaudioEmitteridea re-targeted to v3.1.
Code (comment-only, zero runtime/golden/rng impact): fixed two stale R12 inline return-shape comments in
interiors.js (watchPoints 6..8 → 8/8/12 caps; stage bandPoses[3] → [4] + riserY) + one in
layout.js; added a note at the gigKey hand-build explaining it's deliberately standalone (byte-identical to
citygen's gigKeyFor, not imported — keeps the interiors lib citygen-free). theme.js untouched.
Update 2026-07-15 (round 13, v3.0-beta: the district) — two more rooms + the drummer
R13 §Lane C. The district: two new venue archetypes + the 4-piece band + the RY-facing audit. Interfaces (re)published in docs/LANES/LANE_C_PUB.md — read §0 first (the pinned ry convention + the keeper/browser back-to-front finding for Lane D). Committed C-only by pathspec (the tree had E/B/D changes landing concurrently — procity-shared-tree-hazard).
Shipped (C-owned files only): theme.js, layout.js, interiors.js.
band_room+rslrecipes (theme.js, bothvenue:true), reusing the pub venue machinery with a newvenueSpecfield.band_room= small/scuzzy tin shed (cosy/pokey bias, low 3.4 m ceiling, deck barely a riserdeckY 0.2, near-empty standing floor).rsl= carpet-and-flock club (hall/wide bias, tall 4.0 m, proper stagedeckY 0.4, bistro tables, the biggest crowd cap). A converts a shop withshop.type = kind; C keys the recipe off it.SHOP_TYPESnow auto-enrols both indrawSweep/soak.- 4-piece band (debt #3):
placeStagegrowsstage.bandPosesto 4 — front-line trio (guitar/vocal/bass,y=deckY) + a drummer (role:'drums',seated:true, up-stage on the drum riser,y=riserY=deckY+0.16). Each pose now carries its owny;stage.riserYadded. Applies to all three archetypes (the pub gains a drummer too). Pure geometry — no new rng draw, determinism intact. - Per-kind venue tuning:
placeStage/placeWatchPointsreadrecipe.venueSpec→ deck size/height + crowd cap (pub/band_room 8, rsl 12 — the crowd-cap stress case). Pub values == the R12 constants, so the pub stage geometry is byte-identical (bar the new drummer). - Robust gigKey (debt #1):
audio.gigKey = 'gig-'+genreKey, genre resolvingopts.gig.genreKey→shop.genreKey(A sets it) → a kind default. Matches E's renamed manifest bedsgig-pubrock/gig-grunge/gig-coversexactly (verified: manifest has all three, zero stalepubrock-live). One key, no mapping.
RY-facing audit (debt #4) — see LANE_C_PUB.md §0 for the full write-up + fix. Headline:
- Canonical convention pinned: rig-front = local −Z at ry=0 (every C pose + the box placeholder use it).
- The GLB fleet mesh faces +Z (opposite). Verified in a fresh context two ways: toe-bone probe
(
toe−foot = (0,+0.99)on 5 peds) and dotting a fleet rig planted atcounter.stand.ryagainst the intended customer-facing (−0.99= 180° backward;+π→+0.99). - Finding: keepers + browsers have been facing 180° backward whenever the GLB fleet is loaded —
keepers.jsspawns atpose.rywith no flip, and the mesh front is +Z. The R12 band reads right only viaband.js RY_FLIP=π. Placeholder (−Z) and GLB (+Z) are opposite, so it's inconsistent both ways. - Recommended fix for Lane D (visual-only, no golden moves): normalise at the source — rotate the clone
innerby π inrigs.js buildFigureso every GLB rig faces −Z; thenkeepers.jsstays flip-free (now correct) andband.jsDROPSRY_FLIP. Repro below.
→ Lane F (flags_check.py): two smokes are RED by design — bump band == 3 → == 4. My 4th bandPose
makes the band a 4-piece (D's band.js already iterates bandPoses, so it spawns 4 today), which trips F's
R12 assertions verbatim as the round-13 instructions anticipated ("F's band==3 smoke moves to 4"):
tools/flags_check.py:674—if night['band'] == 3→== 4.tools/flags_check.py:794—if na['inRoom'] and na['band'] == 3 and na['crowd'] > 0→== 4. (The noassets gig still happens — band 4, crowd 8, 0 fetches, 0 console errors; silent-and-happy holds. It only "fails" because 3 is hardcoded.) These are the ONLY qa reds from C; the other 5 gates pass.
→ Lane D: lift each band member to pose.y (front line deckY unchanged; drummer sits at riserY);
seated:true on the drums pose → seated bob; raise band.js CROWD_CAP to the watchPoint cap so the RSL's 12
fill (I measured the full 12). And apply the §0 facing fix. Repro for the audit (paste in the interior test page):
const rigs = await import('./js/citizens/rigs.js'); const THREE = await import('three');
const fleet = rigs.loadPedFleet('models/peds/'); await fleet.whenReady;
const sp = rigs.spawnRig(fleet.all[2], { ry: 0, height: 1.75 }); sp.fig.updateWorldMatrix(true, true);
let foot, toe, wp = new THREE.Vector3();
sp.inner.traverse(o => { if (o.isBone){ o.getWorldPosition(wp);
if (/LeftFoot$/i.test(o.name)) foot = wp.clone(); if (/LeftToeBase$/i.test(o.name)) toe = wp.clone(); }});
console.log('fleet forward at ry=0:', toe.clone().sub(foot).setY(0).normalize()); // → (0, +1) = +Z
Budgets (measured, fresh context, seed 20261990):
- Empty shells (
drawSweepglb-off): band_room 124 · pub 126 · rsl 135 (worst town-wide 161 opshop/hall) ≤350. - Gig-night incl. full crowd + 4-piece: worst RSL 237 draws asset-free (16 rigs) / 77 GLB, 40.9k tris. pub 176/56, band_room 173/53. ≤350 draws / 200k tris. ✓
- 90-build venue soak: 0 determinism · 0 carve · 0 path-fail · worst 8.3 ms; leak geo0/tex0.
soak(60)in the page: PASS 60 rooms, leak 0/0, determinism identical, worst draws 85. qa figure-scale gate: 24 figs human-sized. Shot: docs/shots/laneC/rsl_gig_r13.jpg — RSL, 4-piece facing the crowd, crowd facing the stage (facing reads correct via band.js's flip).
Drummer veto (John): default is Fable's 4-piece ruling (executed). To revert to a front-line trio, drop
bandPoses[3] in layout.js placeStage (1 line) + F reverts the smoke to == 3 — say so before D wires it.
Update 2026-07-15 (round 12, v3.0-alpha) — the pub venue: archetype + watchPoints
R12 §Lane C (venues & gigs, behind ?gigs=1). Interface published in
docs/LANES/LANE_C_PUB.md — the "publish interface first" deliverable D/F build on.
pubarchetype (theme.js,venue:true): low stage + PA cabinets + amps at the back wall (newstage/paSpeaker/ampStackfittings, GLB slotsstage/pa_speaker/ampfor E), bar counter (corner— the barkeep is just a keeper), sticky-carpet floor, the blocked back doorway = the green room.room.watchPoints[]— 6–8 seeded audience floor poses{x,z,ry,dance,slotIndex}facing the stage centre (counter.standry convention).danceseeded ~⅓ true (John: "a bit of both"). Walkable, reachable, clear of keeper/browse/stage.room.stage={x,z,w,d,deckY,frontZ,bandPoses[3]}so D drops the band on the deck.room.audio.gigKey— F passesopts.gig(gig state); a gig-on pub returnsgigKey='gig-'+genreKey(gig-pubrockfor the alpha), else a quiet-night interior. F prefers gigKey over musicKey. (→ E: name the live bedgig-pubrock.)- Flags-off byte-identical: venue code runs only for
recipe.venue, on its ownstage/watchsub- streams — non-pub rooms havewatchPoints:[],stage:null, unchanged audio + placement. - Verified: 40-seed pub sweep — 0 throws · 0 carves (PA
areaFree-guarded off the counter strip) · 0 path-fails · 8 watchPoints each · dance 0.38. 60-room mixed soak (incl. pub) — 0 throws/det/path fails, leak geo0/tex0, worst 19 ms. Non-pub rooms confirmed unchanged.
Update 2026-07-15 (round 11, audio) — room.audio contract
R11 §Lane C ("one contract answer"). Confirmed Fable's proposal: buildInterior now returns
room.audio = { musicKey, toneKey }, seeded per shop — F's interior_mode plays it, B's audio.js
resolves the keys. Full contract + F wiring recipe: docs/LANES/LANE_C_AUDIO.md.
toneKey→ amanifest.audio.ambienceinterior room-tone (always set);musicKey→ amanifest.audio.musicbed, or null (no music). Keys, not files — buildInterior stays synchronous and asset-free; silence on missing/?mute=1/?noassets=1(house audio law).- Maps mirror the manifest's per-type
typesarrays (one source of truth): record/milkbar/video/dept/ arcade always play their bed; opshop/book/toy/pawn/stall are room-tone-only, except a seeded ~1-in-3 keeps a general radio on (thegeneral-tagged milk-bar bed). - Verified (9 types × 8 seeds): 0 bad keys, 0 nondeterminism, every key resolves + matches manifest
typesmembership; deterministic per shop.qa.sh --strictgreen. - Optional visual-source prop (
userData.audioEmitter) deferred to v2.2 (in the note) — out of round scope.
Update 2026-07-15 (round 9) — ship v2.0: browse points (C→D→F seam) + buy-anywhere
Round-9 §Lane C. The round hangs off C1 (two lanes build on it), so it shipped first as a handshake + working code; C2 widens buying beyond the dig.
C1 — browse points (the round's one cross-lane seam). layout.js placeBrowsePoints() exposes
room.browsePoints[] — 1–3 deterministic floor poses in front of browsable fittings, facing the goods,
where Lane D stands browser rigs (F wires occupancy → points). Same coord frame + ry convention as
counter.stand, so D's fleet clones drop straight in. Pure data — no meshes, no draws; the ≤350 law
is untouched by C1. Guarantees (all asserted headless): walkable (occ-free, not reserved), reachable from
spawn (the door→counter flood-fill), ≥0.6 m apart + clear of the keeper stand, deterministic per
shop.seed, ≥1 point in every archetype that has a browsable fitting. A rich candidate ring (4 sides × 2
gaps + diagonals) + a nearest-free-cell fallback covers tight/mismatched rooms. Interface published in
LANE_C_NOTES.md before code (decision #1 half-day handshake) → committed
first so D/F unblock. Validated: 180 type×archetype×seed combos → 0 empties (177×3 pts, 3×1 via
fallback), determinism 0 fails, min-sep 0.7 m, 0 keeper clashes. Test page: cyan browse-post markers on the
path overlay + a HUD count.
C2 — buy-anywhere (decision #2). Book spines + toy boxes get pull-and-buy over the same wallet;
records keep the deep dig. New stockpack.buildBuyableShelf() builds each real shelf as merged,
per-item-addressable meshes — one mesh per atlas (all covers on a shelf drawn from a single seeded
atlas → one draw/shelf), tagged noBatch so batch.js leaves them intact. Aiming a spine/box raises a
pull card (title / author-or-maker / $price / band from the pack index); BUY debits the wallet, adds to
inventory, and collapseBuyItem() zero-areas that item's quad in place (removed from the shelf, one
draw, no geometry churn). Parody stock has no buy mesh → no card ("not for sale"). Fail-soft per pack.
Validated: raycast→card→BUY works end-to-end (item collapses to its centre, cash/inventory update, card
closes); buy-soak 6/6 bought, collapseOk, leak geo 0 / tex 0; draws with ?stock=real record 41 ·
book 58 · toy 57 (≤350); full drawSweep unchanged (GLB-off 168 · GLB-on 344, both pass); placement
determinism 0 fails. Shot: browse_buy_r9.jpg.
→ D: consume room.browsePoints (contract in LANE_C_NOTES) — stand rigs at the first
min(occupancy, 3) points. → F: two new warn smokes (hooks in LANE_C_NOTES): browse-points present +
valid, and shelf-buy (aim → card → wallet debit → collapse). C's soak covers keeper+stock+dig+buy together.
Update 2026-07-15 (round 8) — the town comes alive: buy loop v0 + book/toy real stock
Round-8 §Lane C, theme "the town comes alive". Two deliverables + a verify pass.
C1 — buy loop v0 (new web/js/interiors/wallet.js). A shallow, runtime-only economy: seeded
starting cash, purchases, an in-memory bag. Reload resets; world generation is untouched (purchases
never enter the plan or the seeded room build — the wallet is a separate object the shell reads, never
writes back into gen).
createWallet(seed)→{ cash, start, canBuy, buy(offer), inventory, count, onChange }. Start is seeded ~$60–200 (mulberry32).buydeducts + records on success, returns false unchanged if broke.onChangelets F's street shell subscribe the same wallet to a cash chip.- Test page (
interior_test.html): a#cashChipHUD ($163 · 🛍 N, street + interior) + an inventory panel toggled with I. The dig's pull card gets a BUY IT button (price + band); buying pulls the sleeve into the bag, decrements cash, updates the chip, and depletes that bin (bought bins offer fewer sleeves on re-riffle). Broke → the button no-ops (can't buy).digSoaknow buys up to 2/visit. - Offer shape is the dig offer (
{a,t,price,s}) OR a pack item ({title,artist,price,price_band}) — onebuy()eats both.
C2 — book spines + toy boxes wired to ?stock=real. E shipped the generalized packs
(stock_book_* 311 items / 4 atlases, stock_toy_* 273 items / 5 atlases, alongside stock_record_*
350/6). stockpack.js now keys each pack to its slot via SLOT_FOR = {record:'sleeve', book:'spine', toy:'box'}; stock.js shelfLine consults the adapter and builds real cover planes for book spines
(bookshelves) and toy boxes (cube shelves) — static shelf stock, no dig for those, fail-soft per pack.
Every real mesh samples its atlas material with a baked per-item UV, so a whole shelf still batches to one
draw per atlas.
C3 — validated (all three packs live, ?stock=real):
- Draws ≤350 (the law): record 41, book 42, toy 51 worst over seeds {1990,7,42(,101)}.
- Determinism: same seed → identical draw calls + mesh count, all three types (0 fails).
- Leak-free: 0 geometry/texture delta over 20 build/dispose cycles; buy-soak 12 record-dig visits,
5 bought (wallet depletes to broke → exercises the can't-buy path),
leakGeo 0 / leakTex 0; resting geo back to baseline after close. Atlas textures shared/cached (not per-room); per-item geo ctx-tracked & disposed. - Reload resets the wallet (fresh boot → seeded start again); flag-off +
?noassetsuntouched. - Shot: buyloop_r8.jpg — toy boxes, book spines, record dig → BUY card.
→ F: two new warn-level smokes (hooks in LANE_C_NOTES.md): (1) buy loop —
createWallet deduct/broke/inventory is pure and headless-testable; (2) book/toy packs — enter book or
toy shop with ?stock=real, assert isStock meshes on a shared atlas material (drawSweep ≤350). Buy UI is
test-page-local; the reusable piece for the street shell is wallet.js + onChange.
Update 2026-07-14 (round 7) — ?stock=real: real GODVERSE sleeves in the crates
Round-7 §Lane C: the stockAdapter seam built in v1 finally eats. Lane E's GODVERSE stock pack (parody-
transformed Discogs metadata + cover atlas) feeds record-shop bins + the dig riffle behind ?stock=real.
- Contract handshake (task 1, unblocked E): confirmed E's index schema as-is in
LANE_C_NOTES.md —
{ items:[{id,title,artist,price,price_band,atlas,uv}] }, UV origin top-left (C flips V for WebGL), fail-soft, per-type packs. - New
stockpack.js:preloadStockPack/getStockPack(cached) +makeStockAdapter. Atlas texture + material are shared/cached (like depot GLBs); sleeves are cover PLANEs sampling the atlas at each item's baked UV → all real sleeves in a room batch to one draw (record: 59 draws). - Consumers:
stock.js binFanbuilds real cover planes (else parody canvas);dig.jsriffles real covers and the pull card shows title / artist / $price / band;interiors.jswiresopts.stock='real'through the seam (warn-once + parody fallback if the pack is absent); test page reads?stock=real. - Validated (sample 24-pack): determinism 0 fails/30, leak-free rooms AND real dig (0/0 over 25 cycles —
atlas shared, sleeve geo disposed), draws ≤139 all rooms (record 59), fail-soft on no-pack types (opshop
ok → parody),
?dig=1&stock=realleak-free. Flag-off +?noassetsuntouched (procedural path byte-identical). - Built against E's staged sample (real index format, synthetic covers); real Discogs covers drop in unchanged when E gets the DB (external — John/Fable). Shot: stock_real_r7.jpg.
→ F: new smoke for your strict harness — ?stock=real enter record shop + open dig → assert real
(non-parody) sleeves; the record bin's real sleeves are cover planes with userData.isStock on a shared
atlas material (drawSweep stays ≤350). Hook + adapter in LANE_C_NOTES.
Update 2026-07-14 (round 6) — C1 interior draw batching (≤350 draws/room is law)
Round-6 §Lane C task 1 (the review debt: book room was 1,245 draws GLB-on). Diagnosed the two
drivers — (a) multi-material stock boxes cost one draw PER face (6×), (b) one mesh per stock item —
then batched (new web/js/interiors/batch.js):
- stock.js: boxes are now single-material; every stock mesh tagged
userData.isStock. - batch.js (
batchRoom, called from interiors.js before the GLB upgrade): two-tier merge — room-level merge of allisStockacross every fitting (grouped by material, world transforms baked)- per-fitting merge of fixture frames (kept in-group so bins stay raycast targets and the GLB
upgrade can hide the fixture).
mergeGeometriesfrom vendored BufferGeometryUtils.
- per-fitting merge of fixture frames (kept in-group so bins stay raycast targets and the GLB
upgrade can hide the fixture).
- glb.js: frame-hide is now by
isStocktag (not frame-count), so batched fixtures hide and the room-level stock rides on the GLB.
Result (drawSweep, all 9 types × 6 archetypes, seed 1990):
- GLB-off worst 152 (opshop/hall), GLB-on worst 313 (opshop/hall) — book 1,245 → 189. ≤350 ✓.
- Determinism 0 fails / 810 builds · leak-free · dig still riffles batched bins (bins keep frame as hit target; dig regenerates its own sleeves) · worst warm build 38ms (<50) · visually identical.
- Sweep now ASSERTS ≤350 (
drawSweep()in the test page, folded into the soak;window.PROCITY_C. drawSweepfor F's harness — see LANE_C_NOTES for the hook).
C2 — map the 3 orphaned fittings (published R5, unconsumed). In glb.js: glassCase→glass_case,
magazineRack→magazine_rack, and a NEW crateStack primitive (stack of crates) → crate_stack.
Recipe placements: crateStack in record + opshop (back-room dressing), glassCase added to milkbar,
magazineRack added to pawn (+ moved milkbar's magazineRack window→wall to dodge the door-counter).
Also fixed placeNearCounter — it picked one random side and gave up, so counter-zone fittings
(the glass case) rarely landed; now it tries both sides + offsets, then falls back to a free aisle spot.
All 3 load + render planted/scaled/facing right (yaw 0). Appearance: glassCase 12/12 (toy/pawn/milkbar),
magazineRack 12/12 (milkbar/pawn), crateStack ~11/12 (record/opshop). Contact sheet:
glb_orphans_r6.jpg. drawSweep still ≤350 with them on (worst 177
GLB-on). C3: dig soak re-run after batching + new fittings — riffle works on batched bins, leak-free.
Round 6 complete for Lane C: C1 (batching, 35981ad) + C2/C3 (orphans + dig re-verify) committed.
Draw law satisfied (≤313 was the tightest; ≤177 after C2), determinism/leak/dig all green, qa strict green.
Update 2026-07-14 (round 5) — C1 v1.1 re-map (record_crate + counter_till) · F can tag v1.1
Round-5 §Lane C task 1 (the v1.1 gate). Lane E's re-exports mapped + validated in glb.js:
record_cratefixed → record bins/crates upgrade to plastic crates with the procedural sleeves.counter → 'counter_till'(the real ~1.6 m counter-with-till). Keeper-stand pose unaffected.- Double-till resolved: cash_register attach dropped when counter_till is live (it has its own till); falls back on the primitive; milkshake_mixer stays on the milk-bar bench (lifted to the GLB height).
- Sweep + soak (GLB on):
0 throws / 0 path / 0 det / leak-free / worst 10.4ms; 0 double tills. Primitive path unchanged (glb.js-only edit). Contact sheet: glb_v1_1_r5.jpg. - Report + decision detail: LANE_C_GLB_VALIDATION.md; F note in LANE_C_NOTES.md. → F: C1 committed, v1.1 can be tagged.
C2 — ?dig=1 crate-riffle (v2, behind flag). Ported 90sDJsim dig.js → web/js/interiors/dig.js:
walk to a record bin, press E, riffle its sleeves (scroll/drag), click the front to pull, BUY IT
(STUBBED — toast + remove; getCash constant; real economy is round 6+). Riffle items are seeded per bin
via the stockAdapter(shop,'sleeve') seam (procedural fallback now — parody 90s-AU sleeves). Own scene +
camera, procedural fwip/thunk audio, leak-free (per-open resources freed on close → dig soak 20×
leakGeo 0 / leakTex 0). Flag-off (?dig=0) is byte-identical — buildInterior is untouched; the dig
is wired only in the test page (F owns the shell seam — hook documented in LANE_C_NOTES §"input/mode hook").
Shot: dig_riffle_r5.jpg.
C3 — interior re-measure after Lane E's bake (c522464). E baked the 6 heavy hero props to ≤8k tris.
Re-measured every interior (cache-busted to bypass the dev browser's stale HTTP cache): record 43.6k
(the C3 target, ~273k pre-bake), milkbar 38.9k (was the flagged 468k → over-budget flag CLOSED), all
rooms ≤107k tris. book is the draw-call outlier (1,245 draws from ~9 bookshelf GLBs + primitive spines) —
tris fine, interiors render one-at-a-time so gate 3 unaffected; instancing spines is the future lever.
Full table in [LANE_C_GLB_VALIDATION.md](docs/LANES/LANE_C_GLB_VALIDATION.md §C3).
Round 5 complete for Lane C: C1 (v1.1 re-map) + C2 (?dig=1) + C3 (re-measure) all committed.
Update 2026-07-14 (round 4) — hero props into rooms
Round-4 task (ROUND4_INSTRUCTIONS §Lane C): map the 4 new hero props + cash_register (manifest now 14
fittings), sweep/soak with them on, screenshot each, delete the stale _local_glb/, measure a hero
room. All done. Full report for Lane E: LANE_C_GLB_VALIDATION.md;
contact sheet docs/shots/laneC/glb_heroprops_r4.jpg.
- New mappings (
glb.js):arcade_cabinet→newarcadeCabinetprimitive (video focal prop),listening_booth→listeningCorner(record),drinks_fridge→fridge(milkbar). All load,glb_lawcompliant, yaw 0 (no correction). A new counter-top attach (attachCounterTop) putscash_registeron every counter's till slot (hides the primitive till) andmilkshake_mixeron the milk-bar benchtop — the counter primitive + keeper stand are untouched. - Focal-prop reliability fix: the door-counter path loop was pulling priority-4 focal props (arcade appeared in only 5/24 video rooms). Raised the 3 focal props to priority 6 + placed-first → 24/24 appearance, still 0 carves / 0 path-fails.
- Validation: primitive sweep 810 builds
{throws 0, pathFail 0, detFail 0}; GLB-on soak{throws 0, path 0, det 0, leakGeo 0, leakTex 0, worst 7.3ms}; all 5 new props confirmed in-room. - Room tris measured (task 4): milk-bar GLB-on ≈ 468k tris (over budget) driven by
drinks_fridge(77k) +milkshake_mixer(73k) — reported to Lane E as the #1 bake-pass target (not fixed, per integrator decision #2; interiors render one-at-a-time so gate 3 / the street budget is unaffected). - Cleanup: deleted
web/assets/_local_glb/(R3 validation copy); test page now loads GLBs via the livedepot:path (?localdepot=1still works for offline).localBasehook kept inglb.js. - Standby (C task 5): when Lane E re-exports
record_crate(broken) + a ~2 m counter-with-till, I re-map both. Not landed at commit time.
Update 2026-07-14 (round 3) — GLB hero-prop visual validation
Round-3 task (ROUND3_INSTRUCTIONS §Lane C): flip useGLB on and validate Lane E's fitting GLBs in real
rooms. Done — full report for Lane E in docs/LANES/LANE_C_GLB_VALIDATION.md.
Verdict: 5 of 9 fitting GLBs are good and now live behind useGLB (wire_shelf, clothes_rack,
bookshelf, cube_shelf_wide, work_table); 2 fall back to primitive with a report filed
(record_crate broken — won't load; counter too long/no till → primitive keeps the till +
keeper stand). record bins stay primitive until Lane E re-exports the crate.
Soak with useGLB on (50 rooms, all types): throws 0 · pathFail 0 · determinismFail 0 · leakGeo 0 · leakTex 0 · worst build 10.5ms · 256 upgrades applied — GLB resources dispose leak-free on room exit.
glb.js changes (my file):
manifest.localBase— load GLBs from a served local dir for offline validation (production still usesdepot:<file>; forward-compatible with Lane E's local-depot mode).- Real-metre scale, clamped to the reserved footprint (was fit-to-width, which squashed tall props);
matches
glb_law(metres). Per-kind yaw correction (KIND_TO_YAW) for the Z-major assets (bookshelf/cube/work_table need +90°) per the manifestfacingconvention ("Lane B/C rotate directional props"). room.glbReady(interiors.js) — a promise that resolves when GLB swaps settle, so callers can await before screenshot/leak-check (Lane F can use it in the §3.4 wiring).- Test page (
interior_test.html) gains a GLB props toggle + aGLB props: N upgradedHUD line. Local validation needs the GLBs copied in:cp pipeline/_normalized/procity_fit_*.glb web/assets/_local_glb/(untracked; real GLBs live on the depot). Screenshot grid: docs/shots/laneC/glb_validation_grid.jpg.
Update 2026-07-14 (b) — keeper stand pose (resolves Lane F §8 · unblocks §3.2/§3.3)
Lane F's runbook asked (LANE_F_NOTES §8): "does places tag the counter slot with a stable
kind:'counter' so keepers find it?" Yes — the counter interactable has always carried
mesh.userData.kind === 'counter' (+ interactable:true). But Lane D's keeper needs more than a
tag: keepers.spawn(group,{x,z,ry}) needs where the shopkeeper stands and which way it faces
(keepers.js:28 — "ry = the counter's outward facing"). I computed the customer-facing side
internally but never surfaced it — so Lane D would have read the bench's raw transform and stood a
keeper embedded in the counter, possibly facing a wall. Closed that gap (my files only, purely additive):
- New:
room.counter = { mesh, pose:{x,z,ry}, stand:{x,z,ry} }(interiors.js).poseis the bench;standis the keeper pose — behind the bench on the service side, facing the customer, clamped inside the room, in room-local space (the same space Lane D adds the keeper into viainterior.group).ryuses the rig-front = local −Z convention keepers.js expects. - The same
{x,z,ry}also rides the counter interactable:place.userData.keeperStand— for theplaces-iteration path Lane D/F described. Deep-equal toroom.counter.stand.
Lane D/F wiring (drop-in for §3.2/§3.3):
const room = buildInterior(shop, THREE);
const s = room.counter.stand; // or: room.places.find(p=>p.userData.kind==='counter').userData.keeperStand
keepers.spawn(room.group, { ...s, shopId: shop.id, type: shop.type });
// on exit: keepers.remove(handle) / keepers.disposeAll(), then room.dispose()
Verified in-browser (9 types × 4 seeds = 36 builds, + 50-room soak):
stand in-bounds ✓ · faces-customer ✓ (dot>0 for all) · places-carries-keeperStand ✓ (deep-equal) · stand deterministic ✓ · placement determinism unchanged ✓ · soak avg 3.6ms / worst 7.7ms · leak geo0 tex0 · pathFail 0 · every room has a counter+stand. No change to layout/placement of existing rooms.
Minor known cosmetic (corner counters): a corner counter sits ~0.55 m off the east wall, so the service gap (~0.25 m) is narrower than a person — the keeper stands at the till with a small hidden overlap into the solid waist-high bench (torso reads above the counter, legs behind it). Facing and side are correct; it reads as "keeper at the till." If Lane D/Fable want real standing clearance I can nudge corner counters ~0.2 m off the wall — but that changes those rooms' committed placement + screenshots, so it's a decision, not a bug.
Two contract-drift corrections for the F runbook (§2's Lane-C line was written against an earlier
draft of my API): (1) there is no opts.registry — registry override is the one-time global
mergeRegistry(registry) (a per-build registry mutation broke same-seed determinism; removed in the
review pass). (2) recipe.counterPos is a placement-strategy string (door|forward|back|corner),
not a world position — use room.counter.stand for the keeper pose.
What shipped
All files under my lane's ownership (web/js/interiors/*, web/interior_test.html) — no other lane's
files touched. Lane A's core/registry.js hadn't landed, so the lane is fully standalone (its 9
type recipes live in theme.js) with a mergeRegistry() hook for Lane F to wire the registry later.
| file | role |
|---|---|
| interiors.js | public API — buildInterior(shop, THREE, opts) |
| theme.js | 9 shop-type recipes (archetype/wallpaper/floor bias, clutter, counter pos, fittings mix, stock kind, signage) + aliasing + registry-override hook |
| shell.js | room shell: floor/walls/ceiling from lot×archetype, glazed shopfront + street backdrop, blocked back doorway, interior lighting |
| fittings.js | parametric fittings kit (ported from 90sDJsim + extended): bins, crates, racks, 4 shelf types, VHS aisle, glass case, counter+till, fridge, magazine/spinner racks, armchair, escalator, pegboard, barred window, returns slot, art frames |
| layout.js | the placer: per-archetype zones, shuffled wall-slot system, occupancy grid, guaranteed door→counter flood-fill path |
| stock.js | v1 visual stock: pooled canvas-texture sleeves/spines/boxes/garments/snacks with price stickers (dig.js trick) + stockAdapter hook for BaseGod data |
| context.js | build/dispose/seed backbone: independent seeded sub-streams, shared-geometry cache, leak-free disposeAll() |
| glb.js | optional GLB hero-prop upgrade — reads Lane E's manifest.json, swaps primitives for depot GLBs, placeholder-persists. OFF by default; primitive fallback always |
| interior_test.html | standalone test page (below) |
Public API (the contract Lane B/F build against)
import { buildInterior } from './js/interiors/interiors.js';
import * as THREE from 'three';
const room = buildInterior(shop, THREE, opts?);
scene.add(room.group);
// … player walks; on hitting room.exits[i], fire procity:exitShop …
room.dispose(); // frees ALL per-room GPU resources; removes group from parent
// room = {
// group, // THREE.Group, self-contained (lights included)
// spawn: { x, z, ry }, // enter just inside the door, facing −Z (into the shop)
// exits: [ { x, z, w, toStreet } ], // door back to street
// places: [ …meshes w/ userData ], // interactables: counter, bin, case, fridge, returns, exit
// counter: { mesh, pose, stand }, // pay point. stand:{x,z,ry} = keeper (Lane D) spawn pose
// dims: { W, D, H, archetype, type },
// placement, // deterministic summary (deep-equal per seed)
// pathOK, buildMs, counts(), dispose()
// }
shop is tolerant — recognized fields { id, type, name, seed, storeys, lot:{w,d} }:
- seed (uint32) is the ONLY randomness source; falls back to a hash of id/name/type if absent.
- type — any CITY_SPEC type (
record opshop toy book video pawn milkbar dept stall) or a thriftgod/Overpass alias (music charity pawnbroker market video_games …); defaults toopshop. - lot
{w,d}metres — the room adapts to it (never exceeds it); absent → archetype ranges decide. - storeys drives ceiling height 3.2–4.5m.
opts:{ archetype?: 'cosy'|'gallery'|'wide'|'hall'|'pokey', stockAdapter?, useGLB?, manifest?, manifestUrl? }. Registry override is a one-time global —import { mergeRegistry } from interiors.js; mergeRegistry(registry)at init (NOT a per-build opt — a per-call mutation of the shared recipe table would break same-seed determinism).
Coordinate convention (shared with a future Lane B wiring): +Z = door/street side, −Z = back,
origin = room centre, floor y = 0. Everything is in the returned group's local space.
For Lane B (door wiring, later)
buildInterior is a pure function — call it on procity:enterShop, add room.group to the scene,
teleport the player to room.spawn. On procity:exitShop (player reaches room.exits[0]) call
room.dispose() and restore the street. The street window fake is Lane B's; I own the inside view
(a bright canvas street backdrop plane is already behind the glazing).
For content phase (BaseGod / GODVERSE)
Pass opts.stockAdapter = (shop, slotKind) => ({ texture }) | ({ mesh }) | null. Returning a texture
skins that stock item's face; a mesh replaces the item; null → procedural placeholder. No layout/
fittings changes needed. slotKind ∈ { sleeve, spine, box, snack, magazine, garment, treasure }.
Test page — web/interior_test.html
Serve: cd web && python3 -m http.server 8130 → open /interior_test.html.
- seed input · type dropdown (9) · archetype override (auto + 5) · lot
w×d/auto · re-roll · seed ± - first-person walk (PointerLock + WASD) with occupancy-grid collision + room clamp
- debug toggles: wireframe (F), occupancy grid (G), path (spawn/counter markers)
- ▶ 50-room soak test: builds+disposes 50 seeded rooms, asserts ms/room, leaked geometries vs baseline, and determinism (same seed → identical placement)
Acceptance — all met
| requirement | result |
|---|---|
| Same seed twice → identical room (deep-equal placement) | ✅ 0 mismatches / 810 builds |
| All 9 types × 5 archetypes render sensibly | ✅ contact sheet docs/shots/laneC/_grid_9types_5archetypes.jpg |
| Build < 50ms/room | ✅ steady-state ~4ms, clean-soak worst ~8ms (first-ever build ~60ms one-time cold-JIT; occasional ~60ms GC pauses only under back-to-back torture loops — not a real per-room cost) |
| Dispose leak-free (info.memory → baseline) | ✅ soak leak: geo 0, tex 0 |
| Flood-fill door→counter always exists | ✅ 0 path-fails, 0 carves / 810 builds |
| Runs with zero assets & zero network | ✅ seeded flat-colour fallback under every texture; map only assigned on load |
Full sweep (9 types × {auto+5 archetypes} × 3 seeds × {null,4×4,5×14,20×20,3×3} lots = 810 builds):
throws 0 · pathFail 0 · determinismFail 0 · carved 0. Warm perf: all < 50ms (worst 33.2ms).
Soak (50 rooms): avg 4.0ms · worst 9.6ms · leakGeo 0 · leakTex 0 · detFail 0 · pathFail 0.
Key decisions & notes for review
- 90sDJsim
fittings.jsported (it was written to be lifted):clothesRack,shelfUnit,recordBin,table,counter,pegboard, garment-silhouette canvas trick. Extended with the fittings CITY_SPEC's registry needs (cube/metal/book/VHS shelving, glass case, fridge, spinner, magazine rack, armchair, escalator prop, returns slot, barred window, listening corner). - thriftgod
buildShopported: the 5 archetypes, the shuffled wall-slot system, bins with fanned sleeves wearing covers, corner counter+till, crooked wall art, blocked back doorway. - Determinism via independent seeded sub-streams keyed by a salt string (
ctx.stream('wallpaper')etc.) so adding a fitting in one subsystem never shifts another subsystem's picks. - Leak-free via a tracked build context: per-room geometry/material/CanvasTexture/tiled-file-texture
are freed in
dispose(); globally-cached file textures (core/loaders.loadTex) are shared and not disposed (bounded cache). Dispose-before-texture-load race is guarded (_disposed). - Perf: shared-geometry cache (
ctx.boxGeo/planeGeo/cylGeo) collapses the hundreds of identical stock items in a room down to ~80–110 geometries; spine density tuned so a book barn is ~600 meshes. - GLB upgrades:
web/assets/manifest.json(Lane E) is read if present, primitive fallback always. Not present yet → 100% primitives, which is why the test page runs asset-free.
Integration with sibling lanes (landed in parallel)
- Lane A
core/registry.js— itsSHOP_TYPE_IDSmatch my 9 recipe keys exactly. I kept the interior recipes local (the registry doesn't carry archetype-bias/clutter/counter-pos/stock detail), withmergeRegistry()as the one-time override seam for Lane F. - Lane E
web/assets/manifest.json— itsfittingssection (9 depot GLBs with footprints) is now wired throughglb.js(opt-inuseGLB, primitive fallback). Depot GLBs may not be uploaded yet; verified the fallback (unreachable depot → primitives persist, no crash, no leak). - Lane D
citizens/keepers.js— itskeepers.spawn(group,{x,z,ry,shopId,type})now has a clean source:room.counter.stand(also onplace.userData.keeperStand). See the Update (b) section at the top. Verified the pose is on the service side, faces the customer, and is deterministic.
Adversarial review — 5 findings, all fixed
Ran a 5-dimension adversarial multi-agent review (determinism / disposal-leaks / path-guarantee / three-api / edge-robustness) with a per-finding verify pass. 5 real defects surfaced; all fixed and re-verified (830-build sweep + 50-room soak green after each fix):
| # | sev | file | defect | fix |
|---|---|---|---|---|
| 1 | med | interiors.js | per-build opts.registry permanently mutated the shared recipe table → latent same-seed determinism break across differing registry states |
removed per-build option; registry override is now a one-time global mergeRegistry() (Lane F setup) |
| 2 | low | interiors.js | explicit null opts threw (opts = {} default only covers undefined) |
normalize opts = opts || {} |
| 3 | low | shell.js | empty archetypeBias array → bias[0][0] on [] throws |
fall back to uniform bias when missing or empty |
| 4 | med | layout.js | fittings pulled by the path loop left phantom interactables in places (dangling after dispose) |
collect places from survivors at the end, not incrementally at placement |
| 5 | low | layout.js | rebuildOcc re-stamped wall-mounted fittings that placeAtWall never stamped → phantom floor obstacles after a blocker-pull |
mark wall-mounted noStamp; skip in rebuildOcc and exclude from the removable pool |
Verify evidence (post-fix): nullOpts ok · phantomPlaces 0/332 · determinismAcrossBuilds identical · emptyBias ok · sweep 810 {throws 0, pathFail 0, detFail 0} · soak {avg 4.2ms, worst 8ms, leakGeo 0, leakTex 0}. (Reviewers also confirmed no disposal/leak or three.js-contract defects.)
Not done (out of scope for v1 / depends on other lanes)
- Real item data / economy (content phase —
stockAdapterhook is ready). - GLB hero props are wired (
glb.js, opt-in) but not visually validated end-to-end — needs the depot GLBs actually uploaded (Lane E) +useGLBflipped on by Lane B/F. Primitive path is the tested baseline. - Interior-mapping shader for street windows (Lane B stretch goal).
- Back-room beyond the blocked doorway (v2).