PROCITY/docs/LANES/LANE_F_NOTES.md
m3ultra 78848ed254 Lane F R26 wave 4: the re-run — 6/1/0 → 7/0/0, QA GREEN, v5.0-beta tagged
The fence went in and the hold ended. C ruled, and both lanes titled their commit the same
thing: "my rule was the stale thing." E's one line:

    SLOT_ID_PREFIX = {"real": "sku_", "mint": "mint_"}     # sourcing-scoped, not one space

qa.sh --strict --matrix: 7 passed / 0 failed / 0 warn. selfcheck ALL GREEN 156,212/156,212 ·
MATRIX GREEN 10 towns x 7 gates · flags harness (carrying F's beta gate: 15 crates / 105 pairs /
0 shared titles, sourcing real+mint, ZERO 404s, 0 console errors) · atlas-QA OK, 15 atlases
valid, 15 LICENCE LINES SHOWN — R24's finding #4 at full scale, where one silently printed none.

The ruling is the round's keeper. Fable: prefixes are namespace fences. E's comment makes G's
judgment law: "`sku_` names a POS copy on a shelf, `mint_` names a dealgod listing standing in
for one... a prefix that lies doesn't fail a gate, it SELLS THE WRONG RECORD out of a real shop
to satisfy a minted stand-in. Never treat them as one space (same standing note as plan-ids vs
godverse-ids)."

That closing clause is F's R24 collision — plan id 31 vs godverse id 31, which walked F's own
harness into a cafe — promoted from a finding to a standing law, and re-derived independently by
G in the layer that moves money. The bug F found by entering the wrong shop is the bug G refused
to ship by selling the wrong record.

The rule bent; the id spaces didn't. That is the right direction: a rule describes an intent, and
when a legitimate case fails it, the description is what's wrong — PROVIDED somebody measured
that the intent still holds. F measured it before the ruling (non-positional, seeded, stable,
0 reuse across 15 crates), which is what made the ruling a decision instead of a guess. Holding
the tag was never about the rule being wrong; it was about who gets to say so.

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

1232 lines
91 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.

# LANE F — NOTES (integration runbook + QA gates)
*Lane F is integration + QA. It runs **after** AE 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** AE 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:[<full filename>.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:<file>` → 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-<name>.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 ≤~1015k
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-<genre>``<genre>-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 | **4250 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.681.77 m (crown ≤2.09 m under a 4.0 m ceiling); crowd 1.601.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 24 venues runs 24 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-<genreKey>`), 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.601.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.611.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." AE 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; AE 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.681.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.
---
## 17. Round 17 — v3.2 THE TAIL + THE SCOUT. F: the town matrix + the close (ledger #7).
The tail drained (D's drummer lean + bench-sit, B's wind sway, C's backline verify, E's `--verify` fix) and
the real-map scout ran (E's 5 real AU town caches + A's cache contract + hardened `plan_osm`). F wired the
last mile and gated the whole matrix.
### Real towns boot in the engine (the F wiring)
E's caches sat on disk (`web/assets/towns/*.json`) + registered in the node selfcheck, but **the shell never
loaded them** `?plansrc=osm&town=bendigo_real` silently fell back to Melbourne. F closed the gap: the barrel
(`index.js`) re-exports `registerTownCache`/`validateTownCache`; the shell fetches `assets/towns/<TOWN>.json`
+ registers it before `generatePlanFor` when the town isn't a checked-in fixture. **Fail-soft** 404 /
invalid / `?noassets` plan_osm's fixture fallback; the scout data is opt-in, never on the default or
`?classic` path. Verified: `?plansrc=osm&town=bendigo_real` **Bendigo, 9 shops, a 3-venue district, 0 errors**.
### The town matrix (`tools/qa/town_matrix.py`, opt-in `qa.sh --matrix`) — the v4 evidence
Boots every town on the DEFAULT boot and gates boot / determinism / budget / district / `?noassets`. **All
10 × 5 green:**
| town | shops | budget (draws/tris) | district |
|---|---|---|---|
| synthetic/20261990 · 1234 | 493 · 486 | 124d/57k · 147d/71k | 3v |
| osm/melbourne · katoomba · silverton | 95 · 19 · 12 | 216d · 70d · 70d | 3v |
| real/bendigo · castlemaine · fremantle · katoomba · newtown | 9 · 6 · 21 · 20 · 18 | 82 · 86 · 159 · 69 · 93 d | 3v |
The headline: **the existing CityPlan contract eats real Australian geography** Castlemaine at the 6-shop
floor still lands a 3-venue district; every real town is deterministic, in-budget, and silent under
`?noassets`. No new game systems exactly the bounded scout the round asked for. This table is the evidence
John charters **v4 = THE REAL MAP** on.
### Gotchas
- **A published cache + selfcheck a bootable town.** The node sweep proved the *plan* generates; the
*browser* boot needed the shell to fetch+register the cache. Classic integration seam F owns it.
- **`?noassets` can't fetch a real cache**, so a real-town + noassets boot correctly falls back to a fixture
(still silent-and-fine). The matrix's noassets gate tests that fallback, not the real town as it should.
---
## 18. Round 18 — v4.0-alpha REAL ROADS. **F correctly did NOT run — the gate would have failed by design.**
The alpha gave `katoomba_real` its real OSM street geometry (A's schema v2 `roads[]` + graph lift; E's road
fetch). F's charter deliverable was "as much the failure list as the town" and F, running the gate, found
the alpha **not green**, so F held the tag and filed the list rather than shipping a red release. This was
the round working as designed (Fable's R18 verdict agreed: "F never ran correctly").
**What F caught (all Lane A plan-gen, surfaced with measurements):**
- **`qa.sh` selfcheck RED** `katoomba_real`'s real-roads golden **UNPINNED** (the AEA finalization
handshake never fired: E's roads landed *after* A's commits), and the 4 trailing towns' goldens stale
(pinned on marched output, now roads).
- **Poster-kerb-clearance RED on all 5 real towns** the real edge explosion (232 "main" edges) put spine
posters **1.43.9 m into the road**.
- **Fragmentation** 20 shops scattered over 799 real edges / 607 m no walkable strip; the money-shot
attempt was a vast empty field (D's finding: 105 components, 42% dead street-metres). And B's separate
finding: the `BIG_CITY` heuristic misfired (20 shops < 120 small-town streaming on a 799-edge graph
**280k tris**, breaching the 200k gate).
**What was already green (the isolation held):** real roads ride the **cache schema, not a flag**, so
classic + default + synthetic + fixtures were untouched (`0x3fa36874` frozen) the alpha changed nothing
outside the flagged path. The pipeline worked; the geometry needed the hardening the alpha existed to reveal.
## 19. Round 19 — the v4.0-alpha CLOSE. F: B's budget one-liner (early) + the gate (last).
A resolved the findings this round (fragmentation ruling main component + joins, cull shopless islands;
poster clamp; goldens pinned; junction-protected DP), D re-measured and verified, C re-soaked, B shipped the
town selector. F ran two things:
1. **B's `BIG_CITY` one-liner** (`index.html`, landed EARLY per the order so D/B re-measures saw it):
`BIG_CITY = shops > 120 || (streets.edges > 200)`. A real town can have few shops but a huge road graph;
the graph now trips big-city streaming. **Verified: real Katoomba 799 edges 61 draws / ~12k tris (was
280k), 0 errors.** B proved it; F owns `index.html` so F landed it.
2. **The alpha gate + the roads dimension** `town_matrix.py` grew a `roads` gate: a real cache (schema v2
with `roads[]`) must build the REAL street graph (`norm.mode === 'roads'`, read via the plan_osm
`opts.report` sink), while synthetic/fixture towns stay marched/synthetic by design. Plus the classic +
default gates prove the alpha is still isolated to the cache path. Money shot: **Katoomba's real main
street** asphalt + lane markings, footpaths, a pedestrian crossing, a walking crowd, gig posters,
streetlamps, B's town selector in-frame PROCITY's look on real bones, in budget.
### The honest notes on the alpha (two, both filed for v4.0-beta)
1. **Density.** Real Katoomba's 19-shop cache is genuinely spread ~1 shop per real edge (`shopsOnBest = 1`),
so the alpha is a real *street* with the town's life on it, not a dense retail strip that density is a
beta concern (more shops per cache, or focusing the retail spine).
2. **One open budget finding (non-blocking).** The matrix's worst-case view (`venue_night` NIGHT, the lit
venue block, all posters up) measures **200,965 tris on `katoomba_real`, +965 over the 200k soft ceiling
(0.5%)**. Driver: the poster count scales with the real *edge* count (438 posters for 19 shops), not shop
count. Normal play (the midday money shot) is 121k well under. `qa.sh --strict` is GREEN; this rides
the opt-in `--matrix` warn-gate, not a blocking gate. Beta hardening: A caps posters per real town.
The alpha's job was **real roads that boot, stream in budget in play, place a district, and read as a real
place** that shipped; the venue_night stress view is the one filed edge.
---
## 20. Round 20 — v4.0-beta THE FIVE TOWNS (density). **F never ran — the honest record.**
R20 widened the towns (E: katoomba 2080, five towns to 292 shops, "subtle" enforced by construction),
proved the directive from the crowd side (D's A/B: heroes kept footfall 1719 while the street got 3.8×
busier), capped posters 43831 (A), and ruled the tram by shop adjacency (B: the alpha tram fronted **zero
shops on 4 of 5 real towns** "it really was a highway bus"). Excellent round.
**F did not run, and there was no tag.** Unlike R18 where F ran the gate and *held* on evidence R20's F
slot simply never executed. The round also left C's density door-soak undone, B's selector still hardcoding
`hud.js:11` instead of E's index, and D's relocation finding (katoomba's pub 1,000 m from the retail cluster
a gig with no passing crowd) without an owner. R21 closed all four.
## 21. Round 21 — the v4.0-beta CLOSE. F: the beta gate + the tag (ledger #6).
Waves ran [A, B, E] [C, D] [F]. A biased `pickVenues` toward retail-cluster adjacency (D's finding) and
re-pinned once; B derived the selector from E's `index.json`; C soaked the five towns' doors at density; D
verified the relocation (**A's bias PASSES**); E pulled 22 towns / 1192 shops forward as non-gating data.
### The matrix — 10 towns × 7 gates (`--matrix`)
`boot · determ · district · roads · tram · noassets` **green on all ten**. Two budget flags (below).
Selfcheck **154141/154141**, `qa.sh --strict` GREEN.
### Tram (#6c) — B's R20 ruling verified exactly
| town | verdict |
|---|---|
| newtown_real | **runs, 16 stops** (B's phantom-stop fix: was 149) |
| castlemaine_real | runs, 10 stops |
| katoomba / fremantle / bendigo | **fenced** 3 / 2 / 1 shops fronted, all < the 5-shop floor |
New seam: the shell now exposes `window.PROCITY.tram` B built `routeInfo` *for F's smoke* but nothing
published it. F owns index.html, so F wired it. The matrix reads `{fenced, stops, shopsFronted}`.
### ⚠ The venue_night re-measure (#6b) — RECORDED, and the premise came out false
The brief expected A's poster cap to close F's R19 `+965`-tri edge. **It did not — it widened:**
| | R19 | R21 |
|---|---|---|
| katoomba_real venue_night | 200,965 (+965, **+0.5%**) | **221,935 (+21,935, +11%)** |
| bendigo_real venue_night | | 201k (**+0.5%**) |
| fremantle / newtown / castlemaine | | 184k / 179k / 112k |
**A's cap worked** (posters 438111). The cause is the *density widening*: katoomba 1972 shops + D's 3.8×
busier street more than ate the savings. **Draws are fine everywhere** (151/300 half budget); this is
tris-only, at the worst night stress view. Not a blocking gate (it rides the opt-in `--matrix` warn), and
the brief's instruction was "record the new number" recorded. **Filed as R22's first item**, where the
tri-diet backlog (instrument LOD · tram tri diet) already lives. katoomba is the outlier: a 966-edge graph
carrying 72 shops.
### Ruling (#6d) — selector gating: **always in the HUD**, not `?dbg`-gated
THE REAL MAP's product *is* the towns; gating their discovery behind a debug flag would hide the epoch's
headline feature. It's unobtrusive, shows friendly names, and lives in the HUD overlay. **Corollary (F's own
gap):** money shots must hide the HUD the R19 alpha shot had the selector in-frame because F's harness
screenshotted the full viewport. The beta shot hides it.
### The beta money shot (#6e)
[`docs/shots/v4_beta/fremantle_mainstreet.png`](../shots/v4_beta/fremantle_mainstreet.png) Fremantle's real
street at golden hour: buildings lining both sides down the corridor, footpaths, a crossing, gum trees,
streetlamps, and a genuinely busy crowd (the density widening, visible). 126 draws / 167k tris, HUD hidden.
Posed by projecting the densest **spatial** shop cluster (10 shops within 60 m) onto its front edge and
standing on the road per-edge shop counts are ~1 on a real graph, so "densest edge" is the wrong pose
heuristic for real towns.
---
## 22. Round 22 — **v4.0: THE EPOCH CLOSE.** F: the re-measure, the selector, the tour, the tag.
### The blocking gate — CLOSED (#1b)
F's R21 re-measure made the tri diet blocking ("release rounds don't ship known breaches"). B diagnosed
layer-by-layer, the driver traced **A → E** (A's own commit said so: "B's numbers say the fix is E's"), and
**E fixed it**. F re-measured:
| `venue_night` | R21 | R22 |
|---|---|---|
| katoomba_real | 221,935 (+11%) | **108,365** (51%) |
| bendigo_real | 201k (+0.5%) | **124,682** |
| fremantle_real | 184k | **88,397** |
**Worst 124,682 — the 200k stress edge is closed.** Selfcheck **161,300/161,300** (A's 36 goldens + the
23-town pack). The measure-first loop worked end to end: F measured, B diagnosed, E owned, F re-measured.
### `DBG.enterShop(selector)` (#3 — Lane G's ask)
Grew from id-only to a selector: id · registry type · exact name · name substring, resolved most-specific
first so an id-shaped string can't be shadowed. `DBG.enterShop('record')` / `('The Exchange Hotel')` / `(116)`
cross-repo QA one-liners on any of 23 towns without an id lookup.
### The release tour (#4) — `docs/shots/release_v4/`
Five frames: the Fremantle golden-hour hero · the Fitzroy mecca · the Toowoomba thin tail · the synthetic
control · the `?classic=1` covenant. Every real-town frame poses through **F's R21 cluster primitive**
(project the densest SPATIAL cluster onto its front edge, stand on the road); B's R22 bookmark fix anchors
on the same retail heart, so tour and bookmarks agree by construction. HUD hidden throughout.
**The primitive earned itself twice.** First cut of the tour posed via `DBG.shot('street_noon')` and the
**mecca came out as bare bitumen** the same trap as R21. Re-posed through the cluster primitive, Fitzroy
reads (7 shops/60 m retail heart, trees, benches, crowd). *Per-edge is always the wrong heuristic on a real
graph.*
### ⚠ The honest thin-tail finding (filed for v4.x)
The brief asked for a thin-tail frame as **"proof the floor holds."** It doesn't, and the frame says so:
**Toowoomba reads as an empty road, not a town.** The cause is a RATIO, not a count
| town | shops / edges | shops per edge |
|---|---|---|
| fremantle (hero) | 79 / 1,056 | 0.075 |
| fitzroy (mecca) | 139 / 2,408 | 0.058 |
| **toowoomba (thin tail)** | **12 / 1,459** | **0.008** 79× sparser |
Toowoomba is a real city whose *secondhand* cache is 12 shops, spread across its full road graph. The floor
isn't "≥N shops" it's **shops-per-street-metre**. v4.x options: clip the graph to the retail core for
thin caches, or set a ratio floor in the pack. The frame ships **labelled honestly** rather than swapped for
a flattering town same discipline as the R21 re-measure that got worse and said so.
---
## 23. Round 23 — v5.0-alpha THE FIRST REAL CRATE. **F HELD the tag — the crate has no street.**
**Verdict: `v5.0-alpha` NOT tagged.** The round's headline artifact is real and good G's 120 real
records off a real shop's POS are in the tree, byte-identically re-bakeable. It cannot be reached from
the game. `#7a` (riffle the real crate, buy it) is not a gate that failed; it is a gate that **cannot be
run**. R18's law: *release rounds don't ship known breaches.*
### What F did run
| gate | result |
|---|---|
| **`qa.sh --strict --matrix`** | **7 passed · 0 failed · 0 warn** QA GREEN |
| **town-matrix** | **MATRIX GREEN — 10 towns × 7 gates**, all green |
| citygen selfcheck | **ALL GREEN 152,015/152,015** (was 161,300 E retired toowoomba) |
| flags harness (classic regression · default boot · STRICT) | **GREEN, 0 warnings** the covenant holds through the merge |
| manifest validator · scaffold · consistency · no-giants | GREEN |
| tier 0 `?noassets=1` | **zero fetches, zero console errors** the offline law holds |
| tier 0 default boot, godverse town | town loads (18 real Newtown shops), stock parody |
| tier 1 (v2, town-wide) `?stock=real` | 350-item pack resolves, real cover art |
| **tier 1 (v5, per-shop) — the round's deliverable** | **NEVER FETCHED** |
| tier 2 | N/A by law (no server code this round) |
### The merge that never executed (F did it — charter law #5)
Fable's mid-round merge never ran: `main` had **no `web/assets/stock_godverse/**` at all**, so the branch
F was asked to gate and tag did not contain the round's headline. The review had happened (the shared
checkout was parked on `lane-G-godverse` for it) and C verified the atlas from a temp worktree
deliberately pre-merge and passed. Only the `git merge` was missing. F executed it as the integration
lane, disjointness **verified before merging, not assumed** (G: `G-progress`, `G3 doc`,
`pipeline/godverse_*`, `stock_godverse/**` · main-since-fork: `C-progress`, `LANE_C_PUB` · overlap: none).
Zero conflicts, exactly as law #5 predicted. `43b035a`.
### ⚠ FINDING 1 — the atlas contract contradicts itself. Nobody was wrong; the contract was.
| | says the index is |
|---|---|
| **C's published contract** `LANE_C_PUB:271` | `stock_shop_<godverseShopId>_index.json` |
| **C's own runtime loader** `stockpack.js:25` | `` fetch(`${base}stock_${type}_index.json`) `` |
**These are mutually incompatible.** An atlas named per the published contract can *never* be loaded by
the code that is supposed to load it. G shipped `3962749/stock_record_index.json` **runtime-compatible**,
which is exactly why C's base-override verification passed. E globbed `stock_shop_*_index.json`
**contract-compatible**, which is why E's validator never saw the atlas. Two lanes implemented opposite
halves of a self-contradicting spec and both were correct.
**F's recommendation (measured, not preferred):** amend the contract to G's shape
`stock_godverse/<godverseShopId>/stock_<type>_index.json`. It is the only shape the runtime can load,
it keys per-shop by directory, and it costs G nothing (already emitted). E's glob follows. C owns the
wording (LANE_C_PUB); this is the amendment ledger #3 anticipated.
### ⚠ FINDING 2 — the gate built to catch this was blind, and green
`validate_atlas.py` globs for a filename that cannot exist at runtime, finds nothing, prints *"no
per-shop atlases yet nothing to validate"* and **`return 0`**. `validate_manifest.py:158` propagates
that green (its comment states the intent: *"No atlases yet ⇒ clean pass"*). So the round's QA gate passes
whether the atlas is **absent, misnamed, or perfect** it has never once looked at one. E built it in
wave 1 with no atlas to test against; on first contact (F, at close) it was blind.
The flaw is precise and the fix is a one-liner: the validator **conflates "no atlases exist" with "no
atlases matched my glob."** The first is a legitimate clean pass; the second is exactly the failure it
was written to catch. Cross-check the directory if `stock_godverse/*/` is non-empty but no index
matched, that is an **ERR**, not a pass. *A gate that returns 0 on an empty set had better be certain the
set is empty.*
Pointing it at G's index by hand (filename drift removed) shows the drift is **deeper than the name**:
- **`ERR missing/empty provenance ['license','attribution','generator','snapshot']`** but G *carries*
`generator` and `snapshot`. E's line 68 reads `idx.get(f)` **top-level** while E's own docstring
(line 14) specifies them **nested under `provenance`**, which is where G put them. **E's code
contradicts E's doc**; G's atlas would fail even with E's exact field names.
- **`licence` vs `license`** G used the British spelling; E's `REQUIRED_PROV` wants American.
- **`attribution`** G folds the substance into `licence.covers`; E wants the field.
- **`WARN 2 atlases C's rule is 1/shop`** E built to C's *pre-correction* ceiling; C amended §7.2 to
`≤2 atlases @≤2048²` in the same wave. Stale by one round.
### ⚠ FINDING 3 — `#7a` would have passed vacuously
The brief says: *"boot `newtown_godverse` with `?stock=real` … riffle — **real covers render**."* They do.
F walked into **The Vintage Record** (a real Newtown shop, on its real road) via `DBG.enterShop`, and the
crates filled with **real cover art** from E's **350-item town-wide v2 pack** (`Sunburnt Milk Bar Vol. 2`,
`Midnight Departure`), the same pack every record shop in every town has had since v2. **Not** Monster
Robot Party's 120. Network confirms: every stock fetch goes to `assets/models/`; **zero requests to
`assets/stock_godverse/`, ever.** Had F eyeballed the gate as written, it would have gone green and proven
nothing. *Two vacuous gates in one round same species: both test the presence of a thing rather than
the identity of it.*
### The exact failure list for R24 — three gaps, none of them F-only
1. **The crate has no street.** [E + G] No town cache contains shop `3962749`; `redhill_real` does not
exist. Needs a `build_towns.py` TOWNS entry (~`center (-27.4553, 153.0064), span_km 2.4`) + one
outward-facing Overpass fetch `redhill_real`, then `godverse_town.py redhill_real redhill_godverse`
with monsterrobot injected into `shops[]`. **F did not do this: E's file, G's generator, and an
outward-facing fetch inventing town data is not F's to invent.**
2. **No identity field exists to key an atlas by.** [A + G] Verified in the live runtime: *every* shop in
`newtown_godverse` reports `godverse_id: null` the cache schema carries no such field at all. C's
rule is *"C consumes whatever identity field lands, C does not invent it"* so nobody has landed it.
A owns the CityPlan/cache contract; G emits it.
3. **The runtime wire.** [C + F] `stockpack.js` keys `_packs`/`_resolved` by **type alone** and returns a
cache hit before `base` is ever read (`stockpack.js:24`), so two stocked shops collide **and fail-soft
breaks** an unstocked shop of a cached type inherits another shop's crate. C specced the seam (key by
`type+base`, per-shop preload on entry, LRU=1 + dispose) and owns the file; F then passes the per-shop
base at `interior_mode.js:192` and drops the town-wide preload at `index.html:199`.
**F deliberately did not build its half of #3 this round.** It is untestable while #1 and #2 are open
(no street to walk to, no identity field to pass), and F would be writing against three-quarters of a
spec that does not exist yet. Filed, not guessed. R24 order: **A+G (identity) E (the town) C (the
keying) F (the wire + re-gate)**, and the crate walks.
### Also found
- **The godverse town is in no gate.** `newtown_godverse` is a shipping town cache that boots fine (F
verified: 18 real shops, real roads) but the town-matrix covers `newtown_real`, not it so the one
town the whole epoch is built on is ungated. F's matrix, F's gap; it lands with the R24 re-gate rather
than a held round.
- **A's spacing warn earns immediately:** with toowoomba retired, **`ballarat_real` now warns at median
255 m > 150 m**. The thin tail has a second member — A's warn is doing exactly its job (warn, not error;
E curates). Filed for E's next curation pass.
- **`?noassets=1` + `&town=<key>` silently boots a *different town*** (the fixture, not the cache) — F's
own R17 wiring gates the cache fetch on `!NOASSETS`, correctly (noassets = zero network), but the tier
ladder's *"same shop under `?noassets`"* can't mean the same shop for a real town. Tier 0 for **stock**
is provable on the default boot; tier 0 for the **town** is a fixture fallback by design. Wording, not a bug.
---
## 24. Round 24 — v5.0-alpha THE ALPHA COMPLETION. **The crate WALKS. F held the tag anyway — A's gate.**
**THE CRATE WALKS.** You can boot `redhill_godverse`, walk down Musgrave Road into **Monster Robot
Party**, riffle **its own real crate**, and buy a real record for its real price. Proven falsifiably,
not by eyeball:
| assertion | result |
|---|---|
| room resolved base | **`assets/stock_godverse/3962749/`** (not the town-wide base) |
| **title equality** | **120/120 in Monster Robot's atlas · 0/120 in the generic town pack** |
| **texture provenance** (the pixels) | **2 atlases, both `stock_godverse/3962749/stock_record_atlas_0*.webp` · 0 from the generic pack** |
| network | `stock_godverse/3962749/` index + both atlases, `200` |
| **buy** | `rec_0000` *"Abstract Latin Lounge II (Part II)"* @ **$15** → cash **191 → 176**, debited exactly 15, count 0→1 |
| gone-this-session → back next boot | reboot ⇒ wallet 191/0 restored (no persistence layer exists) |
**Tier ladder, re-proved end to end:** tier 0 `?noassets` on a godverse town → **zero network requests**,
no base, no pack (the offline law survives the wire) · **soft fall live**: `Restoration Station`
(keyed 2289, no atlas) → `404` → null → parody, and the boot log shows those 404s happening in
production · **no cross-contamination**: after visiting other shops the real crate still resolves its
own 120 · **plain towns untouched**: `newtown_real` → 0 keyed, base `null`, 350 items, textures from
`models/`**F's wire is a complete no-op off the godverse path**, byte-identical to pre-v5.
### What F built (#6a)
`godverseBaseFor(shop)`**exported and used by both** the shell's boot preload and the room's lookup,
deliberately: C's cache identity is the LITERAL `(type, base)` pair, so two sides building that string
differently would miss the cache and drop a real shop to parody — a bug that looks exactly like "no
stock" and would sail straight past a covers-render eyeball. One function, one string, one key.
Boot-preloads keyed shops (`getStockPack` is sync and the room is built the instant the door opens, so
a pack first requested at `enter()` is not resolved in time), gated on `stockReal` and never on the id
alone, so `?noassets` fetches nothing.
### ⚠ FINDING 1 — the honest-gate spec was itself vacuous. The law's first application broke the law.
The brief specified #7a assert **"rendered titles ∈ the atlas index (id equality, not 'covers render')."**
**Id equality cannot discriminate.** Measured: both packs number their items **positionally**
`rec_0000, rec_0001, …` — so the atlas's ids are `rec_0000..rec_0119` and the town-wide pack's are
`rec_0000..rec_0349`. **All 120 atlas ids are also generic-pack ids (120/120 overlap).** An id-equality
gate goes **green on the generic pack** — the exact failure the law was written to stop, reproduced
inside the fix for it. *Ids here are slot numbers, not identity.*
**What actually discriminates:** titles (**0 overlap**) and, strongest, the **texture URL** — the file
the GPU samples. A pack that merely looks right cannot fake the URL its pixels come from. F's gate
asserts on both, and `DBG.stockInfo()` exposes them so the assertion is scriptable, not eyeballed.
### ⚠ FINDING 2 — F's own selector was lying about the epoch's shop
`DBG.enterShop(3962749)` returned **`no shop matching`**. A GODVERSE shop has *two* ids — its plan id
(local, renumbered per town: Monster Robot is **8**) and its `godverseShopId` (**3962749**, the real POS
id, which is what the atlas, every G-side query and every cross-repo bug report names it by). F's R22
selector only knew the first. Fixed (plan id still matches first, so no existing call shifts). *The
harness F shipped for Lane G could not name the one shop the epoch is built on.*
### The 8 reds — F did NOT clear them, and did not touch A's file
`156,193/156,201`. They are **not** product defects; F proved the product correct above. They split:
**2 — goldens moved by G's re-emit** (`newtown_godverse` base `0xcf69b387`, gig `0x1e266f60`), exactly as
ledger #2 intends: adding `godverseShopId` changes the cache, so it changes the plan. A ran *before* G,
so A could not pin what G had not yet emitted. Plus 4 UNPINNED warns (`redhill_godverse`,
`redhill_real`). **F did not re-pin**: if conflict #2 resolves by changing the caches, these move again,
and the amendment law wants the spec change and the pin in ONE commit. Pin them with the resolution.
**6 — conflict #2: A's identity sweep contradicts Fable's own standing Ruling 2.** Ruling 2
([FABLE_TO_LANE_G.md:33](FABLE_TO_LANE_G.md)) *mandates* mixed caches — "merge census heroes **WITH** the
donor's texture shops (cafes/bakeries…)". A cafe lifted from OSM has no GODVERSE identity and never will;
minting one would invent identity, which is charter risk #3 itself. A's sweep asserts
`keyed === cache.shops.length` — i.e. that no such cache may exist. It is a gate authored in the same
wave as the thing it gates, with no mixed cache in existence to test against: **the identical species as
E's blind validator in R23**, one round later, in the opposite direction. G measured it and escalated it
to "A's call, Fable's ruling"; nobody ruled; it arrived at F.
**F's measurement of the hazard the gate guards: it does not exist.** `redhill_godverse` 10 keyed /
**10 unique**; `newtown_godverse` 18 keyed / **18 unique**; **zero duplicates**. And partial keying does
not mis-stock — F watched shop 2289 fall soft to parody in the browser, which is precisely what charter
risk #3 prescribes ("fail-soft to tier 0, never mis-stocked"). **A's own two checks already disagree**:
`validateTownCache` reasons it out correctly — *"Missing = tier 0 (soft). Duplicate = mis-stocked (hard).
They are not the same failure"* — and warns; the sweep hard-fails the same condition.
### ⚠ FINDING 3 — the uniqueness check is a silent risk-#3 hole (new; G did not catch this one)
`ok(new Set(seated.map(s => s.godverseShopId)).size === seated.length, 'godverse ids unique')`. On a
mixed cache every texture shop maps to `undefined`, and **all of them collapse into ONE Set entry** — so
the check fails on the undefineds, *not* on a duplicate, and it **cannot detect a real duplicate** while
any shop is unkeyed. It is the arm A called "the arm that earns its keep" for charter risk #3, and on
every cache Ruling 2 mandates it is **structurally incapable of doing its job, in both directions**.
Whoever fixes the coverage assert must not paper over this one: uniqueness belongs **over the defined
ids** (`seated.filter(s => s.godverseShopId !== undefined)`), where it would actually bite. That change
makes the risk-#3 guard **stronger**, not weaker.
### Why F held the tag — and why this hold is not R23's
R23 held because the **product** was broken. **R24's product is finished and proven.** What is red is a
**gate**, in **Lane A's file**, on a **schema question that is A's to answer** ("what marks the godverse
layer?" has at least three valid designs). F did not resolve it, for one reason above all: **F is the
party that benefits.** F changing a red gate to green, in another lane's file, to unblock F's own tag,
on the one check guarding "never mis-stocked" with Monster Robot's real crate as the prize — that move
would retroactively devalue every green F has ever reported. F's only asset is that F never shades a
result. The cost of waiting is an hour; the cost of F being quietly wrong is the charter's worst risk.
**Recommended (A's call): the sweep follows A's own validator — missing ⇒ warn (tier 0, soft), duplicate
⇒ error (hard, over defined ids), coverage asserted over the keyed layer, not the inherited one.** Then
A pins the four goldens in the same commit and F re-gates and tags. It is one commit's work.
### ⚠ FINDING 4 — the licence flag stopped reaching a human, and the gate says OK
R23's blind validator is **fixed** — E's `validate_atlas` now finds and validates the atlas
(`3962749/stock_record_index.json · Monster Robot Party · items=120 atlases=2 px=2048 OK`). But G's
conflict-#1 leftover is real and still open: `validate_atlas.py:198` prints from
`provenance.licence`**British only** — while G's re-emit carries **`provenance.license`** (American,
per C's corrected contract). So `lic = {}`, and the licence line **silently never prints**. E's *check*
(:105) accepts either spelling, so the gate goes **OK**; only the evidence vanished.
That evidence is not decoration. E's own comment at :199 says *"the licence is PRINTED — clearing it is
a human call"* — it is the **mechanism** by which charter law #3 reaches a person, and the string it
failed to print is **`FLAG BEFORE ANY PUBLIC/COMMERCIAL RELEASE`**, on an atlas of a real shop's real
product photographs with real artist names. A gate that is green while the one thing it exists to
surface is invisible is the vacuous-gate law's own species, wearing the licence law's coat.
**Fix (E, one line, G already specified it):** `.get("licence") or .get("license")` at :198.
**F did not patch it.** It is E's file, F is not blocked on it (the tag is held regardless), and G
already routed it. Filing beats reaching across a lane boundary for a one-liner — the same restraint F
applied to A's gate, for the same reason.
### ⚠ FINDING 5 — F's own gate cried the loudest wolf we own (F's bug, F fixed it)
The classic-regression gate reported **`plan fingerprint != golden 0x3fa36874 — determinism/prime-law
broken`**. The covenant was never touched (`0x3fa36874`, node-side, unmoved; G said so too). F wrote
this gate in R16 with `if hash_ok and r.returncode == 0` — **two assertions, one verdict, reported with
the fingerprint's message** — so *any* unrelated red anywhere in Lane A's selfcheck (here: 8
godverse-identity checks in a different town) screams that the byte-identical-forever covenant is
broken. Fixed: two assertions, two verdicts, and the selfcheck failure now names its own lane and
quotes its own reds. *A gate must name what it actually measured — the same law as the vacuous gates,
one turn further round: it isn't enough to fail for a reason, you have to fail for YOUR reason.*
### ⚠ FINDING 6 — the id spaces collide, and it bit F's own selector inside the anti-vacuous gate
Plan ids are per-town (1..N); godverse ids are real POS ids (31, 767, 3962749). **They share a numeric
space.** Silky Oak Furnature's godverseShopId is **31** — and another shop's *plan* id is also **31**.
F's first cut at the R24 selector tried plan id first, so `enterShop(31)` silently entered **Wholefood
Cafe** instead. The smoke's soft-fall assertion then **passed on an unkeyed cafe** — a vacuous pass,
inside the gate F wrote to stop vacuous passes, in the round whose whole theme is *ids that aren't
identity*. Fixed both ends: `'g:<n>'` selects a godverse id explicitly; a bare number stays the plan id
(back-compatible) and now **reports the collision** rather than choosing quietly; and the smoke enters
via `g:` and asserts the shop it landed in **is the one it asked for** before asserting anything about it.
*The lift itself is clean* — F checked: cache 10 keyed → plan 9 keyed (Empire Revival didn't seat), every
one a real secondhand shop. **No cafe is keyed. There is no mis-stocking.**
### ⚠ FINDING 7 — fail-soft-by-404 vs the 0-console-errors law (a contract gap, filed not papered)
F's boot preload must resolve a keyed shop's pack *before* the door opens (`getStockPack` is sync and
the room builds on the same tick), so it probes every keyed shop. 9 of Red Hill's 10 have no atlas →
`404` → null → parody. **That is the fail-soft law working**, and it is also **8 console errors on every
godverse boot** — a browser-level log no `.catch()` can suppress. The house law is zero.
**The tension is structural: you cannot probe blind and stay silent.** The fix is not to loosen the law
but to stop probing for what isn't there: **an atlas index in G's namespace** (or a `hasStock` flag on
the cache's shop entries, A+G) so the shell preloads exactly the shops that have atlases — 404s then
mean a genuinely missing file, which is what fail-soft is *for*. **Filed as a contract ask, not
invented.** Until it lands, F's smoke counts the probes, names them, and fails on any other error —
tolerance stated out loud, never blanket-ignored.
---
## 26. Round 26 — v5.0-beta EVERY CRATE DIFFERENT. F: the manifest consumed, the tolerance retired.
### The 404 tolerance is gone — and that is the point
R24's wire preloaded a pack for **every keyed shop** and ate a `404` for each one with no atlas. That was
the fail-soft law working, and it was also **console errors on every godverse boot**. F's smoke could only
cope by *naming* the probes — and **a tolerance is a confession, not a fix**. F filed the real fix as a
contract ask rather than inventing it: *an atlas index in G's namespace, so we never ask for what isn't
there.* G built it. **F now consumes it** (`assets/stock_godverse/index.json` → `{godverseShopId, types[],
sourcing}`): the shell preloads only manifest-listed `(shop, type)` pairs, and the gate asserts **ZERO
404s with no attribution exceptions**. The 404s are gone because there is nothing absent to ask for — not
because the gate stopped looking. Fail-soft still covers the case that matters (manifest says yes, file is
gone), which is what fail-soft was always *for*. Fetched only when the town has keyed shops, so a plain
town stays byte-identical: no manifest, no fetch.
### Distinctness — G reported 15/15; F measured it, from the indexes of record
Not by entering one lucky shop: **105 pairs across all 15 keyed crates, 0 sharing a title**; **0 crates
overlapping the generic v2 pack**; **0 ids reused across crates** — G's sku fix holds at scale, which is
the R24 vacuity dead at the source rather than papered over in F's gate. 4 crates in Red Hill + 11 in
Newtown = the manifest's 15 exactly, and the manifest lists nothing that isn't on disk.
### Sourcing, not tier — C was right and the gate follows C
C ruled the brief's `tier` field wrong: `tier` is the **charter's ladder rung**, and a mint atlas is *also*
rung 1 — a mint crate is still a deterministic file, still offline, still tier 1. What differs is where the
stock **came from**, which is `sourcing: real|mint`. E built to C's contract over Fable's brief on the
principle that *the contract's author is the authority for the contract's shape*, and Fable ratified it.
F's gate asserts `sourcing`: every index declares it, every index agrees with the manifest, and **a mint
crate can never report as real**. The gate prints it per room, because provenance a human never reads is
provenance that isn't doing anything — the same lesson as R24's silent licence line.
*Note for whoever reads the runtime:* C's pack (`stockpack.js`) does **not** carry `sourcing` through —
the gate reads it from the index of record, which is correct (the index is the artifact; B's street mark
reads the manifest). Not a gap; recorded so nobody re-derives it.
### ⚠ The tolerance came out and immediately caught the leftover
F wired the **boot** preload to the manifest, ran the gate with the 404 tolerance **deleted**, and it went
red on exactly one 404: `stock_godverse/31/stock_opshop_index.json`. That was F's *other* probe — R24 also
preloaded a shop's pack **on entry**, so walking into a keyed shop whose type has no atlas fired a fetch
that could only ever 404. Removed: the door does a lookup, never a fetch. The manifest is consulted once,
at boot, and by the time a door opens the pack is either resolved or was never going to exist.
The lesson is the round's, not just the bug's: **the tolerance was hiding a second instance of the very
thing it tolerated.** While the gate said "8 atlas-probe 404s, expected", the ninth would have been
invisible too. A named exception is still an exception; it earns its keep only until the real fix exists,
and the moment it does, the exception must die or it starts protecting new bugs.
### The gate, green
`DISTINCTNESS 15 crates / 105 pairs / 0 shared · 0 vs the generic pack · SOURCING 15/15 declared, all
agreeing with the manifest · Monster Robot Party → sourcing REAL, 120/120 own titles, 2 own textures / 0
generic · Presents of Mind → sourcing MINT, 16/16 own, 1 own / 0 generic · soft fall silent · ZERO 404s ·
0 console errors, no attribution exceptions.`
### ⚠ THE HOLD — `v5.0-beta` NOT tagged. `atlas-QA FAIL — 14 error(s)`, and it is not the product.
`qa.sh --strict --matrix`**6 passed · 1 failed · 0 warn**. The one failure is the manifest validator:
E's `validate_atlas` rejects **all 14 mint crates** (the real crate passes). Everything else is green —
selfcheck **ALL GREEN 156,212/156,212** · **MATRIX GREEN 10 towns × 7 gates** · flags harness (carrying
F's beta gate) · consistency · figure-scale. **One gate, one assertion, 14 identical errors, one line.**
That ratio is the whole argument for the hold: the beta is finished, and what stands between it and the
tag is a rule meeting a case it was written before.
**The conflict:** `SLOT_ID_PREFIX = "sku_"` (`validate_atlas.py:73`) is a module constant applied to
**every** pack — E reads `sourcing` at :178 but the slot-id check is **sourcing-blind**. C §7.2a's rule is
*"derive from the source's own stable key, never position, unique across ALL packs"*, and it specifies
`sku_<POS sku>` **"for a real-stock (godverse) pack"**. **A mint crate has no POS sku by construction**
it was never in that shop's till. Demanding one demands a thing that cannot exist: the same shape as R24's
sweep demanding a cafe carry a `godverseShopId`, and as the brief's `tier` demanding a rung where the
question was provenance.
**F measured the rule's INTENT, and it holds:** mint ids are `mint_<dealgod listing id>` — a real external
key, **non-positional**, items sorted by it, the bake seeded (`random.Random(158)` over an id-ordered
pool), and **0 ids reused across all 15 crates** (F's own pairwise measurement). Stable, unique,
non-positional. Only the literal prefix fails.
**G refused to go green the easy way, and was right to** — worth recording, because it is F's own R24
finding grown up: *"a POS sku and a dealgod listing id are different id spaces; filing dealgod listing ids
under `sku_` recreates that bug INSIDE the economy — tier-2's sold-means-gone looks up POS skus, and
`sku_6031122` (a dealgod id) is indistinguishable by shape from a real sku."* R24's plan-id-31 vs
godverse-id-31 collision entered the wrong shop; the same collision in the money layer sells the wrong
record. G caught it **before** shipping rather than after. Emitting `sku_` would have made this gate green
and the economy wrong.
**Why F did not fix E's one line:** identical to R24's conflict #2, and the answer has not changed. It is
**E's file implementing C's contract**, the question ("does §7.2a's sku rule bind a mint pack?") is **C's
to answer as the contract's author** — the principle Fable ratified when C overruled `tier` — and **F is
the party that benefits**, since the fix is the only thing between F and the tag it was sent to place.
F's whole worth is that F never shades a result. Cost of waiting: one line. Cost of F being quietly wrong:
a gate that green-lights id-space collisions in the layer that moves money.
**The fix (C rules, E lands it, one line):** gate `SLOT_ID_PREFIX` on `sourcing == 'real'`; for `mint`,
assert `mint_`-prefixed + non-positional + unique — which F has already measured true. Then F re-gates
and tags. **Everything else for the beta is done and green.**
### The fence went in and the hold ended — wave 4
C ruled and both lanes titled their commit the same thing: **"my rule was the stale thing."** E's one line:
```python
SLOT_ID_PREFIX = {"real": "sku_", "mint": "mint_"} # sourcing-scoped, not one space
```
Fable's ruling — **prefixes are namespace fences** — and E's comment makes G's judgment the law:
*"`sku_` names a POS copy on a shelf, `mint_` names a dealgod listing standing in for one… a prefix that
lies doesn't fail a gate, it **SELLS THE WRONG RECORD** out of a real shop to satisfy a minted stand-in.
Never treat them as one space (**same standing note as plan-ids vs godverse-ids**)."*
That closing clause is F's R24 collision — plan id 31 vs godverse id 31, which quietly walked the harness
into a cafe — promoted from a finding to a standing law, and re-derived independently by G in the layer
that moves money. The bug F found by entering the wrong shop is the bug G refused to ship by selling the
wrong record. **The rule bent; the id spaces didn't.** That is the correct direction: a rule is a
description of an intent, and when a legitimate case fails it, the description is what's wrong — provided
somebody has measured that the intent still holds. F measured it before the ruling (non-positional,
seeded, stable, 0 reuse across 15 crates), which is what made the ruling a decision rather than a guess.
**`atlas-QA OK — 15 per-shop atlas(es) valid, 15 licence line(s) shown, 0 warning(s)`** — and that second
number is R24's finding #4 at full scale: fifteen `FLAG BEFORE ANY PUBLIC/COMMERCIAL RELEASE` lines now
reach a human, where one silently printed nothing.