# LANE B — NOTES (measured) ## Round 42 (§42.5) — THE BENCH TURNS TO FACE THE ROAD · `benchStops()` · and the font race that never existed All numbers this section: today's tree, headless Chromium, **fresh contexts** (never a reused tab), port-isolated **no-store** servers, 1280×720, seed 20261990. Budget cells use the **pin method of record** (`budget_walk.py --method stepwalk --stepm 2 --fixed-yaw 0 --laps 2` = 802 stations down the main spine at yaw 0). ROUND42 **ruling 4 is binding** — the street is 291/300 at night, nine draws — so every claim below is an **A/B against a control tree**: a HEAD copy of `web/` with `assets/`, `models/`, `vendor/` **and every `js/` subtree that is not Lane B's** symlinked to the live tree, so another lane's mid-round wave lands identically on both arms. (It did: the night tri count moved +2,090 against R41's pin on **both** arms — E's in-flight §42.1/§42.3 work — and 0 between them.) --- ### §42.5-A — THE BENCH FACED THE WRONG WAY. So did the streetlight. (Lane D's filing, verified) D filed it as "observed while mirroring, not fixed (your file)". **Verified before changing anything**, by reading the InstancedMesh matrices out of a live scene and matching each instance to the edge whose **perpendicular offset equals the placement rule** (position only — so the yaw verdict is not circular). `unmatched: 0` on both arms. | live scene, seed 20261990 | BEFORE (control tree) | AFTER (this round) | |---|---|---| | bench front (local +Z) vs the direction to the road | **90.0° — min = median = max, 60/60** | **0.0° — min = median = max, 60/60** | | bench front vs the street's own direction | 0.0°, 60/60 | 90.0°, 60/60 | | lamp head (local +Z, 1.05 m out) gains … toward the road | **0.00 m — 0 of 89 lamps reach over it** | **+1.05 m — 89 of 89** (the whole arm) | | lamp arm vs the street's own direction | 0.0°, 89/89 | 90.0°, 89/89 | **The code and its own comments had disagreed for 41 rounds.** `furniture.js:203` said *"facing the road"*, `:197` said *"arm reaches over the road"*, and both used `yaw = atan2(ux, uz)` — the yaw that points local +Z **down the street**. D is right on both counts. **The fix is one named convention, not two more literals.** There are exactly two useful yaws on a street edge and the file only ever had one: ``` alongYaw = atan2( ux, uz) → local +Z runs DOWN THE STREET (what bench + lamp used) faceYaw = atan2(-uz, ux) → local +Z runs ACROSS it, along +perp (= alongYaw − π/2) ``` `perp` is `(−uz, ux)`; an item at offset `side · off` on it has the road at `−side · perp`, so the road-facing yaw is `faceYaw + (side > 0 ? π : 0)`. **The `side` term was already there and already correct** — the base was wrong, which is exactly why the defect survived reading. The shelter's `atan2(-uz, ux)` was already `faceYaw`; it is left byte-identical (see the filing below). **THE DEPOT BENCH IS A DIFFERENT BENCH, and it was 90° out from the primitive.** This box reaches the CDN, so a default boot draws **9 of its 60 benches from `procity_street_bench_01.glb`** and 51 from the primitive. Measured off the GLB's own vertex data: it is **0.437 (X) × 0.845 (Y) × 1.215 (Z)** and **all 318 of its backrest verts (y > 0.55) lie in a slab at x ∈ [0.151, 0.219]** — its plank runs along **Z** and its sitter faces **−X**, where the house law is "+Z is the front". Every GLB bench in the town therefore stood square to every primitive one. Fixed by rotating the **geometry** once at load (`GLB_FIX_YAW.bench = +π/2`, maps local −X → +Z), **not** the instance — so a station's yaw means the same thing whichever geometry is drawn, `benchStops()` stays one truth, and D's sitters are right on both. Confirmed in the scene: the bench-shaped instanced mesh goes from a 1.216 m Z-extent to a 1.216 m X-extent, same 949 verts, same 9 instances, **0 draws / 0 tris**. **Proof shots** — same bench station (`edge 3, s = 14, side −1`), same camera pose, both arms, R10 human-sized line off the sitter's own bone bbox: `docs/shots/laneB_r42/bench_seated_{before,after,after_dwired}.jpg`, plus the crowd-frozen geometry pair `bench_geometry_ab.jpg` and the night `streetlight_night_ab.jpg` (130 draws on both arms). > **→ LANE D, THIS IS A BREAKING CHANGE FOR YOUR MIRROR, AND IT IS WHY THE EXPORT EXISTS.** > `sim.js benchStationsFor()` computes `yaw = atan2(ux, uz) + (side > 0 ? π : 0)`. That is now **90° > wrong**. Positions are untouched (your 14/14 position arm stays green); your **yaw arm goes red**, > and `_seatPose()` will put sitters up to 0.38 m off the plank and facing along the street — visible > in `bench_seated_after.jpg`, which ships deliberately un-retouched. `bench_seated_after_dwired.jpg` > is the same frame with the sitter re-seated off the **real bench transform**: that is what 42.4 > delivers. **Import `benchStops(plan)` in your wave and delete the mirror** (§42.5-B). --- ### §42.5-B — `benchStops(plan)` — the export that retires D's mirror D asked for it "next to the existing `busShelterStops(plan)` (same shape, same reason it exists)". Shipped, and the *rule itself* now lives in one function that **both** readers call, so the two cannot drift: `benchStationsOnEdge(e)` is what `buildChunkFurniture` places from and what `benchStops(plan)` publishes. A second copy inside the same file would have been the same defect at a shorter distance. ```js import { benchStops } from '../world/furniture.js'; benchStops(plan) → [{ edgeId, kind, s, side, x, z, yaw, seatLen }] ``` Pure function of the plan — no THREE, no scene, no DOM, no side effects, safe in a worker or in Node. Returns **every** station in the town (not just streamed ones), in `resolveEdges(plan)` order then ascending `s`. | field | type | meaning | |---|---|---| | `edgeId` | number | the plan edge id, in `resolveEdges(plan)` order | | `kind` | `'main'\|'side'\|'lane'` | the edge's class | | `s` | number, m | distance along the edge from its **A** node: 14, 40, 66, … step 26 | | `side` | `+1 \| −1` | which footpath, on the **furniture** perpendicular `(−uz, ux)`. D's own lane perpendicular is `(uz, −ux)` = **minus** this, so D's own-footpath test stays `side === −forward`, unchanged | | `x`, `z` | number, m | the bench origin = the instance translation. **Unchanged by R42** | | `yaw` | number, rad | the instance's Y rotation. Local **+Z is the bench FRONT** and now points **at the road**. A rig front is local −Z after the R13 facing-normalise, so a sitter is still **`yaw + π`** | | `seatLen` | number, m | **1.6** — the plank, so a caller's ±0.38 m two-up offset is not a second copy of B's geometry. (A depot-GLB bench is a shorter **1.216 m** plank; ±0.38 still fits.) | **Gated, not asserted** — D's own arm-6 method, pointed at the export instead of the mirror: **254 stations town-wide; 60 bench instances standing in the streamed window; 60/60 matched by a station within 2 cm, and 60/60 with the yaw identical to < 1e-6 rad.** **CONTROL:** the same stations offset 2 m match **0 of 65** on both counters — so "coincides" is a measurement, not a hit on a neighbouring prop. --- ### §42.5-C — `await document.fonts.ready`: shipped, and it fixes NOTHING. Here is what does. F's R41 ruling: no gate may pin a screenshot byte-hash while *"the sign atlas paints `bold Npx Arial` before the font resolves"*. That diagnosis was mine originally (R41 §41.5-D) and F reproduced the symptom independently. **The symptom is real. The cause is not the font.** **1. The atlas is already deterministic.** Three boots per bookmark, hashing every live sign-atlas canvas: **41–49 canvases per bookmark and the identical sha1 list on all three boots**, every bookmark. At shot time `document.fonts.status === 'loaded'`, `check('bold 74px Arial, sans-serif')` is true, and a fixed probe string measures **859.2744140625 px to the last digit on every boot**. **2. The API cannot even see this game's text.** `document.fonts.size === **0**` at document start and at `ready` — the CSS Font Loading API tracks **font faces**, and PROCITY ships none (house law: all text is canvas, `Arial` is resolved by the platform). An await on `document.fonts.ready` is an await on an empty set. **3. And it already resolved long before the first sign was painted.** 6 boots (3 default, 3 classic), instrumented from an init script that hooks `CanvasRenderingContext2D.fillText`: | | `document.fonts.ready` | first `fillText` in the process | order | |---|---|---|---| | default × 3 | 42.5 / 49.3 / 50.1 ms | 102.3 / 109.2 / 105.1 ms | ready first by **+55 to +60 ms** | | classic × 3 | 38.6 / 40.5 / 50.3 ms | 93.4 / 89.5 / 101.3 ms | ready first by **+49 to +55 ms** | **6 of 6 boots.** There is no window in which a sign could be painted early. **IT IS SHIPPED ANYWAY, at the only site where it is free** — a module-level `await` at the top of `buildings.js`. Every importer (`chunks.js` → the shell → every `buildChunkBuildings` call) is downstream of it, so no atlas can be painted early, and **`buildChunkBuildings` stays synchronous**: the streamer's 4 ms/frame budget is untouched and no chunk build becomes async. **Measured added boot latency, 6 boots per arm, control vs treatment:** first sign paint **102.28 ms → 102.62 ms (+0.34 ms on the mean)** against a control arm whose own spread is **93.7–110.6 ms**; `DBG.ready` **3,822 → 3,802 ms**. It is inside the noise in both directions. It is kept because it costs nothing and because it is the invariant a future webfont would silently break — not because it closed anything. **WHAT ACTUALLY MOVES THE FRAME: THE CROWD.** The citizen sim advances on **wall-clock** time, so two boots settled for the same 2.5 s are two different streets. Two fresh contexts, `?classic=1`, `street_noon`, diffed pixel-for-pixel: | | changed pixels of 921,600 | bounding box | |---|---|---| | as everyone has been booting it | **983** | rows **361–418** — the pedestrian band, nothing else | | with the crowd genuinely out | **0** | *none* | **AND THE FREEZE RECIPE EVERY LANE QUOTES DOES NOT FREEZE ANYTHING.** `?roster=v1&pop=0` boots **140 citizens** (96 active in the window, measured), because `index.html:454` reads `(parseInt(params.get('pop'), 10) || 140)` and **`0` is falsy**. Every "town frozen" measurement in this project since the flag was added has had a live crowd in it. **THE KNOB: `DBG.freeze()`** (`dbg.js`, `?dbg=1` only, no game state a player can reach). Two mechanisms because the roster has two modes — `setPopulation(0)` empties the fixed roster, and hiding `citizens.group` covers the chunk-streamed default whose active set is per-chunk density and ignores `target` entirely. Returns `{ draws: {before, after}, peds: {active, hidden}, stillLive: {weather, tram, magpie, washing, gigs} }` — the draw delta is the proof the crowd left the frame, and `stillLive` names the other wall-clock movers so a harness asserts instead of assuming. #### The 3-boot hash table — F's method, four arms `?classic=1`, `dbg=1&roster=v1&pop=0&weather=0&tram=0&magpie=0&washing=0`, **a fresh context per (bookmark, boot)**, 2.5 s settle, sha256 of the PNG. | bookmark | BEFORE (control tree) | AFTER (fonts fix in) | AFTER + `DBG.freeze()` | BEFORE + the same freeze | |---|---|---|---|---| | `street_noon` | **3 of 3** | **3 of 3** | **1 of 3** ✅ | **1 of 3** ✅ | | `shopfront_detail` | **3 of 3** | **3 of 3** | **1 of 3** ✅ | **1 of 3** ✅ | | `crossroads_busy` | **3 of 3** | **3 of 3** | **1 of 3** ✅ | **1 of 3** ✅ | | `market_square` | 1 of 3 | 1 of 3 | 1 of 3 ✅ | 1 of 3 ✅ | | `night_neon` | 1 of 3 | 1 of 3 | 1 of 3 ✅ | 1 of 3 ✅ | | **stable** | **2/5** | **2/5 — the fix changed nothing** | **5/5** | **5/5** | The fourth column is the control that matters: the **untouched HEAD tree** also goes 5/5 under the same freeze, so the thing that made the bookmarks reproducible is the freeze, not any R42 code. **On `market_square`:** R41 has B calling it rock-solid and F calling it 3-of-3. On this box today it is **stable in all four arms**. Both readings were honest; the bookmark simply depends on whether a citizen happens to be in that frame. > **→ LANE F, THE ANSWER TO YOUR QUESTION: YES — byte-hash screenshot goldens become possible, and > the R41 ruling can be reversed, but not for the reason it was made.** The condition is not "the > font await landed"; it is **"the frame contains nothing that runs on wall-clock time"**. A golden > boot is `?classic=1&dbg=1&weather=0&tram=0&magpie=0&washing=0` **+ `DBG.freeze()` before the > shot** — `?pop=0` is not a substitute and never was. All five bookmarks pin at 1 of 3 that way, on > both trees. Two further asks, both in your files: **(a) fix `index.html:454`** (`Number.isFinite`, > not `||`) so `?pop=0` means zero, and **(b)** if a gate pins a hash, pin it on a `DBG.freeze()` > boot and assert `freeze().stillLive` is all-false in the same run, or the golden will rot the first > time someone shoots without a flag. --- ### §42.5-D — THE BUDGET: nothing was spent (ruling 4) Pin method of record, 802 stations, control tree vs treatment tree, same box, same run: | synthetic, pin (802 stations, yaw 0, laps 2) | CONTROL | R42 | delta | law | |---|---|---|---|---| | default, noon (seg 2) | 282 / 100,346 | **282 / 100,346** | **0 / 0** | ≤300 ✅ | | default, **NIGHT** (seg 5) | 291 / 122,183 | **291 / 122,183** | **0 / 0** | ≤300 ✅ — **the nine-draw margin is intact** | | `?classic=1`, NIGHT | 269 / 90,580 | **269 / 90,580** | **0 / 0** | ≤300 ✅ **byte-exact to the R38/R40/R41 pin** | | `?classic=1`, noon | 269 / 90,592 | **269 / 90,592** | **0 / 0** | ≤300 ✅ | **0 console errors on all eight runs.** Night draws are still **291/300 — Lane B spent 0 of the 9.** Structurally it cannot: a yaw is a rotation in an instance matrix that already existed, and the GLB normalisation is one `rotateY` on a geometry that was already merged. The bookmark frames confirm it frame-by-frame: `night_neon` **98 draws / 14,454 tris on both arms**, and the crowd-frozen bench pose **130 draws on both arms**. **On the night tri number:** R41 pinned 120,093 and today both arms read **122,183**. The +2,090 is on the **control** too — it arrived with another lane's in-flight R42 work through the symlinked `models/`/`assets/`. Recorded so nobody attributes it to this round. ### §42.5-E — CLASSIC, and the goldens `furniture.js` and the new `buildings.js` await contain **no classic flag test at all**, so classic and default are identical to each other by construction (the `ground.js` idiom) and the fix lands on both. What must not move, did not: | classic evidence, control vs treatment | result | |---|---| | fetch surface (blob: object URLs excluded — they carry a random UUID per boot) | **127 URLs on both arms · added `[]` · removed `[]`** | | HUD DOM (`innerHTML` length : body children) | **2355 : 8 on both — identical** | | help line · `#pc-creditlink` · `PROCITY.budget` | unchanged / absent / `{draws:300, tris:200000, radius:2, auto:2, diagnostic:false}` on both | | `night_neon` frame | **98 draws / 14,454 tris on both — delta 0 / 0** | | pin, NIGHT | **269 / 90,580 — byte-exact** | | page errors · console errors | **0 · 0**, on `?dbg=1`, `?classic=1`, `?noassets=1`, `?roster=v1&pop=0` | The only difference the probe finds anywhere is `DBG.freeze` existing — and `DBG` exists only under `?dbg=1`, which is not a player boot. `node web/js/citygen/selfcheck.js` → **ALL GREEN 157,647/157,647**, fingerprint **`0x5f76e76` UNMOVED**. No plan golden can move: furniture is placed from the plan and writes nothing back to it. ### §42.5-F — filed, not fixed (each with its measurement) 1. **→ F: `index.html:454` `?pop=0` is a no-op** (`|| 140`). Above. 2. **→ D: retire `benchStationsFor()` this wave.** Above. Your gate's yaw arm is red until you do. 3. **→ a future round: the bus shelter has the same defect the bench had.** `syaw = atan2(-uz, ux)` is `faceYaw` **without** the `side` term, and the shelter always stands on `+perp`, so its open front (+Z) reads **away** from the road on the primitive — exactly the bench's bug. **Deliberately not touched:** `busShelterStops()`'s yaw is consumed by `tram.js`, and the shipped shelter is a depot GLB whose own axis convention is **unmeasured**. Measure the GLB, then flip both together. 4. **→ E, an asset-contract note:** the depot bench is a **1.216 m** plank where B's primitive is **1.6 m**, and it was authored front-to-−X. B normalises the rotation at its own load seam (above), but a depot prop that disagrees with the house axis is a class of bug, not an instance — the shelter is the next one waiting. **Files:** `web/js/world/furniture.js` · `web/js/world/buildings.js` · `web/js/world/dbg.js` · `docs/shots/laneB_r42/` · these docs. **No git commands run** (lane treaty; Fable commits). --- ## Round 41 (§41.5) — THE CREDITS SURFACE, and A's D3: 81 venue lots stop wearing op-shop paint All numbers this section: today's tree, headless Chromium, **fresh contexts**, port-isolated **no-store** servers, 1280×720, seed 20261990. Budget cells use the **pin method of record** (`budget_walk.py --method stepwalk --stepm 2 --fixed-yaw 0 --laps 2` = 802 stations down the main spine at yaw 0, the worst bearing). ROUND41 ruling 4 is binding — the street budget is spent (291/300 at night, nine draws) — so both items are **zero-draw, measured as an A/B against a control tree**, not asserted. --- ### §41.5-A — THE CREDITS SURFACE (`credits.js` + `assets/credits.json`) PROCITY had **no attribution surface at all**, and two of the obligations were already live: - **OpenStreetMap, ODbL 1.0.** `web/assets/towns/SOURCES.md` states the condition in its own words — *"if a rendered/public build surfaces this data, keep the © OpenStreetMap contributors credit visible"* — and 23 town caches ship. Nothing in the running game showed it. - **three.js r175, MIT.** The notice lives in the vendored file header, which no player can reach. And it is the gate in front of **4,472 more licence-clean motion clips** on the fleet that R41 could not spend for exactly this reason (ruling 2): **100STYLE** (1,620 BVH, CC BY 4.0 — requires crediting Mason et al. 2022), **CMU** (2,548 BVH — credit requested), **ACCAD** (304 BVH, CC BY 3.0 — requires *"Open Motion Project by ACCAD/The Ohio State University"*). R42 can spend them by appending rows. **Where it lives** | file | role | |---|---| | `web/assets/credits.json` | **THE DATA FILE — every lane appends here.** Schema `procity-credits/1`. | | `web/js/world/credits.js` | the panel. DOM only. No lane needs to touch it to be credited. | | `web/js/world/hud.js` | `setCredits(credits, {osm})` — the link, bottom-left. Null-provider gated. | | `web/index.html` | one `[Lane B R41]` dynamic-import block + one term in the `unlock` guard. | **Reachable:** `F2`, or the HUD link at bottom-left. `Esc`, `F2`, the CLOSE button or a scrim click closes it. Opening frees the pointer (the dig-card / sell-card / F8-note contract), which is why the shell's `unlock` handler carries one more term — opening the credits inside a shop must not walk you back out onto the street. **THE ATTRIBUTION RIDES THE LINK.** On an OSM boot the link itself reads `© OpenStreetMap contributors · credits (F2)`; on the synthetic town, where no OSM data is on screen, claiming it would be the opposite of attribution, so it reads `credits & licences (F2)`. The shell passes `osm: PLANSRC === 'osm'`; nothing is sniffed off the plan. #### THE SCHEMA — `web/assets/credits.json`, `procity-credits/1` ⟨C · D · E · G: this is your bit⟩ Append an object to `entries`. **You never touch `credits.js`.** Order inside the file is preserved; the panel only lifts `required: true` rows to the top. ```jsonc { "id": "one-hundred-style", // required · unique · stable kebab key "name": "100STYLE", // required · what a player reads "kind": "motion", // required · engine|data|motion|mesh|texture|audio|stock|tool|font "licence": "CC BY 4.0", // required · the licence NAME, or the literal "unverified" "attribution": "Mason et al. 2022, …", // required key · the EXACT text to show, or null if none is owed "required": true, // required · true = a licence obligation, false = courtesy/none "source": "…url or tool chain…", // required · where it came from "used_for": "…what in the game…", // required · plain English, player-facing "count": 1620, // required key · 0 if not a count "files": ["models/clips/*.glb"], // required key · repo globs, [] if nothing ships "evidence": "pipeline/AUDIT.md §…", // required · the in-repo file that RECORDS it, or "unverified" "flag": "…", // OPTIONAL · an open licence question a human must answer "lane": "E" // optional · who owns the row } ``` **Field law, and why each one is the way it is:** - **`licence: "unverified"` is a legal value and a guess is not.** Two rows ship unverified today (below). A guessed licence is worse than an admitted gap: the gap gets closed, the guess gets cited. - **`attribution` is the exact string, not a description of one.** The panel prints it verbatim inside a box. `null` when nothing is owed — say so explicitly rather than omitting the key. - **`required` is the licence question, not an editorial one.** `true` ⇒ the row sorts to the top and the box is headed ATTRIBUTION REQUIRED. Mixamo and Rokoko are `false` **because their licences ask for nothing** — they are recorded so nobody has to rediscover that. CC BY is always `true`. - **`evidence` must name a file in this repo.** "E told me" is not evidence; `AUDIT.md §…` is. - **`flag` is rendered in amber and counted in the footer.** Use it for a real open question (an unrecorded source, art a human must clear). R25's lesson stands: *the print IS the mechanism* — a flag that only exists in a lane note is a flag nobody sees. Paste-ready when R42 spends the CC-BY banks: ```jsonc { "id": "accad-mocap", "name": "ACCAD Motion Capture Lab", "kind": "motion", "licence": "CC BY 3.0", "attribution": "Open Motion Project by ACCAD/The Ohio State University", "required": true, "source": "https://accad.osu.edu/research/motion-lab/mocap-system-and-data", "used_for": "…", "count": 304, "files": ["models/clips/*.glb"], "evidence": "pipeline/AUDIT.md §R42", "lane": "E" } ``` #### What seeded it — 14 entries, read off this repo, nothing invented | entry | licence | attribution required? | |---|---|---| | OpenStreetMap (23 town caches) | ODbL 1.0 | ✅ **YES** — © OpenStreetMap contributors | | three.js r175 (vendored + 5 addons) | MIT | ✅ **YES** — © 2010–2025 three.js authors | | Adobe Mixamo — `idle`/`walk`/`sit`/`look` | Mixamo royalty-free | no | | Rokoko free mocap — the 4 dance clips | royalty-free incl. commercial | no | | Citizen bodies, base roster (17+2) | **unverified** | **unknown — flagged** | | Citizen bodies, the DJ five | on-device mesh + Mixamo auto-rig | no | | Texture skins (103 in `assets/gen/`) | original, `flux_local` on-device | no | | Sound and music (29 keyed, 34 files) | original, procedural synthesis | no | | Fittings/furniture — house library (18) | **unverified** | **unknown — flagged** | | Fittings/furniture — generated here (14) | original (CC0-equivalent) | no | | Three props via fal-Hunyuan | original (~$0.64, the only outside spend) | no | | GODVERSE stock art (15 atlases) | in-house only | **flagged — not cleared for release** | | The generation stack (FLUX.2-klein-4B Apache-2.0, TRELLIS.2, RMBG-2.0) | see row | no | | Typefaces | n/a — **no font files ship** (canvas text, house law) | no | Sources: `web/assets/towns/SOURCES.md`, `pipeline/AUDIT.md` (R3 hero props · QUARANTINE + its blanket caveat · R11/R12/R13 audio · R36 w1 · R38 §2/§3/§4/§6 · R39 §5/§6), `docs/LANES/LANE_E_NOTES.md` (the by-source-zone provenance table), `web/assets/manifest.json`, the vendored three.js headers. **→ LANE E, two findings the seeding turned up.** Both are recorded in the file as `unverified` + `flag`, which is honest, but neither is *closed*: 1. **The base ped roster (19 GLBs) has no per-asset licence record anywhere in this repo.** They are sha1 copies from 90sDJsim; `docs/CITY_SPEC.md` ("NPC contract") declares the zone as *"web-ok only (Mixamo, CC0/CC-BY, CGTrader RF)"* — and **CC-BY and CGTrader RF can both carry an attribution obligation**, so if one of the 19 is CC-BY the credit is owed *today* and unrecorded. These are the most-seen assets in the game. AUDIT.md documents props in forensic detail and the people not at all. 2. **The 18 house-library fittings/furniture carry AUDIT.md's own blanket caveat** ("no explicit licence/provenance record … before `publish.py` pushes to the shared CDN, record a one-line source/licence per asset"). They have been published since R3. The caveat is 38 rounds old. Neither is Lane B's to close, and neither blocks R41. Both now have a place to be closed *into*. #### Cost — zero, by construction and by measurement - **Zero draws, structurally.** Everything is DOM: no geometry, no material, no texture, no pass. The renderer cannot see it (the R19 selector / R39 fog-map mechanism). - **Zero boot fetches.** `credits.json` is fetched on **FIRST OPEN**, never at boot: a boot that never opens the panel makes **0** requests for it (asserted). Opening it makes **exactly 1**. - **Zero fetches under `?noassets=1`** — the asset law forbids it, so the panel says so honestly rather than fetching anyway or shipping a second built-in copy of the data. - **Classic-gated BY CONSTRUCTION, no flag test in either file.** The shell dynamic-imports `credits.js` only when `!CLASSIC`; classic therefore fetches **neither `credits.js` nor `credits.json`**, has **no `#pc-creditlink`**, **no overlay**, **no injected CSS**, **no `F2` listener**, **no `PROCITY.credits`**, and an **unchanged help line** — all asserted on a live boot. --- ### §41.5-B — A's D3: the venue signage, re-measured at 81 and fixed **The bite, re-measured on today's tree** (golden seed 20261990, gigs on, the whole shipped corpus = synthetic + 3 osm fixtures + 23 town caches): | | | |---|---| | towns | **27** | | shops | **1,838** | | venue lots (`pub` · `band_room` · `rsl`) | **81** — exactly **3 per town, in every town** | | of those, falling through to op-shop signage | **81 of 81** | | types missing from `SHOP_REGISTRY` | `band_room`, `pub`, `rsl` | | `core/registry.js SHOP_TYPES` vs `fixture_plan.js SHOP_REGISTRY` | **12 vs 9** | **A's R40 filing said 81; it reproduces to the digit.** The bite has drifted `:344` (A, R37) → `:465` (A, R40) → **`:483`** on today's tree — the line moves, the defect didn't, which is why the fix is a named resolver and not another literal at a line number. **The fix — one resolver, explicit precedence, `buildings.js signStyleFor(type)`:** 1. `SHOP_REGISTRY[type]` — the **nine palettes Lane B has shipped since v1**, returned unchanged. 2. `SHOP_TYPES[type].sign` — **A's `core/registry.js`**, which has carried gilt (pub) / stencil (band_room) / club (rsl) since R13, and is the source for **any type A adds next**. 3. `SHOP_REGISTRY.opshop` — a type in *neither* table. Unreachable for anything the generator emits. **The venue treatment:** the sign frame is redrawn in the registry's `accent` and a fascia band runs the top inside edge (a pub's painted head-board). `accent` exists **only** on registry-sourced palettes, so this is **venue-gated by construction** — the nine cannot grow one by accident — and the band sits at the TOP so it can never collide with the R26 crate mark at the sign's foot. **What I deliberately did NOT do.** A's filing suggests `SHOP_REGISTRY` shrink to a fallback-only table and all signage read A's palettes. That repaints the **whole town**: `record` would go from muted teal `#1f2e2b` to hot-pink neon `#141428`/`#ff5470`, `stall` from dark to light `#e8e2d0`, `pawn` from yellow-on-red to gold-on-black. That is an **art call for John**, not a bug fix, and R41 is the round whose whole point is visible — landing it unasked inside a signage bug fix would put an unrequested repaint into every tour frame F shoots. **Filed for the playtest session:** *unify the nine sign palettes onto A's registry, yes or no?* One `signStyleFor` line either way. `fixture_plan.js`'s header (the "registry.js hasn't landed yet" line A clocked at 25 rounds — it is now 28) is rewritten to say what that table honestly is. **Proof — a true A/B, control vs treatment, same poses, same frozen town:** **`docs/shots/laneB_r41/venue_signage_ab.png`** is the contact sheet (crops are the *measured* diff bounding box, not a hand-picked region); the full frames are `{before,after}_{pub,band_room,rsl}_signage.png` + `_venue_night.png`. The control is a HEAD copy of `web/` with `assets/`, `models/` and `vendor/` **symlinked to the live tree** (so another lane's mid-round wave is identical on both arms), served on its own no-store port. The town is frozen on both arms so the pair is strictly comparable. | pair | changed pixels | where | |---|---|---| | pub — The Exchange Hotel | **1,088** (0.12% of frame) | **rows 336–365 only** | | band_room — The Vinyl Shed | **9,645** (1.05%) | **rows 349–363 only** | | rsl — Wangaratta RSL | **14,160** (1.54%) | **rows 336–365 only** | **Every changed pixel in all three frames is inside the sign band. Nothing else in the town moves.** **An honest note on the pub**, the venue F will photograph most: it is the *least* visible of the three, because op-shop `#2c2820` and the registry's gilt `#3a2416` are near-neighbour dark browns and cream `#f0e2c0` sits close to gilt `#f2d98a`. **The pub's read comes mostly from the `#c0392b` accent frame** — which is a large part of why the venue treatment exists at all rather than a bare palette swap. The band room (black + `#7ac74f` green) and the RSL (navy `#0f2d4a` + `#c9a227` gold) are unmistakable at a glance. **One measurement artefact found and fixed, worth recording:** the first A/B run showed a second diff region at rows 670–707 on the RSL pair. It was **the v7 game bar** — `#pc-game` is built LAZILY on a later frame, so a per-element `display:none` hide races it and one arm keeps the bar. Hiding via an injected `!important` stylesheet at boot catches whatever is built afterwards. A hide that races the thing it hides will quietly put a UI element into your "geometry changed" number. --- ### §41.5-C — THE BUDGET: nothing was spent (ruling 4) **1. A/B against the HEAD control — the town FROZEN on both arms.** `roster=v1&pop=0` (no citizens) + `weather=0&tram=0&magpie=0&washing=0`, because every one of those moves on **wall-clock** time (R38's recorded trap) and a live town measured twice differs from itself. `gigs` stays ON — the venues are the point. Station lists are computed from the plan on each arm and compared as JSON **before** either sweep runs, so a drifted list fails the run instead of quietly voiding the comparison. 34 stations × 4 yaws = **136 samples per arm**: | frozen town, 136 station×yaw samples/arm | HEAD worst | R41 worst | per-sample delta | |---|---|---|---| | synthetic, **NIGHT** (the budget's worst class) | 283 draws / 116,681 tris | **283 / 116,681** | **0 draws AND 0 tris on 136/136** | | synthetic, noon | 273 / 96,396 | **273 / 96,396** | **0 and 0 on 136/136** | | `katoomba_real`, NIGHT | 167 / 109,208 | **167 / 109,208** | **0 and 0 on 136/136** | | `?classic=1`, NIGHT | 264 / 90,578 | **264 / 90,578** | **0 and 0 on 136/136** | **544 of 544 samples identical in both counters, across four boot classes. 0 console errors on either arm, every arm.** **2. The pin method of record, on the treatment tree** — and it reproduces R40 exactly: | synthetic, pin method (802 stations, yaw 0, laps 2) | R40 pin | R41 measured | law | |---|---|---|---| | default, noon (seg 2) | 282 | **282** / 100,346 tris | ≤300 ✅ | | default, **NIGHT** (seg 5) | 291 | **291** / 120,093 tris | ≤300 ✅ — **the nine-draw margin is intact** | | `?classic=1`, NIGHT | 269 / 90,580 | **269 / 90,580** | ≤300 ✅ **byte-exact** | **Margin at night is still 9 draws. Lane B spent 0 of them.** **3. The new import is free.** `buildings.js` now imports `SHOP_TYPES` from `core/registry.js` — which `citygen/index.js → plan.js` already pulls on **every** boot including classic, so it adds **zero fetches**: the classic fetch surface is **121 URLs on both arms, `added []`, `removed []`**. ### §41.5-D — CLASSIC PURITY, and a pre-existing non-determinism it exposed | classic evidence | result | |---|---| | plan carries venue types | **0 of 493 shops** — the resolver's registry arm is *structurally* unreachable in classic | | draws / tris, 136 station×yaw samples | **0 delta on 136/136**, both counters | | pin, NIGHT | **269 / 90,580** — byte-exact to the R38/R40 pin | | fetch surface | **121 URLs, added `[]`, removed `[]`** — `credits.js`/`credits.json` never requested | | HUD DOM (`innerHTML` length : body children) | **2354:8 on both arms — IDENTICAL** | | `#pc-creditlink` · `#pc-credits` · `#pc-credits-style` · `PROCITY.credits` · help-line text | **absent / absent / absent / absent / unchanged** | **→ LANE F, a finding, not a defect of this round.** I also pixel-hashed five classic bookmark frames (sha256 of the PNG) and only **2 of 5** matched — so I ran **HEAD against HEAD** as the noise floor, and it is **the same 2 of 5**. `street_noon`, `shopfront_detail` and `crossroads_busy` are **not byte-reproducible across boots at HEAD**, with citizens/weather/tram/washing/magpie all off and a 2.5 s settle; `night_neon` and `market_square` are rock-solid on every run. The likely mechanism is the **sign atlas**: `buildings.js` paints `bold ${fs}px Arial` into a canvas while chunks build in a race-dependent order, so a cell drawn before the font resolves gets different metrics. Consequence for you: **a screenshot golden pinned on those three bookmarks cannot be a byte-hash** — it needs a perceptual/threshold compare, or the atlas needs a font-ready await first. My treatment sits exactly at the noise floor, which is why the draw/tri/DOM/fetch evidence above is the load-bearing part. ### §41.5-E — gates - `node web/js/citygen/selfcheck.js` → **ALL GREEN 157,647/157,647**, fingerprint **`0x5f76e76` UNMOVED**. No plan golden can move: signage is a canvas texture and the credits surface is DOM — neither writes a plan field. - `tools/qa/r40_lane_b.py` → all assertions green (the R40 rulings still hold under R41: default declares 300/not-diagnostic, classic's selector shows 27 options with the one named towns-fetch exception, `?r=3` declares 420 and measures 386 ≤ 420 while still > 300). - `tools/flags_check.py` → **GREEN, 0 fails / 0 warns.** No new `POST_V2_EXCEPTIONS` entry is needed: that gate inspects the **classic** fetch surface, and classic requests neither credits file. - The credits surface asserted live on four boot classes (default / classic / `?noassets=1` / `plansrc=osm`): 14 entries render, 2 required-attribution boxes, 3 open-licence-question boxes, the ODbL string on screen verbatim, `licence unverified` shown as such, exactly 1 `credits.json` fetch and only on first open, **0 console errors on every arm**. **Files:** new `web/assets/credits.json` · new `web/js/world/credits.js` · `web/js/world/hud.js` · `web/js/world/buildings.js` · `web/js/world/fixture_plan.js` · `web/index.html` (`[Lane B R41]` marks) · `docs/shots/laneB_r41/` · these docs. **No git commands run** (lane treaty; Fable commits). --- ## Round 40 (§40.3 + routed §40.4) — the ?r= flag learns its own law, classic keeps its shop window, and the fog gets its signage All numbers this section: today's tree, headless Chromium, fresh contexts, port-isolated no-store servers, 1280×720, seed 20261990 unless a town is named. Budget cells use **the pin method of record** (R37/R38: `budget_walk.py --method stepwalk --stepm 2 --fixed-yaw 0 --laps 2` = 802 stations down the main spine at yaw 0, the worst bearing). ### §40.3-1 — THE ?r=3 BREACH: re-measured 391, and the ruling is GATE, not shave **The carried "307" was a stale bookmark, as the round suspected — the true number is 391.** The R37-era 307/317 pin predates five rounds of default-on content; nobody had re-measured the override since. On today's tree, same method as the budget of record: | synthetic, pin method | noon (seg 2) | NIGHT (seg 5) | live chunks max | tris worst | |---|---|---|---|---| | default (`r` auto = 2) | **282** | **291** | 32 | 120,093 ✅ pin reproduced (R38: 282/292) | | `?classic=1` | — | **269** | 32 | 90,580 ✅ **byte-exact to the R38 pin** | | **`?r=3`** | **382** | **391** | **48** | 132,829 (tris legal) | **Why shave was refused, with the numbers:** the breach is structural, not fat. `?r=3` carries 45–48 live chunks against the default's 31 (R+1 dispose window over a 7×7 vs 5×5 set), and the excess is 91–100 draws. Lane B's per-chunk cost is already collapsed to ~1 draw per kind per chunk (facade atlas, merged instancing — R2 close-out); there is no 91-draw shave available in a diagnostic boot that is not a content cut to the DEFAULT town. Shaving a diagnostic override by making the shipped town poorer is backwards. **So the flag now documents its own budget law and qa asserts it (the round's option b):** - `index.html` (`[Lane B R40]`, the RADIUS block): any `?r=` **above** the auto radius declares `PROCITY.budget = { draws: 420, tris: 200000, radius, auto, diagnostic: true }` and prints a `console.warn` naming the law ("DIAGNOSTIC boot … ≤420 draws … not a supported boot"). At or below auto: `{ draws: 300, diagnostic: false }` — the street law, unchanged. - **The ceiling is 420** = 391 measured + ~7% for crowd/streamer jitter (the R37 lesson: settle policy and station phase move a walked worst by single digits). If the town grows past it, the gate goes red and the ceiling is re-pinned CONSCIOUSLY — that is what a pinned law is for. - `hud.js` warn threshold reads the boot's own law (`budget.draws`), so the readout stops shaming a documented diagnostic at 300 and starts telling the truth at 420. Default boots render identical DOM (same 300). - `dbg.js` `info().budget` carries the law, so **a harness asserts against the boot's declared ceiling, never a hardcoded 300**. - **`tools/qa/r40_lane_b.py` (NEW file — races no lane) is the enforcing gate**, all arms non-vacuous: default declares 300/not-diagnostic · `?r=3` declares 420/diagnostic + the warn fired · a night stepwalk under `?r=3` measures ≤ its own ceiling (read 386 ≤ 420 at 8 m stations) **and** > 300 (the exemption is load-bearing, not vacuous — if it ever reads ≤300 the gate prints "retire the exemption" rather than failing) · tri law unchanged (131,893 ≤ 200k) · 0 console errors. **→ Lane F: one line wires it into qa.sh** — and README.md's `?r=N` row still quotes 307; please update it to the law (391 measured R40, ceiling 420, gated + warned). ### §40.3-2 — CLASSIC'S TOWN SELECTOR: the ruling was already true by construction; now it is deliberate and gated Fable ruled classic SHOWS the selector ("information, not gameplay state"). Verified on today's tree: `?classic=1` boots with the selector **present and visible, all 27 options** (synthetic + 3 OSM fixtures + the 23-town real/godverse pack), its towns-fetch surface is **exactly the one named POST_V2_EXCEPTION** (`assets/towns/index.json`, Fable's R37 ruling), navigation preserves the query string so a town picked FROM classic STAYS classic, **zero draws** (a ``, so rebuilding the options never clears it). **→ Lane F:** unchanged from R19 — **no shell seam**, the selector is entirely `hud.js` (already built by `createHUD`), and it lives inside `#pc-hud` so `setVisible(false)` hides it for the beta money shot. Your R19 gating call (always-in-HUD vs `?dbg`) is still yours; one line either way. --- ## Round 20 (Fable's ROUND20 → Lane B) — the tram ruling + ground polish (ledger #3/#4, v4.0-beta) ### 1. The tram ruling (ledger #3) — route by SHOP ADJACENCY, fence per-town `tram.js` `spinePolyline` now walks from each dead-end of the main subgraph, **greedily taking the unused main edge that fronts the most shops** (`lot.frontEdge`), and keeps the best-scoring path (ties → longer). Component logic isn't needed — a walk can't leave its component. **Synthetic + fixtures are byte-identical, by construction and by measurement.** A synthetic main node has ≤2 mains so there's never a choice, and its two dead-ends tie exactly ⇒ strict `>` keeps the pre-R20 start ⇒ the same route. Verified by running the old and new logic against the same plan: **identical polylines** (Boolarra Heads: 7 points, 6 edges, 810 m, 172 shops fronted, 2 stops — unchanged). | town | shops | shops fronting mains | OLD route | NEW route | stops | tram | |---|---|---|---|---|---|---| | synthetic | 493 | 172 | 172 | **172** (810 m) | 2 | RUNS (unchanged) | | fixture melbourne / katoomba | 95 / 19 | 0 / 0 | 0 | 0 (378 m) | 3 | RUNS (unchanged) | | fixture silverton | 12 | — (0 mains) | — | — | 0 | no-op (unchanged) | | **REAL newtown** | 64 | 32 | **0** | **8** (1924 m) | 16 | **RUNS** | | **REAL castlemaine** | 22 | 11 | 1 | **6** (2184 m) | 10 | **RUNS** | | REAL katoomba | 69 | 7 | **0** | 3 (3784 m) | — | **FENCED** | | REAL fremantle | 79 | 4 | **0** | 2 (3054 m) | — | **FENCED** | | REAL bendigo | 35 | 7 | **0** | 1 (4376 m) | — | **FENCED** | The alpha tram fronted **0 shops on 4 of 5 real towns** — it really was a highway bus. Shop-adjacency improves every town, but on katoomba/fremantle/bendigo the best main chain still fronts <5 shops, so per Fable's fallback the tram is **fenced per-town**. **The fence keys off the REAL-ROADS signal (`edges > 200`), never shop count alone** — because the marched fixtures legitimately front **0 shops on their mains** (the marched spine is bare; the avenues carry the shops), so a naive shop-count fence would have killed their v2 tram. Synthetic (22 edges) and fixtures (15) are an order of magnitude under the threshold and can never be fenced. Threshold: `shopsFronted < 5`. ### 2. Bug found + fixed en route: phantom tram stops on real graphs The stop projection mapped **every shelter in the town** onto the route with no distance gate — harmless on synthetic (2 shelters, both on the spine), nonsense on a real graph: **Newtown produced 149 stops** (~9 min of pure dwell at points the tram never passes). Added a **30 m** gate (a shelter sits `halfRoad+1.7` ≈ ≤16 m off its own centreline, so every genuine stop survives): **newtown 149 → 16, castlemaine 37 → 10**; synthetic **2** and fixtures **3** unchanged. ### 3. Ground polish at real intersections (ledger #4) — measured, NO fix needed Real Katoomba: **325 degree-3+ junctions, 28 under 30°, worst 4.3°** (near-parallel). Viewed at grazing eye level (the worst case for z-fighting) the worst junction reads **clean — no z-fight, no gaps**: continuous tarmac, correct footpath/kerb. Why it holds: the layers are already y-separated (road `0.0` / footpath `0.02` / kerb `0.06`) so nothing co-plane-fights across layers, and road-on-road overlap at junctions is **invisible by design** — `OVERLAP = 1.5` makes strips overrun their nodes on purpose, and they share one skin, one y and one merged draw, so a UV mismatch can't read on uniform asphalt. - *Cosmetic only (v4.0 candidate, not a defect):* at ultra-acute (<10°) junctions the footpath/kerb strips of the two near-parallel roads double up on the sliver between them. Reads busy, not broken. ### 4. Selector polish (ledger #5) — BLOCKED on E E's `web/assets/towns/index.json` hasn't landed (`build_towns.py` still WIP), so `REAL_TOWNS` stays hardcoded this round. It's a one-line swap to `fetch('assets/towns/index.json')` when E lands. **→ Lane F (B→F tram verdict + selector):** tram **runs** on synthetic, all fixtures, **newtown** and **castlemaine**; **fenced** on **katoomba, fremantle, bendigo** (`routeInfo.fenced` + `reason` are exposed for your smoke). Selector gating (always-on HUD vs `?dbg`) is your call — one line either way. **→ Lane A (finding, with measurements):** only a small slice of shops front a **main** edge on real towns — katoomba **7/69**, fremantle **4/79**, bendigo **7/35**, newtown 32/64, castlemaine 11/22. The real high street is coming out classified **`side`**, which is *why* the tram must be fenced on three towns. If your main/side classifier promoted shop-dense ways to `main`, the fence would lift and the tram would find the actual high street. Worth a look beyond the tram: **`onMain` also gates `gigs.js pickVenues` pub placement**. --- ## Round 19 (Fable's ROUND19 → Lane B) — the town selector (ledger #4, beta pull-forward, non-blocking) Shipped the in-game town picker; verified fresh Chromium. **No `index.html` seam** — it lives in `hud.js`, which already owns its DOM overlay. ### `hud.js` town selector A compact `