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
86 lines
6.8 KiB
JavaScript
86 lines
6.8 KiB
JavaScript
const mutation = (target, value) => ({ type: 'set', target, value });
|
|
const addOne = target => mutation(target, { op: 'add', args: [{ ref: target }, 1] });
|
|
const override = (target, value, scope = 'scenario') => ({ type: 'override', target, value, scope, transition: { in: '1s', out: '2s', easing: 'ease-in-out' } });
|
|
|
|
export function scenarioAudio() {
|
|
return {
|
|
audio: { buses: { ambient: { gain: 0.5 }, effects: { gain: 0.5 } } },
|
|
sounds: {
|
|
bed: { name: 'Distant hum', bus: 'ambient', usage: ['automatic'], cadence: { class: 'ambient' }, recipe: {
|
|
mode: 'continuous', release: '1s', nodes: { tone: { type: 'oscillator', frequency: 55 }, trim: { type: 'gain', gain: 0.04 } }, routes: [{ from: 'tone', to: 'trim' }, { from: 'trim', to: 'output' }]
|
|
} },
|
|
drone: { name: 'Passing resonance', bus: 'ambient', usage: ['scenario'], recipe: {
|
|
mode: 'continuous', release: '2s', nodes: { tone: { type: 'oscillator', frequency: 165 }, trim: { type: 'gain', gain: 0.035 } }, routes: [{ from: 'tone', to: 'trim' }, { from: 'trim', to: 'output' }]
|
|
} },
|
|
beat: { name: 'Soft impulse', bus: 'effects', usage: ['automatic', 'scenario', 'manual'], cadence: { class: 'routine', overlap: false, cooldown: '3s' }, recipe: {
|
|
mode: 'oneshot', release: '50ms', nodes: { hit: { type: 'impulse', duration: '8ms', amplitude: 0.06 } }, routes: [{ from: 'hit', to: 'output' }]
|
|
} }
|
|
}
|
|
};
|
|
}
|
|
|
|
export function withTemporaryScenario(document) {
|
|
if (!['exhibit-a', 'exhibit-b'].includes(document.meta.id)) return document;
|
|
const result = structuredClone(document);
|
|
result.meta.description = 'Procedural audiovisual reference with cadence and a temporary scenario override; final acceptance remains Phase 9.';
|
|
result.parameters = { ...result.parameters, activity: { type: 'number', default: 0.4, min: 0, max: 1, label: 'Activity' } };
|
|
result.bindings = [...(result.bindings ?? []), { source: 'parameters.activity', target: 'visuals.camera.zoom', scale: 0.25, offset: 1 }];
|
|
result.cadence = { intensity: { ref: 'parameters.activity' } };
|
|
Object.assign(result, scenarioAudio());
|
|
result.scenarios = { disturbance: { name: 'Temporary disturbance', trigger: { type: 'random-interval', min: '30s', max: '90s' },
|
|
duration: '12s', cooldown: '20s', priority: 70, onStart: [override('parameters.activity', 0.95), { type: 'sound', sound: 'drone' }], timeline: [] } };
|
|
return result;
|
|
}
|
|
|
|
export const longScenarioExhibit = {
|
|
xzbt: '0.1', meta: { id: 'exhibit-e', name: 'Exhibit E — Long Passage', version: '0.1.0', description: 'A forty-minute procedural passage: approach, repeated signals, branching disturbance, and recovery.' },
|
|
runtime: { seed: 42 }, parameters: { activity: { type: 'number', default: 0.35, min: 0, max: 1, label: 'Activity' } },
|
|
state: { mode: { type: 'string', initial: 'normal' }, beats: { type: 'integer', initial: 0 }, journeys: { type: 'integer', initial: 0 } },
|
|
...scenarioAudio(), cadence: { intensity: { ref: 'parameters.activity' }, minGap: '1500ms' },
|
|
bindings: [{ source: 'parameters.activity', target: 'visuals.camera.zoom', scale: 0.2, offset: 1 }],
|
|
visuals: { scene: { coordinateSpace: 'virtual', width: 960, height: 540, background: '#101d25' }, systems: {
|
|
horizon: { type: 'graphic', content: {
|
|
ring: { type: 'ring', radius: 150, innerRadius: 148, position: { x: 480, y: 270 }, style: { fill: '#80beb5' } },
|
|
line: { type: 'line', position: { x: 240, y: 270 }, to: { x: 480, y: 0 }, style: { stroke: '#dfc18d', strokeWidth: 2 } }
|
|
} },
|
|
signal: { type: 'graphic', lifecycle: 'spawned', spawn: { lifetime: '4s', release: '1s' }, content: {
|
|
ring: { type: 'ring', radius: 165, innerRadius: 160, position: { x: 480, y: 270 }, style: { fill: '#efbe79' }, behaviors: [{ type: 'rotate', speed: 12 }] }
|
|
} }
|
|
} },
|
|
events: {
|
|
signal: { actions: [addOne('state.beats'), { type: 'sound', sound: 'beat' }, { type: 'spawn', target: 'visuals.systems.signal' }] },
|
|
recover: { actions: [mutation('state.mode', 'normal')] }
|
|
},
|
|
scenarios: {
|
|
journey: { name: 'Long passage', priority: 70, group: 'passage', trigger: { type: 'random-interval', min: '45m', max: '60m' },
|
|
concurrency: { mode: 'exclusive', scope: 'group', policy: 'defer' }, duration: '40m', cooldown: '5m',
|
|
onStart: [addOne('state.journeys'), mutation('state.mode', 'approach'), { type: 'sound', sound: 'drone' }, override('parameters.activity', 0.7)],
|
|
timeline: [
|
|
{ id: 'arrival', at: '1m', actions: [{ type: 'event', event: 'signal' }] },
|
|
{ after: 'arrival', delay: { random: { min: '20s', max: '50s' } }, repeat: { count: 12, every: { random: { min: '1m', max: '2m' } } }, actions: [{ type: 'event', event: 'signal' }] },
|
|
{ at: '15m', choose: [
|
|
{ weight: 3, actions: [mutation('state.mode', 'drift'), { ...override('parameters.activity', 0.5, 'duration'), duration: '5m' }] },
|
|
{ weight: 1, actions: [mutation('state.mode', 'disturbance'), { ...override('parameters.activity', 0.95, 'duration'), duration: '5m' }, { type: 'event', event: 'signal' }] }
|
|
] },
|
|
{ at: '30m', actions: [{ type: 'event', event: 'recover' }] },
|
|
{ at: '40m', actions: [mutation('state.mode', 'normal')] }
|
|
], onComplete: [mutation('state.mode', 'normal')], onCancel: [mutation('state.mode', 'normal')]
|
|
}
|
|
}
|
|
};
|
|
|
|
export const scenarioChallenge = {
|
|
...structuredClone(longScenarioExhibit),
|
|
meta: { id: 'scenario-challenge', name: 'Scenario Challenge', version: '1.0.0', description: 'Short, multi-minute, event, condition and concurrent scenario acceptance cases.' },
|
|
state: { ...longScenarioExhibit.state, armed: { type: 'boolean', initial: false } },
|
|
events: { ...longScenarioExhibit.events, request: { actions: [mutation('state.mode', 'requested')] } },
|
|
scenarios: {
|
|
brief: { name: 'Five seconds', duration: '5s', timeline: [{ at: '0ms', actions: [{ type: 'event', event: 'signal' }] }], onStart: [override('parameters.activity', 0.9)] },
|
|
minutes: { name: 'Three minutes', duration: '3m', timeline: [{ at: '10s', repeat: { count: 8, every: '20s' }, actions: [{ type: 'event', event: 'signal' }] }] },
|
|
condition: { name: 'Condition hold', trigger: { type: 'condition', when: { op: 'eq', left: { ref: 'state.armed' }, right: true }, for: '2s' }, duration: '5s', timeline: [], onStart: [override('parameters.activity', 0.6)] },
|
|
event: { name: 'Event trigger', trigger: { type: 'event', event: 'request' }, duration: '5s', timeline: [], onStart: [{ type: 'event', event: 'signal' }] },
|
|
exclusive: { name: 'Exclusive group', priority: 80, group: 'g', concurrency: { mode: 'exclusive', scope: 'group' }, duration: '10s', timeline: [], onStart: [{ type: 'sound', sound: 'drone' }] },
|
|
deferred: { name: 'Deferred group', priority: 50, group: 'g', concurrency: { mode: 'exclusive', scope: 'group', policy: 'defer' }, duration: '5s', timeline: [], onStart: [{ type: 'event', event: 'signal' }] }
|
|
}
|
|
};
|