Lane F R15 (v3.0): identity continuity — wire D's tonight-roster into the crowd (ledger #3)

The ped you followed into a venue now turns up in its crowd. D published the seam in R14; F wires the three touches:
- interior_mode.js: crew.spawn({ roster: rosterOf(shop.id) }) — the front crowd slots become tonight's roster
- index.html: rosterOf=(id)=>citizens.tonightRoster(id) into interior_mode; onAdmit=(e)=>citizens.recordVenueEntry(id,e)
  on the queue (each admit relays into the roster); the clear is automatic on the existing setGig(id,false)
- flags_check.py: smoke_continuity — asserts the seam (crowd includes roster-cap, pedIndex in tonightRoster),
  empty-roster injects nobody (shell side of D's byte-identical law), + ledger #5 settled-tris honesty

Verified: 2 queue admits carry into the crowd; empty roster 0-injected; settled interior 71k->182k tris
(C's finding — interior gate is draws <=54, no violation). 0 console errors.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
m3ultra 2026-07-16 11:19:19 +10:00
parent 146b5f3d2d
commit 687d23f654
3 changed files with 100 additions and 10 deletions

View File

@ -931,6 +931,88 @@ def smoke_gigs(p):
b.close()
def smoke_continuity(p):
"""R15 identity continuity (ledger #3): the ped who queued/surged into a venue turns up in its crowd.
Exercises F's real relay end-to-end — VenueQueue.admitOne() → onAdmit → citizens.recordVenueEntry →
tonightRoster(id) GigCrew.spawn({roster}) the front crowd slots carry those identities. Asserts the
seam on identities (crewInfo.fromRoster + member.pedIndex tonightRoster), the invariant crowd
roster cap, and that an EMPTY roster injects nobody (D's byte-identical law, proven here on the shell
side; D's sim soak proves the sim side). Also ledger #5 — reports SETTLED tris (after the async
instrument GLBs land), not the pre-load number C caught the R13 smoke reporting."""
head('SMOKE: continuity (R15 — the ped you followed is in the crowd · empty-roster clean · settled tris)')
if not gigs_landed(p):
print('· gigs=1 not landed (no plan.gigs) — skipping (Lane A)')
return
b, pg, errs = new_page(p)
try:
boot(pg, 'gigs=1')
pg.keyboard.press('Space')
res = pg.evaluate("""async () => {
const P = window.PROCITY, D = window.DBG;
const id = P.gigs.venueShopIds[0];
const rosterOf = () => (P.citizens.tonightRoster(id) || []).map(e => e.pedIndex);
// 1) EMPTY-ROSTER PATH enter before any admit; crew must inject only what's in the roster
// (usually 0). This is the shell side of D's "empty roster = byte-identical to R13" law.
D.setSegment(5); // NIGHT the gig is on
const rosterAtEmpty = rosterOf();
D.enterShop(id); await new Promise(r => setTimeout(r, 550));
const ei = P.interiorMode.crewInfo;
const emptyFromRoster = ei ? ei.fromRoster : -1, emptyCrowd = ei ? ei.crowd : 0;
D.exitShop(); await new Promise(r => setTimeout(r, 250));
// 2) POPULATED PATH let the street loop spawn the queue, then admit a few EXPLICITLY (rAF
// auto-drain is throttled in automation, per B's note). Each admit fires onAdmit → recordVenueEntry.
for (let i = 0; i < 8; i++) await new Promise(r => requestAnimationFrame(r));
const q = P.venueQueues && P.venueQueues.get(id);
const admitted = [];
if (q) for (let i = 0; i < 3 && q.count() > 0; i++) { const e = q.admitOne(); if (e) admitted.push(e.pedIndex); }
const roster = rosterOf();
D.enterShop(id); await new Promise(r => setTimeout(r, 700)); // build + crew spawn
const info = P.interiorMode.crewInfo, room = P.interiorMode.current;
const crowd = P.interiorMode.crew ? P.interiorMode.crew.members.filter(m => m.part === 'crowd') : [];
const rosterCrowd = crowd.filter(m => m.fromRoster).map(m => m.pedIndex);
// 3) SETTLED TRIS (ledger #5) — immediately after build vs after the instrument GLBs + rig atlas land
P.renderer.info.reset(); P.renderer.render(P.interiorMode.scene, P.camera);
const trisImmediate = P.renderer.info.render.triangles, draws = P.renderer.info.render.calls;
await new Promise(r => setTimeout(r, 1600));
P.renderer.info.reset(); P.renderer.render(P.interiorMode.scene, P.camera);
const trisSettled = P.renderer.info.render.triangles, drawsSettled = P.renderer.info.render.calls;
return { rosterAtEmpty, emptyFromRoster, emptyCrowd, admitted, roster, rosterCrowd,
fromRoster: info ? info.fromRoster : -1, crowd: info ? info.crowd : 0,
watchPoints: room ? room.watchPoints.length : 0,
trisImmediate, draws, trisSettled, drawsSettled };
}""")
# empty-roster path: crew injects exactly what's in the roster at that moment (0 unless surge beat us)
want_empty = min(len(res['rosterAtEmpty']), res['emptyCrowd'])
if res['emptyFromRoster'] == want_empty:
OK(f"empty roster: crew injected {res['emptyFromRoster']} from a {len(res['rosterAtEmpty'])}-entry roster "
f"(crowd {res['emptyCrowd']} — no phantom continuity, R13-identical path)")
else:
FAIL(f"empty roster: fromRoster {res['emptyFromRoster']} != roster∩cap {want_empty} (roster leaked)")
# the queue admits were recorded into tonightRoster (the onAdmit → recordVenueEntry relay)
roster, admitted, rosterCrowd = res['roster'], res['admitted'], res['rosterCrowd']
if admitted and all(a in roster for a in admitted):
OK(f"relay: {len(admitted)} queue admits → tonightRoster ({admitted} ⊆ roster of {len(roster)})")
else:
FAIL(f"relay: queue admits {admitted} not all in tonightRoster {roster} (onAdmit→recordVenueEntry broke)")
# THE SEAM — crowd ⊇ roster ∩ cap, and every roster-crowd member's pedIndex is genuinely in the roster
cap = res['crowd']
want = min(len(roster), cap)
subset_ok = set(rosterCrowd).issubset(set(roster))
if res['fromRoster'] == want and want > 0 and subset_ok and set(roster[:cap]).issubset(set(rosterCrowd)):
OK(f"continuity seam: {res['fromRoster']} of {cap} crowd came from the roster "
f"(crowd ⊇ roster∩cap ✓, all pedIndex ∈ tonightRoster ✓) — the ped you followed is in the crowd")
else:
FAIL(f"continuity seam: fromRoster {res['fromRoster']} vs roster∩cap {want}; rosterCrowd {rosterCrowd} "
f"vs roster {roster} (subset={subset_ok})")
# ledger #5 — settled-tris honesty (report both; interior gate is draws, which stays low)
OK(f"settled tris: {res['trisImmediate']:,} pre-load → {res['trisSettled']:,} settled "
f"(instrument GLBs; draws {res['draws']}{res['drawsSettled']}{GIG_DRAWS_MAX} gate)")
if errs: FAIL(f"continuity: {len(errs)} console error(s); first: {errs[0][:140]}")
else: OK('continuity: 0 console errors')
finally:
b.close()
def main():
srv = ensure_server()
try:
@ -955,7 +1037,8 @@ def main():
smoke_shelfbuy(p) # new (warn) — buy-anywhere (book/toy shelves)
smoke_tram(p) # new (warn) — tram (auto-skips if not landed)
smoke_audio(p) # R11 — audio house-law (silent-happy · pre-gesture · mute · noassets · leak)
smoke_gigs(p) # R12 — v3.0-alpha gig layer (schedule · band · crowd · budget · cover · noassets)
smoke_gigs(p) # R12/13 — the district gig layer (schedule · band · crowd · budget · cover · noassets)
smoke_continuity(p) # R15 — identity continuity (the ped you followed is in the crowd · settled tris)
finally:
if srv: srv.terminate()

View File

@ -188,7 +188,8 @@ const gigState = (GIGS_ON && plan.gigs && plan.gigs.length) ? createGigState({ p
// [Lane F R9] occupancyOf closure defers to `citizens` (declared just below); only invoked at enter()
// time (door click), long after citizens is initialized — so the forward reference is safe.
const interiorMode = createInteriorMode({ THREE, renderer, camera, plan, fleet, useGLB: !NOASSETS,
dig: DIG_ON, stockReal: STOCK_REAL, wallet, occupancyOf: (id) => citizens.occupancyOf(id), gigState });
dig: DIG_ON, stockReal: STOCK_REAL, wallet, occupancyOf: (id) => citizens.occupancyOf(id),
rosterOf: (id) => citizens.tonightRoster(id), gigState }); // [R15] identity continuity: the crowd IS tonight's roster
// [Lane F §F2] the riffle is cursor-driven (DOM buttons + click-a-sleeve), so release pointer-lock while it's
// 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".
@ -385,7 +386,10 @@ function frame() {
if (open && !q) {
q = new VenueQueue({ citySeed: plan.citySeed, fleet });
q.spawn(scene, { queueZone: venuePresentation && venuePresentation.queueZoneFor(id),
gigId: (gigState.gigOf(id) || {}).gigId | 0 });
gigId: (gigState.gigOf(id) || {}).gigId | 0,
// [R15] identity continuity: relay each queue admit → D's tonight roster, so the
// punter who queued turns up in the interior crowd (clear is automatic at setGig(id,false)).
onAdmit: (entry) => citizens.recordVenueEntry(id, entry) });
venueQueues.set(id, q);
} else if (!open && q) { q.disposeAll(); venueQueues.delete(id); }
if (q) q.update(dt);

View File

@ -38,7 +38,7 @@ const WALLA_KEY = 'crowd-walla'; // [R13] manifest.ambience — crowd murmur l
export function createInteriorMode({ THREE, renderer, camera, plan, fleet = null, useGLB = false,
dig: digEnabled = false, stockReal = false, wallet = null,
occupancyOf = null, gigState = null }) {
occupancyOf = null, gigState = null, rosterOf = null }) {
const scene = new THREE.Scene();
scene.background = new THREE.Color('#0e0b07');
@ -228,15 +228,18 @@ export function createInteriorMode({ THREE, renderer, camera, plan, fleet = null
seedKey: `${shop.id}#${i}`,
}));
}
// [Lane F R12 — the gig itself] Lane D's GigCrew at Lane C's poses: 3 on the deck, up to 8 at the watch
// points (~⅓ seeded to dance — John's "a bit of both"). D's own spawn caps the crowd at watchPoints.length
// and reuses the gate-protected spawnRig path, so the R10 no-giants gate covers these rigs too.
// Instruments are D's primitives this round — E's instrument GLBs landed but nothing wires them yet
// (D's `opts.instrumentFor` seam is open); asset law says a primitive band is still a gig. → LANE_F_NOTES §12.
// [Lane F R12 — the gig itself] Lane D's GigCrew at Lane C's poses: a 4-piece on the deck, up to 8/12 at
// the watch points (~⅓ seeded to dance — John's "a bit of both"). D's own spawn caps the crowd at
// watchPoints.length and reuses the gate-protected spawnRig path, so the R10 no-giants gate covers these
// rigs too; E's instrument GLBs ride in via D's instrumentFor seam.
// [Lane F R15 — identity continuity (ledger #3)] pass D's TONIGHT ROSTER so the front crowd slots ARE the
// punters who came in (surge occupants + queue admits, keyed by venue). D fills the rest with seeded
// strangers; an EMPTY roster is byte-identical to R13 (D's law), which is what a fresh/quiet venue gets.
crewInfo = null;
if (gigOn && current.stage && current.watchPoints && current.watchPoints.length) {
if (!crew) crew = new GigCrew({ citySeed: plan.citySeed, fleet });
crewInfo = crew.spawn(current.group, { stage: current.stage, watchPoints: current.watchPoints, gig: tonight });
crewInfo = crew.spawn(current.group, { stage: current.stage, watchPoints: current.watchPoints,
gig: tonight, roster: rosterOf ? rosterOf(shop.id) : [] });
}
doorReturn = { x: camera.position.x, y: camera.position.y, z: camera.position.z, ry: camera.rotation.y };
camera.position.set(current.spawn.x, EYE, current.spawn.z);