diff --git a/F-progress.md b/F-progress.md index a9553a9..7473abc 100644 --- a/F-progress.md +++ b/F-progress.md @@ -4,7 +4,20 @@ --- -## Round 6 (finish v2 wiring + pay review debts → v2.0-alpha) — IN PROGRESS +## Round 7 (the flip round → v2.0-beta) — IN PROGRESS + +Critical path resolved: E1 ped-merge (`447188a`), E2 stock pack (`0c12dac`), **D's GO** (`43edabf`), +C1 `?stock=real` (`3724a65`) all landed. F executes flip → strict harness → tag. + +| task | status | +|---|---| +| **F1 — roster flip (prime-law amendment)** | ✅ **DONE + verified.** `index.html`: streamed roster is now the **default** (`chunkStream:{radius:2}` per D's spec); `?roster=v1` = escape hatch (fixed roster); `?roster=stream` still accepted (no-op). Hours-aware `setNightLivelyChunks` on the open-late block. **Same commit** re-pins the flags-off baseline: **old (163 draws/19k tris, roster-off) → new (164 draws/~66k tris, roster-on)** — draws barely move (E1's merge = ~1 draw/rig, the whole point), tris = the crowd (D worst-view 65,899; drift-WARN now draws-only, tris under the hard cap). **Plan goldens UNCHANGED (`0x3fa36874`).** Also fixed the harness's roster probe (was reading a nonexistent prop → blind) + flipped the assertion to require streamMode ON + added an escape-hatch check. Verified: default boot streamMode:true (146–150 active), `?roster=v1` streamMode:false (96), both render, 0 errors, ≤300 draws, harness GREEN. | +| **F2 — harness to strict** | ⏳ next: flags failures fail qa.sh; add `stock=real` + `weather=1` smokes (new flags at warn). | +| **F3 — tag v2.0-beta** | ⏳ after F2 + B weather / A osm-parity land (decision #6). | + +--- + +## Round 6 (finish v2 wiring + pay review debts → v2.0-alpha) — DONE ✅ (v2.0-alpha tagged) Sequencing (Fable): **F1 enforcement harness lands FIRST** (protects the round), then F re-smokes as lanes land, adds C's ≤350-draw interior assertion when C1 lands, extends the determinism gate if A's diff --git a/docs/shots/laneF/flip_default_busy.png b/docs/shots/laneF/flip_default_busy.png new file mode 100644 index 0000000..56e1bd6 Binary files /dev/null and b/docs/shots/laneF/flip_default_busy.png differ diff --git a/docs/shots/laneF/flip_v1_escape.png b/docs/shots/laneF/flip_v1_escape.png new file mode 100644 index 0000000..ce24835 Binary files /dev/null and b/docs/shots/laneF/flip_v1_escape.png differ diff --git a/tools/flags_check.py b/tools/flags_check.py index 6f4bbd1..8bf0425 100644 --- a/tools/flags_check.py +++ b/tools/flags_check.py @@ -27,8 +27,15 @@ ROOT = pathlib.Path(__file__).resolve().parent.parent PORT = 8130 HOST = f'http://127.0.0.1:{PORT}' SEED = 20261990 -GOLDEN_HASH = 0x3fa36874 # frozen determinism anchor (Lane A selfcheck) -BASE_DRAWS, BASE_TRIS = 163, 19000 # flags-off DBG.shot('street_noon') snapshot (v1.1, measured); ±40% warn tolerance +GOLDEN_HASH = 0x3fa36874 # frozen determinism anchor (Lane A selfcheck) — UNCHANGED by the R7 flip +# flags-off DBG.shot('street_noon') snapshot. RE-PINNED at the R7 roster flip (prime-law amendment): +# old (v1.1, roster-off): 163 draws, 19000 tris +# new (R7, roster default-on): 164 draws, ~66000 tris +# Draws barely move — E1's ped sub-mesh merge makes each near-rig ~1 draw, which is exactly what let +# the roster flip to full density. Tris jump is the streamed crowd (each near-rig ~2.4k tris); it is +# inherently variable frame-to-frame (how many rigs are near at sample time), so the drift WARN below +# is DRAWS-ONLY — tris is guarded strictly by the STREET_TRIS_MAX hard cap, not a soft snapshot. +BASE_DRAWS, BASE_TRIS = 164, 66000 STREET_DRAWS_MAX, STREET_TRIS_MAX = 300, 200_000 KNOWN_FLAGS = ['winmap=1', 'dig=1', 'roster=stream'] # plansrc=osm auto-appended if Lane A landed it @@ -98,7 +105,7 @@ def flags_off_regression(p): return { draws:i.drawCalls, tris:i.tris, mode:i.mode, winmap: !!(P.winmap && P.winmap.active), digActive: !!(P.interiorMode && P.interiorMode.digActive), - streaming: !!(c.streaming || c.mode==='stream') }; + streamMode: !!c.streamMode }; // R7: real roster-mode flag (was reading a nonexistent prop) }""") # determinism anchor — run Lane A's selfcheck in-process (golden fingerprint) r = subprocess.run(['node', 'web/js/citygen/selfcheck.js'], cwd=str(ROOT), @@ -108,23 +115,40 @@ def flags_off_regression(p): if hash_ok and r.returncode == 0: OK(f'plan fingerprint == golden {hex(GOLDEN_HASH)} (selfcheck green)') else: FAIL(f'plan fingerprint != golden {hex(GOLDEN_HASH)} — determinism/prime-law broken') - if state['winmap'] or state['digActive'] or state['streaming']: - FAIL(f"a v2 subsystem is ACTIVE on default boot: {state}") - else: OK('all v2 subsystems inactive on default boot (winmap/dig/roster off)') + # winmap + dig stay OPT-IN (off by default). The streamed roster is now the shipping DEFAULT + # (R7 flip / prime-law amendment) — so it must be ON, not off, on a no-flag boot. + if state['winmap'] or state['digActive']: + FAIL(f"an opt-in v2 flag is active on default boot (winmap/dig): {state}") + else: OK('opt-in flags off on default boot (winmap/dig)') + if state['streamMode']: + OK('R7 roster flip in effect — streamed roster ACTIVE on default boot') + else: FAIL('roster flip REGRESSED — default boot is not streaming (expected streamMode:true)') if state['draws'] <= STREET_DRAWS_MAX and state['tris'] <= STREET_TRIS_MAX: - OK(f"flags-off street view within v1 budget (draws {state['draws']}≤{STREET_DRAWS_MAX}, tris {state['tris']}≤{STREET_TRIS_MAX})") - else: FAIL(f"flags-off street view OVER v1 budget: draws {state['draws']}, tris {state['tris']}") + OK(f"flags-off street view within budget (draws {state['draws']}≤{STREET_DRAWS_MAX}, tris {state['tris']}≤{STREET_TRIS_MAX})") + else: FAIL(f"flags-off street view OVER budget: draws {state['draws']}, tris {state['tris']}") + # Drift WARN is DRAWS-ONLY — tris is crowd-variable post-flip (guarded by the hard cap above). dd = abs(state['draws'] - BASE_DRAWS) / BASE_DRAWS - dt = abs(state['tris'] - BASE_TRIS) / BASE_TRIS - if dd <= 0.40 and dt <= 0.40: - OK(f"draws/tris within 40% of v1.1 snapshot ({state['draws']} vs {BASE_DRAWS}, {state['tris']} vs {BASE_TRIS})") + if dd <= 0.40: + OK(f"draws within 40% of re-pinned baseline ({state['draws']} vs {BASE_DRAWS}); tris {state['tris']} (crowd-variable, ≤cap)") else: - WARN(f"draws/tris drifted >40% from v1.1 snapshot ({state['draws']} vs {BASE_DRAWS}, {state['tris']} vs {BASE_TRIS}) — investigate") + WARN(f"draws drifted >40% from baseline ({state['draws']} vs {BASE_DRAWS}) — investigate") if errs: FAIL(f"{len(errs)} console error(s) on default boot; first: {errs[0][:140]}") else: OK('0 console errors on default boot') + + # R7 escape hatch: ?roster=v1 must restore the fixed roster (streamMode:false), render, no errors. + b2, pg2, errs2 = new_page(p) + try: + boot(pg2, 'roster=v1') + esc = pg2.evaluate("() => ({ streamMode: !!(window.PROCITY.citizens||{}).streamMode, mode: window.DBG.info().mode })") + if not esc['streamMode'] and esc['mode'] == 'street': + OK('escape hatch ?roster=v1 → fixed roster (streamMode:false), renders') + else: FAIL(f"escape hatch ?roster=v1 broken: {esc}") + if errs2: FAIL(f"?roster=v1: {len(errs2)} console error(s); first: {errs2[0][:140]}") + finally: + b2.close() finally: b.close() diff --git a/web/index.html b/web/index.html index 2f62d48..c8b2331 100644 --- a/web/index.html +++ b/web/index.html @@ -147,10 +147,26 @@ const interiorMode = createInteriorMode({ THREE, renderer, camera, plan, fleet, // open and re-lock (on click) after. digActive gates the unlock→leaveShop guard below so opening a bin // doesn't read as "walked out of the shop". window.addEventListener('procity:digOpen', () => player.unlock()); -// [Lane F integration] street pedestrians (Lane D). graph = plan.streets directly; with the rig fleet the -// near tier shares GLB meshes (cheap). `?pop=` tunable. setTimeOfDay/update in the street loop. -const citizens = new CitizenSim({ renderer, scene, camera, citySeed: plan.citySeed, graph: plan.streets, fleet }); -citizens.setPopulation((parseInt(params.get('pop'), 10) || 140) >>> 0); // midday ≈96 active +// [Lane F §F1 — ROUND 7 ROSTER FLIP · prime-law amendment] The chunk-streamed roster is now the +// DEFAULT: the town is permanently busier. `?roster=v1` restores the old fixed roster (escape hatch); +// `?roster=stream` is still accepted (now a no-op — stream is default). Gated on Lane D's GO: E1's +// ped sub-mesh merge makes each near-rig ~1 draw, so full density (perChunk 16) = 241 worst-street +// draws ≤300. Plan goldens do NOT move (roster doesn't touch the plan). See LANE_D_NOTES → Lane F. +const rosterV1 = params.get('roster') === 'v1'; +const citizens = new CitizenSim({ renderer, scene, camera, citySeed: plan.citySeed, graph: plan.streets, + fleet, chunkStream: rosterV1 ? null : { radius: 2 } }); // default-on +citizens.setPopulation((parseInt(params.get('pop'), 10) || 140) >>> 0); // fixed-roster (?roster=v1) size; stream = per-chunk density +// [Lane F] hours-aware: keep the open-late block lively at night (Lane D3 setNightLivelyChunks). Keys via +// sim.chunkKeyAt so they match sim's own chunk-key format; openLate shop = hours close ≥22 (Lane A contract). +if (!rosterV1) { + const late = plan.shops.find((s) => s.hours && s.hours[1] >= 22); + const lot = late && plan.lots.find((l) => l.id === late.lot); + if (lot) { + const keys = []; + for (let dx = -1; dx <= 1; dx++) for (let dz = -1; dz <= 1; dz++) keys.push(citizens.chunkKeyAt(lot.x + dx * 64, lot.z + dz * 64)); + citizens.setNightLivelyChunks(keys); + } +} document.addEventListener('visibilitychange', () => citizens.setPaused(document.hidden)); // spawn on the footpath near the west end of the strip, looking east down main street