PROCITY/docs/LANES/LANE_F_NOTES.md
type-two c9e233a7e5 Fable (integrator): ROUND 34 CLOSED — GODBAY hammers, all green; v7.0 needs only the first five minutes
qa.sh --strict --matrix: 7 passed · 0 failed · 0 warn · 0 skipped, MATRIX 10 towns green. New
STRICT smoke_godbay: consigned through the real crate DOM · ONE-HAMMER determinism (a frozen
pre-sleep export slept twice lands byte-identical — the seeded gavel, measured not trusted) ·
honest bounded resolution (net $19 ≤ 2×$24×0.88) · the wake toast carried the morning paper ·
the ride to melbourne hammered a listing on arrival · ?game=0 has no crate to list from · 0
console errors. Money shots in docs/shots/v7_beta/ (the dawn paper + the ⚖ crate). The full
v7 loop now closes end to end: dig the gem → keeper cash now or the gavel tomorrow → the
wantlist points at another town → the ride costs a day and hammers your listings on arrival.
Remaining for the v7.0 tag: THE FIRST FIVE MINUTES (onboarding), the tour, docs freeze — and
John's hands on the whole thing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 19:32:06 +10:00

140 KiB
Raw Blame History

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; 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; 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 CitizenSimnew 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 KeeperManagernew 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 mapcounter 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_ONinteriorMode, 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 + tagqa.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_realBendigo, 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 REDkatoomba_real's real-roads golden UNPINNED (the A→E→A 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 dimensiontown_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 20→80, five towns to 292 shops, "subtle" enforced by construction), proved the directive from the crowd side (D's A/B: heroes kept footfall 17→19 while the street got 3.8× busier), capped posters 438→31 (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 438→111). The cause is the density widening: katoomba 19→72 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 — 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 contractLANE_C_PUB:271 stock_shop_<godverseShopId>_index.json
C's own runtime loaderstockpack.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.jsonruntime-compatible, which is exactly why C's base-override verification passed. E globbed stock_shop_*_index.jsoncontract-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 positionallyrec_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) 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.licenceBritish 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 == 0two 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 --matrix6 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:

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.


27. Round 27 — v5.0 THE LIVE CRATE. F: the reader, and the server killed on purpose.

⚠ RULING NEEDED — "ZERO console errors on server death" is unachievable, and F measured why

The brief's arm 2: "kill the server mid-session → next read degrades to tier 1 with ZERO console errors." It cannot. A fetch to a dead host logs net::ERR_CONNECTION_REFUSED at the browser level — no .catch() suppresses it, because nothing in JS ever sees it. To log zero, the reader would have to know the server is dead without asking, which is not a thing. One refused connection is the irreducible cost of discovering death.

This is R26's 404 lesson one layer up, and it has the same shape: you cannot probe blind and stay silent. R26's answer was to stop probing blind — the manifest told us what existed. There is no manifest for "is the server alive"; the only way to know is to ask.

F changed its own assertion, and the new one is STRICTLY STRONGER:

  • ZERO errors from game code (nothing threw, nothing warned) — the part that was always the point.
  • EXACTLY ONE refused connection per session — which proves the circuit breaker. Without the breaker it is one error per shop entry, forever. "Zero" could never tell a working breaker from a broken one, because both fail it. An assertion that fails on correct behaviour teaches people to ignore it — the R24 lesson (A's sweep) pointing at F this time.

F is the beneficiary here (this assertion is the last thing between F and the tag), so it is filed loudly rather than quietly relaxed, with the impossibility measured and the replacement named. Fable ratifies or overrules at the retro. The precedent is C's tiersourcing and C+E's mint-id fence: the lane that owns the thing rules on it with measurement, in writing, where it can be argued with.

⚠ F's own frame assertion was weak — a number without a control is not a measurement

ARM 2 first reported worst frame 201ms and passed, because F had written < 250ms — an arbitrary number. 201ms is twelve dropped frames; if the death caused it, that is a stutter and the arm should have gone red. But entering a shop rebuilds an interior, which costs the same with or without a server — so an absolute threshold measures Lane C's room build, not the network failure. Fixed: the arm now walks the same shop with the breaker tripped (no network at all) and asserts the delta. The question is "did the death cost anything", not "is entering fast". F caught this in F's own gate, one round after telling A the same thing.

What the gate proves (arms 1 + sandbox, unchanged and green)

gone[3] → 117 survivors · items[] 120, identity AND ORDER intact · COLLATERAL 0 — one sale removes one record, it does not re-stock the shop (C's §8, and the assertion C warned was the cheap one to get wrong: both pick-then-omit and filter-the-array hide the sold record, so absence proves nothing). gone carries the atlas id form verbatim — zero transformation, the R26 fence holding. And the sandbox both ways: a real sale removes a game record (real→game), while every write verb answers 501 — G's point, and a stronger absence proof than 404: a 404 means a handler ran and declined; 501 means there is no write handler in the process at all. A mint shop 404s: only the one real shop is live, which is the point.

The subject exists only because G built it. Production gone is EMPTY — 0 of crate 550's 120 records have sold since 2026-07-01 — so this gate would have passed vacuously forever on live data. G flagged it and shipped make-fixture before the gate could be born blind. "A fixture is not a lie; a green assert over an absent subject is."

The tour — 3 of 4 frames, and the 4th is FILED not shipped

docs/shots/release_v5/: the real crate (Monster Robot Party's own 120 records in its bins, price stickers, the keeper at the counter — the money shot, and it reads), a mint crate (captioned MINT: plausible, not real — a tour is exactly where that distinction gets quietly lost), and the classic covenant (Boolarra Heads, 0x3fa36874, five epochs on). Zero page errors.

The frontage frame does not land, and F is not shipping it green. Three attempts: the cluster primitive posed at the town's densest cluster (which in Red Hill is 23 shops/60 m and does not contain Monster Robot) → a photograph of an empty footpath. Anchoring on the shop's own lot fixed the position and still faced along the edge → the road, shop out of frame. Facing the lot along its road-normal still frames bitumen. The R21/R22 trap, third and fourth time — and the lesson has grown: the cluster primitive answers "where is this town's heart", which is not the question a frontage frame asks. Red Hill's heart isn't Monster Robot; a sparse town has no heart to speak of. Anchor on the SUBJECT when the frame is about the subject.

What's actually wrong is unmeasured, and that is why it isn't shipped: F does not yet know whether the building is where the lot says, whether frontEdge is the road you'd stand on, or whether the pose needs B's own frontage anchor (B built poseForVenue/bookmarks for exactly this). Next: ask B — B owns the frontage and shipped the stocked-shopfront mark this round; F re-deriving B's anchor in a tour script is how you get three bad frames. The interior money shot needed none of this: the room poses itself.

⚠ THE HOLD — v5.0 NOT tagged. Every real town's shops face away from their street.

B's micro-task found it while fixing F's frame, and F's three photographs of empty road are the evidence: it was never the pose. plan_osm.js:443 aims the lot's Z at the road ("facade (Z) faces road"); buildings.js:404 draws the facade on the local +Z face. Exactly opposite. The shopfront — sign, door, R26 crate mark — lands on the far side, and the street gets a blank wall. Since R18. v4.0 shipped on it. Nine rounds of tours shot it. katoomba_real's street_noon, the bookmark the tours have used since R9, is 46 draws of bare road.

Root cause: CITY_SPEC contradicts itself:71 (the lots contract) says Z, :335 (R15 posters) says "+Z, B's canon" — and each generator implements one half. Synthetic follows B's canon and looks right, which is why every golden passes and nobody saw it for nine rounds. The epoch's signature species, one last time, and the worst instance of it: not a gate that fails on correct behaviour, but a gate suite that is green over a product that is wrong.

F verified it, and F's first check was VACUOUS — worth recording, because it nearly ended the round the wrong way. F measured each lot's ry against CITY_SPEC:71's own convention and got 35/35 and 72/72 FACING — a clean contradiction of B. But plan_osm implements :71, so testing :71's data against :71's rule is self-agreement: it cannot fail. A self-consistent check over a self-contradicting spec passes under either convention — which is precisely why nine rounds of checks never caught this, and why F nearly reported "B is wrong" with a green number in hand. The only test that sees it compares the two conventions. F's frames already had.

Not F's to fix, and B was right not to either: it is A's file, it moves every real-town plan (so every real-town golden re-pins under the amendment law — 36 of them), and a self-contradicting spec is Fable's to rule, not a lane's to pick a side of. B proved the one-liner works (ry = atan2(-nx,-nz) → katoomba 0/72 → 72/72, and F's money shot appears 4 m from Musgrave Road) and reverted it. That is the same discipline F applied to A's sweep and E's validator, arriving from the other direction.

Order: Fable rules the spec → A lands the line + re-pins the goldens in one commit (amendment law) → F re-gates, re-shoots the tour, tags v5.0. Everything else for the release is done and green: the kill-the-server gate, the reader, the sandbox, the interior money shot (interiors pose themselves and are untouched by this — the crate is real either way).

And B's own postscript is the lesson twice over: B credited "chunk-buildings 348 tris (0.2%)" to the instancing work in R22 — 348 tris is ~29 boxes. It wasn't efficiency, it was the buildings being absent from frame. B read the evidence as a win for five rounds. A number you like is the easiest vacuous gate of all.

Wave 5 — the covenant amended, and the frame that took five tries

A's fix landed and A caught the ruling's premise: it was never "the real towns since R18". Synthetic is broken identically — 0 toward / 681 away, dot = 1.0000 — and so is the marched path, and all three fixtures. EVERY TOWN, EVERY PATH, SINCE v1. A booted ?classic=1, stood on the synthetic main street, found three blank grey boxes, walked behind them and found NUMBAT PLAYTHINGS facing the dirt. The covenant town has been a row of back walls since the first commit.

F re-pinned its own files per A's handoff (A doesn't edit other lanes' files, correctly): GOLDEN_HASH 0x3fa36874 → 0x5f76e76 · GIG_GOLDEN → 0xec7a2d39 · melbourne · katoomba · the captions. And A caught a live hazard in F's gate: hash_ok carried or ('3fa36874' in r.stdout) — a bare-string OR-clause that was dead the moment the covenant moved, and worse than dead, because it would go green off a sentence about the old covenant appearing in stdout. Deleted. The check derives from GOLDEN_HASH or it doesn't exist.

John's ruling is the epoch's best line: the covenant is anti-drift machinery, not a defect preservative. 0x3fa36874 did exactly its job for five epochs — it never drifted. It just was never right, and no hash can catch what was wrong in the first commit. A golden proves nothing changed; it cannot prove anything is correct. Every gate we own was green over a world of back walls, and the only thing that ever saw it was a human standing in the street looking at a wall.

The frontage frame took five attempts and four of them were F's fault:

  1. cluster pose → the town's heart, which in Red Hill isn't Monster Robot. Empty road.
  2. lot-anchored, facing along the edge → the road, shop beside the lens.
  3. lot-normal facing → bitumen.
  4. B's helper — and F threw the result away. poseForShopFront returns a pose; it does not apply one. F called it, error-checked it, and never touched the camera, so the frame was the spawn paddock. A helper that hands you a value is not a helper that does the thing — and F's own error-check passed, because the call succeeded perfectly. The vacuous check, one last time, in the last frame of the epoch.
  5. Applied it → MONSTER ROBOT PARTY, sign, door, awning, a ped walking in off Musgrave Road.

Attempts 13 were also never winnable: the shops faced backwards the whole time. F was posing a camera at a wall and tuning the geometry. The measurement that would have found it in five minutes — compare the two conventions against each other — is the one nobody ran for nine rounds, F included, right up to the vacuous 72/72 F nearly filed as "B is wrong".


28. Round 28 — v5.1 THE SPIKE AND THE SWEEP. The sweep landed. The spike did not reach the game.

⚠ F's pose gate has NO SUBJECT — SKIPPED with the reason, per the law

The brief: "New-clip figure checks (lean/look poses through the scale gate), a bookmark spot-check (leaning peds in old frames)." There are no new poses in the game, so there is nothing to check:

  • lean does not exist. E measured it rather than assumed it — the stash's 34 clips contain no lean, and find -iname "*lean*" across ultra returns nothing. The brief and the kickoff both named it as already-cached; the premise was half wrong, and E found it the only way anyone ever finds these: by looking instead of trusting the list.
  • look.glb is shipped, verified, published — and NOTHING BINDS IT. loadPedFleet() loads walk.glb, idle.glb, and sit (gated). No look. E's own handoff said it plainly: "→ D: it's ready. Gate it behind an opt the way rigs.js gates sit." D's entire R28 commit is LANE_D_NOTES.md — 48 lines, docs only. The run order said "When E's clips land: D (#1 wiring)"; the log says D committed BEFORE E shipped (afad68b precedes 640bdbb). Nobody was careless — the wave order was, and the file on disk looks exactly like a delivered spike from every angle except the one that matters.

The R17/R23 pattern, fifth appearance, and the cheapest one yet to have missed: an asset on disk, verified, published to a public depot, sha1-checked by fetching the bytes BACK — and no consumer. Every check E ran was real and passed. None of them could see that nothing loads it. A gate on the artifact cannot see the wire.

So: v5.1 SHIPS, and it does not claim the spike

Nothing is broken — an unbound clip is inert, the game is unchanged and correct. This is not R18's "known breach"; it is an unfinished item, and holding a polish tag over an inert file would be precious. What ships is the sweep, and the tag says exactly that:

  • C's audioEmitter — the till rings from the counter, the bed from the stage. C's own idea, parked since R7, and C found its own v3.0 note had specified it wrong on both halves ("the panner was inert").
  • Two items KILLED with the measurement — B's awning ripple and D's instrument LOD, the oldest tri item on the books. They don't leave the backlog "deferred"; they leave it gone, with the number that killed them. A backlog that only grows is a backlog nobody measures.
  • look.glb — staged on the depot, and honestly described as staged.

Filed for R29 (D, one opt): rigs.js gains look the way it gates sit; F passes it from the shell. Then the pose gate has a subject and F will run it. v6 has not begun in-game — the tag must not say it has.


29. Round 29 wave 0 — the pose gate gains its subject. v6's first clip is in the game.

R28's gate had no subject and skipped with the reason printed. D bound the clip in wave 0, so the gate binds too — and it is now permanent, not a hand-proof:

assertion result
default boot: look.glb bound 6.5 s, 64 tracks
the glance reaches the game peak 46 of 396 peds glancing · 0 console errors
?classic=1: the clip never fetched — the zero-fetch-delta covenant holds
?classic=1: the behaviour inert at the source — 0 of 269 peds, and the rng stream still draws

That last row is the whole design, and it's D's cleverness worth naming: the determinism lives in the DRAW, not the outcome. D takes from a dedicated glanceRng stream always — so the seeded sequence is identical whether or not a clip exists — and only flips the flag when fleet.lookClip is real. Under ?classic the roll still happens, the flag never sets, nothing is fetched, and the covenant is byte-identical. Add a behaviour without touching a hash.

And the gate asserts the clip's ABSENCE under classic, not just "0 glancing." "0 glancing" alone passes on a fleet that failed to load entirely — the vacuous form of this exact check. Present on the default boot + absent under classic is the pair that can't both be satisfied by a broken fleet.

⚠ F's gate crashed the suite before it ran — worth recording

The first cut checked for its subject with pg.goto('.../look.glb'). Navigating to a .glb starts a download, Playwright aborts the navigation, and the exception took flags_check down with it — before a single assertion ran. A gate that dies looking for its subject is worse than one that skips: it doesn't report "no subject", it reports nothing at all, and takes the suite with it. The subject-presence check now reads the disk. The vacuous-gate law has a sibling: a gate must survive its own subject being absent.

No tag — wave 0 rides Spike 2's close, which is gated on John's footage. Nothing else is owed on m3.


30. Round 30 — v7.0-alpha THE SAVE CORE (ledger #1) + SLEEP=SAVE=TOMORROW (ledger #3)

§30.1 THE CONTRACT — window.PROCITY.game (PUBLISHED FIRST, per the brief; B builds on this, C reads it at the counter)

window.PROCITY.game is null under ?classic=1 and under ?game=0; on every other boot it is:

window.PROCITY.game = {
  day,          // getter, int ≥ 1. DAY 1 IS THE PRE-v7 TOWN: stock streams carry no day salt on day 1,
                // so a fresh game's shops are byte-identical to a ?game=0 boot. Rotation begins at the
                // first sleep. (Falsifiable: day-1 stock == no-game stock; day-2 stock != day-1 stock.)
  cash,         // getter, int dollars — always equals PROCITY.wallet.cash()
  collection,   // getter → the LIVE array. READ-ONLY for every consumer (C's §9.3 law); the only
                // mutations are game.recordFind (buy seam) and game.removeFind (sell seam).
  townKey,      // string for THIS boot: `${plansrc}/${town || 'default'}@${seed}`
                //   e.g. 'synthetic/default@20261990' · 'osm/redhill_godverse@20261990'
  save(),       // → bool. Serializes schema procity-save/1 → localStorage['procity-save'].
  load(),       // → bool (ran once at construction). Corrupted/foreign blob → LOUD reject
                //   (console.error), raw stashed at localStorage['procity-save.rejected'], fresh start.
                //   THE TOWN NEVER BREAKS: the save holds only player deltas (the delta law), so a bad
                //   save can cost you your stuff, never the world.
  sleep(),      // → the new day (int). day+1 → gig weekNight re-keys to (day  1) % 7 (see the §30.3
                //   amendment) → wake at DAWN (lighting segment 0) → save(). Stock streams re-seed (§30.3).
  export(),     // → JSON string — the exact procity-save/1 payload (John moves machines).
  import(json), // → bool. Validated exactly like load(). true ⇒ state replaced + saved + day-derived
                //   state applied (weekNight etc). false ⇒ loud reject, CURRENT STATE UNTOUCHED.
  wallet,       // the wallet-compatible facade — PROCITY.wallet IS this object when the game is on.
                //   Full Lane C v0 interface: cash() start() canBuy(p) buy(o) sell(o, offer)
                //   inventory() count() onChange(fn). buy/sell/inventory keep C's exact semantics
                //   (sell credits the offer + drops the matching v0-inventory entry, per §9.3);
                //   cash is game-backed so a loaded save's cash is authoritative.
  recordFind(shop, info),  // F's buy-seam internal (interior_mode calls it) — appends a collection
                           //   entry stamped with townKey + day. Exposed so the gates can drive it.
  removeFind(entry),       // → bool. Identity (===) removal — THE only way the collection shrinks.
                           //   C's sell counter: onSell ⇒ F calls wallet.sell(item, offer) +
                           //   game.removeFind(entry). Returns false (nothing moves) if entry ∉ collection.
};

Collection entry shape (C's §9.2 asks honoured: type stamped at buy time, title/artist carried):

{ townKey,            // where in the world — the boot key above
  shopId,             // plan shop id (always present)
  godverseShopId,     // the real POS id, ONLY when the shop has one (the two id spaces stay separate — R24 law)
  type,               // the ITEM's stock type: 'record' (any dig, incl. an opshop bin) · 'book' · 'toy' (shelf
                      //   buys). Sellability is type = type, fail-closed (C §9.2) — this field is load-bearing.
  sku,                // pack item id (`sku_*` / `mint_*` / `rec_*` — the sku IS the identity, R25/26 fences)…
  slotId,             // …OR, for parody items with no pack id: `<binKey>#<offerIndex>` (dig). Exactly one
                      //   of sku|slotId is set.
  title, artist,      // display (B's collection card; C's sell card falls back to sku/slotId when absent)
  pricePaid,          // int dollars actually debited
  dayFound }          // game.day at purchase

Save schema procity-save/1 (localStorage key procity-save):

{ schema: 'procity-save/1', day, cash, town: townKey, collection: [entries], savedAt }

THE DELTA LAW, enforced by shape: no plan, no shop, no stock, no clock, no world field of any kind ever enters this object — the world regenerates from seed. Validation on load/import is exact-schema-string + field checks (day int ≥ 1 · cash finite ≥ 0 · collection entries carry townKey/pricePaid/dayFound and one of sku|slotId); any miss rejects the WHOLE save loudly. Versioning: a future procity-save/2 migrates-or-rejects, never silently coerces.

Save points: sleep() and beforeunload. (Note, measured against the brief's "save, day+1" order: sleep increments FIRST and saves the post-sleep state, so disk always carries the morning you woke into — the literal order would leave disk one day stale between sleep and unload. Same falsifiable outcome, one fewer window.)

§30.2 The laws, wired from birth

  • ?classic=1createGame is NEVER CALLED: no game object, no wallet facade, no listener, ZERO localStorage touches (save.js has no module-scope storage access — the classic-purity gate in ledger #5 measures this rather than trusting it). ?game=0 → same absence, C's plain createWallet runs the session exactly as pre-v7. Default boot → game on (the v3.1 flip precedent).
  • Game money is game money: the save carries dollars that exist nowhere else; import validates shape, not provenance — there is no bridge to anything real, in either direction.

§30.3 SLEEP = TOMORROW — the rotation law (ledger #3, runtime only)

  • The day salt. The parody/mint stock pick streams re-seed per (shop, day) by salting the EXISTING runtime streams — plan generation NEVER sees day (A verifies the boundary, ledger #6):
    • interior stock: buildInterior(shop, THREE, { …, stockDay: day })ctx.stockSalt = ':d'+day → layout.js appends it to the stk-* sub-stream salts (one marked seam line each in interiors.js + layout.js — [Lane F R30 seam]). Fittings/layout/audio streams untouched: the ROOM is the same room every day; what rotates is which items the streams pick into it.
    • the dig: binSeed(shop.seed, binKey + '@d' + day) (interior_mode, F-owned).
    • day === 1 or no game layer ⇒ NO salt (byte-identical to pre-v7 — the day-1 convention above).
  • REAL-sourced stock never rotates. A shop whose per-shop pack the godverse manifest declares sourcing:'real' (Monster Robot Party) gets NO day salt, ever — its crate is a real crate. mint crates and parody stock rotate. (The town-wide v2 packs are real images used as seeded set-dressing, not a real shop's inventory — they rotate with the parody streams. Ruled here so nobody re-derives it.)
  • Gig night = (day 1) % 7 — AMENDED from the brief's literal day % 7, with the measurement. day starts at 1, so the literal form keys a FRESH game's first night to night 1 — flipping the default boot's gigs off night 0 before the player ever sleeps, breaking the R13 cover/band gates and B's night-0-keyed frontage, and contradicting the day-1-is-the-pre-v7-town convention this same contract establishes for stock. (day 1) % 7 keeps the brief's intent exactly — every sleep walks the existing seeded week — and day 1 IS night 0. gig_state.js (F-owned) gains setWeekNight(n) + get weekNight; every venue's latch re-keys to its night-n gig (plan.gigs is already the whole week; the three hard-won latch laws untouched). Cover stamps reset with the re-key (new night ⇒ cover due), correct by John's R12 ruling. Fable ratifies or overrules at review — filed loudly, not slipped in.
  • Wake at dawn: sleep()lighting.setSegment(0) (fires procity:segment, so every latch + facade observes the morning — the listens-not-polls law does the work for free).

§30.4 Cross-lane notes on this contract

  • → B (collection UI + SLEEP surface, ledger #4): everything you need is PROCITY.game (day/cash/collection/sleep()) + wallet.onChange for the cash chip. Cover thumbs: resolve via the entry's sku against the shop's pack where present; parody entries have no thumb (title/artist card is correct-and-readable, per the brief).
  • → B (found while wiring weekNight): venue.js:38 keys "tonight" to night 0 — correct pre-game, but after the first sleep the marquee/frontage would advertise night-0's band while the latch plays night-(day%7)'s. PROCITY.gigs.weekNight (and game.day) are published; one line in venue.js when you touch the HUD. Filed, not silently fixed (your file).
  • → C (sell counter, §9.3): your asks are honoured — type stamped at buy time, title/artist carried, removal via game.removeFind(entry), credit via the facade's sell(item, offer) (mirrors your wallet.js exactly). The E-key routing hook (aimed bin → dig · near counter + sellable → sell card · else shelf-buy) is F's and lands with the sell wiring pass, not this one.
  • → A (ledger #6): the day salt rides buildInterior opts / binSeed only — grep-provable that generatePlan*/plan_osm never receive day.

§30.5 RESULTS — implemented, measured, committed (53fcd3c contract · 207bcff implementation)

All measured in FRESH browser contexts on a port-isolated no-store server (:8791 — the module-cache lesson), seed 20261990, plus redhill_godverse?stock=real for the crate legs. Seven legs, all green:

leg measured
the contract default boot: game live — day 1 · cash $191 == wallet.cash() · collection 0 · townKey synthetic/default@20261990 · weekNight 0 · full API incl. wallet.sell · 0 errors
classic purity ?classic=1 AND ?game=0: game === null, ZERO Storage-prototype calls (instrumented before any page script, not trusted) · wallet $191 (C's v0) · 0 errors
buy → collection dig pull: $12 debited, entry {type:'record', slotId:'20_-317#0', pricePaid:12, dayFound:1, title:'Sunburnt'} — debit == pricePaid, shape == §30.1
sell seam halves wallet.sell credited $3 (179→182) · removeFind true then false on re-remove · collection 0
sleep sleep() → day 2 · woke at DAWN (seg 0) · weekNight 1 · localStorage payload keys exactly [cash, collection, day, savedAt, schema, town] — the delta law by shape, no world field
rotation parody shop vertex-data fp: day 1 3396372961 → day 2 1818111125 (rotates) · re-entry same day byte-identical · after reload still identical (cross-boot determinism) · ?game=0 fp == day-1 fp (the day-1 convention, measured)
persistence reload: day 2 · cash · collection 1 · weekNight 1 all restored (beforeunload save) · export() valid · import() of garbage + foreign schema rejected loudly, state untouched; real payload adopted + weekNight re-keyed
corrupt save pre-seeded bad blob → 1 loud reject, raw stashed at procity-save.rejected, fresh start ($191, day 1), town boots, 0 other errors
the crates REAL (Monster Robot Party g:3962749): base stock_godverse/3962749/, fp identical day 1 → day 2 — real never rotates · MINT (Presents of Mind g:767): fp rotates — mint does

qa.sh --strict 6 passed · 0 failed · 0 warn over the landed layer — classic regression, default-boot gate, buy-v0, cover (paid + free), the R26 crate gate, R27 live gate, glance, no-giants: all green with the game layer default-ON.

Two of F's own harness bugs caught by the run (recorded per house habit):

  1. The post-sleep CLOSED door. First fp read after sleep() failed to enter the shop — because the town woke at DAWN and the shop was correctly closed (the hours law doing its job). The harness now sets midday before entering. A verify that forgets the world has rules will read the rules as bugs.
  2. The fingerprint was vacuous for parody rooms. It hashed mesh transforms + first UVs — but batchRoom merges parody stock into merged geometry at IDENTITY transform, so it hashed constants and reported "no rotation" over stock that rotates. (Mint showed through only because atlas UV rects land in the first 8 floats.) Fixed: hash the vertex data (position + uv arrays), where the day-salted picks/jitters actually live. The vacuous-measurement species, caught in F's own tool, same round it re-read the law to everyone else.

Live cross-lane note (seen mid-session, not F's commit): B picked up §30.4 within the hour — venue.js "tonight" is already a live getter off PROCITY.gigs.weekNight, and hud.js has the collection/SLEEP surface in flight. The contract-first order did exactly what it exists to do.

Held / next session (per the brief): ledger #5 gates (save/load determinism scripted session · no-pump v0 · classic-purity localStorage gate · rotation determinism) — a LATER F session. The sell-card E-key routing (dig → sell → shelf priority, §9.4) rides the sell wiring pass alongside it.

§30.6 THE GATES SESSION (wave 3) — the routing landed, five gates wired STRICT, all green

All measured on the house harness (:8130, fresh Playwright contexts per gate — no module cache to lie). Committed as five new STRICT smokes in tools/flags_check.py (smoke_sell_routing · smoke_save_determinism · smoke_nopump · smoke_classic_purity · smoke_rotation), so every future qa.sh run re-proves them; the session-local instruments from §30.5 are now suite property.

THE E-KEY SELL ROUTING (§9.4, the held join) — landed + verified live through the real UI: interior_mode.js grew trySell() (C's createSell/nearCounter, lazily built, only when the game is on) and the E chain is now: AIMED bin → dig · else near counter with ≥1 sellable → sell card · else forgiving nearest-bin → dig · else shelf-buy. F does both §9.3 halves in onSell, in this order: game.removeFind(item) FIRST (false ⇒ keeper veto — no credit can exist for an item not in the collection), then wallet.sell(item, offer). Shell seams (3 lines, index.html): procity:sellOpenplayer.unlock(), and sellActive guards the Esc-leaves-shop + click-relock paths (the dig's exact contract). The full loop, measured through the real DOM: dig pull $9 debited → entry {type:'record', sku|slotId, pricePaid:9} → E at the counter → C's card → SELL +$4 (< $9, the clamp) → collection 1→0 → back on the street B's HUD reads $186 · CRATE 0. Negative legs: empty collection ⇒ E opens NOTHING (open() false, zero DOM); ?game=0 ⇒ no game, no card, zero errors.

One routing amendment, filed with the measurement (F's own file, F's own routing): binUnderAim's forgiving fallback picks the nearest bin within 2.5 m of the camera and ignores aim — at a counter that sits near a bin it would swallow every E and the sell card could never open there. The chain now asks aimed-raycast FIRST (the §9.4 contract's literal "aimed bin"), lets the sell trigger decline, and only then runs the forgiving pick. Both sides proven live: at Moe Vinyl's counter-adjacent bin the card now opens (sell:true, dig:false — the old chain opened the dig); by a bin away from the counter with a sellable held and aim wrong, the dig still opens (dig:true, sell:false — the fallback survives).

THE GATES (ledger #5) — each with its subject named and touched:

gate verdict + numbers
(a) save/load determinism scripted session buy 2 · sell 1 · sleep 2× (via B's real SLEEP button) → day 3 · $177 · 1 item → export → FRESH browser → import: state byte-equal (243 B) + savedAt-normalized export byte-equal (269 B) · plan fp unchanged 0xb1c7843e across export→import (the delta law's falsifiable form) · weekNight re-keyed to 2 == (31)%7 · 0 errors both contexts
(b) no-pump v0 5 real round trips (dig UI pull+BUY → counter card SELL-all): 5/5 strictly negative, deltas [6,6,6,6,6], Σ $30 — identical deltas are the deterministic re-seeded crate, not a stuck instrument (the controls prove discrimination)
(b) C's negative control broke player ($0 via import): dig BUY disabled, clicked anyway — zero debit, zero entry · shelf path: wallet.buy INVOKED once (counted, not mocked), returned false, recordFind unreachable — no entry, cash still $0 · positive arm: same aim with cash → debit $14 == entry.pricePaid, 1 entry — C's pump is dead, and the control can tell
(c) classic purity Storage prototype instrumented before any page script: ?classic=1 AND ?game=00 calls, game null, 0 of B's 7 game DOM ids · POSITIVE control: default boot → instrument saw the procity-save getItem + all 7 ids present (a zero nobody proved the instrument could see would be vacuous) · plan fp game-on == game-off 0xb1c7843e (the layer moves zero plan bytes; classic's own fp differs by design — its golden is classic_regression's)
(d) rotation determinism VERTEX-DATA fp (position+uv arrays), per the wave-3 binding — transforms measured vacuous in §30.5: day 1 0x50edd91b (88,320 bytes / 20 stock meshes — subject present, printed) · re-entry byte-identical · day 2 0x2121f187 ≠ day 1, re-entry identical · day 3 0x2f4a9b93 ≠ both · cross-boot: day-3 fp byte-identical after a full reboot off the persisted save · ?game=0 fp == day-1 fp (the pre-v7 town IS day 1, measured again from the suite)
(e) the suite all five wired STRICT into flags_check (FAIL-level, house pattern) · qa.sh --strict --matrix full run: see §30.6 numbers below

Harness bug #3 of the round (recorded per house habit): the routing gate first read B's HUD inside the shop and called it stale — but the HUD is hidden and unrefreshed in interior mode by design (hud.setVisible(m === 'street'); gameUI.refresh() rides the street loop). The gate now walks back onto the street and reads it where a player could — $186 · CRATE 0, correct. Same species as §30.5's closed-door: a verify that forgets the world's rules reads the rules as bugs.

Design fact, filed not fixed (alpha-acceptable, for the beta ledger): parody/mint dig stock has no gone tracking — the same $11 sleeve is back in the bin on re-entry (the crate is a seeded stream, not an inventory). Buying it again is possible and loses money every time (the $6/trip measurement is exactly this); scarcity-per-day would need the save to carry per-day pull records. The no-pump clamp is what makes the infinite crate safe. Rule it in beta with the guide bands, not here.


31. Round 31 — the v7.0-alpha CLOSE: THE DIG FLIP (wave 1) + THE TAG (wave 2)

§31.1 The alpha-blocker (Fable's wave-0 playtest) and the flip

The dig was default-OFF on a plain boot. web/index.html still carried the v2-era opt-in (const DIG_ON = params.has('dig') && params.get('dig') !== '0') six epochs after the README's flags table began claiming "dig on" — and every R30 game-loop gate booted dig=1 EXPLICITLY, so the whole suite was green while the boot every player gets had no crate-riffle at all: E at a bin did nothing. The gates tested the game; nobody tested that the default boot IS the game.

The flip (one line + the law): DIG_ON = flagOn('dig') — default-ON, ?dig=0 opts out, ?classic=1 master-off: the exact pattern the other flipped flags ride. dig joins the PROCITY.flags intent surface (the gates read intent there). Measured both ways in fresh contexts on a port-isolated server (:8951):

boot measured
NAKED (no params) E at a bin → riffle OPENED + closed clean · flags.dig true · 0 errors
?dig=0 opt-out honoured — riffle does NOT open, ZERO dig DOM
?classic=1 dig INERT at the bin (E falls through trySell (null game) + shelf no-op) · ZERO pcdg-* DOM · ZERO Storage-prototype calls (instrumented) · 0 errors — the covenant untouched
?noassets=1 naked dig opens on parody canvas · 0 errors (existing law holds)

(Observation filed, pre-existing, NOT touched: assets/gen/*.jpg skin textures fetch under ?noassets — 22 fetches, fail-soft, present since v1; the dig added zero fetches to that boot.)

§31.2 The gate that makes the regression impossible (vacuous-gate law, turned on F's own suite)

Two new STRICT legs in tools/flags_check.py:

  • default_boot_gate grows dig REACHABILITY on the NAKED boot: enter a record shop, E at an aimed bin, digActive must go true (and close clean via WALK OUT); plus a PROCITY.flags.dig intent assert. Un-flip the default again and this line goes red. Negative control: the identical measurement on a ?dig=0 boot reads opened:false (pre-flight, :8951) — the detector discriminates.
  • classic_regression grows the dig-INERT leg: the old digActive assert was vacuously false on the street; it is now measured AT THE BIN — E opens nothing, zero dig DOM, filed as "measured, not assumed".

Harness lesson #4 of the epoch (house habit): the explicit-flag blindfold. A gate that force-feeds its subject's flag (dig=1 in every boot query, belt-and-braces) can never notice the default is broken. Where a flag is CLAIMED default-on, at least one gate must exercise it with the flag ABSENT. Now true for dig; gigs/weather/winmap/tram always had it (the default-boot gate boots naked by construction).

§31.3 Port isolation (small, suite-wide)

PROCITY_QA_PORT env override added to flags_check.py, qa/interior_scale_check.py, qa/town_matrix.py. ensure_server() silently REUSES anything parked on :8130 — this round that was Fable's own preview server, and a shared server is somebody else's cache semantics + somebody else's lifecycle. The R31 re-gate rode :8931; default behaviour unchanged.

§31.4 The splash (same file, same commit)

"Every door opens (soon)" — stale by six epochs — is gone: the tagline now sells the loop ("Every door opens. Dig the crates, sell the flips, sleep — the town remembers.") and the controls line carries the game beats (E riffle a bin / sell at the counter · C your crate · SLEEP to save). README manual gains the matching row (sell at the counter · C crate · SLEEP = save/advance/rotate).

§31.5 The re-gate, the shot, the tag — RESULTS

All on the FINAL tree (commit 50385c0), suite port-isolated on :8931 (PROCITY_QA_PORT — Fable's preview holds :8130, untouched):

  • qa.sh --strict --matrix: 7 passed · 0 failed · 0 warn · 0 skipped. flags_check 0 fails / 0 warns — both new R31 legs green in the run (dig REACHABLE on the naked boot — E at a bin opened the riffle (and closed clean) · ?classic dig INERT at the bin — E opened nothing, zero dig DOM), all five R30 STRICT gates re-proven over the flipped tree, classic covenant fingerprint green, no-giants green. MATRIX GREEN — 10 towns (synthetic + fixtures + real caches) × 7 gates.
  • The money shot: docs/shots/v7_alpha/crate_panel.png — Monster Robot Party's real frontage (redhill_godverse, stock=real), THE CRATE open with 3 real finds (The Return $40 · First Filtration Of The Duplex Brains $15 · Blue Blackness $20 — real cover thumbs off the per-shop atlas, every row honest: paid $N · Monster Robot Party · day 1), game bar day 1 · $116 · CRATE 3 · SLEEP. Driven through the REAL loop with the dig param ABSENT (the flip carries the shot): enter → E at three different bins → riffle → pull → BUY ×3 → street → C. 0 console errors. Script: session scratchpad (crate_shot.py) — the PNG is the deliverable.
  • The tag: v7.0-alpha, annotated, LOCAL ONLY on the notes commit — Fable reviews and pushes main + tag together (treaty).

§31.6 THE BETA QUEUE (Fable's playtest, filed NOT fixed — John's ruling queue)

  1. Spawn faces dirt/building-backs instead of the strip — wants a cluster-pose for spawns.
  2. THE CRATE is unreachable inside shops — C is street-only by design (hud.js); a player wants it at the counter.
  3. Dawn sky reads dusk-orange (cosmetic).
  4. The pull card's value hint is THE BIBLE (chartered — the beta's guide bands).
  5. Automation-only, unverified in person: dig Esc-close under synthetic events.
  6. (carried from §30.6) no per-day gone tracking on parody/mint dig stock — scarcity-per-day needs the save to carry pull records; rule it with the guide bands.

§32 — R32: v7.0-beta wave 1 — THE BIBLE + scarcity + the §31.6 queue (Fable, solo round, JING5)

The round in one line: the guide bands land (derived, not baked), gems exist (parody bins only, by construction), a bought slot stays gone for the day (pulls ride the save), and §31.6 items 13 are fixed. All gates green; the no-pump law survives the §9.1 basis swap structurally.

§32.1 What shipped, by seam

  • js/interiors/bible.js (C) — the band table mirroring E's pipeline thresholds (record 8/25/60 · book 5/15/40 · toy 6/20/50), bandRange/guideText/sellBasis. Derivation ruling: NO pack re-emit (charter risk #3 never opens); if E's thresholds move, bible.js moves in the same commit.
  • dig.js (C) — parody sleeves roll seeded bands (25/45/22/8%) + a 10% gem on collector/grail (asking strictly under the keeper's own bandLow offer: collector $29 vs $12 · grail $520 vs $30); fixed 9-draw stream shape per item; rare now ≡ grail (matches real packs). Real offers carry band off price_band. open({gone}) omits pulled slots AFTER the draw (the R27 pick-then-omit seam, zero collateral) — real via 'sku:<id>', parody via original offer index (o.i survives filtering, identity preserved). The pull card + sell card gain the guide stamp (guide $824 / $60+) — present-fields-only, and the card NEVER says "gem": spotting asking-under-guide is the player's skill.
  • sell.js (C) — §9.1 basis swap executed exactly as pre-ratified: sellOffer(sellBasis(entry)) = min(bandLow1, max(1, floor(bandLow·0.5))) when banded, pricePaid basis for alpha-era entries (they sell exactly as before — no migration needed).
  • save.js (F) — optional pulls: string[] on procity-save/1 (validated loudly when present; absent on alpha saves — old savers drop it silently, never reject). recordPull/pullsFor seams; pruned on sleep/import (day-tagged keys from other days are unreachable by construction; untagged = real-sourced, kept — sold means gone). Cap 600 FIFO. Collection entries gain optional band (recordFind stamps dig + shelf buys).
  • interior_mode.js (F) — pull keys ${shopId}|${binKey}${pullTag}#${slot}; pullTag = @d<game.day> for rotating crates (INCLUDING day 1 — the seed stays untagged per the day-1 convention, the ledger doesn't), '' for real-sourced. The SEED input is untouched (A's rotation boundary holds). digOffers honest surface (index/ask/band/sku) for the gate.
  • hud.js + index.html (B/F) — §31.6-2: #pc-game rides document.body (position:fixed, z 30), ticks in every mode (hud.tickGame() from the interior/map branches), yields to the dig/sell cards and the map; C toggles the crate anywhere. §31.6-1: cluster-pose spawn — main-street shop doors (block kind, both generators), stand at the door nearest the cluster heart, face the longer run of shopfronts; pure plan math, zero draws; ?classic=1 keeps the frozen pose. §31.6-3: DAWN swaps the dome to high-cirrus (lighting dawnSky opt, null under classic); a weather setSky is now an explicit override that always wins.

§32.2 THE GATE (smoke_bible, STRICT) + the R30 amendment

Scans EVERY bin of the record shop off digOffers: band law on all offers (in-band unless a collector/grail gem), then runs the buy-EVERYTHING ledger on the gem's bin (leg is non-vacuous by construction — 80 offers/5 bins, bin1 holds 2 gems on the pinned seed, grail asking $19 vs keeper $30). Ledger closes exactly: Σask $333 → Σoffer $135 (gems alone +$21, every non-gem strictly negative). Scarcity: the emptied bin re-opens at 0, STAYS 0 across a full reload (16 pulls in the save); negative control: ?game=0 re-opens it at 16 — the detector discriminates. 0 console errors. R30 amendment (sell_routing): credited < paid was the alpha inequality; the gate now asserts the exact formula (credited == min(basis1, basis//2), basis = bandLow-or-pricePaid, credited < basis) — the fence moved from the receipt to the band, which is what charter law #2 always said.

§32.3 Environment note (JING5)

web/assets/models/ is GITIGNORED build output — a fresh checkout runs stock=real/book-toy/ no-pump gates RED with 404s (the R31-green tree lives on m3ultra). Fixed here by rsync from m3ultra. If a third machine joins the rotation, sync that dir (49 M) or re-run E's pipeline.

§33 — R33: v7.0-beta wave 2 — THE HUNT (wantlist · rumor · travel) (Fable, solo, JING5)

  • save.js: optional wants: [{artist?|title?|sku?}] on procity-save/1 (validated exact-and-loud when present; ≥1 string field per entry; cap 40; absent-when-empty keeps the alpha byte shape). recordWant (case-insensitive dedupe) / findWant (a want covers an item when every field the WANT names matches — sku exact, artist/title by value) / removeWant (identity). TRAVEL in adopt(): adopted save's town !== townKeyday += 1, traveled = true, prunePulls — load AND import both pay the fare; same-town adoption free (save-determinism byte-equality untouched). game.traveled getter → the shell's arrival toast.
  • dig.js: the pull card's ☆ WANT line — construction-gated on opts.onWant (game absent ⇒ zero want DOM, the classic-pure pattern). ☆ click → ★ ON THE WANTLIST (a stamp, not a button, once wanted).
  • interior_mode.js: onWant/isWanted seams into dig.open; buy of a matching item removes the want (+ '✓ off the wantlist' toast). THE RUMOR in the enter() banner: seeded xmur3('rumor:<citySeed>:<shopId>:<day>') → 45% roll → hunted want + a town from RUMOR_TOWNS (the repo's real cached towns, current town excluded). Runtime-only stream — A's boundary holds; no game / empty wants ⇒ the line never exists.
  • hud.js: THE WANTLIST section in the crate panel (non-empty only): artist/title rows, ✕ stops hunting; refresh watches wants length so an open panel follows adds/clears.
  • Gate: smoke_wantlist_travel STRICT (see ROUND33_INSTRUCTIONS for the green run) — real card path, seeded rumor scan, auto-clear, +1-day travel with same-town negative control and the return fare, ?game=0 zero-DOM control, 0 console errors.
  • Matrix note: town_matrix shares one browser across towns, so travel drifts the day as it walks the matrix — safe because determinism double-boots the SAME town (free by the same-town rule) and no matrix gate reads absolute day. Reasoned before landing, not after.
  • The shot found what the gate didn't (harness lesson #5 candidate): R32's crate-in-shops surface BUILT only in street update() frames — a door entered fast after boot had no game surface inside at all, and the interior tickGame() happily refreshed a UI that never existed. Green gates missed it because every gate's boot idles on the street long enough to build. Fixed: tickGame() now builds AND refreshes (any mode); found by taking the money shot, which entered the shop faster than any gate does. Shots are a detector, not decoration.

§34 — R34: GODBAY, the offline auction (v7.0 close opens) (Fable, solo, JING5)

  • save.js: godbayHammer(entry, townKey) — THE ONE-HAMMER LAW: seeded by ITEM IDENTITY (godbay:<townKey>:<sku|slotId>:<dayFound>:<pricePaid>), never by resolution day — a passed-in item re-lists to the same result forever, no retry farm. Fixed 4-draw stream. Rolls: 15% passed in (comes home free) · uniform hammer across the band (grail top = 2×low; unbanded alpha finds auction around pricePaid — honest, never invented) · 12% bidding war ×1.52.0 · 12% commission, net floor $1. EV ≈ 0.8×mid — under in-band asking EV, so random flipping bleeds and only FINDS profit; the keeper's half-of-low stays the instant floor, the auction is the gambler's ceiling (bounded 2×high×0.88). PRNG imported from core/prng.js; the band table imported from C's bible.js (one authority — no mirror after all).
  • listings optional on procity-save/1 (entry shape + listDay, same back-compat law). listFind (identity, entry leaves the crate — can't also sell it) · resolveAuctions() runs in adopt() AND sleep(): sleep, travel, and day-moving imports all hammer what they pass. takeAuctionNews() drains the transient morning paper (NOT saved — the ledger is cash/ collection; the paper is a toast).
  • hud.js: ⚖ LIST on crate find rows (mail-in consignment) + AT AUCTION section; rebuild watches listings. index.html: the wake toast and the arrival toast fold the paper in.
  • Gate smoke_godbay (STRICT): consign through the real crate DOM → save carries it · ONE-HAMMER determinism (a frozen pre-sleep export slept twice lands byte-identical) · bounded honest resolution (SOLD net $19 on a standard record, ≤ 2×24×0.88) · the wake toast carried "⚖ SOLD Cane Toad Blues — hammer $22, $19 after fees" · the ride to melbourne hammered a listing on arrival · ?game=0 has no crate to list from · 0 console errors.
  • G's later LIVE tier must match these laws (the ladder rule: the offline tier is the contract).