diff --git a/F-progress.md b/F-progress.md index 2023791..a20c1f0 100644 --- a/F-progress.md +++ b/F-progress.md @@ -13,7 +13,7 @@ lanes land, adds C's ≤350-draw interior assertion when C1 lands, extends the d | task | status | |---|---| | **F1 enforcement harness** | ✅ **DONE + wired.** `tools/flags_check.py` (self-contained server, headless, ~90s): **flags-off regression** (golden hash `0x3fa36874` + subsystems-inactive + ≤300/≤200k budget + ±40% snapshot + 0 errors) · **per-flag smokes** (winmap/dig/roster: cross ≥2 chunks + enter interior; dig opens+closes a riffle) · **all-on combo** (flag-composition law, decision #4). Wired into `qa.sh` as a **warn-level gate** (`--no-flags` skips fast; auto-skips without the Playwright venv). Current: **all smokes GREEN**, regression clean. | -| **F2 plansrc (if A lands)** | ⏳ harness auto-detects + includes `?plansrc=osm`; extend determinism gate to pin (seed, plansrc) hashes. | +| **F2 plansrc** | ✅ **DONE + verified.** A landed the full seam (`52eb109`). Wired the shell bootstrap (`index.html`, F-owned): `?plansrc=osm` → `citygen.generatePlanFor(seed,'osm')` → real-data town **"Melbourne", 95 shops** (synthetic default = "Boolarra Heads" unchanged). Pinned **both goldens** in `scaffold_check.mjs`: synthetic `0x3fa36874`, **osm `0x34cfdec0`** (deterministic, 2 runs). Harness auto-detects plansrc + smokes it (enters OSM shop "Mui Karaoke Rooms") + includes it in the all-on combo. Also restored `binUnderAim`'s **nearest-bin fallback** (matches C's test page) so E riffles a nearby bin without perfect aim / before the bin GLB finishes loading. Synthetic golden frozen (selfcheck 1362/1362). | | **≤350 interior draw assertion** | ⏳ add to harness once C1 (batching) lands (decision #2; book room's 1,245 is the bug). | | **F3 tag `v2.0-alpha`** | ⏳ all landed flags smoke-green + regression green + qa strict green → tag (3 flags OK if A no-shows). | diff --git a/tools/qa/scaffold_check.mjs b/tools/qa/scaffold_check.mjs index 0433141..2658b37 100755 --- a/tools/qa/scaffold_check.mjs +++ b/tools/qa/scaffold_check.mjs @@ -213,6 +213,26 @@ if (existsSync(R('web/js/citygen/plan.js'))) { else fail('chunkIndex(plan) not exported from plan.js'); info(`plan(1234): ${p1?.shops?.length ?? '?'} shops, ${p1?.lots?.length ?? '?'} lots, ` + `${p1?.streets?.edges?.length ?? '?'} edges, name "${p1?.name ?? '?'}"`); + + // [F2] plan-source selector (?plansrc=osm) — pin BOTH goldens keyed by (seed, plansrc). + // Lane A contract (LANE_A_NOTES F2): synthetic 0x3fa36874 (selfcheck), osm 0x34cfdec0. + // Guarded: no-op until A ships generatePlanFor + generatePlanOSM. + try { + const bar = await import(R('web/js/citygen/index.js')); + if (typeof bar.generatePlanFor === 'function' && typeof bar.generatePlanOSM === 'function') { + const { xmur3 } = await import(R('web/js/core/prng.js')); + const oa = bar.generatePlanOSM(20261990), ob = bar.generatePlanOSM(20261990); + assert(JSON.stringify(oa) === JSON.stringify(ob), + 'generatePlanOSM(20261990) is deterministic across two runs', 'OSM plan NON-DETERMINISTIC'); + const oh = (xmur3(JSON.stringify(oa))() >>> 0).toString(16); + assert(`0x${oh}` === '0x34cfdec0', + `osm plan fingerprint == golden 0x34cfdec0 ("${oa.name}", ${oa.shops?.length} shops)`, + `osm plan fingerprint DRIFTED: 0x${oh} (expect 0x34cfdec0)`); + assert(bar.generatePlanFor(20261990, 'osm').source === 'osm' + && (bar.generatePlanFor(20261990, 'synthetic').source || 'synthetic') === 'synthetic', + 'generatePlanFor routes synthetic|osm correctly', 'generatePlanFor selector mis-routes'); + } + } catch (e) { fail(`osm plan-source check threw: ${e.message}`); } } } catch (e) { fail(`importing/running plan.js threw: ${e.message}`); diff --git a/web/index.html b/web/index.html index 728aeaf..2f62d48 100644 --- a/web/index.html +++ b/web/index.html @@ -71,12 +71,18 @@ const seed = (parseInt(params.get('seed'), 10) || 20261990) >>> 0; // the whole town runs 100% on primitives/flat-colour with ZERO network. Everything below is fail-soft // anyway (primitives persist if a GLB never lands); this flag makes "no network at all" explicit + testable. const NOASSETS = params.has('noassets') && params.get('noassets') !== '0'; +// [Lane F §F2] plan-source selector (v2, ?plansrc=osm). Default 'synthetic' = the byte-identical +// golden-hash generator (prime law); 'osm' = Lane A's real-data fixture importer (generatePlanFor, +// LANE_A_NOTES F2). Zero network either way. Guarded: falls back to synthetic generatePlan, then fixture. +const PLANSRC = params.get('plansrc') === 'osm' ? 'osm' : 'synthetic'; let plan; try { const citygen = await import('./js/citygen/index.js'); - const gen = citygen.generatePlan || citygen.default; + const gen = citygen.generatePlanFor + ? (s) => citygen.generatePlanFor(s, PLANSRC) // A's selector (synthetic | osm) + : (citygen.generatePlan || citygen.default); // pre-R6 fallback (synthetic only) plan = gen ? gen(seed) : fixturePlan(seed); - console.log('[procity] using Lane A generatePlan'); + console.log(`[procity] plan source: ${PLANSRC} — ${plan.name}, ${plan.shops.length} shops`); } catch (e) { plan = fixturePlan(seed); console.log('[procity] Lane A citygen absent — running on fixture_plan'); diff --git a/web/js/world/interior_mode.js b/web/js/world/interior_mode.js index 502f4c1..2fbcc73 100644 --- a/web/js/world/interior_mode.js +++ b/web/js/world/interior_mode.js @@ -60,7 +60,13 @@ export function createInteriorMode({ THREE, renderer, camera, plan, fleet = null while (o && !(o.userData && o.userData.kind === 'bin')) o = o.parent; return o; } - return null; + // Fallback (matches Lane C's interior_test.html): nearest bin within reach. Uses the bin GROUP's + // world position, so it works even before the bin's GLB mesh has finished loading (async) and is + // forgiving of imperfect aim — stand next to a bin, press E. + const cp = camera.position, wp = new THREE.Vector3(); + let best = null, bd = 2.5; + for (const b of bins) { b.getWorldPosition(wp); const d = wp.distanceTo(cp); if (d < bd) { bd = d; best = b; } } + return best; } function openDig(bin) {