Lane F R6 F2: wire ?plansrc=osm shell selector + pin osm golden + dig nearest-bin fallback

A landed the plansrc seam (52eb109); F owns the shell bootstrap. index.html: ?plansrc=osm
-> citygen.generatePlanFor(seed,'osm') (real-data 'Melbourne' 95 shops); default synthetic
'Boolarra Heads' unchanged (prime law). scaffold_check.mjs: pin BOTH goldens keyed by
(seed,plansrc) — synthetic 0x3fa36874, osm 0x34cfdec0 (deterministic) — + assert the selector
routes synthetic|osm. flags_check auto-detects + smokes plansrc + the all-on combo (4 flags).

interior_mode.js: restored binUnderAim's nearest-bin fallback (Lane C test-page parity) so E
riffles a nearby bin without perfect aim / before the bin GLB async-loads — fixes the dig
smoke's raycast-miss under fast headless AND improves real UX.

qa.sh --strict GREEN; flags_check GREEN (all 4 flags + all-on combo, 0 console errors).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
m3ultra 2026-07-14 22:59:45 +10:00
parent 5db3155991
commit 781baea4e2
4 changed files with 36 additions and 4 deletions

View File

@ -13,7 +13,7 @@ lanes land, adds C's ≤350-draw interior assertion when C1 lands, extends the d
| task | status | | 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. | | **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). | | **≤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). | | **F3 tag `v2.0-alpha`** | ⏳ all landed flags smoke-green + regression green + qa strict green → tag (3 flags OK if A no-shows). |

View File

@ -213,6 +213,26 @@ if (existsSync(R('web/js/citygen/plan.js'))) {
else fail('chunkIndex(plan) not exported from plan.js'); else fail('chunkIndex(plan) not exported from plan.js');
info(`plan(1234): ${p1?.shops?.length ?? '?'} shops, ${p1?.lots?.length ?? '?'} lots, ` + info(`plan(1234): ${p1?.shops?.length ?? '?'} shops, ${p1?.lots?.length ?? '?'} lots, ` +
`${p1?.streets?.edges?.length ?? '?'} edges, name "${p1?.name ?? '?'}"`); `${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) { } catch (e) {
fail(`importing/running plan.js threw: ${e.message}`); fail(`importing/running plan.js threw: ${e.message}`);

View File

@ -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 // 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. // 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'; 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; let plan;
try { try {
const citygen = await import('./js/citygen/index.js'); 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); 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) { } catch (e) {
plan = fixturePlan(seed); plan = fixturePlan(seed);
console.log('[procity] Lane A citygen absent — running on fixture_plan'); console.log('[procity] Lane A citygen absent — running on fixture_plan');

View File

@ -60,7 +60,13 @@ export function createInteriorMode({ THREE, renderer, camera, plan, fleet = null
while (o && !(o.userData && o.userData.kind === 'bin')) o = o.parent; while (o && !(o.userData && o.userData.kind === 'bin')) o = o.parent;
return o; 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) { function openDig(bin) {