PROCITY/F-progress.md
m3ultra 8c275c711b Lane F R11: wire interior audio beds + audio smokes + gate scope
Interior audio (interior_mode): enter -> PROCITY.audio.playInterior(room.audio) with Lane C's
{musicKey,toneKey} (fade in), exit -> stopInterior() (0.7s fade). Guarded/no-op when
muted/noassets/pre-gesture -- silent-and-happy.

New smoke_audio gate (flags harness, fresh Chromium per the R10 standing law): pre-gesture silence
(ctx not unlocked), 0 boot errors, ?mute=1 muted, ?noassets=1 zero audio fetches, enter/exit
AudioNode leak-free (12 cycles, 8 shops with a music bed, live sources bounded at 5). Gesture via
keyboard -- a mouse-click gesture tripped PointerLockControls in headless.

scaffold_check: scoped the Math.random generation-ban to exempt runtime FX PLAYBACK
(web/js/world/audio.js) -- Lane B's footstep SFX variant/gain jitter is transient, not world
generation (which shop plays which bed is seeded upstream in interiors.js). Noted to B.

Soak with audio on PASS (heap 48MB stable, 0 errors, no drift). qa.sh --strict GREEN 6/6.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 17:13:19 +10:00

322 lines
29 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# PROCITY — Lane F progress (INTEGRATION & QA)
**Session:** 2026-07-14 · **Author:** PROCITY-F · **For review by:** fable
---
## Round 11 (the audio round) — DONE ✅ (v2.1)
John greenlit "generate freely". E generated the pack (23 assets, `web/assets/audio/`, 8.7 MB ≤ 25),
B built the WebAudio engine (`audio.js`, self-unlocking, self-ticking), C added `room.audio =
{musicKey,toneKey}` to buildInterior. **Lane F wired + verified + tagged v2.1:**
| task | result |
|---|---|
| **F1 — interior beds** | ✅ `interior_mode` enter → `PROCITY.audio.playInterior(current.audio)` (fade in), exit → `stopInterior()` (0.7 s fade). Guarded/no-op when muted/noassets/pre-gesture. 2 lines, the seam B/C designed for. |
| **F2 — audio smokes** | ✅ new `smoke_audio` in the flags harness (fresh Chromium): **pre-gesture silence** (ctx not unlocked), **0 boot errors**, **?mute=1** muted, **?noassets=1 zero audio fetches**, **enter/exit leak-free** (12 cycles, 8 shops with a music bed, live AudioNodes bounded at 5, made 42). Caught + fixed my own gesture-via-mouse-click tripping PointerLockControls in headless → gesture via keyboard. |
| **F3 — soak + tag** | ✅ soak with audio on **PASS** (heap 48 MB stable, 0 errors, no drift). `qa.sh --strict` **GREEN 6/6**. **v2.1 tagged + pushed.** |
**Cross-lane (F's own gate, noted to B):** B's `audio.js:143,145` use `Math.random()` for footstep SFX
variant + gain jitter — legitimate transient runtime FX (which shop plays which bed is seeded upstream
in `interiors.js`), but it tripped F's generation-determinism `Math.random` ban. F **scoped the ban
correctly**: `web/js/world/audio.js` is runtime *playback*, not generation → exempted in
`scaffold_check.mjs` (B may instead add a `// cosmetic` marker; either is fine).
---
## Round 10 (fix the R9 giant-rig blocker, re-shoot, re-tag) — verify + ship
R9 shipped a blocking regression: interior rig figures came out ~2× room scale (keeper head 3.83 m in
a 3.4 m room) because `rigs.js buildFigure` normalized off the head bone's **bind-pose** Y. **Lane D
fixed it** (R10, commit `3432fcc`): normalize by the feet→crown **span**, plus a new `qa.sh --strict`
no-giants gate (`interior_scale_check.py`). Lane F verify + re-shoot + soak + re-tag:
| task | result |
|---|---|
| **F1 — verify the fix** | ✅ `qa.sh --strict` **GREEN 6/6** incl. the new scale gate: **24 interior figs / 6 shops all crown ∈ [1.4,2.0] m and under-ceiling** (Second Time Redfern, the giant scene: 1.661.86 m). Live-measured too — the 3.83 m I first saw was a **stale ES-module cache in a day-old browser tab**, not a regression (served + fresh-chromium code both correct). |
| **F2 — re-shoot** | ✅ `browsers.png` re-shot: browser at the rack + keeper at the counter, **human-sized** (*figures human-sized vs door/fittings: yes*). `night_crowd.png` re-framed to a readable "lit VIDEO REGAL amid CLOSED town" — **see the honest note below.** |
| **F3 — soak** | ✅ full-default soak **PASS**: 37 chunks, 20 shops, **heap 54 MB stable (no perf drift** from the keeper fix — it's a build-time scale calc), draws 248, **0 errors**. Tri budget still ~327k > 200k (pre-existing rig-model item, D/E decimation — not an R10 change, not the blocker). |
| **F4/6 — docs** | ✅ tour README updated with stable v2 filenames + captions; this addendum is the R10 release note. |
| **F5 — tag** | ⏸ `v2.0` annotated tag staged on the fixed commit; **holding the push for John** given the night_crowd finding (a public release + his flagged money shot). |
**Honest finding — `night_crowd` has no crowd, and it can't without new work.** John flagged it as
near-black/empty. Re-framed it readable (clean night, tight on the neon). But a visible *street* crowd
does not form: Lane D's night patronage parks ducked-in patrons **hidden inside** the shop (they are
the interior browsers in `browsers.png`), and seed 20261990's video shop is a **low-traffic corner**
(tested default streamed roster + patronage-on at night: **0 patronage, 0 visible peds** at the shop).
This is not a framing bug and not (clearly) a regression — it's that the "night crowd" was always the
*interior* presence. A visible outdoor queue is a **v3 Lane-D mechanic** (loiter-outside-the-openLate-
shop). The ~20 fps in night captures is a **headless software-GL artifact** (128 draws / 65k tris is light).
→ Flagged for D: confirm night patronage at the openLate shop still behaves as the v2.0-rc notes claim.
---
## Round 9 (the v2.0 ship round) — DONE (v2.0 tag pulled, re-tagged in R10)
All lanes landed (C browse points + buy-anywhere, D browser rigs, A CITY_SPEC freeze + closing-time
ruling, E VRAM audit + half-res book/toy atlases, B tram + tour bookmarks). F wired the one cross-lane
seam, graduated the harness, shot the tour, soaked, and tagged the epoch.
| task | status |
|---|---|
| **F1 — interior presence (C→D→F)** | ✅ `interior_mode` reads `occupancyOf(shop.id)` (closure to `citizens`) and spawns browser rigs at C's `room.browsePoints`, one per patronage occupant, **cap 3**; freed by `exit()`'s `keepers.disposeAll`. Door points carry `shopId`. Verified: enter occupied "Second Time Redfern" → 1 browser == `min(occ,3)`, disposed clean, 0 errors. |
| **also wired** | ✅ **Tram** (`?tram=1`, `createTram`+update). **Buy-anywhere** (C2): E at a book-spine/toy-box `buyMesh` buys over the wallet (`collapseBuyItem`+toast); preload book+toy packs. Verified: bought a book spine, cash 191→177. |
| **F2 — smokes to strict** | ✅ buy-v0 / patronage / book-toy / katoomba **graduated warn→STRICT**. New warn smokes: interior-presence, shelf-buy, tram. Fixed the book-stock check to count `isStock OR buyMesh` (C2 tags spines `buyMesh`). `qa.sh --strict` GREEN 5/5, harness 0 fails / 0 warns. |
| **F3 — tour + soaks** | ✅ `tools/v2_tour.py`**11 shots** (`docs/shots/v2_tour/`, un-letterboxed) incl. the **rain-verandah money shot** (rain + winmap-lit open video shop + CLOSED dark neighbours at night) + browsers + dig-real + katoomba + tram_stop + contact sheet. **Both soaks PASS** (full-default + all-on: 37 chunks, 20 shops, heap stable, 0 errors, leak-free). |
| **F4 — docs freeze + tag v2.0** | ✅ Flags table finalized (below); **`v2.0` tagged** on a green tree. |
| **Marshal** | C1 (the round's SPOF) landed early; all lanes committed; no slips. |
### v2.0 flags — FINAL
| flag | owner | status | default |
|---|---|---|---|
| roster | D | **graduated → DEFAULT-ON** (R7 flip); `?roster=v1` escape hatch | on |
| patronage | D | default-on (`?patronage=0` off) | on |
| `?dig=1` | C | opt-in (crate riffle) | off |
| `?winmap=1` | B | opt-in (parallax window glass) | off |
| `?stock=real` | C/E | opt-in (real GODVERSE covers/spines/boxes + buy-anywhere) | off |
| `?weather=1` | B | opt-in (seeded rain/overcast/clear; `?weather=rain` forces) | off |
| `?plansrc=osm[&town=]` | A | opt-in (real-OSM towns: melbourne, katoomba) | off (synthetic) |
| `?tram=1` | B | opt-in (spine tram loop) | off |
| `?noassets=1` | — | opt-in (primitive-only, zero-network) | off |
Determinism goldens frozen: synthetic `0x3fa36874` · osm/melbourne `0x34cfdec0` · osm/katoomba `0x0f652510`.
---
## Round 8 (catch-up + the town comes alive → v2.0-rc) — DONE ✅
All lanes landed (E packs, B weather, A 2nd town, C buy, D patronage). F wired the shell seams,
graduated the harness, tagged.
| task | status |
|---|---|
| **shell wiring (F owns index.html)** | ✅ **A** `&town=<key>``generatePlanFor(seed,'osm',{town})` (Katoomba, 19 shops, verified). **B** weather (3-line contract): `createWeather` on `?weather=1`, `PROCITY.weather={state,intensity}` ({clear,0} when off for D), `weather.update` in loop. **C** session `wallet` (createWallet(seed)) bound to the dig BUY (`getCash`/`onBuy` in interior_mode). **D** patronage: shop door-points per chunk → `citizens.setShops`, `?patronage=0` off-switch, `citizens.setWeather` each frame. All boot clean (0 console errors, settled). |
| **F1 — smoke graduations + additions** | ✅ `stock=real` + `weather=rain` **graduated warn→STRICT**. New warn smokes: **buy-v0** (dig BUY→wallet: cash 191→179, bag 0→1), **patronage** (7 peds visit shop doors/~18s), **book/toy stock** (6/11 real atlas meshes). Katoomba golden `0x0f652510` pinned in the determinism gate (fixed a hex zero-pad bug it caught). |
| **F2 — patronage baseline** | ✅ No re-pin needed — flags-off regression held (patronage nets *fewer* draws, inside tolerance; ≤300 cap + draws-±40% both green). |
| **F3 — tag v2.0-rc** | ✅ **TAGGED.** qa.sh --strict GREEN 5/5 (harness strict). |
| **Marshal** | All lanes committed round-8; no slips this round. |
---
## Round 7 (the flip round → v2.0-beta) — DONE ✅ (v2.0-beta tagged)
Critical path resolved: E1 ped-merge (`447188a`), E2 stock pack (`0c12dac`), **D's GO** (`43edabf`),
C1 `?stock=real` (`3724a65`) all landed. F executes flip → strict harness → tag.
| task | status |
|---|---|
| **F1 — roster flip (prime-law amendment)** | ✅ **DONE + verified.** `index.html`: streamed roster is now the **default** (`chunkStream:{radius:2}` per D's spec); `?roster=v1` = escape hatch (fixed roster); `?roster=stream` still accepted (no-op). Hours-aware `setNightLivelyChunks` on the open-late block. **Same commit** re-pins the flags-off baseline: **old (163 draws/19k tris, roster-off) → new (164 draws/~66k tris, roster-on)** — draws barely move (E1's merge = ~1 draw/rig, the whole point), tris = the crowd (D worst-view 65,899; drift-WARN now draws-only, tris under the hard cap). **Plan goldens UNCHANGED (`0x3fa36874`).** Also fixed the harness's roster probe (was reading a nonexistent prop → blind) + flipped the assertion to require streamMode ON + added an escape-hatch check. Verified: default boot streamMode:true (146150 active), `?roster=v1` streamMode:false (96), both render, 0 errors, ≤300 draws, harness GREEN. |
| **F2 — harness to strict + new-flag smokes** | ✅ **DONE + verified.** `qa.sh` flags gate `warn_gate → run_gate` — the harness (flags-off regression + 4 existing flags + all-on combo) is now **STRICT** (a FAIL fails qa). Wired **`?stock=real`** (F owned the seam): shell preloads the record pack + passes `stockReal`; `interior_mode` builds `makeStockAdapter(getStockPack(shop.type))` and passes it to **both** `buildInterior` (static crate sleeves) and `dig.open` (riffle) — fail-soft to parody if the pack's absent. Added `stock=real` + `weather=1` smokes at **warn-level** (decision #4). Verified: crates show **real GODVERSE covers** (smoke: 7 real atlas sleeve meshes, non-parody batched); weather auto-skips (Lane B not landed); `qa.sh --strict` GREEN (5 gates). |
| **F3 — tag v2.0-beta** | ✅ **TAGGED** (no-hostage rule). Criteria met: roster default-on + escape hatch ✓, harness strict+green ✓, `?stock=real` real sleeves ✓, qa strict green ✓. |
| **⚠ schedule-harness flag** | **B (`?weather=1`) and A (osm parity + 2nd town) did NOT land round-7** — no commits, tree clean. Both are additive (not headline keys); tagged v2.0-beta without them per the no-hostage rule. Weather smoke is in the harness ready to graduate the moment B ships. Flagging for Fable: two lanes slipped this round. |
---
## Round 6 (finish v2 wiring + pay review debts → v2.0-alpha) — DONE ✅ (v2.0-alpha tagged)
Sequencing (Fable): **F1 enforcement harness lands FIRST** (protects the round), then F re-smokes as
lanes land, adds C's ≤350-draw interior assertion when C1 lands, extends the determinism gate if A's
`?plansrc` lands, and tags `v2.0-alpha` when all-green (3 flags acceptable if A no-shows).
| task | status |
|---|---|
| **F1 enforcement harness** | ✅ **DONE + wired.** `tools/flags_check.py` (self-contained server, headless, ~90s): **flags-off regression** (golden hash `0x3fa36874` + subsystems-inactive + ≤300/≤200k budget + ±40% snapshot + 0 errors) · **per-flag smokes** (winmap/dig/roster: cross ≥2 chunks + enter interior; dig opens+closes a riffle) · **all-on combo** (flag-composition law, decision #4). Wired into `qa.sh` as a **warn-level gate** (`--no-flags` skips fast; auto-skips without the Playwright venv). Current: **all smokes GREEN**, regression clean. |
| **F2 plansrc** | ✅ **DONE + verified.** A landed the full seam (`52eb109`). Wired the shell bootstrap (`index.html`, F-owned): `?plansrc=osm``citygen.generatePlanFor(seed,'osm')` → real-data town **"Melbourne", 95 shops** (synthetic default = "Boolarra Heads" unchanged). Pinned **both goldens** in `scaffold_check.mjs`: synthetic `0x3fa36874`, **osm `0x34cfdec0`** (deterministic, 2 runs). Harness auto-detects plansrc + smokes it (enters OSM shop "Mui Karaoke Rooms") + includes it in the all-on combo. Also restored `binUnderAim`'s **nearest-bin fallback** (matches C's test page) so E riffles a nearby bin without perfect aim / before the bin GLB finishes loading. Synthetic golden frozen (selfcheck 1362/1362). |
| **≤350 interior draw assertion** | ✅ **DONE.** C1 batching landed (`35981ad`). Added an interior draw-budget sweep to `flags_check.py` (enter all 9 shop types, assert ≤350 draws/room, dig on so it also confirms batching kept riffling). **Caught a real bug in my own `interior_mode.js`**: it rendered the interior without `renderer.info.reset()` while `autoReset=false`, so draws *accumulated* per frame (inflated 864/644/…). Fixed (reset per interior frame, both dig + walk render). True per-frame result: **all 9 room types ≤350 (worst dept 96)** — C's batching verified. |
| **F3 tag `v2.0-alpha`** | ⏳ all landed flags smoke-green + regression green + qa strict green → tag (3 flags OK if A no-shows). |
Base: v1.1 (`3a831fe`), HEAD `40fefd9`. `qa.sh --strict` GREEN.
---
## Round 5 (v1.1 close-out + v2 foundations) — DONE ✅ (v1.1 tagged; ?dig=1 wired)
Lane F role: **v1.1 watch** + v2 flag-seam wiring + QA-harness-for-v2 + shared-tree marshal.
Full tracking in [`LANE_F_NOTES §10`](docs/LANES/LANE_F_NOTES.md).
| task | status |
|---|---|
| **fal un-park check** | ✅ Verified already done — `039c179` un-parked fal in Lane E's brief (decision #4 + Lane E task 3: glass-case + bus-shelter + optional magazine-rack, ≤2 attempts, costs logged in AUDIT.md). **Nothing for F to change.** |
| **archive R3/R4 instruction claims** (F4) | ✅ Added "SUPERSEDED by R5" banner to `ROUND3_INSTRUCTIONS.md` + `ROUND4_INSTRUCTIONS.md`. |
| **v2 flags table** (F4) | ✅ Scaffolded in LANE_F_NOTES §10 (`?plansrc=osm` A, `?winmap=1` B, `?dig=1` C, `?roster=stream` D) — all pending owning-lane specs. |
| **F1 tag v1.1** | ✅ **TAGGED.** C1 (`aa9c140`: record_crate fixed + counter_till re-map) + E1 (`c522464`: 6 hero props baked 288k→48k, ≤8k each, live on depot) landed. Verified: depot serves baked props (counter_till 7999, drinks_fridge 8000, listening_booth 7986 tris ≤8k), C's `glb.js` maps them, record/opshop interiors render the **counter_till GLB** with keeper stand pose working + no double till, bins upgraded. Re-captured the 2 interior tour shots (fresh chromium → baked props). qa GREEN. `git tag v1.1` (local). |
| **F2 wire `?dig=1`** | ✅ **DONE + verified.** C shipped `dig.js` + the flag but left the in-game input/mode hook to F (LANE_C_NOTES §C2). Wired the consumer pattern into `interior_mode.js` (F-owned): aim at a `userData.kind==='bin'` fitting + press **E**`createDig`/`dig.open` (per-bin seed via `binSeed(shop.seed,pos)`); the dig owns the frame (render `dig.scene`, no interior walk); WALK OUT / Esc closes → back to the room. Shell seam (`index.html`): `?dig=1``dig:` opt; `procity:digOpen` releases pointer-lock (riffle is cursor-driven); unlock→leaveShop guard reads `interiorMode.digActive` so opening a bin ≠ leaving. **Fresh-chromium smoke:** E opens riffle (seeded sleeves + prices render), WALK OUT closes to interior, **leak-free** (steady-state geo/tex flat over 3 cycles), **0 console errors**. Flag off ⇒ no listener/no dig ⇒ byte-identical to v1. |
| **F2 — other v2 seams** | B `?winmap` / D `?roster=stream` are self-contained (no shell input hook needed); smoked clean (boot, 0 errors, budget OK). A `?plansrc` not taken. |
| **F3 v2 QA harness** | ⏳ determinism gate (first-interior-hash per seed) · flags-off regression check (v2-prime-law enforcement) · v2 smoke → qa.sh warn-gate. Build as flags land. |
Base state: v1.0 (`2698b40`), `qa.sh --strict` GREEN 4/4, tree clean.
---
## Round 4 (v1-tag round) — DONE ✅ (all 6 gates green; v1.0 tagged)
Executed Fable's `ROUND4_INSTRUCTIONS.md` Lane F tasks. All lanes committed R4; a 6-auditor
adversarial v1-readiness workflow returned **GO**; `v1.0` tagged locally on the F finalization commit.
| task | status |
|---|---|
| **F1 setExposure** | ✅ Wired (`9416d0a`) **then correctly removed** (`aad9baa`): D3 (`a5e4b64`) made the impostor `toneMapped:true`, so `citizens.setExposure()` became a no-op tone-mapped globally via `renderer.toneMappingExposure`. Removed only *after* D committed (removing earlier would regress night brightness). Final state: no dead call. |
| **F2 storeys-checker** | ✅ `permittedMax = max>=2 ? min(max+1,3) : max` (Lane A's rule, `9416d0a`). Consistency **0 warnings** across 6 seeds; single-storey-type-at-2 still trips. **A confirmed** (`5b8c806`). |
| **F3 gate-3 budget** | ✅ **PASS.** E1 decimated peds (`518678d`, 924k→46k, ≤3k each) + **D2 sign-off** (`a5e4b64`, fleet ≈59.6k at 24-cap). Official measure: busiest node, pop 200, midday, 12-yaw peak → **draws 138 (≤300) · tris ~31k (≤200k)**. Baseline 372k. |
| **F4 gate-6 shots** | ✅ **PASS.** Against committed B2 (`6323d58`, `dbg.js` letterbox+poses): `shots.py v1_tour`**10 full-frame 1280×720, no letterbox**; §3.5 **night closed-facade** (CLOSED plates + lit openLate video "VIDEO REGAL") + real **record/opshop interiors** (keeper + hero props). Fixed `shots.py` so interior shots capture via `enterShop(id)` (were falling back to a street bookmark). |
| **F5 tag v1.0** | ✅ **TAGGED** on the F finalization commit (gate table all-green + shots committed). Local only — John pushes. |
| **audit** | 6-auditor v1-readiness workflow: 5 GREEN (gates 1/3/5/6 + release hygiene) + 1 procedural "land finalization commit first" → resolved. GO. |
---
## Round 3 (lead lane) — DONE, verified live
Executed Fable's `ROUND3_INSTRUCTIONS.md` Lane F tasks. All wiring verified in-browser on seed 20261990.
| task | result |
|---|---|
| **§3.5 hours / closed shops** | `isOpen()` gate on `enterShop`. Midday: open. **Night (22:00): all closed except the openLate video rental** (Video Regal [11,23]) — verified toy shop blocked, video enters. |
| **§3.4 `?noassets`** | **Zero network** (Perf API: 0 GLB/depot/manifest/ped-model requests); 100% primitive, legible. |
| **§3.4 interior GLB** | `useGLB` → fitting GLBs load from the **3GOD depot** (bookshop shelves, +8 meshes, visually confirmed). |
| **§3.4 rig fleet** | 19 ped GLBs → peds **and** keepers upgrade placeholder→**GLB rigs**; **leak-free** (shared-cache proven, cycle2/3 flat). |
| **`--depot` on qa.sh** | `validate_manifest.py --depot` = 0 err (16 GLBs live). **qa.sh GREEN 4/4.** |
| **Gate 2 soak** | **PASS** both modes — assets: 20 interiors leak-free (+GLB-cache warmup), heap 98MB stable; noassets: 37 chunks, heap 15MB. 0 console errors. |
| **Gate 4 asset-free** | **PASS.** |
| **Gate 6 shots** | `docs/shots/v1_tour/` (10 shots) + contact sheet captured; letterbox + 3 fallback bookmarks are Lane B dbg.js. |
**Findings → other lanes** (details in [`LANE_F_NOTES §9`](docs/LANES/LANE_F_NOTES.md)):
- **D/E:** rig ped GLBs are **tri-heavy** (~45k each) → gate 3 tris **~279k > 200k** budget (draws fine ~200/300).
Decimate the ped GLBs; F holds `?pop=140` until then.
- **B:** add 3 shots bookmark poses; fix the headless composer letterbox in `DBG.shot()`; render the §3.5
closed-facade visual (dark windows + CLOSED plate; predicate `window.PROCITY.isOpen`).
- Fixed **5 F-owned harness bugs** so `shots.py`/`soak.py` actually run (DBG async-poll, `DBG.plan`→`PROCITY.plan`,
`enterShop` bare-id, `shop.lot` is an id, midday-pin + GLB-cache warmup + budget→warning).
**Git:** working tree synced to canonical `main` (862bbcd) mid-session — my R2 work is committed as `aa4ed4f`.
R3 edits below are staged by pathspec (see Housekeeping). Nothing pushed.
---
## TL;DR (round 2 — for context)
All five lanes (AE) landed; F stood up the QA harness, **adversarially reviewed and fixed its own
tooling**, then wired the lanes into **a walkable, living town** — interiors, shopkeepers, and street
crowds all integrated and verified live:
1. **Interiors — every door opens (§3.2).** A door opens into a themed, seeded, self-lit 90s-AU
interior; walk it, walk back out (or Esc), room disposes. Verified across **all 9 shop types**,
**leak-free**, worst build **8.4 ms**. F-owned bridge [`web/js/world/interior_mode.js`](web/js/world/interior_mode.js).
2. **Shopkeepers (§3.3a).** A keeper stands at the counter of every shop and **turns to greet** you;
disposed with the room, **leak-free over 5 cycles**. (Asset-free placeholder; rig-fleet upgrade later.)
3. **Street crowds (§3.3b).** Deterministic pedestrians walk the footpaths (`graph = plan.streets`
directly), density follows time-of-day (busy midday, empty at night), LOD-tiered. **Draw budget
holds** — worst main-street view **~191 draws** (budget 300). Shell wiring in [`index.html`](web/index.html).
4. **Plan swap (§3.1)** + **facade skins fix** (prior pass): boots on Lane A's real town, textured.
`tools/qa.sh` is **GREEN (4/4 gates, 0 skipped)**. Remaining: manifest GLB upgrades + `?noassets`
(§3.4 — also upgrades keepers/peds to shared rigs), hours/closed-shops (§3.5), draw-budget shave.
Scoped in [`docs/LANES/LANE_F_NOTES.md`](docs/LANES/LANE_F_NOTES.md) §3.4§3.5,§7.
---
## Lane readiness — ALL COMPLETE ✅ (`node tools/qa/scaffold_check.mjs`)
| Lane | status | notes |
|---|---|---|
| A CITYGEN | **5/5** | plan.js, registry.js, names.js, selfcheck.js, map.html — determinism gate green, verified in-browser |
| B STREETSCAPE | **4/4** | index.html shell, chunks.js streamer, buildings.js, fixture_plan.js — walkable town verified live |
| C INTERIORS | **2/2** | interiors.js (`buildInterior`), interior_test.html |
| D CITIZENS | **3/3** | rigs.js, sim.js (`CitizenSim`), keepers.js (`KeeperManager`), citizens_test.html |
| E ASSETS | **3/3** | manifest.json (validates green), validate_manifest.py, AUDIT.md + pipeline |
---
## What F built / did this session
### QA harness (F-owned, `tools/`) — trustworthy, all green
| file | what | state |
|---|---|---|
| `tools/qa.sh` | gate runner; `--strict` = v1 umbrella | ✅ 4/4 green |
| `tools/qa/scaffold_check.mjs` | scaffold + PRNG determinism (golden-locked) + live readiness matrix + plan-determinism | ✅ |
| `tools/qa/consistency_check.mjs` | cross-lane gate: plan ↔ registry ↔ assets ↔ manifest | ✅ |
| `tools/shots.py` · `tools/soak.py` | Playwright beauty-tour + 10-min soak/budget/`--noassets` (gates 2,3,4,6) | ✅ ready; need Lane B `window.DBG` hook (§4) |
### Adversarial self-review (Workflow, 23 agents)
Ran a 4-auditor + verifier workflow against F's own scripts + runbook. It confirmed **14 real
defects in F's tooling** (false-confidence checks + wrong API assumptions). **All applied:**
- `scaffold_check`: JSON round-trip was a **tautology** (fixed → real deep-compare w/ `-0` norm);
Math.random-ban regex had **false negatives** (fixed → anchored to comments); irange golden check
re-seeded per draw (fixed → single stream).
- `consistency_check`: added a **storeys-vs-registry-range** check, fixed a dead late-night branch,
removed a dead metric.
- `soak.py`: heap heuristic no longer silently passes when heap isn't reported; shop count now
requires **verified interior loads** (not just `enterShop` calls); `--minutes` now honored.
- `shots.py`: fixed an `IndexError` on a trailing `--seed`.
- **Runbook** (`LANE_F_NOTES.md`): corrected against the **real landed APIs**`chunkIndex` returns
`{chunkSize,chunks}` (not callable); chunks is a `createChunkManager` **factory** with no lifecycle
hooks; `CitizenSim` is **graph-driven, not per-chunk**; keepers use `spawn/remove/disposeAll`.
### Integration passes (verified live in-browser)
- **`web/js/world/interior_mode.js`** (new F-owned bridge) + thin marked seam in `index.html`
**§3.2 interiors: every door opens.** `enterShop` looks the shop up in the plan, attaches its lot
dims, calls Lane C `buildInterior`, and switches to a new `interior` mode that renders the room in
its own scene and walks it via the room's occupancy grid. Exit on walk-out / `procity:exitShop` /
Esc → `dispose()` + player restored to the door. **Verified:** door → Toy Shop renders; exit
state-machine correct; **leak-free** (134 geo/37 tex → 217 loaded → 134/37 after dispose); all **9
shop types** `ok+pathOK`, worst build **8.4 ms**; **0 console errors**.
- **Keepers (§3.3a)** — wired into `interior_mode.js`: `KeeperManager` (no fleet → placeholder actor),
spawned at the counter stand pose (`places.find(p=>p.userData.keeperStand)`), ticked in the interior
loop, `disposeAll()` on exit. **Verified:** 11-mesh keeper at the counter, **greets** the player
(turns 0.21 rad, eases back), **leak-free over 5 cycles/5 types** (net growth 0).
- **Citizens (§3.3b)** — `CitizenSim` in `index.html`: `graph = plan.streets` (no adapter needed),
`setPopulation(140)` (`?pop=`), `setTimeOfDay((seg+frac)/6)` + `update(dt)` in the street loop,
`visibilitychange→setPaused`. Fixed a NaN bug (`getClock().hour` is a string). **Verified:** 96
active midday, peds walk footpaths ~1.5 m/s, budget-safe (worst **~191 draws** across all main edges).
- **`web/js/citygen/index.js`** (barrel) → game boots on Lane A's real generated town (was the
fixture). Verified: console "using Lane A generatePlan", HUD "Boolarra Heads · 493 shops".
- **`web/js/world/skins.js`** (1-line chokepoint fix) → `facadeMat` normalizes full-filename skins.
Verified: `facade-*.jpg` requests now **200 OK**, town textured (was all flat-grey).
### Docs (F-owned)
- `docs/LANES/LANE_F_NOTES.md` — integration runbook (real APIs, wiring steps, QA gates, DBG hook request).
- `docs/V2_IDEAS.md` — parked scope (content firehose, override layer, weather, venue, …).
- `docs/shots/` — reference-shot tree (the other lanes are already dropping acceptance shots into it).
---
## Findings for the lanes (F flags; the two blocking-visual ones are already fixed)
| # | lane | finding | status |
|---|---|---|---|
| 1 | B | shell imported non-existent `js/citygen/index.js` → ran on fixture | **fixed by F** (barrel) |
| 2 | B | `skins.js facadeMat` double-wrapped full-filename skins → facades 404 | **fixed by F** (normalize) |
| 3 | B | street measures **~334 draw calls** at spawn (radius 3) — over CITY_SPEC ≤300 | open — shave (radius 2 or more merging) |
| 4 | A | ~10% of shop names have a **doubled possessive** ("Tex's's Toys") — `names.js` | open (cosmetic) |
| 5 | A | some shops have **storeys outside the registry type range** (e.g. video at 2, range [1,1]) | open — confirm corner-anchor intent |
| — | contract | `facadeSkin` is a full filename in the registry but skins.js expected a bare key | **worth a CITY_SPEC line** so ground/awning/interior skins avoid the same trap |
---
## Remaining integration (scoped in LANE_F_NOTES §3; ready to execute)
1.~~**Interiors (§3.2)**~~**DONE**.
2.~~**Citizens + keepers (§3.3)**~~**DONE** (both verified live; see Integration passes above).
3. **Manifest + `?noassets` (§3.4)** — GLB upgrades from `manifest.json`; primitive-fallback flag.
Doubles as the **rig-fleet upgrade** that makes keepers/peds cheap (shared meshes) so `?pop=` can rise.
4. **Hours / closed shops (§3.5)** and the **draw-budget shave** (finding 3).
5. **Full QA gates 14,6** once Lane B exposes the `window.DBG` hook (§4) my harnesses drive.
*Next natural step is §3.4 (manifest + `?noassets`) — it's the asset-free gate AND the fleet upgrade
that upgrades placeholder keepers/peds to shared GLB rigs. §3.5 (hours) is a small data-driven follow-on.*
**Note on verification:** pointer-lock doesn't engage in the automated browser, so a real *walk* is
best playtested by a human (`cd web && python3 -m http.server 8130` → `localhost:8130?seed=20261990`).
F drives/inspects via `window.PROCITY` + JS for programmatic checks.
## Housekeeping
- Nothing committed (all working tree). F's edits are minimal, commented `[Lane F integration]`,
reversible. **New F-owned file:** `web/js/world/interior_mode.js` (interior + keeper bridge). Shell
seam edits in `index.html`: interior wiring (import, bridge create, `enterShop`/`leaveShop`, loop
branch, `setMode` lock, Esc-exit, M/P guards, `window.PROCITY` hooks) + citizens (import, `CitizenSim`
create + `setPopulation`, street-loop `setTimeOfDay`+`update`, `visibilitychange` pause). Also edited:
`skins.js` (facade fix, prior pass). Suggested commit once reviewed — **use a pathspec** (lanes share
one tree, see memory): `git commit -- web/js/world/interior_mode.js web/index.html web/js/world/skins.js
web/js/citygen/index.js tools docs/LANES/LANE_F_NOTES.md docs/V2_IDEAS.md docs/shots F-progress.md`
msg `Lane F: integration — interiors + keepers + street peds + QA harness + barrel + skins fix`.
- A local `python3 -m http.server 8130` is running for the smoke test (harmless; kill when done).
- Lane prompts hard-code `/Users/jing/Documents/PROCITY`; real path is `/Users/m3ultra/Documents/procity`.