PROCITY/docs/LANES/LANE_F_NOTES.md
m3ultra 5485d67ae8 Lane F R5 start: archive R3/R4 instruction claims + v2 flags table + v1.1 watch
- ROUND3/ROUND4_INSTRUCTIONS.md: 'SUPERSEDED by R5' banner (F4 archive task).
- LANE_F_NOTES §10: round-5 tracking — v2 flag-seam table (?plansrc/?winmap/?dig/?roster,
  all pending owning-lane specs), v2 QA-harness plan, v1.1 watch.
- F-progress: round-5 section. Confirmed fal already un-parked at 039c179 (no F change needed).
- v1.1 tag is on watch for Lane C re-map (C1) + Lane E hero-prop bake (E1).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 20:48:19 +10:00

25 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.


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 pending A
?winmap=1 B parallax interior-mapping window shader none (B owns glass); F adds the flag pass-through if needed pending B
?dig=1 C dig.js crate-riffle on procedural stock real work — interior-mode input seam (interiorMode.onInteract(fitting)); F owns the mode state machine pending C spec
?roster=stream D chunk-streamed citizen roster wire the flag + confirm B's ctx.onChunkBuilt/onChunkDisposed feed pending D

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.