Files
XZBT/tools/gc5-lifecycle-model.mjs
T

187 lines
6.4 KiB
JavaScript

/** 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;
}
}