feat(phase0): complete shared contracts and milestone plan

This commit is contained in:
2026-09-05 12:08:36 -07:00
parent ee774d0307
commit 63895c4fe7
23 changed files with 1433 additions and 161 deletions
+87
View File
@@ -0,0 +1,87 @@
/**
* Deterministic executable model for the XZBT 0.1 GC3 resolution contract.
*
* This is a Phase 0 contract oracle, not the production runtime resolver.
*/
export function clamp(value, minimum, maximum) {
return Math.min(maximum, Math.max(minimum, value));
}
export function bindingValue(source, { scale = 1, offset = 0, clamp: limits } = {}) {
let result = source * scale + offset;
if (limits) result = clamp(result, limits[0], limits[1]);
return result;
}
export function smoothBinding(previous, input, dtSeconds, tauSeconds) {
if (tauSeconds === 0 || previous === undefined) return input;
const alpha = 1 - Math.exp(-dtSeconds / tauSeconds);
return previous + alpha * (input - previous);
}
export function easingValue(name, t) {
const bounded = clamp(t, 0, 1);
switch (name) {
case 'linear':
return bounded;
case 'ease-in':
return bounded * bounded;
case 'ease-out':
return 1 - (1 - bounded) * (1 - bounded);
case 'ease-in-out':
return bounded < 0.5
? 2 * bounded * bounded
: 1 - ((-2 * bounded + 2) ** 2) / 2;
default:
throw new RangeError(`Unsupported easing: ${name}`);
}
}
export function lerp(from, to, amount) {
return from + (to - from) * amount;
}
export function attackValue(origin, target, progress, easing = 'linear') {
return lerp(origin, target, easingValue(easing, progress));
}
export function releaseValue(releaseStart, currentLower, progress, easing = 'linear') {
return lerp(currentLower, releaseStart, 1 - easingValue(easing, progress));
}
export function selectWinningOverride(overrides) {
return overrides
.filter((override) => override.live !== false)
.reduce((winner, candidate) => {
if (!winner) return candidate;
if (candidate.priority !== winner.priority) {
return candidate.priority > winner.priority ? candidate : winner;
}
return candidate.activationSequence > winner.activationSequence ? candidate : winner;
}, null);
}
export function resolveNumericTarget({
base,
binding,
automation,
overrides = [],
modulation = 0,
safetyClamp = [-Infinity, Infinity]
}) {
const afterBinding = binding ?? base;
const lower = automation ?? afterBinding;
const winner = selectWinningOverride(overrides);
const afterOverride = winner ? winner.value : lower;
const beforeClamp = afterOverride + modulation;
return {
base,
afterBinding,
lower,
winner: winner?.id ?? null,
afterOverride,
beforeClamp,
resolved: clamp(beforeClamp, safetyClamp[0], safetyClamp[1])
};
}
+143
View File
@@ -0,0 +1,143 @@
/** Phase 0 executable contract model for XZBT 0.1 GC4. */
const UINT32_RANGE = 0x1_0000_0000;
const STEP_MS = 1000 / 60;
function rotateLeft(value, count) {
return ((value << count) | (value >>> (32 - count))) >>> 0;
}
function multiply32(left, right) {
return Math.imul(left, right) >>> 0;
}
export function fnv1a32(text) {
let hash = 0x811c9dc5;
for (const byte of new TextEncoder().encode(text)) {
hash ^= byte;
hash = multiply32(hash, 0x01000193);
}
return hash >>> 0;
}
export function splitMix32(seed) {
let state = seed >>> 0;
return () => {
state = (state + 0x9e3779b9) >>> 0;
let value = state;
value = multiply32(value ^ (value >>> 16), 0x21f0aaad);
value = multiply32(value ^ (value >>> 15), 0x735a2d97);
return (value ^ (value >>> 15)) >>> 0;
};
}
export function deriveStreamState(rootSeed, domain, stableInstanceKey) {
if (!Number.isInteger(rootSeed) || rootSeed < 0 || rootSeed >= UINT32_RANGE) {
throw new RangeError('Root seed must be an unsigned 32-bit integer.');
}
const hash = fnv1a32(`xzbt-0.1\0${rootSeed}\0${domain}\0${stableInstanceKey}`);
const expand = splitMix32(hash);
const state = [expand(), expand(), expand(), expand()];
if (state.every((word) => word === 0)) state[3] = 1;
return state;
}
export class Xoshiro128StarStar {
constructor(state) {
if (!Array.isArray(state) || state.length !== 4 || state.some((word) => !Number.isInteger(word))) {
throw new TypeError('xoshiro128** state must contain four integer words.');
}
this.state = state.map((word) => word >>> 0);
if (this.state.every((word) => word === 0)) throw new RangeError('xoshiro128** state cannot be all zero.');
}
nextUint32() {
const state = this.state;
const result = multiply32(rotateLeft(multiply32(state[1], 5), 7), 9);
const temporary = (state[1] << 9) >>> 0;
state[2] ^= state[0];
state[3] ^= state[1];
state[1] ^= state[2];
state[0] ^= state[3];
state[2] ^= temporary;
state[3] = rotateLeft(state[3], 11);
for (let index = 0; index < 4; index++) state[index] >>>= 0;
return result >>> 0;
}
nextFloat() {
return this.nextUint32() / UINT32_RANGE;
}
}
export function createRandomStream(rootSeed, domain, stableInstanceKey) {
return new Xoshiro128StarStar(deriveStreamState(rootSeed, domain, stableInstanceKey));
}
export class FixedStepClock {
constructor({ stepMs = STEP_MS, maxTicksPerTurn = 8, maxElapsedMs = 250 } = {}) {
this.stepMs = stepMs;
this.maxTicksPerTurn = maxTicksPerTurn;
this.maxElapsedMs = maxElapsedMs;
this.tickIndex = 0;
this.accumulatorMs = 0;
this.lastNowMs = undefined;
this.pauseReasons = new Set();
}
setPauseReason(reason, active, nowMs) {
const wasPaused = this.pauseReasons.size > 0;
if (active) this.pauseReasons.add(reason);
else this.pauseReasons.delete(reason);
const isPaused = this.pauseReasons.size > 0;
if (wasPaused !== isPaused) this.lastNowMs = nowMs;
}
advance(nowMs) {
if (this.lastNowMs === undefined) {
this.lastNowMs = nowMs;
return { ticks: [], stalled: false };
}
if (this.pauseReasons.size > 0) {
this.lastNowMs = nowMs;
return { ticks: [], stalled: false };
}
const observed = Math.max(0, nowMs - this.lastNowMs);
this.lastNowMs = nowMs;
const accepted = Math.min(observed, this.maxElapsedMs);
this.accumulatorMs += accepted;
const available = Math.floor((this.accumulatorMs + 1e-9) / this.stepMs);
const count = Math.min(available, this.maxTicksPerTurn);
const ticks = [];
for (let index = 0; index < count; index++) ticks.push(++this.tickIndex);
this.accumulatorMs -= count * this.stepMs;
const stalled = observed > this.maxElapsedMs || available > this.maxTicksPerTurn;
if (available > this.maxTicksPerTurn) {
this.accumulatorMs = Math.min(this.accumulatorMs, this.stepMs - 1e-9);
}
return { ticks, stalled };
}
}
export function planAudioUnlock(invocations, logicalSeconds) {
return invocations.map((invocation) => {
if (invocation.start < logicalSeconds) {
if (invocation.kind === 'one-shot') return { id: invocation.id, action: 'skip' };
if (invocation.end === undefined || invocation.end > logicalSeconds) {
return { id: invocation.id, action: 'start-continuous', phase: logicalSeconds - invocation.start, rampMs: 20 };
}
return { id: invocation.id, action: 'dispose' };
}
return { id: invocation.id, action: 'schedule', delay: invocation.start - logicalSeconds };
});
}
export const GC4_CONSTANTS = Object.freeze({
stepMs: STEP_MS,
maxTicksPerTurn: 8,
maxElapsedMs: 250,
audioLookaheadMs: 100,
unlockRampMs: 20
});
+186
View File
@@ -0,0 +1,186 @@
/** Phase 0 executable contract model for XZBT 0.1 GC5. */
export const DISPATCH_BUDGET = 1024;
export const TERMINATION_HOOK_LIMIT = 256;
export const CLEANUP_TICKS = 5 * 60;
export class ConditionTrigger {
constructor(holdTicks = 0) {
this.holdTicks = holdTicks;
this.armed = false;
this.trueTicks = 0;
}
evaluate(value) {
if (!value) {
this.armed = true;
this.trueTicks = 0;
return false;
}
if (!this.armed) return false;
this.trueTicks++;
if (this.trueTicks < Math.max(1, this.holdTicks)) return false;
this.armed = false;
this.trueTicks = 0;
return true;
}
}
export class DeferredStartQueue {
constructor(defaultExpiryTicks = 5 * 60 * 60) {
this.defaultExpiryTicks = defaultExpiryTicks;
this.requests = new Map();
}
defer({ definitionId, priority = 50, creationTick, documentOrder, expiryTicks, sampledInputs }) {
if (this.requests.has(definitionId)) return false;
this.requests.set(definitionId, {
definitionId,
priority,
creationTick,
documentOrder,
expiresAt: creationTick + (expiryTicks ?? this.defaultExpiryTicks),
sampledInputs
});
return true;
}
dispatch(tick, isEligible) {
for (const [id, request] of this.requests) {
if (tick >= request.expiresAt) this.requests.delete(id);
}
const ordered = [...this.requests.values()].sort((left, right) =>
right.priority - left.priority || left.creationTick - right.creationTick || left.documentOrder - right.documentOrder
);
const started = [];
for (const request of ordered) {
if (!isEligible(request)) continue;
started.push(request);
this.requests.delete(request.definitionId);
}
return started;
}
}
export function drainDispatch(initialQueue, expand, budget = DISPATCH_BUDGET) {
const queue = [...initialQueue];
const processed = [];
let consumed = 0;
while (queue.length > 0 && consumed < budget) {
const unit = queue.shift();
consumed++;
processed.push(unit);
const added = expand(unit) || [];
queue.push(...added);
}
return {
processed,
consumed,
exhausted: queue.length > 0,
discarded: queue,
failedOwnerIds: [...new Set(queue.map((unit) => unit.ownerId).filter(Boolean))]
};
}
export class OwnershipModel {
constructor() {
this.tickIndex = 0;
this.state = new Map();
this.owners = new Map();
this.resources = new Map();
this.diagnostics = [];
this.nextResourceId = 1;
}
startScenario(id, { onStart = [], onComplete = [], onCancel = [] } = {}) {
const owner = { id, status: 'STARTING', blocked: false, onComplete, onCancel, terminalCause: null };
this.owners.set(id, owner);
const succeeded = this.executeActions(onStart, id, 'ordinary');
if (succeeded) owner.status = 'ACTIVE';
return owner;
}
executeActions(actions, ownerId, phase = 'ordinary') {
const owner = this.owners.get(ownerId);
for (let index = 0; index < actions.length; index++) {
const action = actions[index];
if (phase === 'ordinary' && owner?.blocked) return false;
try {
if (phase === 'termination' && !['set', 'sound', 'fail'].includes(action.type)) {
throw new Error(`Action '${action.type}' is not permitted in a termination hook.`);
}
if (action.type === 'set') {
this.state.set(action.target, action.value);
} else if (action.type === 'sound') {
if (action.continuous || action.ownership === 'persistent') {
throw new Error('Termination hooks permit only nonpersistent one-shot sounds.');
}
this.createResource(ownerId, { kind: 'termination-sound', releaseTicks: action.releaseTicks ?? 0 });
} else if (action.type === 'resource') {
this.createResource(ownerId, action);
} else if (action.type === 'event') {
if (!this.executeActions(action.actions || [], ownerId, phase)) return false;
} else if (action.type === 'fail') {
throw new Error(action.message || 'injected failure');
}
} catch (error) {
this.diagnostics.push({ code: 'ERR_ACTION_FAILURE', ownerId, phase, index, message: error.message });
if (phase === 'termination') continue;
if (action.critical) {
this.terminate(ownerId, 'failure');
return false;
}
}
}
return true;
}
createResource(ownerId, { kind = 'generic', releaseTicks = 0, ownership = 'scenario' } = {}) {
const effectiveOwner = ownership === 'persistent' ? 'performance' : ownerId;
const id = `resource-${this.nextResourceId++}`;
this.resources.set(id, { id, kind, ownerId: effectiveOwner, releaseTicks, cleanupAt: null });
return id;
}
terminate(ownerId, cause) {
const owner = this.owners.get(ownerId);
if (!owner || owner.blocked) return;
owner.blocked = true;
owner.terminalCause = cause;
const hook = cause === 'complete' ? owner.onComplete : owner.onCancel;
this.executeActions(hook.slice(0, TERMINATION_HOOK_LIMIT), ownerId, 'termination');
if (hook.length > TERMINATION_HOOK_LIMIT) {
this.diagnostics.push({ code: 'ERR_DISPATCH_BUDGET', ownerId, phase: 'termination' });
}
for (const resource of this.resources.values()) {
if (resource.ownerId !== ownerId) continue;
const boundedRelease = Math.min(resource.releaseTicks, CLEANUP_TICKS);
if (boundedRelease === 0) this.resources.delete(resource.id);
else {
resource.ownerId = `cleanup:${ownerId}`;
resource.cleanupAt = this.tickIndex + boundedRelease;
resource.forceAt = this.tickIndex + CLEANUP_TICKS;
resource.forceRequired = resource.releaseTicks > CLEANUP_TICKS;
}
}
owner.status = cause === 'complete' ? 'COMPLETED' : cause === 'failure' ? 'FAILED' : 'CANCELLED';
}
tick(count = 1) {
for (let step = 0; step < count; step++) {
this.tickIndex++;
for (const resource of [...this.resources.values()]) {
if (!resource.cleanupAt || this.tickIndex < resource.cleanupAt) continue;
if (resource.forceRequired && this.tickIndex >= resource.forceAt) {
this.diagnostics.push({ code: 'WARN_CLEANUP_FORCED', resourceId: resource.id });
}
this.resources.delete(resource.id);
}
}
}
ownedResourceCount(ownerId) {
return [...this.resources.values()].filter((resource) => resource.ownerId === ownerId).length;
}
}
+136 -13
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env node
/**
* XZBT 0.1 Structural & Semantic Exhibit Validator
* Zero external dependencies. Conforms to XZBT Format Specification 0.1 (Revision 0.2).
* Zero external dependencies. Conforms to XZBT Format Specification 0.1 (Revision 0.3).
*/
import { readFileSync } from 'node:fs';
@@ -11,6 +11,20 @@ const ID_REGEX = /^[a-z][a-z0-9_-]*$/;
const REF_PATH_REGEX = /^[a-z][a-z0-9_-]*(\.[a-z][a-z0-9_-]*)+$/;
const DURATION_REGEX = /^([0-9]+(?:\.[0-9]+)?)(ms|s|m|h)$/;
const RUNTIME_SIGNAL_TYPES = new Map([
['signals.time.elapsed', 'number'],
['signals.time.delta', 'number'],
['signals.audio.low', 'number'],
['signals.audio.mid', 'number'],
['signals.audio.high', 'number'],
['signals.audio.energy', 'number'],
['signals.pointer.x', 'number'],
['signals.pointer.y', 'number'],
['signals.viewport.width', 'number'],
['signals.viewport.height', 'number'],
['signals.scenario.active', 'boolean']
]);
const ALLOWED_ROOT_KEYS = new Set([
'xzbt',
'meta',
@@ -58,6 +72,16 @@ const ALLOWED_STATE_KEYS = new Set([
'max'
]);
const ALLOWED_BINDING_KEYS = new Set([
'source',
'target',
'scale',
'offset',
'clamp',
'smoothing',
'when'
]);
const OPERATOR_ARITY = {
abs: 1,
negate: 1,
@@ -389,37 +413,92 @@ export class ExhibitValidator {
}
const graph = new Map(); // target -> [sources]
const targetWriters = new Map();
this.doc.bindings.forEach((binding, idx) => {
const path = `$.bindings[${idx}]`;
if (typeof binding !== 'object' || binding === null) {
if (typeof binding !== 'object' || binding === null || Array.isArray(binding)) {
this.addError('ERR_SCHEMA_VALIDATION', path, 'Binding must be an object.');
return;
}
if (typeof binding.from !== 'string' || !REF_PATH_REGEX.test(binding.from)) {
this.addError('ERR_INVALID_REFERENCE', `${path}.from`, `Invalid from reference: '${binding.from}'.`);
for (const key of Object.keys(binding)) {
if (!ALLOWED_BINDING_KEYS.has(key)) {
this.addError('ERR_UNKNOWN_FIELD', `${path}.${key}`, `Unrecognized field in BindingSpec: '${key}'.`);
}
}
if (typeof binding.source !== 'string' || !REF_PATH_REGEX.test(binding.source)) {
this.addError('ERR_INVALID_REFERENCE', `${path}.source`, `Invalid source reference: '${binding.source}'.`);
} else {
this.resolveReference(binding.from, `${path}.from`);
this.resolveReference(binding.source, `${path}.source`);
}
if (typeof binding.to !== 'string' || !REF_PATH_REGEX.test(binding.to)) {
this.addError('ERR_INVALID_REFERENCE', `${path}.to`, `Invalid to reference: '${binding.to}'.`);
if (typeof binding.target !== 'string' || !REF_PATH_REGEX.test(binding.target)) {
this.addError('ERR_INVALID_REFERENCE', `${path}.target`, `Invalid target reference: '${binding.target}'.`);
} else {
this.validateBindingTarget(binding.target, `${path}.target`);
}
if (binding.transform) {
this.validateValueSpec(binding.transform, `${path}.transform`);
if (binding.scale !== undefined && (typeof binding.scale !== 'number' || !Number.isFinite(binding.scale))) {
this.addError('ERR_TYPE_MISMATCH', `${path}.scale`, 'Binding scale must be a finite number.');
}
if (binding.smoothing) {
if (binding.offset !== undefined && (typeof binding.offset !== 'number' || !Number.isFinite(binding.offset))) {
this.addError('ERR_TYPE_MISMATCH', `${path}.offset`, 'Binding offset must be a finite number.');
}
if (binding.clamp !== undefined) {
if (
!Array.isArray(binding.clamp) ||
binding.clamp.length !== 2 ||
!binding.clamp.every((value) => typeof value === 'number' && Number.isFinite(value))
) {
this.addError('ERR_SCHEMA_VALIDATION', `${path}.clamp`, 'Binding clamp must be two finite numbers.');
} else if (binding.clamp[0] > binding.clamp[1]) {
this.addError('ERR_OUT_OF_BOUNDS', `${path}.clamp`, 'Binding clamp minimum cannot exceed maximum.');
}
}
if (binding.smoothing !== undefined) {
if (typeof binding.smoothing !== 'string' || !DURATION_REGEX.test(binding.smoothing)) {
this.addError('ERR_INVALID_DURATION', `${path}.smoothing`, `Invalid duration: '${binding.smoothing}'.`);
}
}
if (typeof binding.from === 'string' && typeof binding.to === 'string') {
if (!graph.has(binding.to)) graph.set(binding.to, []);
graph.get(binding.to).push(binding.from);
if (binding.when !== undefined) {
this.validateConditionSpec(binding.when, `${path}.when`);
}
const sourceType = this.getReferenceType(binding.source);
const targetType = this.getReferenceType(binding.target);
if (sourceType && targetType) {
const usesNumericTransform =
binding.scale !== undefined || binding.offset !== undefined || binding.clamp !== undefined ||
(binding.smoothing !== undefined && binding.smoothing !== '0ms');
const sourceNumeric = sourceType === 'number' || sourceType === 'integer';
const targetNumeric = targetType === 'number' || targetType === 'integer';
if (usesNumericTransform && (!sourceNumeric || !targetNumeric)) {
this.addError('ERR_TYPE_MISMATCH', path, 'Binding transforms and smoothing require numeric source and target types.');
} else if (!usesNumericTransform && sourceType !== targetType && !(sourceNumeric && targetNumeric)) {
this.addError('ERR_TYPE_MISMATCH', path, `Binding source type '${sourceType}' does not match target type '${targetType}'.`);
}
}
if (typeof binding.source === 'string' && typeof binding.target === 'string') {
if (!graph.has(binding.target)) graph.set(binding.target, []);
graph.get(binding.target).push(binding.source);
const priorWriter = targetWriters.get(binding.target);
if (priorWriter !== undefined) {
this.addError(
'ERR_CONFLICTING_BINDING',
`${path}.target`,
`Binding target '${binding.target}' is already written by $.bindings[${priorWriter}].`
);
} else {
targetWriters.set(binding.target, idx);
}
}
});
@@ -454,6 +533,41 @@ export class ExhibitValidator {
}
}
validateBindingTarget(refPath, location) {
const parts = refPath.split('.');
const namespace = parts[0];
if (namespace === 'parameters' || namespace === 'state') {
if (parts.length !== 2) {
this.addError('ERR_UNSUPPORTED_TARGET', location, `Target '${refPath}' is not a scalar ${namespace} target.`);
return;
}
this.resolveReference(refPath, location);
return;
}
if (namespace === 'audio' && parts.length === 4 && parts[1] === 'buses' && parts[3] === 'gain') {
const busId = parts[2];
if (!this.doc.audio?.buses?.[busId]) {
this.addError('ERR_INVALID_REFERENCE', location, `Target '${refPath}' refers to a missing audio bus '${busId}'.`);
}
return;
}
this.addError('ERR_UNSUPPORTED_TARGET', location, `Target '${refPath}' is not exposed by the shared 0.1 target registry.`);
}
getReferenceType(refPath) {
if (typeof refPath !== 'string') return null;
const parts = refPath.split('.');
if (parts[0] === 'parameters' && parts.length === 2) return this.doc.parameters?.[parts[1]]?.type || null;
if (parts[0] === 'state' && parts.length === 2) return this.doc.state?.[parts[1]]?.type || null;
if (parts[0] === 'audio' && parts[1] === 'buses' && parts[3] === 'gain') return 'number';
if (parts[0] === 'signals') return RUNTIME_SIGNAL_TYPES.get(refPath) || null;
if (parts[0] === 'modulators') return 'number';
return null;
}
resolveReference(refPath, location) {
const parts = refPath.split('.');
const namespace = parts[0];
@@ -476,6 +590,15 @@ export class ExhibitValidator {
`Reference '${refPath}' refers to non-existent state variable '${stateId}'.`
);
}
} else if (namespace === 'signals') {
if (!RUNTIME_SIGNAL_TYPES.has(refPath)) {
this.addError('ERR_INVALID_REFERENCE', location, `Reference '${refPath}' is not a declared 0.1 runtime signal.`);
}
} else if (namespace === 'modulators') {
const modulatorId = parts[1];
if (!this.doc.modulators || !this.doc.modulators[modulatorId]) {
this.addError('ERR_INVALID_REFERENCE', location, `Reference '${refPath}' refers to non-existent modulator '${modulatorId}'.`);
}
}
}