# LANE F — NOTES (integration runbook + QA gates) *Lane F is integration + QA. It runs **after** A–E land. This file is the durable technical spec: the exact wiring edits, the contracts F needs from each lane, and the QA gate procedures. It is written **before** A–E finished so the wiring is a fast, mechanical pass the moment they do — and so each lane can see the small hook F will ask of it and build it in up front.* Status of this file: **living**. Update the ✅/⏳ markers as wiring lands. Snapshot of lane readiness lives in [`F-progress.md`](../../F-progress.md); run `node tools/qa/scaffold_check.mjs` for the live matrix. > **v2.0 FROZEN (round 9).** Nine rounds: seed → walkable town → weather, a shopping crowd, > follow-a-ped-into-a-shop presence, and real stock you can buy. Current gate reality + the > final v2 flags table live in [`F-progress.md`](../../F-progress.md); `qa.sh --strict` GREEN 5/5 > (scaffold+determinism · consistency · citygen selfcheck · manifest --depot · v2 flags harness STRICT). > CityPlan v2 contract frozen by Lane A in CITY_SPEC; validated live by selfcheck (1727/1727) + goldens. --- ## 0. Ground truth / environment - Repo root on this machine: `/Users/m3ultra/Documents/procity` (the lane prompts say `/Users/jing/Documents/PROCITY` — that path is from the authoring machine; **use the real path above**). Reference repos are cloned alongside: `../90sDJsim`, `../thriftgod`. Ultra box (existing 3D library / Blender / clip bank): `ssh johnking@100.91.239.7`. - Serve: `cd web && python3 -m http.server 8130`. Node 26, Python 3.14 present. - `web/package.json` has `"type":"module"` (Lane A) so `node web/js/**/*.js` runs as ESM. - **F never edits another lane's internals.** F adds *wiring* — new files it owns, and small, clearly-marked hook edits at the seams. Where a hook must live inside a lane's file, F first looks for the hook that lane already exposed (see contracts below); the lane owns its file, F owns the glue. ## 1. Files Lane F owns / creates | path | purpose | |---|---| | `tools/qa.sh` | the QA gate runner (scaffold + citygen selfcheck + manifest validator) ✅ | | `tools/qa/scaffold_check.mjs` | scaffold + PRNG determinism law + **live lane-readiness matrix** ✅ | | `tools/shots.py` | Playwright tour-capture → `docs/shots/v1_tour/` + contact sheet (ported from 90sDJsim `tools/shots.py`) | | `tools/soak.py` | Playwright 10-minute scripted-walk soak (chunk/heap/leak/error gate) | | `web/js/citygen/index.js` | barrel so B's `import('./js/citygen/index.js')` resolves to A's `plan.js` ✅ (§3.1) | | `web/js/world/interior_mode.js` | **interior bridge** — B's shell ↔ C's `buildInterior`; owns the `interior` mode branch ✅ (§3.2) | | `docs/V2_IDEAS.md` | parked-scope catalogue ✅ | | `docs/shots/` | reference shot tree (`laneA…E/`, `v1_tour/`, `before/`, `references/`) | | `F-progress.md` | status report for fable | The wiring is kept as **focused F-owned modules** (e.g. `interior_mode.js`) that the shell imports, rather than logic sprinkled through B's files. Each seam edit inside `index.html` is one line, commented `[Lane F integration]`, and reversible. That keeps ownership clean: B owns the shell + mode machine, C/D own their libraries, F owns the glue modules and the marked seams that call them. --- ## 2. Contracts F depends on (verify these as lanes land) Pulled from CITY_SPEC + each lane prompt. The scaffold check's readiness matrix tracks presence; these are the **shapes** F wires against. If a lane ships a different shape, fix it here and in the wiring, not by guessing. > **⚠ Corrected against the REAL landed code (2026-07-14).** The bullets below are what the lanes > actually shipped — several differ from the spec-derived guesses in an earlier draft (verified by > reading each file + the Lane F self-audit workflow). **Lane A — `web/js/citygen/plan.js`** *(landed, verified)* - `generatePlan(citySeed) → CityPlan` (schema in CITY_SPEC §Layer 1). <3ms, deterministic, JSON-clean. - `chunkIndex(plan) → { chunkSize, chunks }` where `chunks["cx,cz"] = { lots:[id…], shops:[…], edges:[…] }` (a **plain object, not a callable**; a lot may bucket into several chunks). Also exports `chunkKey(cx,cz)`. - `web/js/core/registry.js` exports `SHOP_TYPES` — per type `{ label, facades:[.jpg…], sign, interior, fittings:[…], storeys:[min,max], hours:{open,close} }`. **`facades` are full filenames** (`facade-timber-teal.jpg`), not bare keys — see the skins seam in §7. - `web/map.html` — 2D plan viewer (verified rendering real towns). `web/js/citygen/index.js` **is the barrel Lane F added** so the shell's `import('./js/citygen/index.js')` resolves (§3.1). **Lane B — `web/index.html` + `web/js/world/*`** *(landed, verified live)* - Mode state machine `MODE ∈ {map, street, interior}` (interior branch not yet used), one renderer. - **`createChunkManager(plan, scene, ctx)`** — a **factory**, not a class; returns `{ update(pos), warmup(pos), getColliders(x,z), setNight(bool), getDoorMeshes(), count, dispose() }`. **No `onChunkBuilt/onChunkDisposed` lifecycle hooks** — so §3.3 must drive citizens off the street graph, not chunk lifecycle (or F adds a hook; prefer graph-driven). - Door pick is wired via HUD: `createHUD({ onEnterShop: (shopId,name)=>enterShop(...) })`; `enterShop()` dispatches `procity:enterShop` and is otherwise **a stub** (`hud.showToast('interiors coming')`) — this is the seam F completes in §3.2. Interior returns via `procity:exitShop`. - HUD shows draws / tris / fps / seed / time-of-day (F reads these for the budget gate). - Exposes `window.PROCITY = { plan, scene, camera, renderer, chunks, lighting, player, skins }` (F builds the `window.DBG` harness hook on top of this — §4). **Lane C — `web/js/interiors/interiors.js`** *(landed, verified)* - `buildInterior(shop, THREE, opts={registry,archetype,stockAdapter}) → { group, spawn:{x,z,ry}, exits:[…], places:[…], dims, recipe:{counterPos,…}, dispose() }`. Matches the wiring plan. - Caller must attach lot dims: pass `{ ...shop, lot: plan.lots.find(l=>l.id===shop.lot) }`. `dispose()` frees tracked GPU resources (soak gate asserts baseline). **Lane D — `web/js/citizens/*`** *(landed, verified)* - `sim.js` exports **`class CitizenSim`** — `new CitizenSim({ renderer, scene, camera, citySeed, graph, fleet, group })`. It is **street-graph driven, not per-chunk**: there is no `spawnRoster/despawn/setDaySegment` — build it once from the street graph and tick it in the loop. - `keepers.js` exports **`class KeeperManager`** — `new KeeperManager({ camera, citySeed, fleet })`, then `spawn(target, { x, z, ry, shopId, type }) → handle`, `remove(handle)`, `disposeAll()`, `update(dt, playerPos)`. (Not `spawnFor/disposeFor`.) **Lane E — `web/assets/manifest.json` + `pipeline/validate_manifest.py`** - Manifest: `{depot, fittings:{id:{file,footprint,thumb}}, furniture:{…}, skins:{facade,sky,ground,wall,interior}}`. - `python3 pipeline/validate_manifest.py` green (F's gate 5 runs it). - B/C read the manifest for GLB upgrades; **primitive fallbacks must still fire under `?noassets=1`**. --- ## 3. Wiring checklist (LANE_F §Wiring — expanded to exact steps) All of this lives in `web/js/world/wiring.js` (F-owned) + the minimal hooks each lane exposes. ### 3.1 Plan source swap (A → B) ✅ DONE (verified live) - Lane B already reads `?seed=N` (default `20261990`), guards the citygen import, and surfaces the town name + seed in the start panel and HUD. **But it imported `./js/citygen/index.js`, which did not exist** (Lane A shipped `plan.js`), so it silently fell back to the 14-shop fixture. - **Fix:** Lane F added `web/js/citygen/index.js` (barrel re-exporting `generatePlan/chunkIndex/chunkKey` from `plan.js`). Verified live: shell logs "using Lane A generatePlan", HUD shows the real town ("Boolarra Heads", 523 shops), 31 chunks stream. - Map key **M** is wired by Lane B to an in-shell minimap (not map.html). Fine for v1. - **Gate:** plan determinism asserted green by `scaffold_check.mjs`. Map-PNG determinism pending the shot harness + DBG hook (§4). ### 3.2 enterShop → interiors (B ↔ C) ✅ DONE (verified live, 2026-07-14) Implemented as an **F-owned bridge module `web/js/world/interior_mode.js`** (`createInteriorMode`), plus a thin marked seam in the shell. The bridge renders the interior into its **own dark THREE.Scene** — the street scene is simply left frozen (not disposed → instant return, CITY_SPEC L3 "pause, not dispose"), which is cleaner than hiding B's ground+chunk meshes in a shared scene. ``` enterShop(shopId, name): // shell seam (was the toast stub) shop = plan.shops.find(s => s.id === shopId) // id is numeric; matches door.shopId lot = plan.lots.find(l => l.id === shop.lot) // attach {w,d} — Lane C sizes the room to it interiorMode.enter({ ...shop, lot:{w,d} }, name); setMode('interior') interiorMode.enter(shop): // F module current = buildInterior(shop, THREE); scene.add(current.group) // Lane C doorReturn = {x,y,z,ry of the player on the street} camera → current.spawn (x, 1.6, z, ry); exitArmed = false loop [MODE==='interior']: if interiorMode.update(dt,keys) leaveShop() update: walk via current._debug.grid occ/bounds (mirrors interior_test.html) → renderer.render(ownScene) exit arms once player steps >2m from the door, then fires within 1m of exits[].toStreet leaveShop() / 'procity:exitShop' / Esc(unlock): // three exit routes, all → leaveShop interiorMode.exit(): current.dispose() (frees GPU + unparents); camera → doorReturn setMode('street') // pointer stayed locked → walking resumes at once ``` - **Verified live** (seed 20261990 "Boolarra Heads"): door → **Toy Shop "Numbat Playthings"** renders (ivy wallpaper, stocked shelves, counter, exit doorway); exit state-machine correct (no instant-exit at spawn; arms then fires at the door); **leak-free** — GPU memory 134 geo/37 tex → 217 loaded → **back to 134/37 after `dispose()`**. Swept **all 9 shop types** (toy/video/pawn/ opshop/book/record/stall/dept/milkbar), every one `ok+pathOK+leftClean`, worst build **8.4ms** (budget 50), zero console errors. - **Gate:** determinism (byte-identical placement per seed) is asserted by Lane C's own soak page; memory-to-baseline after `dispose()` confirmed above. Real pointer-lock walk-in is the one bit the automated browser can't drive — needs a human (or the DBG hook in §4). ### 3.3 Citizens + keepers (D) **Keepers ✅ DONE (verified live).** Wired into the interior bridge (`interior_mode.js`), not the shell: `new KeeperManager({ camera, citySeed: plan.citySeed })` — **no `fleet` passed → asset-free placeholder actor** (rig-fleet upgrade is later, with §3.4). On `enter`, find the counter interactable (`places.find(p => p.userData.keeperStand)` — Lane C tags `{x,z,ry}` there) and `keepers.spawn(interior.group, { x, z, ry, shopId, type })`; tick `keepers.update(dt)` each interior frame (playerPos defaults to the camera); `keepers.disposeAll()` on `exit`, before the room disposes. - **Verified:** placeholder keeper (11-mesh humanoid) stands at the counter pose (record shop: `{2.55,-2.46, ry 1.571}`), **turns to greet** a nearby player (0.21 rad) and eases back when they leave (~0), disposes to 0. **Leak-free**: 5 enter/leave cycles over 5 shop types → after-leave memory constant at 96 geo/16 tex, **net growth 0**; 0 console errors. **CitizenSim (street peds) ✅ DONE (verified live).** Happy surprise: **`plan.streets` already IS the graph** — `{ nodes:[{id,x,z}], edges:[{id,a,b,width,kind}] }` — so `graph: plan.streets` passes straight in, **no adapter needed** (the runbook's earlier `streetGraphFrom` is unnecessary). In the shell: `new CitizenSim({ renderer, scene, camera, citySeed: plan.citySeed, graph: plan.streets })`, `setPopulation(140)` (`?pop=` tunable), then in the **street** branch each frame: `setTimeOfDay((clk.seg + clk.frac)/6)` + **`update(dt)`** — note `update` takes **only dt** and reads `this.camera.position` internally (the runbook's `update(dt,pos)` was wrong). `visibilitychange → setPaused`. No fleet → asset-free placeholder(near)/impostor(mid) tiers; sim self-manages NEAR/MID/FAR LOD off camera distance (not chunk-driven — chunks.js has no lifecycle hook). - **⚠ gotcha (fixed):** `getClock().hour` is a **display string** (`'12:30'`), so `hour/24` → NaN → 0 peds. Use the numeric day fraction `(seg + frac)/6` instead. - **Verified:** 140 pop → **96 active midday** (density curve; `~7` at night), peds walk footpaths at ~1.5 m/s, deterministic per seed (Lane D property; F passes the same graph+seed). **Budget:** 1 near placeholder ≈ **11 draws** (11 meshes), bounded by `NEAR_MAX=24`; but a hamlet never fills it — worst across all 6 main/high edges was **near 3, ~191 composer draws** (budget 300). Coexists with interiors+keepers (round-trip regression clean); 0 console errors. - **Note for §3.4:** placeholder near-tier is the draw cost; the **rig-fleet upgrade** (shared meshes + impostor promotion) is what lets `?pop=` rise well past 140. Until then 140 is the safe lively default. ### 3.4 Manifest + `?noassets` + rig-fleet upgrade ✅ DONE (R3, verified live) Wired in the shell (all gated by `NOASSETS = ?noassets=1`): - **Rig fleet:** `loadPedFleet('models/peds/')` (19 char GLBs + walk/idle clips exist locally) → passed to `new CitizenSim({…,fleet})` and (via `createInteriorMode({…,fleet})`) to `KeeperManager`. Peds + keeper **upgrade placeholder→shared GLB rigs** once `fleet.whenReady` resolves (verified: `citizens.mode==='rig'`, `keeperKind==='rig'`, detailed characters on the street + behind counters). - **Interior GLB:** `preloadManifest()` at boot + `buildInterior(shop,THREE,{useGLB:!NOASSETS})` → Lane C `glb.js` swaps fitting primitives for **depot GLBs** (`depot:` → 3GOD, reachable). Verified: bookshop fittings load real GLBs (+8 meshes, dense shelves). *Not all kinds map* — `counter` is intentionally primitive; `record_crate` is a known-failing GLB → fail-soft to primitive. - **`?noassets=1`:** `fleet=null`, manifest not preloaded, `useGLB=false`. Verified **zero network** (Performance API: 0 GLB, 0 depot, 0 manifest, 0 ped-model requests); 100% primitive, still legible. - **Leak-free:** rig keeper + interior GLBs dispose clean — the persistent geometry is the intentional **shared `loadGLB` cache** (cold entry +N geo, re-entries flat: proven cycle2/3 = 189→189). soak.py pre-warms this cache so the gate is honest. - **⚠ finding (D/E):** GLB **character models are triangle-heavy** (~45k each near rig) → busiest view tris ~279k, soak peak ~1.0M, over the **200k** soft budget. Draws are fine (~200/300). **Decimate the ped GLBs** (and heavy fittings) to hit the tri budget; **F holds `?pop=140`** until then (raising pop adds near rigs = more tris). See Cross-lane requests §9. ### 3.5 Hours / closed shops ✅ DONE (R3, verified live) - `currentHour()` parses `lighting.getClock().hour` (`'HH:MM'`→float); `isOpen(shop)` = `open ≤ H < close`. Both exposed on `window.PROCITY`. **`enterShop` gates on it**: closed → `🔒 CLOSED · opens HH:00`, no build. - Lane A ships exactly one **`openLate:true`** landmark/town — always the **video rental** (Video Regal `[11,23]` etc.). Verified: **midday** toy shop `[10,16]` enters; **night (22:00)** toy shop blocked, **only the video shop opens** (the night destination). - **Cross-lane (B):** the facade *visual* closed-state (dark windows + CLOSED plate) + a "CLOSED" door tooltip are Lane B's rendering — `window.PROCITY.isOpen(shop)` is the predicate. See §9. ### 3.5 Hours / closed shops (A data → B facade + C gate) ⏳ - `isOpen(shop, seg)` from `shop.hours:[open,close]` vs the current day segment (map 6 segments → hour). Closed ⇒ B renders dark windows + CLOSED plate (facade state); enterShop → locked toast. - Exactly one late-night shop per town (Lane A seeds a shop whose `hours` run late). **F asserts it exists**: `plan.shops.some(s => s.hours[1] >= 22)` (tune threshold to A's convention). - **Gate:** at night, ≥1 open shop; closed shops visibly closed + un-enterable. --- ## 4. QA gates — ALL GREEN ✅ (v1.0 criterion met, R4). `tools/qa.sh --strict` is the static umbrella. Measured against the fully-committed R4 tree (seed 20261990, "Boolarra Heads"), verified live + by a 6-auditor adversarial v1-readiness workflow (2026-07-14) → **GO**. | # | gate | criterion | result (R4) | |---|---|---|---| | 1 | **Determinism** | seed → identical plan (deep-equal + golden fingerprint) + selfcheck | ✅ scaffold_check green; selfcheck **1301/1301**, fingerprint **0x3fa36874** (intended A2 shift, not drift); two node processes → identical `sha256(plan)` per seed; chunkIndex covers every lot | | 2 | **Soak** | walk ≥30 chunks, ≥15 shops; geo/tex baseline after each interior; heap stable; **0** console errors | ✅ **37 chunks, 20 shops, heap 38→38 MB, 0 errors, leak-free** | | 3 | **Budget** | busiest intersection: ≤300 draws, ≤200k tris (settled clean measure) | ✅ **draws 138 · tris ~31k** (baseline 372k; peds decimated 924k→46k ≤3k each, D2 sign-off) | | 4 | **Asset-free** | `?noassets=1` full playthrough, zero crashes, zero network | ✅ **heap 13→13 MB, 0 errors, 0 asset requests** | | 5 | **Selfchecks** | `selfcheck.js` + `validate_manifest.py --depot` green via qa.sh | ✅ **qa.sh --strict 4/4**; manifest `--depot`: **23 GLBs live, 0 err** | | 6 | **Shots** | `docs/shots/v1_tour/` 10 beauty shots + contact sheet | ✅ **10 full-frame 1280×720, no letterbox**, 3 new poses resolve; incl. §3.5 **night closed-facade** (CLOSED plates + lit openLate video) + real **record/opshop interiors** (keeper + hero props) | > Note: soak peak-draw/tri *warnings* are transient teleport/impostor-atlas-bake sampling spikes; > gate 3's settled busiest-intersection measure is the authoritative budget check (draws 138 / tris 31k). ### DBG hook — LANDED (Lane B `web/js/world/dbg.js`, `?dbg=1`) `window.DBG` exposes `ready / shot(name) / teleport(x,z,ry) / setSegment(seg) / enterShop(id) / exitShop() / info()` and drives `tools/shots.py` + `tools/soak.py` (mirrors 90sDJsim's `window.DBG`). Bookmarks: street_noon, arcade, market_square, milkbar_dusk, night_neon, crossroads_busy, residential_collar, warehouse_fringe. **Interior tour shots** (record/opshop) are captured by `enterShop(id)` directly (not via `shot()`, which forces street mode) — see `tools/shots.py`. --- ## 5. Order of operations once lanes land 1. ✅ `node tools/qa/scaffold_check.mjs` — A's plan determinism + registry confirmed present. 2. ✅ Wire §3.1 (plan swap) → generated seed boots on the shell (barrel + skins fix, verified live). 3. ✅ Wire §3.2 (interiors) → door opens, enter/walk/exit + dispose verified across all 9 types. 4. ✅ Wire §3.3 (citizens + keepers) → keeper greets at the counter; peds walk the graph, budget-safe. 5. ✅ Wire §3.4 (manifest + `?noassets` + rig-fleet) and §3.5 (hours) — all verified live (R3). 6. ◑ Gates: 2 & 4 **PASS** (soak/asset-free), 5 **GREEN** (`--depot`), 3 tris-blocked on D/E decimation, 6 shots need Lane B bookmark poses. `docs/shots/v1_tour/` captured. 7. ⏳ `tools/qa.sh --strict` green + gate 3 tris ⇒ tag v1 (pending D/E ped-GLB decimation). ## 7. Live smoke-test findings (2026-07-14, real generated town, seed 20261990) Booted the wired game in a browser and drove the camera via `window.PROCITY`. State: `Boolarra Heads · 523 shops · 711 lots · 31 chunks live · 31 door meshes · 24.7k tris`. - ✅ **Seed → real town** works after the barrel fix (§3.1). Contiguous strip, awnings, verandah posts, doors, windows, gum trees, sky dome, 6-segment day cycle — all render. - ✅ **Facade skins seam (FIXED).** `skins.js facadeMat` built `assets/gen/facade-.jpg` but Lane A's `facadeSkin` is the **full filename** (`facade-fibro-blue.jpg`) → every facade requested `facade-facade-…jpg.jpg` (404 → flat colour). Lane F normalized the name at the `facadeMat` chokepoint (strip a leading `facade-` + image extension; idempotent for bare keys). Verified: facade JPEGs now return **200 OK**, town is textured. *Root cause is a cross-lane naming contract ambiguity — registry stores filenames, skins.js expected bare keys. Worth a CITY_SPEC line so ground/awning/interior skins don't hit the same trap.* - ⚠ **Budget:** ~**334 draw calls** at spawn (radius 3), over the CITY_SPEC ≤300. Tris fine (~25k). Options for Lane B: drop default radius to 2, or merge more per chunk. Gate 3 will fail until shaved. - ⏳ **Interiors not wired** (§3.2), **citizens not wired** (§3.3), **manifest/`?noassets` not wired** (§3.4) — the remaining integration passes. ## 8. Open questions for the lanes (surface, don't guess) - **B:** will you expose the `window.DBG` hook (§4) and the `ChunkManager` lifecycle callbacks (§3.3)? If not, F wires citizens via a MutationObserver-style scan — slower, please prefer callbacks. - **A:** which field marks the "open late" shop, and what's the `hours` encoding (24h ints? segment idx)? - **C:** does `places` tag the counter slot with a stable `kind:'counter'` so keepers find it? - **E:** is `?noassets` honored inside your manifest-consuming code, or does F gate it at the shell? - **D:** roster spawn/despawn API name + does it accept a chunk key directly? --- ## 9. Cross-lane requests (Lane F → others, R3) — the integration surfaced these; not silent-editing your files **→ Lane D / Lane E — decimate the ped GLBs (blocks gate 3 tris).** The rig fleet (`web/models/peds/*.glb`) is wired and works, but the character models are triangle-heavy: **~45k tris each near rig**, so the busiest-intersection view hits **~279k tris** (soak peak ~1.0M) vs the **200k** CITY_SPEC budget. Draws are fine (~200/300 — the shared-mesh sharing worked). Please **decimate/LOD the 19 ped GLBs** (target ≤~10–15k tris each) so gate 3 passes; then F can raise `?pop=` past 140. Same applies to any heavy interior fitting GLB. **→ Lane B — three dbg.js / shell items:** 1. **shots.py bookmark poses (gate 6).** `dbg.js BOOKMARKS` defines `street_noon/arcade/market_square/ milkbar_dusk/night_neon`, but F's v1_tour also uses `warehouse_fringe / residential_collar / crossroads_busy` — these fall back to `street_noon` (identical frames). Please add those 3 poses. 2. **shots letterbox.** In headless (1280×720), `DBG.shot()`→`composer.render()` renders into a square sub-region even though renderer/canvas/camera are correctly sized — the EffectComposer render targets don't rebuild on the initial resize. A real viewport toggle in shots.py didn't fix it; likely the composer needs an explicit `setSize`/reset on first `shot()`. (In-game, real window sizes render fine.) 3. **§3.5 closed-facade visual.** F gates *enterability* by hours (`window.PROCITY.isOpen(shop)`); the *visual* closed state — dark windows + a CLOSED plate, and a "CLOSED" door tooltip in `hud.js` — is your facade/HUD render. Predicate is ready on `window.PROCITY`. **Note:** F-owned harness bugs found + fixed this round (so the gates actually run): `shots.py`/`soak.py` polled `window.DBG` before its async install (race → false "absent"); read `DBG.plan` (unexposed → use `window.PROCITY.plan`); `enterShop({shopId})` vs the bare-id contract; `shop.lot` treated as an object (it's a lot **id**); soak now pins midday (else §3.5 closes shops mid-walk) + pre-warms the shared GLB cache (else the one-time upload reads as a leak) + budget peaks demoted to warnings (DBG.info samples spike during streaming; gate 3's clean single-pass is authoritative). --- ## 10. Round 5 — v1.1 close-out + v2 flag seams (Lane F tracking) Round 5 (v1.1 polish + v2 foundations behind flags). **v2 prime law:** default boot stays v1-identical; `GOLDEN.hash 0x3fa36874` must not move; `qa.sh --strict` green at every commit. ### v1.1 tag — ⏳ WATCH (blocked on C1 + E1) Tag `v1.1` once **Lane C** re-maps `counter_till` + `record_crate` (C1) and **Lane E** bakes the hero props ≤8k (E1). F then: verify record/milkbar/opshop interiors (GLB counter+till correct, bins upgraded, baked props look right), re-capture ONLY the 2 interior tour shots if improved, qa green → `git tag v1.1` (local). *Nothing to tag yet — HEAD `039c179`, no round-5 lane commits.* ### v2 flag seams — table (F wires `web/index.html` + `tools/` as lanes document them) Every flag **default-off + independent**. Wire only from the owning lane's NOTES call-site spec. | flag | owner | what | F wiring | state | |---|---|---|---|---| | `?plansrc=osm` | A | second plan source (OSM fixture) behind the CityPlan contract | select in shell plan bootstrap | ❌ **A did not run R5** — task carries to R6 | | `?winmap=1` | B | parallax interior-mapping window shader | none needed (self-contained in `buildings.js`) | ✅ landed `2a923b6` | | `?dig=1` | C | dig.js crate-riffle on procedural stock | wired by F (`26d68cd`): `DIG_ON` → `interiorMode`, Esc/pointer-lock guards | ✅ landed `7131f53` + F2 | | `?roster=stream` | D | chunk-streamed citizen roster | none needed (self-contained in `sim.js`) | ✅ landed `93a3168` | *(table updated by Fable in the R5 review — F3 harness extensions below did NOT land in R5; they are the top Lane F item for R6.)* ### v2 QA harness extensions (F3) — build as flags land - **Determinism gate:** pin `plan-hash` + `first-interior-hash` per `(seed, plansrc)`. Synthetic stays `0x3fa36874`; add the osm-fixture hash when A lands. - **Flags-off regression check:** boot no-flags → assert plan hash + a settled draws/tris snapshot within tolerance of the v1 baseline (catches a v2 flag leaking into the default path). *This is the enforcement arm of the v2 prime law — build early, before flags land.* - **v2 smoke:** boot each flag on, walk 2 chunks, enter 1 interior (+1 dig if C landed), 0 console errors. Wire into `qa.sh` as a **non-strict (warn) gate** this round; strict next round. ### Marshal Shared-tree arbiter this round: F owns `web/index.html` + `tools/`; if two lanes collide on a file, F arbitrates. Cross-lane asks → the owning lane's NOTES, not silent edits. --- ## 12. Round 12 — v3.0-alpha gig layer (`?gigs=1`). F wires it last; findings below. **What F built.** `web/js/world/gig_state.js` (new, F-owned) — the Friday-night state machine; the `?gigs=1` seam + `custom_bands.json` bootstrap in `index.html`; the cover charge at the door; the band + crowd + gig-bed wiring in `interior_mode.js`; `smoke_gigs` in `tools/flags_check.py` (6 gates) + the v3 arm of the flags-off regression. ### The state machine (`gig_state.js`) — the two things that are load-bearing 1. **It listens, it doesn't poll.** `lighting.js` dispatches `procity:segment` on every segment change; the latch rides that. A poll-only machine sees only the segments it is sampled on — driving NIGHT→DAWN→MORNING→NIGHT with nothing reading in between (player inside the pub, audio muted, so neither F's street loop nor B's audio rAF is reading) **skipped the night roll entirely**: the night never advanced and the cover was never due again. Caught live, fixed, re-verified. 2. **`state` is a getter, not a field.** B's audio engine self-ticks on its own rAF and reads `PROCITY.gigs.state` while the player is *inside* a shop — i.e. when F's street loop is not running. A pushed field goes stale there and the spill lies about the gig. 3. **The night rolls on WHERE the clock lands, not on watching `done` go by.** First cut incremented the night when leaving `'done'` forwards — which meant a clock *jump* straight from NIGHT to MORNING (never touching DAWN) never rolled the night at all, and **a broke punter walked in on last night's paid stamp**. `smoke_gigs` caught it; stepping with `[`/`]` always passes through DAWN, so only a jump (DBG/shots/soak all jump) exposed it. Now: the latch clears wherever it clears, and landing in the **daytime** (MORNING…ARVO) is what rolls the night; landing on the **doors** means we rewound out of the gig — same night, stays paid. `done` is real, not decorative: the segment→hour map tops out at NIGHT (22:00) so the gig owns the whole night segment, and "after the gig" can only be DAWN (closing time — band packing up, crowd draining per A's closing-time ruling). Latch clears on any non-DAWN segment so rewinding the clock (`[`) doesn't leave `done` stuck on a midday town — it did exactly that on the first verify. ### ⚠ Finding 1 (Lane C ↔ Lane E) — the gig bed key never met. **F bridged it; needs a contract line.** `LANE_C_PUB.md` asks E to name the live bed **`gig-pubrock`** (C emits `room.audio.gigKey = 'gig-' + genreKey`). E shipped it as **`pubrock-live`** (`gig:true`, `types:['pub']`) — which is also the key B's street spill hardcodes. Neither is wrong; they just never met. Unresolved, `AUDIO.music['gig-pubrock']` is `undefined` and **the pub plays to a silent room** — and because the audio law fails soft, nothing errors: you just get a band miming. F resolves it at the glue (`interior_mode.js GIG_BED` + a generic `gig-` → `-live` fallback). **This is the R1 facade-skin filename trap again** (registry stored filenames, `skins.js` expected bare keys). → **C/E/Fable: pick one and put it in CITY_SPEC as a named contract**, so genre #2 doesn't rediscover this. F's smoke now asserts `gigKey` resolves. ### ⚠ Finding 2 (Lane D + Lane E) — E's instrument GLBs landed; nothing wires them. E shipped 5 instrument GLBs this round (`electric_guitar`, `bass_guitar`, `drum_kit`, `guitar_amp`, `mic_stand`, all in `manifest.fittings` with footprints + thumbs). D's band holds **primitives** and published `spawn(..., { instrumentFor })` as the injection seam — D-progress says E's GLBs "drop in via `opts.instrumentFor(role)` **when they land**". They have landed. Nobody owns the join. Not blocking (asset law: a primitive band is still a gig, and it reads fine in the shot), but it is a dangling R12 deliverable. F did **not** do it silently: picking manifest ids and orienting/scaling instruments inside D's rig is D's call, and the R12 no-drummer question below wants answering in the same pass. → **R13.** ### ⚠ Finding 3 (ROUND12 spec ↔ Lane C) — the band has no drummer. ROUND12 §Lane D.1 specifies "guitarist/bassist strum-sway, **drummer seated bob**", and E was asked for "drum kit (one merged kit)". C's `stage.bandPoses` are **guitar / vocal / bass** — a front-line trio; the drum riser is set-dressing with no pose. D built to C's poses and flagged it. So the shipped alpha band is 3-across-the-front with a mic stand where the spec expected a kit. Cosmetic, non-blocking, deterministic — but it is a **spec/implementation divergence that no gate can catch**. → **Fable/John: rule for R13** (add a 4th `drums` pose on the riser, or amend the spec to a front-line trio). ### Gate lesson — "no giants" must measure STATURE, not world crown. The R10 gate asserts head-bone world Y ∈ [1.4, 2.0]. The band stands on C's **0.32 m stage deck**, so a perfectly human 1.73 m guitarist has a world crown of **2.05 m** and a naive world-Y check **fails a correct band** (it did, first run). `smoke_gigs` measures `crown − base.y` for the band/crowd and keeps the world-crown assert only for "never clips the ceiling", which is the real R10 failure mode. `tools/qa/interior_scale_check.py` is unchanged (keepers/browsers stand on the floor, base.y = 0) — but **if anyone ever puts a keeper on a platform, that gate inherits this bug.** ### → Lane B (small, R13): no venue bookmark for shots `dbg.js BOOKMARKS` has no venue pose, and `DBG.shot(name)` forces street mode **and re-poses/re-segments** — so there is no way to drive a scripted "lit pub frontage at night, gig on" street shot (the alpha's other money shot; your marquee + posters + spill are the whole street story of this round). F verified the frontage lights correctly (`emissiveIntensity` 1.7 at `on`, 71 draws / 29.6k tris at the venue) but shot it by hand. **Ask:** add a `venue_night` bookmark (pose at the pub frontage, `seg:5`) — then F's `tools/qa/gig_shot.py` can capture the street side too. This is the same "3 missing bookmark poses" ask from R3 §9, so it may be worth doing the set in one pass. ### Harness note — where the browser gates actually run This session's box had **no Chromium and no Chrome**, so every Playwright gate (flags harness + scale gate) is un-runnable locally until `tools/.venv/bin/python -m playwright install chromium`. The repo at `/Volumes/m3ultra/Documents/procity` is a **mount of m3ultra's `~/Documents/procity`** — same tree, so `ssh m3ultra@100.89.131.57` runs the real gates on the real files. **Gotcha:** node is not on the non-interactive ssh PATH — `env PATH=/opt/homebrew/bin:$PATH tools/qa.sh --strict`, or every node gate reports a bogus FAIL (`node: command not found`). ### R12 numbers (measured, seed 20261990 → "Boolarra Heads", venue = The Thornbury Hotel) | thing | number | |---|---| | gig-night interior, rig fleet | **42–50 draws** · ~40k tris (budget **≤350**) | | gig-night interior, `?noassets=1` placeholders (**worst**) | **171 draws** (budget ≤350) | | gig-night street at the venue | 69 draws · 29.6k tris (budget 300 / 200k) | | band / crowd | **3** on the deck · **8** at 8 watch points · **3 dancing / 5 standing** | | figure scale | band stature 1.68–1.77 m (crown ≤2.09 m under a 4.0 m ceiling); crowd 1.60–1.77 m | | leak, 4 gig enter/exit cycles | geo 66→66 · tex 63→63 (**zero**) | | tonight's covers (7 nights) | `10, 0, 0, 3, 2, 0, 9` — 3 free of 7 (~half, as seeded) | | custom band names surfacing | **7 of John's 10** fill all 7 nights; `?noassets=1` → pure generator | --- ## 13. Round 13 — v3.0-beta THE DISTRICT (`?gigs=1`). F wires it last; findings below. **What F built (all 7 tasks from ROUND13 §Lane F).** All committed Lane-F-only; verified green on m3ultra (`qa.sh --strict`, the focused district gate, the scale gate). A single WARN — the queue — is a documented handoff to D, not a hole (below). 1. **Per-venue state machine** (`gig_state.js`, rewritten). One latch PER VENUE keyed by `venueShopId`; a town of 2–4 venues runs 2–4 independent latches off the SAME `procity:segment` spine. All three hard-won R12 laws are preserved verbatim inside `createVenueLatch` (listens-not-polls · `state` is a getter · night rolls on WHERE the clock lands). New runtime surface on `window.PROCITY.gigs`: **`.byVenue`** → `{ [id]: state }` (B's `venue.js`/`audio.js` read it), per-venue **`stateOf/onOf/openOf/gigOf/coverOf/ bandNameOf/paidOf(id)` + `markPaidOf(id)` + `.venueShopIds`**. The R12 single-venue alias (`.state/.on/.venueShopId/.cover/.paid/…` → the **primary** venue = `gigs[0]`'s) is kept **one round** so B's mid-round single-venue callers don't break — it retires next round. Contract pinned in CITY_SPEC §v3. *Tonight* per venue = its night-0 gig (matches A's posters + B's frontage, all night-0-keyed); a dark-tonight venue latches 'quiet' all session, which is true to the seeded week. 2. **Walla + applause** (`interior_mode.js`). While a venue's gig is `on`, the crowd bed `ambience['crowd-walla']` rides the interior TONE layer under the live band bed (you hear the room, not the fridge hum) — it reuses B's existing `playInterior({musicKey,toneKey})`, no new audio API. On the `on → done` edge while the player is inside (crowd's last-song → the clock rolls NIGHT→DAWN), a one-shot `sfx['applause']` sting fires ONCE (`applauded` latch, re-armed per enter). Fail-soft throughout (no-op muted / `?noassets` / pre-gesture). Verified live: `iMusic=gig-pubrock`, `iTone=crowd-walla`, edge fires, 0 errors. 3. **Cover charge PER VENUE** (`index.html`). The door seam now keys off the shop being ENTERED (`openOf/coverOf/paidOf/markPaidOf(shopId)`), so a night at two venues is two covers; each latch holds its own runtime paid-stamp. Verified: paid ($10 debited once, re-entry free), broke-knockback, free night. 4. **Debt #1 CLOSED — and the R12 bridge was a LIVE BUG.** E renamed the pub bed `pubrock-live → gig-pubrock` this round (the canonical `gig-`), so F's R12 `GIG_BED`/`gigBedKey` bridge — which rewrote `gig-pubrock → pubrock-live` — was now pointing at a manifest key that **no longer exists**: the band would have mimed in a silent room. DELETED. The room's `audio.gigKey` (already the manifest key) feeds the engine directly, zero mapping. Verified across two genres live: `gig-pubrock` (pub), `gig-covers` (rsl). 5. **Gate hardening — stature into the shared gate** (`interior_scale_check.py`, debt #6). The human-size assert now measures **stature = crown − foot** (feet→crown span, platform-independent), with the world-crown "never clips the ceiling" assert kept as its OWN line. The day a keeper stands on a platform, the gate won't fail a correct figure. Floor figures foot≈0 ⇒ stature≈crown ⇒ backward-compatible. Verified: 24 figs across 6 shops, statures 1.60–1.86 m. 6. **District smokes** (`flags_check.py smoke_gigs`, extended). band 3→**4** · a new **§2b DISTRICT** loop that enters EVERY playing venue and asserts its `gigKey` resolves to a real manifest bar (debt #1 across 3 genres), per-venue crowd ≤ its own watchPoints, 4-piece, no-giants-by-stature · **§2c street budget** at the busiest venue block (settled-floor sample) · queue probe (WARN-pending, below) · plus the R12 determinism / cover / noassets / flags-off arms. All green. ### ⚠ Cross-lane ask (Lane F → Lane D) — the QUEUE seam. F's smoke is READY; it WARN-skips until D exposes it. D owns the outdoor queue (`web/js/citizens/queue.js` `VenueQueue`, charter item, landing this round). B has already published the zone: **`window.PROCITY.venuePresentation.queueZoneFor(id) → {x,z,ry,len}`** and `.queueZones`. F drives the surge per venue via `citizens.setGig(id, open)` (done). What F's smoke needs to verify "queue ≤ cap and drains by done" is a read seam on the sim — **proposed contract:** `window.PROCITY.citizens.queueInfo(venueShopId) → { count, cap }` (live count while doors/on; `0` once the gig is `done`). Wire the queue's spawn to `queueZoneFor(id)` at `doors` and expose `queueInfo`, and F's smoke flips WARN→assert with no further change. (If D picks a different accessor name, tell F and F adjusts the one line in `smoke_gigs §2b`.) ### R13 numbers (measured, seed 20261990 → "Boolarra Heads", 3 venues) | thing | number | |---|---| | district | **3 venues** — 2 playing tonight (pub #116 The Exchange Hotel · rsl #404), 1 dark (reads true) | | live beds resolve | pub → `gig-pubrock` · rsl → `gig-covers` (both real manifest bars, debt #1 across genres) | | band / crowd | **4-piece** each venue · pub crowd **8/8** watch points · **rsl crowd 12/12** (the cap stress case) | | gig-night interior (rig) | **62 draws** · 34k tris (budget ≤350) · `?noassets` still a full 4-piece gig | | street @ venue block, night | **99 draws** · 28k tris (budget 300 / 200k) — lit frontage + posters + queue zone in | | figure scale | band+crowd stature **1.61–1.81 m**, tallest crown 2.12 m under the 4 m ceiling | | cover, per venue | $10 debited once · re-entry free · broke knockback · free night (seed 1234) — all hold | | flags-off prime law | fingerprint **0x3fa36874** unchanged · gig layer wholly absent · 0 errors | **Status:** F's wiring is done, committed, and green. **Waiting on Lane D** for (a) the queue seam (`queueInfo`) so the queue smoke goes live, and (b) the band polish (E's instrument GLBs in hand, drummer facing/RY_FLIP) — the band already lands at 4, human-sized, 0 errors, so this is polish not a blocker. Once D commits: re-run the gates, flip the queue assert, capture the district money shots, tag **v3.0-beta**. *(R13 epilogue: D landed mid-session; F wired the queue (D shipped `VenueQueue`, left the street-side lifecycle to F), the queue smoke went live, both money shots shipped, and **`v3.0-beta`** was tagged + pushed. The `queueInfo` proposal above was superseded by D's `VenueQueue.count()` + F's shell `queueCountOf`.)* --- ## 14. Round 14 — F never ran; the brief carried in full to R15. R14 was the "v3.0 release round." A–E landed excellent hardening (D's identity-continuity seam, B's alias-reader retirement + `queue_night` hero + 19-bookmark tour audit, A's 400-seed invariant sweep + §v3 freeze, C's adversarial venue audit, E's provenance freeze) **but Lane F never ran** — so v3.0 was not tagged and F's whole release brief (continuity wiring, alias deletion, week soak, the tour, the tag) was untouched. Fable carried it verbatim into R15. Nothing F-authored landed in R14. ## 15. Round 15 — THE v3.0 RELEASE CLOSE (`?gigs=1`). F ran last; the six-item brief, all green. A first (poster flip + golden re-pin) → B/C verify → D one-line amp move → **F closes**. All deps were in before F ran (verified against origin). The six F items (ROUND15 §Lane F), each committed atomically by pathspec: 1. **Continuity wiring + smoke** (ledger #3) — D published the seam in R14; F wired its three touches: `crew.spawn(…, { roster: rosterOf(shop.id) })` in `interior_mode.js` (front crowd slots become tonight's roster), `rosterOf:(id)=>citizens.tonightRoster(id)` + `onAdmit:(e)=>citizens.recordVenueEntry(id,e)` on the queue in the shell (the clear is automatic on `setGig(id,false)`). New `smoke_continuity` asserts the seam on **identities** (crowd ⊇ roster ∩ cap · every `member.pedIndex ∈ tonightRoster` · `crewInfo.fromRoster`), the empty-roster path injects nobody (shell side of D's byte-identical law), and **settled tris** (ledger #5). Commit `687d23f`. 2. **Alias deletion** (ledger #2) — the R12 single-venue scalar alias is **gone**; every read is keyed by `venueShopId`. Deleted the alias block + `primaryId`/`P` in `gig_state.js`; migrated `flags_check.py` + `gig_shot.py` (`.venueShopId→venueShopIds[0]` · `.state→stateOf(id)` · `.cover→coverOf(id)` · `.bandName→bandNameOf(id)`); deleted the two CITY_SPEC §v3 alias sentences + added the omitted `nightOf(id)`. B's R14 sweep left the cross-lane readers clean; only F's tools remained. One atomic commit `146b5f3`. 3. **Full-week soak** (`tools/qa/week_soak.py`, opt-in `qa.sh --soak`, non-strict) — 7 nights × every playing venue in one context: latch state per `plan.gigs` (dark venues quiet), cover **stamped per venue per night** (debited once, re-entry free, due again next night), roster **clears per night** (no accumulation), and geo/tex return to a **warmed baseline** across ≥20 cycles — a leak is unbounded per-cycle growth; warm-up cache residue **plateaus** (the memTrail proves it). Commit `edc0c47`. 4. **The money-shot tour** (`tools/qa/tour_shots.py`, ledger #4/#5) — one deterministic run → `docs/shots/ release_v3/` + `contact.html`: `venue_night`, `district_posters`, **`queue_night` (John's hero — A's flipped frontage poster now reads from the street)**, `gig_interior` (4-piece + real instruments + a roster crowd), `rain_verandah`, `tram_stop`. **Settled discipline**: measured after the async instrument GLBs / posters / chunks land (shoot-twice), clean per-group loads; queue driven **live** (explicit admits, not throttled rAF auto-drain). Commit `6a35b29`. 5. **Docs assembly** — README v3 section (what shipped · flags table · the tour), this §14/§15 runbook, F-progress current, the five freezes confirmed committed (A `a3911a4`/`35da83c`, B `eee60e0`/`a0b5784`, C `62e38c5`/`2446ae3`, D `9a65f51`/`66aec0f`, E `55f6698`). 6. **Acceptance + tag** — `qa.sh --strict` 6/6 + `smoke_continuity` + `week_soak.py` green → tag **`v3.0`**. ### Numbers (measured, seed 20261990 → "Boolarra Heads", 3 venues) | thing | number | |---|---| | district | 3 venues — 2 playing (The Exchange Hotel `gig-pubrock` + an RSL `gig-covers`), 1 dark | | gig interior, settled | 4-piece · crowd 8 (2 from the roster) · **56 draws** · **35k→182k tris** (pre-load→settled; gate is draws ≤350) | | street @ venue block, night | `queue_night` 121 draws / 62k tris · `district_posters` 186 / 134k (busiest) — all ≤300/200k | | week soak | 28 enter/exit cycles · $60 covers · geo/tex flat across the back half (115/86→115/84) · 0 errors | | continuity seam | 2 queue admits carried into the crowd · empty roster 0-injected · `member.pedIndex ∈ tonightRoster` | | **settled-tris honesty** (ledger #5) | C was right: the interior settles to **~182k** tris after the 5 instrument GLBs load, not the ~34k measured pre-load | ### The gotcha ledger closed this round - **Alias retirement is a three-party move**: B swept cross-lane readers (R14), A left the spec sentences for F (avoiding a doc-ahead-of-code race), F deleted the code + migrated its own tools + the spec in ONE commit. No half-renamed state ever existed. - **Settled measurement** (ledger #5): a headless smoke that reads `renderer.info` right after `enterShop` sees the room BEFORE its async instrument GLBs decode — ~34k, not the ~182k settled truth. The tour + the continuity smoke both wait/shoot-twice now; the interior gate stays on **draws** (which is load-order stable), so there was never a budget violation, only a dishonest number. - **Drive live-spawned things explicitly**: automation throttles rAF, so the queue's seeded auto-drain may not fire on schedule — `smoke_continuity`/`week_soak`/`tour_shots` all call `admitOne()` / step the loop explicitly rather than trust the timer. --- ## 16. Round 16 — v3.1 THE FLIP (`?classic=1` is the new covenant). F bookends; opening below, close after. The town shows its best by default. After three v3 stages the gig layer flips ON, and the mature v2 flags (`weather`, `winmap`, `tram`) flip with it. F owns the flip; A–E landed their v3.1 polish against it (D's seated drummer, B's town-wide pools + default re-baseline, C's backline unification, A's corner-poster + no-spine fixture, E's `sit.glb`). ### The flip (ledger #1) — the covenant MOVED from "flags off" to `?classic=1` - **Shell** (`index.html`): `const CLASSIC = has('classic') && get('classic')!=='0'`, `const flagOn = name => !CLASSIC && get(name)!=='0'`. `gigs`/`weather`/`tram` construct via `flagOn`; `loadPedFleet({sit:!CLASSIC})` gives the drummer his clip while keeping classic zero-fetch; `window.PROCITY.flags` publishes the intent. - **`buildings.js`**: `winmap` reads the SAME rule (default-on, classic-off) — the one cross-lane flip line, because winmap's flag lives in B's file. Marked `[Lane F R16 THE FLIP]`. - **Two goldens, two gates:** classic → `0x3fa36874` (byte-identical v2 forever, the frozen regression target); default → the gig golden `0xb1d48ea1`. `flags_off_regression` became **`classic_regression`** (boot `?classic=1`); a new **`default_boot_gate`** proves the no-flag boot is the full experience (all four flags on, gig layer present, gig golden green, budget). Every gig smoke re-points to the DEFAULT boot. ### Seated-stature gate (ledger #3) — published in the brief, no mid-round handshake A seated figure fails the standing no-giants floor by design. Contract (in the round brief so D built to it): D tags seated figs `fig.userData.procitySeated = true`; F's `interior_scale_check.py` + `flags_check.py` accept them at **stature ∈ [0.9, 1.5] m** (still < ceiling, feet planted); standing stays [1.4, 2.0]. Landed in the opening commit. Verified live: the seated drummer passes at **1.32 m stature**. ### Numbers (measured, seed 20261990 → "Boolarra Heads") | thing | number | |---|---| | `?classic=1` | byte-identical v2: fingerprint 0x3fa36874, gig layer absent, streamed roster on, 160 draws / 21k tris | | default boot | all four flags on · gig golden 0xb1d48ea1 · 14 gigs, 15 posters · **112 draws @ the venue block** (≤300) | | district (default) | 3 venues, **3 playing** (corner-poster re-pin shifted the schedule) · 3 genres live | | seated drummer | **1.32 m** stature — passes the seated band [0.9,1.5]; the standing crew 1.68–1.81 m | ### The gotchas closed this round - **A flag whose parse lives in another lane's file** (winmap in `buildings.js`) still flips as part of F's structural change — one marked line, same `!classic && !=='0'` rule as the shell. - **selfcheck prints the base fingerprint in its summary but asserts the GIG golden silently**, so the default-boot gate gates on the ALL-GREEN run (which includes the gig-golden assert), not a stdout substring. - **`P.winmap` isn't exposed** — the default gate verifies winmap *intent* via `window.PROCITY.flags`; the actual shader is B's own `winmap` smoke. - **`?classic` must zero the fetch delta**, so `sit.glb` is gated on `!CLASSIC` (not just on the fleet) — a raw sit fetch under classic would break the covenant even though the drummer never spawns there.