feat(scenario): implement the phase 6 scenario director

Add Format Specification section 21 (Scenario Model 0.1) and the runtime
that realizes it: the scenario director, seven trigger classes, timelines
with repeats and branches, admission and concurrency control, nested
ownership with bounded cleanup, and the production GC5 resource counters.

Exhibit E provides a reproducible forty-minute long-scenario reference and
scenario-challenge.xzbt covers the PRD 131 cases. A standalone
acceptance/soak page is built by tools/build-scenario-acceptance.mjs.

252 automated checks pass. The two-hour real-duration development soak
required by the GC6 schedule remains pending.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01M7dgfQ12mpM4JjSMv3inLA
This commit is contained in:
2026-09-06 22:54:31 +00:00
co-authored by Claude Opus 5
parent c4332363a9
commit 1cde2f9f68
29 changed files with 15553 additions and 54 deletions
+34 -4
View File
@@ -2,6 +2,7 @@ import { ActionExecutor } from './actions.js';
import { SeededRNG } from './rng.js';
import { ResolutionEngine } from './resolution.js';
import { CadenceSubsystem } from './cadence.js';
import { ScenarioDirector } from './scenario.js';
export class CommonGrammarPerformance {
constructor(record, rootSeed, options = {}) {
@@ -22,6 +23,9 @@ export class CommonGrammarPerformance {
onParameterChange: options.onParameterChange
});
this.actions = new ActionExecutor(this.engine, { diagnostics: this.diagnostics });
this.actions.audio = options.audio ?? null;
if (this.actions.audio) this.actions.audio.useLogicalClock = true;
this.scenarios = new ScenarioDirector({ document: record.document, resolution: this.engine, actions: this.actions, rng: this.rng, diagnostics: this.diagnostics, onTrace: options.onScenarioTrace });
this.cadence = new CadenceSubsystem({
document: record.document,
rng: this.rng,
@@ -34,6 +38,7 @@ export class CommonGrammarPerformance {
this.frameRequest = null;
this.lastFrame = undefined;
this.accumulator = 0;
this.pauseReasons = new Set();
this.boundFrame = (now) => this.frame(now);
this.boundPointer = (event) => {
this.engine.signals.set('signals.pointer.x', event.clientX);
@@ -64,14 +69,16 @@ export class CommonGrammarPerformance {
const observed = Math.max(0, now - this.lastFrame);
this.lastFrame = now;
const accepted = Math.min(observed, 250);
if (this.pauseReasons.size) {
this.onFrame(this.engine);
this.frameRequest = globalThis.requestAnimationFrame(this.boundFrame);
return;
}
this.accumulator += accepted;
const step = 1000 / 60;
let ticks = 0;
while (this.accumulator + 1e-9 >= step && ticks < 8) {
this.actions.resetBudget();
this.engine.advance(step);
this.cadence.advance(step);
this.onTick(step, this.engine);
this.tick();
this.accumulator -= step;
ticks += 1;
}
@@ -86,9 +93,30 @@ export class CommonGrammarPerformance {
setAudio(audio) {
this.actions.audio = audio;
if (audio) audio.useLogicalClock = true;
this.cadence.setAudio(audio);
}
tick() {
if (this.pauseReasons.size || this.scenarios.disposed) return;
const step = 1000 / 60;
this.actions.resetBudget();
this.engine.advance(step);
this.actions.audio?.advanceLogical?.();
this.cadence.advance(step);
this.scenarios.update();
this.onTick(step, this.engine);
}
// Development acceleration executes the same fixed ticks without rendering.
advanceTicks(count) {
if (!Number.isSafeInteger(count) || count < 0) throw new RangeError('Tick count must be a nonnegative safe integer.');
for (let i = 0; i < count; i++) this.tick();
}
pause(reason = 'user') { this.pauseReasons.add(reason); this.lastFrame = undefined; this.accumulator = 0; }
resume(reason = 'user') { this.pauseReasons.delete(reason); this.lastFrame = undefined; this.accumulator = 0; }
setParameter(id, value) {
const result = this.engine.setParameter(id, value);
this.onUpdate(this.engine);
@@ -109,6 +137,7 @@ export class CommonGrammarPerformance {
}
async deactivate() {
this.scenarios.dispose();
if (this.frameRequest !== null && typeof globalThis.cancelAnimationFrame === 'function') globalThis.cancelAnimationFrame(this.frameRequest);
this.frameRequest = null;
this.lastFrame = undefined;
@@ -122,6 +151,7 @@ export class CommonGrammarPerformance {
}
async dispose() {
this.scenarios.dispose();
if (this.frameRequest !== null && typeof globalThis.cancelAnimationFrame === 'function') globalThis.cancelAnimationFrame(this.frameRequest);
this.frameRequest = null;
if (typeof globalThis.removeEventListener === 'function') {