@victframework/runtime 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/dist/agent-governance.d.ts +285 -0
  2. package/dist/agent-governance.js +438 -0
  3. package/dist/agent-governance.js.map +1 -0
  4. package/dist/agent-registry.d.ts +103 -0
  5. package/dist/agent-registry.js +758 -0
  6. package/dist/agent-registry.js.map +1 -0
  7. package/dist/agent-types.d.ts +433 -0
  8. package/dist/agent-types.js +480 -0
  9. package/dist/agent-types.js.map +1 -0
  10. package/dist/authority.d.ts +83 -0
  11. package/dist/authority.js +170 -0
  12. package/dist/authority.js.map +1 -0
  13. package/dist/boundary-conformance.d.ts +51 -0
  14. package/dist/boundary-conformance.js +364 -0
  15. package/dist/boundary-conformance.js.map +1 -0
  16. package/dist/control-conformance.d.ts +68 -0
  17. package/dist/control-conformance.js +1131 -0
  18. package/dist/control-conformance.js.map +1 -0
  19. package/dist/control-in-memory.d.ts +257 -0
  20. package/dist/control-in-memory.js +886 -0
  21. package/dist/control-in-memory.js.map +1 -0
  22. package/dist/control-types.d.ts +1017 -0
  23. package/dist/control-types.js +779 -0
  24. package/dist/control-types.js.map +1 -0
  25. package/dist/durable-run.d.ts +73 -0
  26. package/dist/durable-run.js +262 -0
  27. package/dist/durable-run.js.map +1 -0
  28. package/dist/effect-policy.d.ts +22 -0
  29. package/dist/effect-policy.js +45 -0
  30. package/dist/effect-policy.js.map +1 -0
  31. package/dist/errors.d.ts +36 -0
  32. package/dist/errors.js +71 -0
  33. package/dist/errors.js.map +1 -0
  34. package/dist/in-memory-stores.d.ts +16 -0
  35. package/dist/in-memory-stores.js +522 -0
  36. package/dist/in-memory-stores.js.map +1 -0
  37. package/dist/index.d.ts +41 -0
  38. package/dist/index.js +26 -0
  39. package/dist/index.js.map +1 -0
  40. package/dist/operator-config.d.ts +141 -0
  41. package/dist/operator-config.js +289 -0
  42. package/dist/operator-config.js.map +1 -0
  43. package/dist/orchestration-activation.d.ts +116 -0
  44. package/dist/orchestration-activation.js +223 -0
  45. package/dist/orchestration-activation.js.map +1 -0
  46. package/dist/orchestration-canary-conformance.d.ts +10 -0
  47. package/dist/orchestration-canary-conformance.js +726 -0
  48. package/dist/orchestration-canary-conformance.js.map +1 -0
  49. package/dist/orchestration-commands.d.ts +47 -0
  50. package/dist/orchestration-commands.js +538 -0
  51. package/dist/orchestration-commands.js.map +1 -0
  52. package/dist/orchestration-conformance.d.ts +59 -0
  53. package/dist/orchestration-conformance.js +594 -0
  54. package/dist/orchestration-conformance.js.map +1 -0
  55. package/dist/orchestration-driver-types.d.ts +163 -0
  56. package/dist/orchestration-driver-types.js +21 -0
  57. package/dist/orchestration-driver-types.js.map +1 -0
  58. package/dist/orchestration-driver.d.ts +48 -0
  59. package/dist/orchestration-driver.js +1030 -0
  60. package/dist/orchestration-driver.js.map +1 -0
  61. package/dist/orchestration-in-memory.d.ts +18 -0
  62. package/dist/orchestration-in-memory.js +1313 -0
  63. package/dist/orchestration-in-memory.js.map +1 -0
  64. package/dist/orchestration-join-conformance.d.ts +20 -0
  65. package/dist/orchestration-join-conformance.js +542 -0
  66. package/dist/orchestration-join-conformance.js.map +1 -0
  67. package/dist/orchestration-plan.d.ts +52 -0
  68. package/dist/orchestration-plan.js +265 -0
  69. package/dist/orchestration-plan.js.map +1 -0
  70. package/dist/orchestration-race-conformance.d.ts +35 -0
  71. package/dist/orchestration-race-conformance.js +1265 -0
  72. package/dist/orchestration-race-conformance.js.map +1 -0
  73. package/dist/orchestration-remediation-conformance.d.ts +54 -0
  74. package/dist/orchestration-remediation-conformance.js +632 -0
  75. package/dist/orchestration-remediation-conformance.js.map +1 -0
  76. package/dist/orchestration-store-types.d.ts +568 -0
  77. package/dist/orchestration-store-types.js +2 -0
  78. package/dist/orchestration-store-types.js.map +1 -0
  79. package/dist/pack-conformance.d.ts +59 -0
  80. package/dist/pack-conformance.js +222 -0
  81. package/dist/pack-conformance.js.map +1 -0
  82. package/dist/pack-install.d.ts +44 -0
  83. package/dist/pack-install.js +126 -0
  84. package/dist/pack-install.js.map +1 -0
  85. package/dist/registry.d.ts +107 -0
  86. package/dist/registry.js +481 -0
  87. package/dist/registry.js.map +1 -0
  88. package/dist/runtime.d.ts +158 -0
  89. package/dist/runtime.js +1341 -0
  90. package/dist/runtime.js.map +1 -0
  91. package/dist/serialization.d.ts +26 -0
  92. package/dist/serialization.js +162 -0
  93. package/dist/serialization.js.map +1 -0
  94. package/dist/store-conformance.d.ts +45 -0
  95. package/dist/store-conformance.js +876 -0
  96. package/dist/store-conformance.js.map +1 -0
  97. package/dist/store-errors.d.ts +49 -0
  98. package/dist/store-errors.js +48 -0
  99. package/dist/store-errors.js.map +1 -0
  100. package/dist/store-types.d.ts +331 -0
  101. package/dist/store-types.js +14 -0
  102. package/dist/store-types.js.map +1 -0
  103. package/dist/store-validation.d.ts +63 -0
  104. package/dist/store-validation.js +164 -0
  105. package/dist/store-validation.js.map +1 -0
  106. package/dist/stream-hub.d.ts +151 -0
  107. package/dist/stream-hub.js +402 -0
  108. package/dist/stream-hub.js.map +1 -0
  109. package/dist/testing.d.ts +25 -0
  110. package/dist/testing.js +19 -0
  111. package/dist/testing.js.map +1 -0
  112. package/dist/types.d.ts +221 -0
  113. package/dist/types.js +2 -0
  114. package/dist/types.js.map +1 -0
  115. package/package.json +45 -0
@@ -0,0 +1,1341 @@
1
+ import { compileGraph, executeGraph } from '@victframework/kernel';
2
+ import { OrchestrationDriver } from './orchestration-driver.js';
3
+ import { ORCHESTRATION_LIMITS } from './orchestration-driver-types.js';
4
+ import { cancelRun as orchestrationCancelRun, processDueTimers as processOrchestrationDueTimers, recoverOrchestration as recoverOrchestrationCommands, resolveBlocked as resolveBlockedCommand, signalWait, } from './orchestration-commands.js';
5
+ import { loadManifest } from './orchestration-activation.js';
6
+ import { deriveIdempotencyKey, deriveInvocationId } from './orchestration-activation.js';
7
+ import { NEUTRAL_JSON_CONTRACT_ID } from '@victframework/contracts';
8
+ import { CapabilityRegistry } from './registry.js';
9
+ import { decideEffectAuthorization } from './effect-policy.js';
10
+ import { VictRuntimeError, classifyInvocationFailure, runtimeError, sanitiseThrownError, } from './errors.js';
11
+ import { DurableRunTracker } from './durable-run.js';
12
+ import { createInMemoryStores } from './in-memory-stores.js';
13
+ import { deepFreeze, parseStoredJson, toCanonicalJson } from './serialization.js';
14
+ import { VictStoreError } from './store-errors.js';
15
+ import { ACTIVATION_MANIFEST_SCHEMA, ACTIVATION_MANIFEST_SCHEMA_V2, RUN_EVENT_SCHEMA, } from './store-types.js';
16
+ const RETENTION_VALUES = ['none', 'summary', 'full'];
17
+ const RECOVERY_CODE = 'VICT_RUN_INTERRUPTED_BY_RESTART';
18
+ const RECOVERY_REASON = 'The process executing this run ended before the run reached a terminal state.';
19
+ const RECOVERY_REMEDIATION = 'Automatic resume is unavailable at this stage. Inspect the run and its events, then start a deliberate new run; the new run receives a new run id.';
20
+ export { RECOVERY_CODE, RECOVERY_REASON, RECOVERY_REMEDIATION };
21
+ /**
22
+ * The usable in-process Vict runtime.
23
+ *
24
+ * Composes a capability registry, the active activation snapshot, execution
25
+ * policy, and durable semantic stores (activation catalog + execution
26
+ * store). Contains no ARA-specific logic.
27
+ *
28
+ * Safety model:
29
+ * - Activation is atomic from the caller's perspective: the candidate is
30
+ * compiled, snapshotted, and published/selected in the catalog BEFORE the
31
+ * in-memory snapshot is replaced. A failed compile or storage write leaves
32
+ * the previously active graph selected and runnable.
33
+ * - Activation manifests are immutable and durable; every run pins exactly
34
+ * one activationVersion (RUN-001), persisted with the run.
35
+ * - Runs execute against the pinned snapshot; in-flight runs cannot observe
36
+ * registry changes. Every run transition and its events are committed
37
+ * atomically (DATA-003) with optimistic concurrency, and the durable
38
+ * write-ahead rule holds: a capability is invoked only after its
39
+ * `node.started` intent (and every preceding durable write) has committed.
40
+ * - Test doubles are snapshotted at run start; replacing a double mid-run
41
+ * affects only later runs. Duplicate `registerDouble` is rejected; use
42
+ * `replaceDouble`.
43
+ * - Effect policy is enforced before any capability runs (see effect-policy).
44
+ * Irreversible effects never run their real implementation in simulate or
45
+ * test modes; in those modes a registered safe double may run, and without
46
+ * one the operation is blocked.
47
+ * - Run records are retained according to the runtime's `payloadRetention`
48
+ * (default `'summary'`): complete payloads are stored only under explicit
49
+ * `'full'` retention — which transfers responsibility for the persisted
50
+ * content to the caller/operator. Thrown error messages are never stored.
51
+ */
52
+ export class VictRuntime {
53
+ #registry;
54
+ #stores;
55
+ #clock;
56
+ #ids;
57
+ #defaultOverrides;
58
+ #defaultMaxSteps;
59
+ #retention;
60
+ #orchestrationOptions;
61
+ #active;
62
+ #orchestrationDriverInstance;
63
+ constructor(options = {}) {
64
+ this.#registry = new CapabilityRegistry(options.authority === undefined
65
+ ? undefined
66
+ : {
67
+ ...(options.authority.grants !== undefined ? { grants: options.authority.grants } : {}),
68
+ ...(options.authority.configuration !== undefined
69
+ ? { configuration: options.authority.configuration }
70
+ : {}),
71
+ ...(options.authority.secrets !== undefined
72
+ ? { secrets: options.authority.secrets }
73
+ : {}),
74
+ });
75
+ const retention = options.payloadRetention ?? 'summary';
76
+ if (!RETENTION_VALUES.includes(retention)) {
77
+ throw new VictRuntimeError('VICT_RUNTIME_INVALID_RETENTION', `payloadRetention must be one of ${RETENTION_VALUES.map((value) => `'${value}'`).join(', ')}; received ${JSON.stringify(retention)}.`);
78
+ }
79
+ this.#retention = retention;
80
+ if (options.stores !== undefined) {
81
+ const stores = options.stores;
82
+ if (typeof stores.catalog?.publish !== 'function' ||
83
+ typeof stores.execution?.createRun !== 'function') {
84
+ throw new VictRuntimeError('VICT_RUNTIME_INVALID_STORES', 'stores must provide conforming `catalog` (ActivationCatalog) and `execution` (ExecutionStore) ports.');
85
+ }
86
+ this.#stores = stores;
87
+ }
88
+ else {
89
+ this.#stores = createInMemoryStores();
90
+ }
91
+ this.#clock = options.clock ?? { now: () => Date.now() };
92
+ this.#ids = options.ids ?? {
93
+ runId: () => `run_${globalThis.crypto.randomUUID()}`,
94
+ };
95
+ this.#defaultOverrides = options.policy ?? {};
96
+ this.#defaultMaxSteps = options.maxSteps;
97
+ const orchestrationOptions = options.orchestration ?? {};
98
+ this.#orchestrationOptions = {
99
+ concurrency: orchestrationOptions.concurrency,
100
+ operatorAuthorized: orchestrationOptions.operatorAuthorized ?? false,
101
+ time: orchestrationOptions.time,
102
+ ownerId: orchestrationOptions.ownerId ?? `owner_${globalThis.crypto.randomUUID()}`,
103
+ leaseMs: orchestrationOptions.leaseMs ?? 30_000,
104
+ };
105
+ }
106
+ /** The Stage 03 durable orchestration driver (lazily constructed). */
107
+ #orchestrationDriver() {
108
+ if (this.#orchestrationDriverInstance === undefined) {
109
+ const orchestration = this.#stores.orchestration;
110
+ if (orchestration === undefined) {
111
+ throw new VictRuntimeError('VICT_RUNTIME_INVALID_STORES', 'Durable orchestration requires a store set with a conforming `orchestration` port (OrchestrationStore). Provide @victframework/store-sqlite ≥ Stage 03 or the default in-memory stores.');
112
+ }
113
+ this.#orchestrationDriverInstance = new OrchestrationDriver({
114
+ registry: this.#registry,
115
+ clock: this.#clock,
116
+ ids: this.#ids,
117
+ defaultOverrides: this.#defaultOverrides,
118
+ retention: this.#retention,
119
+ ownerId: this.#orchestrationOptions.ownerId,
120
+ leaseMs: this.#orchestrationOptions.leaseMs,
121
+ orchestration,
122
+ catalog: { get: (activationVersion) => this.#stores.catalog.get(activationVersion) },
123
+ ...(this.#orchestrationOptions.time !== undefined
124
+ ? { time: this.#orchestrationOptions.time }
125
+ : {}),
126
+ });
127
+ }
128
+ return this.#orchestrationDriverInstance;
129
+ }
130
+ /**
131
+ * Drive an existing orchestration run to terminal/quiescent against its
132
+ * EXACT pinned activation. Selection changes never affect this run; a
133
+ * newer selected activation applies only to future runs.
134
+ */
135
+ async resumeRun(runId, options = {}) {
136
+ return this.#orchestrationDriver().resumeRun(runId, options);
137
+ }
138
+ /** Idempotently deliver one signal to one exact durable wait. */
139
+ async signal(command) {
140
+ const deps = this.#orchestrationDriver().deps;
141
+ const registry = this.#registry;
142
+ const parseWithPinnedContract = async (activationVersion, contractId, payload) => {
143
+ const manifest = await loadManifest({ catalog: { get: (version) => this.#stores.catalog.get(version) } }, activationVersion);
144
+ const contractRevisions = new Map();
145
+ for (const contract of manifest.contracts) {
146
+ contractRevisions.set(contract.id, contract.revision);
147
+ }
148
+ const revision = contractRevisions.get(contractId);
149
+ const contract = revision === undefined
150
+ ? registry.getContract(contractId)
151
+ : registry.getContractRevision(contractId, revision);
152
+ if (!contract) {
153
+ return {
154
+ ok: false,
155
+ message: `Contract '${contractId}' required by the pinned activation is not registered.`,
156
+ };
157
+ }
158
+ let parsed;
159
+ try {
160
+ parsed = contract.parse(payload);
161
+ }
162
+ catch {
163
+ // Stage 04: a throwing author parser is a sanitized structured
164
+ // rejection — the wait remains open and the thrown message is
165
+ // never echoed.
166
+ return {
167
+ ok: false,
168
+ message: `The signal payload could not be validated by contract '${contractId}' (revision '${revision ?? contract.revision}'): the contract parser threw. The wait remains open.`,
169
+ };
170
+ }
171
+ if (!parsed.ok) {
172
+ return {
173
+ ok: false,
174
+ message: `The signal payload was rejected by contract '${contractId}' revision '${revision ?? contract.revision}'; the wait remains open.`,
175
+ };
176
+ }
177
+ return { ok: true };
178
+ };
179
+ return signalWait(deps, command, parseWithPinnedContract);
180
+ }
181
+ /** Idempotently request cancellation of one orchestration run. */
182
+ async cancel(command) {
183
+ const driver = this.#orchestrationDriver();
184
+ const result = await orchestrationCancelRun(driver.deps, command);
185
+ if (result.ok && result.status === 'accepted') {
186
+ // Cooperatively abort in-flight capability contexts: each capability
187
+ // observes its abort signal and unwinds; the durable transition
188
+ // boundary classifies the attempt as cancelled. No downstream node or
189
+ // retry can start from an aborted attempt.
190
+ driver.abortInflight(command.runId);
191
+ }
192
+ return result;
193
+ }
194
+ /** Resolve bounded due timers (timer waits, wait timeouts, retry backoff). */
195
+ async processDueTimers(options = {}) {
196
+ const driver = this.#orchestrationDriver();
197
+ const deps = driver.deps;
198
+ return processOrchestrationDueTimers(deps, {
199
+ runId: options.runId,
200
+ limit: options.limit ?? ORCHESTRATION_LIMITS.defaultTimerBatch,
201
+ }, async (activationVersion) => {
202
+ try {
203
+ const graph = await driver.resolveGraphForDriver(activationVersion);
204
+ return { ok: true, graph };
205
+ }
206
+ catch {
207
+ return { ok: false };
208
+ }
209
+ }, () => deps.clock.now());
210
+ }
211
+ /**
212
+ * Explicit boot-time effect-aware recovery for orchestration runs:
213
+ * reclaim policy-permitted expired claims (pure/read recompute; keyed
214
+ * write retries with the same key) and block ambiguous unsafe work.
215
+ * Historical Stage 02 sequential recovery is unchanged.
216
+ */
217
+ async recoverOrchestration(options = {}) {
218
+ const driver = this.#orchestrationDriver();
219
+ const deps = driver.deps;
220
+ const summary = await recoverOrchestrationCommands(deps, options, async (runId, attempt) => {
221
+ const run = await deps.orchestration.getOrchestrationRun(runId);
222
+ if (!run) {
223
+ return { action: 'skip', reason: 'run not found' };
224
+ }
225
+ try {
226
+ const graph = await driver.resolveGraphForDriver(run.activationVersion);
227
+ const node = graph.getNode(attempt.nodeId);
228
+ if (!node) {
229
+ return {
230
+ action: 'block',
231
+ reason: 'the pinned activation cannot resolve the attempt node',
232
+ };
233
+ }
234
+ const retry = node.retry;
235
+ const effect = attempt.effectClass;
236
+ if (effect === 'pure' || effect === 'read') {
237
+ return { action: 'reclaim' };
238
+ }
239
+ if (effect === 'write') {
240
+ const keyed = attempt.idempotencyKey !== null;
241
+ if (!keyed) {
242
+ return {
243
+ action: 'block',
244
+ reason: 'A write capability without keyed idempotency has an unknown outcome after process loss; it is never replayed.',
245
+ };
246
+ }
247
+ if (retry !== undefined && attempt.attemptNumber < retry.maxAttempts) {
248
+ return { action: 'reclaim' };
249
+ }
250
+ return {
251
+ action: 'block',
252
+ reason: 'The keyed write retry policy is exhausted; the outcome remains ambiguous.',
253
+ };
254
+ }
255
+ return {
256
+ action: 'block',
257
+ reason: 'An irreversible capability has an unknown outcome after process loss; it is never replayed.',
258
+ };
259
+ }
260
+ catch {
261
+ return {
262
+ action: 'skip',
263
+ reason: 'the exact pinned activation is unavailable; the claim is left for explicit resolution',
264
+ };
265
+ }
266
+ });
267
+ if (options.resume === true && summary.reclaimed.length > 0) {
268
+ const runIds = [...new Set(summary.reclaimed.map((entry) => entry.runId))];
269
+ for (const runId of runIds) {
270
+ await this.resumeRun(runId, { concurrency: options.concurrency });
271
+ }
272
+ }
273
+ return summary;
274
+ }
275
+ /**
276
+ * Bounded authorized operator resolution for one blocked run. Denied by
277
+ * default: only runtimes explicitly constructed with
278
+ * `orchestration.operatorAuthorized: true` may resolve blocked work.
279
+ */
280
+ async resolveBlocked(input) {
281
+ if (this.#orchestrationOptions.operatorAuthorized !== true) {
282
+ return {
283
+ ok: false,
284
+ code: 'VICT_ORCH_OPERATOR_DENIED',
285
+ message: 'Operator resolution is denied: no explicit operator authorization is configured for this runtime.',
286
+ };
287
+ }
288
+ const driver = this.#orchestrationDriver();
289
+ const deps = driver.deps;
290
+ return resolveBlockedCommand(deps, input, async (runId, action, output) => {
291
+ const run = await deps.orchestration.getOrchestrationRun(runId);
292
+ if (!run) {
293
+ return { ok: false, code: 'VICT_ORCH_UNKNOWN_RUN', message: 'Run not found.' };
294
+ }
295
+ let graph;
296
+ try {
297
+ graph = await driver.resolveGraphForDriver(run.activationVersion);
298
+ }
299
+ catch (error) {
300
+ return {
301
+ ok: false,
302
+ code: 'VICT_ORCH_ACTIVATION_UNAVAILABLE',
303
+ message: error instanceof VictRuntimeError
304
+ ? error.message
305
+ : 'The exact pinned activation could not be resolved.',
306
+ };
307
+ }
308
+ const envelopeIdentity = {
309
+ runId: run.runId,
310
+ graphId: run.graphId,
311
+ graphVersion: run.graphVersion,
312
+ capabilitySetVersion: run.capabilitySetVersion,
313
+ activationVersion: run.activationVersion,
314
+ };
315
+ const now = deps.clock.now();
316
+ const VictErr = (code, message) => ({
317
+ code,
318
+ message,
319
+ retryable: false,
320
+ });
321
+ if (action === 'cancel') {
322
+ return {
323
+ ok: true,
324
+ command: {
325
+ runId,
326
+ action: 'cancel',
327
+ reasonCode: input.reasonCode,
328
+ },
329
+ events: [
330
+ {
331
+ type: 'run.cancelled',
332
+ requestId: input.resolutionId,
333
+ reasonCode: input.reasonCode,
334
+ steps: run.steps,
335
+ ...envelopeIdentity,
336
+ timestamp: now,
337
+ },
338
+ ],
339
+ };
340
+ }
341
+ if (action === 'fail') {
342
+ return {
343
+ ok: true,
344
+ command: {
345
+ runId,
346
+ action: 'fail',
347
+ reasonCode: input.reasonCode,
348
+ failCode: input.failCode,
349
+ },
350
+ events: [
351
+ {
352
+ type: 'run.failed',
353
+ steps: run.steps,
354
+ error: VictErr(input.failCode ?? 'VICT_ORCH_OPERATOR_FAILED', 'The run was failed by an authorized operator resolution.'),
355
+ ...envelopeIdentity,
356
+ timestamp: now,
357
+ },
358
+ ],
359
+ };
360
+ }
361
+ const snapshot = await deps.orchestration.getOrchestrationSnapshot(runId);
362
+ const blockedToken = snapshot?.tokens.find((token) => token.status === 'blocked');
363
+ if (!blockedToken) {
364
+ return {
365
+ ok: false,
366
+ code: 'VICT_ORCH_NOT_BLOCKED',
367
+ message: 'The run has no blocked token.',
368
+ };
369
+ }
370
+ const node = graph.getNode(blockedToken.nodeId);
371
+ if (input.action === 'retry') {
372
+ if (node?.retry === undefined) {
373
+ return {
374
+ ok: false,
375
+ code: 'VICT_ORCH_OPERATOR_DENIED',
376
+ message: 'Retry is only permitted for work that declares a retry policy.',
377
+ };
378
+ }
379
+ return {
380
+ ok: true,
381
+ command: { runId, action: 'retry', reasonCode: input.reasonCode },
382
+ events: [],
383
+ };
384
+ }
385
+ // confirm_applied: the output must pass the ORIGINAL pinned output contract.
386
+ if (node?.outputContractId === undefined) {
387
+ return {
388
+ ok: false,
389
+ code: 'VICT_ORCH_OPERATOR_CONFLICT',
390
+ message: 'Confirm-applied requires the blocked node to declare an output contract.',
391
+ };
392
+ }
393
+ const bindings = await driver.resolveBindingsForDriver(run.activationVersion);
394
+ const contract = bindings.contracts.get(node.outputContractId);
395
+ if (!contract) {
396
+ return {
397
+ ok: false,
398
+ code: 'VICT_ORCH_ACTIVATION_UNAVAILABLE',
399
+ message: `Contract '${node.outputContractId}' is not resolvable from the pinned activation.`,
400
+ };
401
+ }
402
+ let parsed;
403
+ try {
404
+ parsed = contract.parse(output);
405
+ }
406
+ catch {
407
+ return {
408
+ ok: false,
409
+ code: 'VICT_RUNTIME_CONTRACT_PARSER_THREW',
410
+ message: `The confirmed output could not be validated by contract '${node.outputContractId}': the contract parser threw; confirm-applied cannot bypass validation.`,
411
+ };
412
+ }
413
+ if (!parsed.ok) {
414
+ return {
415
+ ok: false,
416
+ code: 'VICT_ORCH_SIGNAL_CONTRACT_REJECTED',
417
+ message: `The confirmed output was rejected by contract '${node.outputContractId}'; confirm-applied cannot bypass validation.`,
418
+ };
419
+ }
420
+ const successTarget = graph.successTargetOf(node.id);
421
+ const invocationId = deriveInvocationId({
422
+ runId,
423
+ activationVersion: run.activationVersion,
424
+ lineage: blockedToken.lineage,
425
+ nodeId: node.id,
426
+ });
427
+ if (successTarget === undefined) {
428
+ return {
429
+ ok: true,
430
+ command: {
431
+ runId,
432
+ action: 'confirm_applied',
433
+ reasonCode: input.reasonCode,
434
+ continuation: {
435
+ kind: 'complete',
436
+ outputSummary: await import('@victframework/kernel').then((k) => k.summarizeOutput(output)),
437
+ output,
438
+ },
439
+ checkpoint: undefined,
440
+ output,
441
+ failCode: undefined,
442
+ },
443
+ events: [],
444
+ };
445
+ }
446
+ return {
447
+ ok: true,
448
+ command: {
449
+ runId,
450
+ action: 'confirm_applied',
451
+ reasonCode: input.reasonCode,
452
+ continuation: {
453
+ kind: 'advance',
454
+ toNodeId: successTarget,
455
+ payload: parsed.value,
456
+ },
457
+ checkpoint: { tokenId: blockedToken.tokenId, payload: parsed.value },
458
+ output,
459
+ failCode: undefined,
460
+ idempotencyKey: deriveIdempotencyKey({
461
+ runId,
462
+ activationVersion: run.activationVersion,
463
+ lineage: blockedToken.lineage,
464
+ nodeId: node.id,
465
+ invocationId,
466
+ }),
467
+ },
468
+ events: [],
469
+ };
470
+ });
471
+ }
472
+ registerCapability(definition) {
473
+ this.#registry.registerCapability(definition);
474
+ return this;
475
+ }
476
+ /**
477
+ * Install a batch atomically (used by `installCapabilityPack`). Stages
478
+ * every contract/capability/double and commits only on full success.
479
+ */
480
+ installCapabilityPackBatch(install) {
481
+ this.#registry.installBatch(install);
482
+ }
483
+ registerContract(contract) {
484
+ this.#registry.registerContract(contract);
485
+ return this;
486
+ }
487
+ /** Register a test double. Duplicate registration is rejected; use `replaceDouble`. */
488
+ registerDouble(capabilityId, invoke, options = {}) {
489
+ this.#registry.registerDouble(capabilityId, invoke, options);
490
+ return this;
491
+ }
492
+ /** Explicitly replace an existing test double. Later runs use the replacement; in-flight runs do not. */
493
+ replaceDouble(capabilityId, invoke) {
494
+ this.#registry.replaceDouble(capabilityId, invoke);
495
+ return this;
496
+ }
497
+ /** True when a test double is registered for the capability. */
498
+ hasDouble(capabilityId) {
499
+ return this.#registry.hasDouble(capabilityId);
500
+ }
501
+ /** The eligible modes of a registered test double (undefined when none is registered). */
502
+ getDoubleModes(capabilityId) {
503
+ return this.#registry.getDoubleModes(capabilityId);
504
+ }
505
+ /**
506
+ * Compile and activate a graph atomically from the caller's perspective:
507
+ *
508
+ * 1. compile and resolve the candidate completely;
509
+ * 2. snapshot capability and captured contract parsing semantics;
510
+ * 3. build and validate the serializable manifest;
511
+ * 4. publish and select it in the durable catalog (one transaction);
512
+ * 5. only then replace the active in-memory snapshot.
513
+ *
514
+ * A compile failure returns a structured rejection; a storage failure
515
+ * throws a structured store error. In both cases the previously active
516
+ * graph remains selected and runnable, and durable state never claims an
517
+ * activation the catalog did not select.
518
+ */
519
+ async activate(definition) {
520
+ const result = compileGraph({
521
+ definition,
522
+ capabilities: this.#registry.capabilityIndex(),
523
+ contracts: this.#registry.contractEnvironment(),
524
+ });
525
+ if (!result.ok) {
526
+ return {
527
+ ok: false,
528
+ issues: result.issues,
529
+ previousGraph: this.activeGraph(),
530
+ };
531
+ }
532
+ const snapshot = this.#captureSnapshot(result.graph);
533
+ const manifest = this.#buildManifest(result.graph);
534
+ const canonicalManifest = toCanonicalJson(manifest);
535
+ await this.#stores.catalog.publishAndSelect({
536
+ publish: { manifest, canonicalManifest },
537
+ select: { graphId: result.graph.id },
538
+ });
539
+ this.#active = snapshot;
540
+ return {
541
+ ok: true,
542
+ graphId: result.graph.id,
543
+ graphVersion: result.graph.graphVersion,
544
+ capabilitySetVersion: result.graph.capabilitySetVersion,
545
+ activationVersion: result.graph.activationVersion,
546
+ nodeCount: result.graph.nodeCount,
547
+ };
548
+ }
549
+ /**
550
+ * Restore the exact durable activation for a graph definition.
551
+ *
552
+ * The current registered code (capabilities and contracts) must reproduce
553
+ * the stored activation exactly: the definition is recompiled against the
554
+ * live registry, all three version identities are recomputed, and the
555
+ * rebuilt canonical manifest is compared with the stored one. On an exact
556
+ * match the activation becomes the active in-memory snapshot. On any
557
+ * mismatch the stored manifest is preserved, the currently active graph is
558
+ * left unchanged, no capability is executed, and no “closest” revision is
559
+ * chosen.
560
+ *
561
+ * By default the activation currently selected for `definition.id` is
562
+ * restored; pass `activationVersion` to restore a specific one.
563
+ */
564
+ async restoreActivation(definition, options = {}) {
565
+ let stored;
566
+ if (options.activationVersion !== undefined) {
567
+ stored = await this.#stores.catalog.get(options.activationVersion);
568
+ if (!stored) {
569
+ return {
570
+ ok: false,
571
+ code: 'VICT_RUNTIME_ACTIVATION_NOT_FOUND',
572
+ message: `No stored activation '${options.activationVersion}' exists in the catalog.`,
573
+ expectedActivationVersion: options.activationVersion,
574
+ };
575
+ }
576
+ }
577
+ else {
578
+ stored = await this.#stores.catalog.getSelected(definition.id);
579
+ if (!stored) {
580
+ return {
581
+ ok: false,
582
+ code: 'VICT_RUNTIME_ACTIVATION_NOT_FOUND',
583
+ message: `No activation is selected for graph '${definition.id}' in the catalog.`,
584
+ };
585
+ }
586
+ }
587
+ let manifest;
588
+ try {
589
+ manifest = parseManifest(stored);
590
+ }
591
+ catch {
592
+ return {
593
+ ok: false,
594
+ code: 'VICT_RUNTIME_ACTIVATION_UNAVAILABLE',
595
+ message: 'The stored activation manifest is corrupt and cannot be read.',
596
+ expectedActivationVersion: stored.activationVersion,
597
+ };
598
+ }
599
+ const compiled = compileGraph({
600
+ definition,
601
+ capabilities: this.#registry.capabilityIndex(),
602
+ contracts: this.#registry.contractEnvironment(),
603
+ });
604
+ if (!compiled.ok) {
605
+ return {
606
+ ok: false,
607
+ code: 'VICT_RUNTIME_ACTIVATION_UNAVAILABLE',
608
+ message: 'The current registered code cannot compile the graph; required capabilities or contracts are missing or invalid.',
609
+ expectedActivationVersion: stored.activationVersion,
610
+ issues: compiled.issues,
611
+ };
612
+ }
613
+ const rebuilt = this.#buildManifest(compiled.graph);
614
+ const rebuiltCanonical = toCanonicalJson(rebuilt);
615
+ if (rebuilt.activationVersion !== stored.activationVersion ||
616
+ rebuiltCanonical !== stored.canonicalManifest) {
617
+ return {
618
+ ok: false,
619
+ code: 'VICT_RUNTIME_ACTIVATION_MISMATCH',
620
+ message: 'The current registered code does not reproduce the stored activation. No capability was executed and the active graph is unchanged.',
621
+ expectedActivationVersion: stored.activationVersion,
622
+ actualActivationVersion: rebuilt.activationVersion,
623
+ differences: describeManifestDifferences(manifest, rebuilt),
624
+ };
625
+ }
626
+ const snapshot = this.#captureSnapshot(compiled.graph);
627
+ this.#active = snapshot;
628
+ return {
629
+ ok: true,
630
+ graphId: compiled.graph.id,
631
+ graphVersion: compiled.graph.graphVersion,
632
+ capabilitySetVersion: compiled.graph.capabilitySetVersion,
633
+ activationVersion: compiled.graph.activationVersion,
634
+ nodeCount: compiled.graph.nodeCount,
635
+ };
636
+ }
637
+ /**
638
+ * Explicit boot-time recovery (single local owner). Finds runs left in a
639
+ * nonterminal running state by a previous process, atomically transitions
640
+ * each to `blocked`, and appends one safe interruption event. Never
641
+ * invokes or replays a capability. Repeated recovery is idempotent.
642
+ */
643
+ async recoverInterruptedRuns() {
644
+ return this.#stores.execution.recoverInterruptedRuns({
645
+ code: RECOVERY_CODE,
646
+ reason: RECOVERY_REASON,
647
+ remediation: RECOVERY_REMEDIATION,
648
+ timestamp: this.#clock.now(),
649
+ });
650
+ }
651
+ activeGraph() {
652
+ const active = this.#active;
653
+ if (!active) {
654
+ return undefined;
655
+ }
656
+ return {
657
+ id: active.graph.id,
658
+ version: active.graph.graphVersion,
659
+ capabilitySetVersion: active.graph.capabilitySetVersion,
660
+ activationVersion: active.graph.activationVersion,
661
+ entryNodeId: active.graph.entryNodeId,
662
+ nodeCount: active.graph.nodeCount,
663
+ };
664
+ }
665
+ /**
666
+ * Execute the active graph. Capability-only graphs keep the verified
667
+ * Stage 02 sequential engine; graphs with control nodes run on the Stage
668
+ * 03 durable orchestration driver (token/attempt state machine). Run
669
+ * transitions and events are committed to the store atomically as the
670
+ * run progresses; when `run()` resolves, the durable record is current.
671
+ */
672
+ async run(input, options = {}) {
673
+ const snapshot = this.#requireActive();
674
+ if (snapshot.graph.hasControlNodes) {
675
+ return (await this.#orchestrationDriver().startRun(snapshot.graph, input, options.mode ?? 'normal', this.#ids.runId(), {
676
+ concurrency: options.concurrency,
677
+ onEvent: options.onEvent,
678
+ ...(options.policy === undefined ? {} : { policy: options.policy }),
679
+ }));
680
+ }
681
+ const mode = options.mode ?? 'normal';
682
+ const overrides = options.policy ?? this.#defaultOverrides;
683
+ const doubles = this.#registry.snapshotDoubles();
684
+ const runId = this.#ids.runId();
685
+ const tracker = new DurableRunTracker(this.#stores.execution, {
686
+ runId,
687
+ graphId: snapshot.graph.id,
688
+ graphVersion: snapshot.graph.graphVersion,
689
+ capabilitySetVersion: snapshot.graph.capabilitySetVersion,
690
+ activationVersion: snapshot.graph.activationVersion,
691
+ mode,
692
+ retention: this.#retention,
693
+ entryNodeId: snapshot.graph.entryNodeId,
694
+ });
695
+ const onEvent = (event) => {
696
+ tracker.onEvent(event);
697
+ options.onEvent?.(event);
698
+ };
699
+ let output;
700
+ try {
701
+ output = await executeGraph({
702
+ graph: snapshot.graph,
703
+ input,
704
+ mode,
705
+ maxSteps: options.maxSteps ?? this.#defaultMaxSteps,
706
+ ports: this.#buildPorts(snapshot, doubles, overrides, onEvent, runId,
707
+ // Durable write-ahead boundary: no capability is invoked before
708
+ // its durable intent (and everything before it) has committed.
709
+ () => tracker.awaitDurableBoundary()),
710
+ });
711
+ }
712
+ catch (error) {
713
+ // Kernel execution failed (e.g. a storage fail-fast): settle the
714
+ // durable queue, then surface the storage failure or the original error.
715
+ return await tracker.settle(error);
716
+ }
717
+ await tracker.finish(output);
718
+ return toRunResult(output);
719
+ }
720
+ /**
721
+ * Execute a single node of the active graph in isolation (mode forced to
722
+ * `'test'`). The isolated compile resolves against the activation snapshot,
723
+ * so post-activation registry changes cannot affect it. Does not traverse
724
+ * edges, does not change the active graph, and does not write durable run
725
+ * records. The trace is returned directly.
726
+ */
727
+ async runNode(nodeId, input, options = {}) {
728
+ const snapshot = this.#requireActive();
729
+ const node = snapshot.graph.getNode(nodeId);
730
+ if (!node) {
731
+ throw new VictRuntimeError('VICT_RUNTIME_UNKNOWN_NODE', `Active graph '${snapshot.graph.id}' has no node '${nodeId}'.`);
732
+ }
733
+ const isolatedDefinition = {
734
+ id: `${snapshot.graph.id}#isolated:${nodeId}`,
735
+ entry: nodeId,
736
+ nodes: [
737
+ {
738
+ id: node.id,
739
+ capability: node.capability,
740
+ input: node.inputContractId,
741
+ output: node.outputContractId,
742
+ },
743
+ ],
744
+ edges: [],
745
+ };
746
+ const isolated = compileGraph({
747
+ definition: isolatedDefinition,
748
+ capabilities: snapshot.descriptors,
749
+ contracts: snapshot.contractEnvironment,
750
+ });
751
+ if (!isolated.ok) {
752
+ throw new VictRuntimeError('VICT_RUNTIME_UNKNOWN_NODE', `Isolated node graph for '${nodeId}' failed to compile: ${isolated.issues.map((issue) => issue.message).join(' ')}`);
753
+ }
754
+ // Isolated tests always run in 'test' mode with no policy overrides and
755
+ // no durable records, so no invocation boundary applies here.
756
+ // Irreversible effects never run their real implementation here: a
757
+ // registered safe double may run, and without one the node is blocked.
758
+ const doubles = this.#registry.snapshotDoubles();
759
+ const output = await executeGraph({
760
+ graph: isolated.graph,
761
+ input,
762
+ mode: 'test',
763
+ maxSteps: options.maxSteps ?? this.#defaultMaxSteps,
764
+ ports: this.#buildPorts(snapshot, doubles, {}, options.onEvent, undefined),
765
+ });
766
+ return toRunResult(output);
767
+ }
768
+ /** All stored runs (assembled views with their traces), oldest first. */
769
+ async listRuns() {
770
+ const runs = await this.#stores.execution.listRuns();
771
+ const records = [];
772
+ for (const run of runs) {
773
+ records.push(await this.#composeRunRecord(run));
774
+ }
775
+ return deepFreeze(records);
776
+ }
777
+ /** One stored run (assembled view with its trace), or undefined. */
778
+ async getRun(runId) {
779
+ const run = await this.#stores.execution.getRun(runId);
780
+ if (!run) {
781
+ return undefined;
782
+ }
783
+ return deepFreeze(await this.#composeRunRecord(run));
784
+ }
785
+ async #composeRunRecord(run) {
786
+ const events = await this.#stores.execution.listEvents(run.runId);
787
+ const trace = events.map((event) => storedEventToKernelEvent(event));
788
+ const first = trace[0];
789
+ const last = trace.at(-1);
790
+ const record = {
791
+ runId: run.runId,
792
+ graphId: run.graphId,
793
+ graphVersion: run.graphVersion,
794
+ capabilitySetVersion: run.capabilitySetVersion,
795
+ activationVersion: run.activationVersion,
796
+ mode: run.mode,
797
+ status: run.status,
798
+ startedAt: first?.timestamp ?? run.createdAt,
799
+ durationMs: first && last ? Math.max(0, last.timestamp - first.timestamp) : 0,
800
+ steps: run.steps,
801
+ retention: run.retention,
802
+ currentNodeId: run.currentNodeId,
803
+ recordRevision: run.recordRevision,
804
+ trace,
805
+ ...(run.outputSummary !== undefined ? { outputSummary: run.outputSummary } : {}),
806
+ ...(run.output !== undefined ? { output: run.output } : {}),
807
+ ...(run.error !== undefined ? { error: run.error } : {}),
808
+ };
809
+ return record;
810
+ }
811
+ #requireActive() {
812
+ const active = this.#active;
813
+ if (!active) {
814
+ throw new VictRuntimeError('VICT_RUNTIME_NO_ACTIVE_GRAPH', 'No active graph. Compile and activate a graph with runtime.activate(definition) first.');
815
+ }
816
+ return active;
817
+ }
818
+ /**
819
+ * Capture the immutable snapshot for a freshly compiled graph: frozen
820
+ * copies of the execution-relevant capability bindings and captured
821
+ * (bound) contract parsing handles the graph requires. Deliberately
822
+ * narrow — the snapshot contains only what the activated graph needs, so
823
+ * later registrations are invisible to it.
824
+ */
825
+ #captureSnapshot(graph) {
826
+ const bindings = new Map();
827
+ const contracts = new Map();
828
+ const requireContract = (contractId) => {
829
+ const existing = contracts.get(contractId);
830
+ if (existing) {
831
+ return existing;
832
+ }
833
+ const live = this.#registry.getContract(contractId);
834
+ if (!live) {
835
+ throw new VictRuntimeError('VICT_RUNTIME_UNKNOWN_NODE', `Activation snapshot could not resolve contract '${contractId}' required by graph '${graph.id}'.`);
836
+ }
837
+ // Capture the parse callable BY VALUE (bound now): replacing
838
+ // `contract.parse` on the caller-owned object later cannot change what
839
+ // this activation executes.
840
+ const captured = {
841
+ id: live.id,
842
+ revision: live.revision,
843
+ expected: live.expected,
844
+ parse: live.parse.bind(live),
845
+ };
846
+ contracts.set(contractId, Object.freeze(captured));
847
+ return captured;
848
+ };
849
+ for (const nodeId of graph.nodeIds) {
850
+ const node = graph.getNode(nodeId);
851
+ if (!node || (node.kind !== 'capability' && node.kind !== 'decision')) {
852
+ continue; // control nodes carry no capability binding
853
+ }
854
+ const live = this.#registry.getCapability(node.capability);
855
+ if (!live) {
856
+ throw new VictRuntimeError('VICT_RUNTIME_CAPABILITY_MISSING', `Activation snapshot could not resolve capability '${node.capability}' required by graph '${graph.id}'.`);
857
+ }
858
+ if (!bindings.has(node.capability)) {
859
+ bindings.set(node.capability, Object.freeze({
860
+ id: live.id,
861
+ revision: live.revision,
862
+ effect: live.effect,
863
+ invoke: live.invoke,
864
+ inputContractId: live.input?.id,
865
+ inputRevision: live.input?.revision,
866
+ outputContractId: live.output?.id,
867
+ outputRevision: live.output?.revision,
868
+ ...(descriptorAuthorityOf(live) !== undefined
869
+ ? { authority: descriptorAuthorityOf(live) }
870
+ : {}),
871
+ }));
872
+ }
873
+ if (live.input !== undefined) {
874
+ requireContract(live.input.id);
875
+ }
876
+ if (live.output !== undefined) {
877
+ requireContract(live.output.id);
878
+ }
879
+ if (node.inputContractId !== undefined) {
880
+ requireContract(node.inputContractId);
881
+ }
882
+ if (node.outputContractId !== undefined) {
883
+ requireContract(node.outputContractId);
884
+ }
885
+ }
886
+ const frozenBindings = new Map(bindings);
887
+ const frozenContracts = new Map(contracts);
888
+ const descriptors = {
889
+ getCapabilityDescriptor: (capabilityId) => {
890
+ const binding = frozenBindings.get(capabilityId);
891
+ if (!binding) {
892
+ return undefined;
893
+ }
894
+ return {
895
+ id: binding.id,
896
+ revision: binding.revision,
897
+ effect: binding.effect,
898
+ inputContractId: binding.inputContractId,
899
+ inputRevision: binding.inputRevision,
900
+ outputContractId: binding.outputContractId,
901
+ outputRevision: binding.outputRevision,
902
+ ...(binding.idempotency !== undefined ? { idempotency: binding.idempotency } : {}),
903
+ ...(binding.authority !== undefined ? { authority: binding.authority } : {}),
904
+ };
905
+ },
906
+ };
907
+ const contractEnvironment = {
908
+ has: (contractId) => frozenContracts.has(contractId),
909
+ isCompatible: (from, to) => from === undefined ||
910
+ to === undefined ||
911
+ from === to ||
912
+ from === NEUTRAL_JSON_CONTRACT_ID ||
913
+ to === NEUTRAL_JSON_CONTRACT_ID,
914
+ // Hand the kernel a frozen object whose parse is the captured callable.
915
+ get: (contractId) => {
916
+ const captured = frozenContracts.get(contractId);
917
+ if (!captured) {
918
+ return undefined;
919
+ }
920
+ const view = Object.freeze({
921
+ id: captured.id,
922
+ revision: captured.revision,
923
+ expected: captured.expected,
924
+ parse: (input) => captured.parse(input),
925
+ });
926
+ return view;
927
+ },
928
+ };
929
+ return Object.freeze({
930
+ graph,
931
+ bindings: frozenBindings,
932
+ contracts: frozenContracts,
933
+ descriptors,
934
+ contractEnvironment,
935
+ });
936
+ }
937
+ /**
938
+ * Build the serializable activation manifest for a compiled graph from the
939
+ * current registry. Contains only serializable meaning; canonical form is
940
+ * stable across processes (registration order, timestamps and functions
941
+ * never enter it).
942
+ */
943
+ #buildManifest(graph) {
944
+ const bindings = [];
945
+ const contractById = new Map();
946
+ const recordContract = (contractId) => {
947
+ if (contractId === undefined) {
948
+ return;
949
+ }
950
+ const live = this.#registry.getContract(contractId);
951
+ if (!live) {
952
+ throw new VictRuntimeError('VICT_RUNTIME_UNKNOWN_NODE', `Activation manifest could not resolve contract '${contractId}' required by graph '${graph.id}'.`);
953
+ }
954
+ contractById.set(live.id, { id: live.id, revision: live.revision });
955
+ };
956
+ for (const nodeId of graph.nodeIds) {
957
+ const node = graph.getNode(nodeId);
958
+ if (!node || (node.kind !== 'capability' && node.kind !== 'decision')) {
959
+ continue; // control nodes carry no capability binding
960
+ }
961
+ const live = this.#registry.getCapability(node.capability);
962
+ if (!live) {
963
+ throw new VictRuntimeError('VICT_RUNTIME_CAPABILITY_MISSING', `Activation manifest could not resolve capability '${node.capability}' required by graph '${graph.id}'.`);
964
+ }
965
+ const inputId = node.inputContractId ?? live.input?.id;
966
+ const outputId = node.outputContractId ?? live.output?.id;
967
+ const inputRevision = node.inputContractId
968
+ ? this.#registry.getContract(node.inputContractId)?.revision
969
+ : live.input?.revision;
970
+ const outputRevision = node.outputContractId
971
+ ? this.#registry.getContract(node.outputContractId)?.revision
972
+ : live.output?.revision;
973
+ bindings.push({
974
+ capability: live.id,
975
+ revision: live.revision,
976
+ effect: live.effect,
977
+ input: inputId === undefined ? null : { id: inputId, revision: inputRevision ?? 'unknown' },
978
+ output: outputId === undefined ? null : { id: outputId, revision: outputRevision ?? 'unknown' },
979
+ ...(live.idempotency === undefined ? {} : { idempotency: live.idempotency }),
980
+ ...(descriptorAuthorityOf(live) !== undefined
981
+ ? { authority: descriptorAuthorityOf(live) }
982
+ : {}),
983
+ });
984
+ recordContract(live.input?.id);
985
+ recordContract(live.output?.id);
986
+ recordContract(node.inputContractId);
987
+ recordContract(node.outputContractId);
988
+ }
989
+ const dedupedBindings = dedupeCanonical(bindings);
990
+ // Stage 03: control-node metadata for v2 manifests (control graphs only).
991
+ const waits = [];
992
+ const forks = [];
993
+ const joins = [];
994
+ for (const nodeId of graph.nodeIds) {
995
+ const node = graph.getNode(nodeId);
996
+ if (!node) {
997
+ continue;
998
+ }
999
+ if (node.kind === 'wait' && node.wait !== undefined) {
1000
+ const wait = node.wait;
1001
+ waits.push(wait.kind === 'signal'
1002
+ ? {
1003
+ nodeId,
1004
+ kind: 'signal',
1005
+ name: wait.name,
1006
+ contract: wait.contract === undefined
1007
+ ? null
1008
+ : {
1009
+ id: wait.contract,
1010
+ revision: this.#registry.getContract(wait.contract)?.revision ?? 'unknown',
1011
+ },
1012
+ ...(wait.timeoutMs !== undefined ? { timeoutMs: wait.timeoutMs } : {}),
1013
+ }
1014
+ : { nodeId, kind: 'timer', delayMs: wait.delayMs });
1015
+ if (wait.kind === 'signal' && wait.contract !== undefined) {
1016
+ recordContract(wait.contract);
1017
+ }
1018
+ }
1019
+ if (node.kind === 'fork' && node.join !== undefined) {
1020
+ forks.push({
1021
+ forkId: nodeId,
1022
+ joinId: node.join,
1023
+ branchKeys: graph.branchKeysOf(nodeId),
1024
+ maxConcurrency: node.maxConcurrency ?? null,
1025
+ });
1026
+ }
1027
+ if (node.kind === 'join') {
1028
+ const outputId = node.outputContractId;
1029
+ joins.push({
1030
+ joinId: nodeId,
1031
+ forkId: node.fork ?? '',
1032
+ outputContract: outputId === undefined
1033
+ ? null
1034
+ : {
1035
+ id: outputId,
1036
+ revision: this.#registry.getContract(outputId)?.revision ?? 'unknown',
1037
+ },
1038
+ });
1039
+ if (outputId !== undefined) {
1040
+ recordContract(outputId);
1041
+ }
1042
+ }
1043
+ }
1044
+ // Computed AFTER every recordContract call (capability bindings, wait
1045
+ // signal contracts, and join output contracts) so the pinned contract
1046
+ // environment is complete.
1047
+ const contracts = [...contractById.values()].sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
1048
+ const isControl = graph.hasControlNodes;
1049
+ return {
1050
+ manifestSchema: isControl ? ACTIVATION_MANIFEST_SCHEMA_V2 : ACTIVATION_MANIFEST_SCHEMA,
1051
+ graphId: graph.id,
1052
+ graph: graph.toDefinition(),
1053
+ graphVersion: graph.graphVersion,
1054
+ capabilitySetVersion: graph.capabilitySetVersion,
1055
+ activationVersion: graph.activationVersion,
1056
+ bindings: dedupedBindings,
1057
+ contracts,
1058
+ ...(isControl ? { waits, forks, joins } : {}),
1059
+ };
1060
+ }
1061
+ #buildPorts(snapshot, doubles, overrides, onEvent, runId, beforeInvoke) {
1062
+ const bindings = snapshot.bindings;
1063
+ const contracts = snapshot.contractEnvironment;
1064
+ return {
1065
+ descriptors: snapshot.descriptors,
1066
+ contracts,
1067
+ onEvent,
1068
+ ...(beforeInvoke !== undefined ? { beforeInvoke } : {}),
1069
+ clock: this.#clock,
1070
+ ids: {
1071
+ runId: runId !== undefined ? () => runId : () => this.#ids.runId(),
1072
+ errorId: this.#ids.errorId
1073
+ ? () => this.#ids.errorId?.() ?? `err_${randomId()}`
1074
+ : undefined,
1075
+ },
1076
+ policy: {
1077
+ authorize(request) {
1078
+ const decision = decideEffectAuthorization(request, overrides);
1079
+ if (decision.allowed && decision.useDouble) {
1080
+ const registered = doubles.get(request.capabilityId);
1081
+ if (registered === undefined) {
1082
+ return {
1083
+ allowed: false,
1084
+ useDouble: false,
1085
+ reason: `Effect class '${request.effect}' requires a registered test double in '${request.mode}' mode and none is registered.`,
1086
+ remediation: `Register a test double for capability '${request.capabilityId}' with runtime.registerDouble().`,
1087
+ };
1088
+ }
1089
+ // A pack-declared double is eligible ONLY in its declared modes.
1090
+ // A double declared only for 'test' never runs in 'simulate', and
1091
+ // doubles never run in 'normal' mode (the policy never asks).
1092
+ if (!registered.modes.has(request.mode)) {
1093
+ return {
1094
+ allowed: false,
1095
+ useDouble: false,
1096
+ reason: `The registered test double for capability '${request.capabilityId}' is not eligible in '${request.mode}' mode (eligible modes: ${[...registered.modes].sort().join(', ')}).`,
1097
+ remediation: `Register a double eligible in '${request.mode}' mode, or run in a mode the double declares.`,
1098
+ };
1099
+ }
1100
+ }
1101
+ return decision;
1102
+ },
1103
+ },
1104
+ capabilities: {
1105
+ async invoke(capabilityId, input, context) {
1106
+ const useDouble = context.useDouble;
1107
+ const double = useDouble ? doubles.get(capabilityId)?.invoke : undefined;
1108
+ const binding = useDouble ? undefined : bindings.get(capabilityId);
1109
+ const invocationContext = {
1110
+ runId: context.runId,
1111
+ graphId: context.graphId,
1112
+ graphVersion: context.graphVersion,
1113
+ capabilitySetVersion: context.capabilitySetVersion,
1114
+ activationVersion: context.activationVersion,
1115
+ nodeId: context.nodeId,
1116
+ capabilityId,
1117
+ mode: context.mode,
1118
+ step: context.step,
1119
+ invokedVia: useDouble ? 'double' : 'real',
1120
+ };
1121
+ // Thrown messages are untrusted content: they are never copied into
1122
+ // the structured error, only a safe type name and a correlation id.
1123
+ const failureFor = (target) => {
1124
+ return {
1125
+ ok: false,
1126
+ error: runtimeError('VICT_RUNTIME_CAPABILITY_MISSING', `No ${target} available for capability '${capabilityId}' in the pinned activation snapshot.`, { capabilityId, nodeId: context.nodeId }),
1127
+ };
1128
+ };
1129
+ if (useDouble) {
1130
+ if (!double) {
1131
+ return failureFor('test double');
1132
+ }
1133
+ try {
1134
+ return { ok: true, value: await double(input, invocationContext) };
1135
+ }
1136
+ catch (cause) {
1137
+ const sanitised = sanitiseThrownError(cause);
1138
+ return {
1139
+ ok: false,
1140
+ error: runtimeError('VICT_RUNTIME_CAPABILITY_THREW', `Test double for '${capabilityId}' threw during invocation; the thrown message is not retained.`, {
1141
+ capabilityId,
1142
+ nodeId: context.nodeId,
1143
+ invokedVia: 'double',
1144
+ ...sanitised,
1145
+ }),
1146
+ };
1147
+ }
1148
+ }
1149
+ if (binding) {
1150
+ try {
1151
+ return {
1152
+ ok: true,
1153
+ value: await binding.invoke(input, invocationContext),
1154
+ };
1155
+ }
1156
+ catch (cause) {
1157
+ // Stage 04: structured authority failures (permission denied,
1158
+ // secret/configuration unavailable) keep their stable code;
1159
+ // any other throw is reduced to CAPABILITY_THREW.
1160
+ const classified = classifyInvocationFailure(cause, capabilityId, {
1161
+ nodeId: context.nodeId,
1162
+ invokedVia: 'real',
1163
+ });
1164
+ return {
1165
+ ok: false,
1166
+ error: runtimeError(classified.code, classified.message, classified.details),
1167
+ };
1168
+ }
1169
+ }
1170
+ return failureFor('implementation');
1171
+ },
1172
+ },
1173
+ };
1174
+ }
1175
+ }
1176
+ function dedupeCanonical(items) {
1177
+ const sorted = [...items].sort((a, b) => {
1178
+ const keyA = toCanonicalJson(a);
1179
+ const keyB = toCanonicalJson(b);
1180
+ return keyA < keyB ? -1 : keyA > keyB ? 1 : 0;
1181
+ });
1182
+ const deduped = [];
1183
+ let previous;
1184
+ for (const item of sorted) {
1185
+ const key = toCanonicalJson(item);
1186
+ if (key !== previous) {
1187
+ deduped.push(item);
1188
+ previous = key;
1189
+ }
1190
+ }
1191
+ return deduped;
1192
+ }
1193
+ /** Validate a stored activation's manifest against its row before use. */
1194
+ function parseManifest(stored) {
1195
+ const parsed = parseStoredJson(stored.canonicalManifest, 'activation manifest');
1196
+ if (!parsed ||
1197
+ typeof parsed !== 'object' ||
1198
+ (parsed.manifestSchema !== ACTIVATION_MANIFEST_SCHEMA &&
1199
+ parsed.manifestSchema !== ACTIVATION_MANIFEST_SCHEMA_V2) ||
1200
+ typeof parsed.activationVersion !== 'string' ||
1201
+ typeof parsed.graphId !== 'string' ||
1202
+ typeof parsed.graphVersion !== 'string' ||
1203
+ typeof parsed.capabilitySetVersion !== 'string' ||
1204
+ !Array.isArray(parsed.bindings) ||
1205
+ !Array.isArray(parsed.contracts) ||
1206
+ typeof parsed.graph !== 'object' ||
1207
+ parsed.graph === null) {
1208
+ throw new VictStoreError('VICT_STORE_INVALID_RECORD', 'The stored activation manifest is not a valid manifest for this schema.', { operation: 'catalog.parseManifest', activationVersion: stored.activationVersion });
1209
+ }
1210
+ if (parsed.activationVersion !== stored.activationVersion) {
1211
+ throw new VictStoreError('VICT_STORE_INVALID_RECORD', 'The stored activation manifest does not match its record identity.', {
1212
+ operation: 'catalog.parseManifest',
1213
+ activationVersion: stored.activationVersion,
1214
+ });
1215
+ }
1216
+ if (parsed.graphId !== stored.graphId ||
1217
+ parsed.graphVersion !== stored.graphVersion ||
1218
+ parsed.capabilitySetVersion !== stored.capabilitySetVersion) {
1219
+ throw new VictStoreError('VICT_STORE_INVALID_RECORD', 'The stored activation manifest disagrees with its identity columns.', { operation: 'catalog.parseManifest', activationVersion: stored.activationVersion });
1220
+ }
1221
+ return parsed;
1222
+ }
1223
+ /** Safe, structural description of a manifest mismatch (no payload values involved). */
1224
+ function describeManifestDifferences(stored, rebuilt) {
1225
+ const differences = [];
1226
+ if (stored.graphVersion !== rebuilt.graphVersion) {
1227
+ differences.push(`graphVersion differs: stored ${stored.graphVersion.slice(0, 18)}…, rebuilt ${rebuilt.graphVersion.slice(0, 18)}… (topology or declaration changed)`);
1228
+ }
1229
+ if (stored.capabilitySetVersion !== rebuilt.capabilitySetVersion) {
1230
+ differences.push('capabilitySetVersion differs (capability revisions, effect classes or contract bindings changed)');
1231
+ const storedByCapability = new Map(stored.bindings.map((b) => [b.capability, b]));
1232
+ for (const binding of rebuilt.bindings) {
1233
+ const expected = storedByCapability.get(binding.capability);
1234
+ if (!expected) {
1235
+ differences.push(`capability '${binding.capability}' is not part of the stored activation`);
1236
+ continue;
1237
+ }
1238
+ if (expected.revision !== binding.revision) {
1239
+ differences.push(`capability '${binding.capability}' revision: stored '${expected.revision}', current '${binding.revision}'`);
1240
+ }
1241
+ if (expected.effect !== binding.effect) {
1242
+ differences.push(`capability '${binding.capability}' effect: stored '${expected.effect}', current '${binding.effect}'`);
1243
+ }
1244
+ for (const side of ['input', 'output']) {
1245
+ const expectedContract = expected[side];
1246
+ const actualContract = binding[side];
1247
+ if (expectedContract?.id !== actualContract?.id ||
1248
+ expectedContract?.revision !== actualContract?.revision) {
1249
+ differences.push(`capability '${binding.capability}' ${side} contract: stored ${expectedContract ? `${expectedContract.id}@${expectedContract.revision}` : 'none'}, current ${actualContract ? `${actualContract.id}@${actualContract.revision}` : 'none'}`);
1250
+ }
1251
+ }
1252
+ }
1253
+ }
1254
+ if (JSON.stringify(stored.graph) !== JSON.stringify(rebuilt.graph)) {
1255
+ differences.push('graph declaration differs from the stored activation');
1256
+ }
1257
+ if (differences.length === 0) {
1258
+ differences.push('activationVersion differs for an unclassified reason');
1259
+ }
1260
+ return differences;
1261
+ }
1262
+ /** Validate a stored event row and rebuild the typed kernel event. */
1263
+ function storedEventToKernelEvent(event) {
1264
+ if (event.eventSchema !== RUN_EVENT_SCHEMA) {
1265
+ throw new VictStoreError('VICT_STORE_INVALID_RECORD', 'A stored event carries an unsupported event schema version.', { operation: 'execution.readEvent', runId: event.runId });
1266
+ }
1267
+ const parsed = parseStoredJson(event.payload, 'run event');
1268
+ if (!parsed ||
1269
+ typeof parsed !== 'object' ||
1270
+ parsed.seq !== event.seq ||
1271
+ parsed.type !== event.type ||
1272
+ parsed.runId !== event.runId ||
1273
+ parsed.graphId !== event.graphId ||
1274
+ parsed.graphVersion !== event.graphVersion ||
1275
+ parsed.capabilitySetVersion !== event.capabilitySetVersion ||
1276
+ parsed.activationVersion !== event.activationVersion ||
1277
+ typeof parsed.timestamp !== 'number' ||
1278
+ !Number.isFinite(parsed.timestamp)) {
1279
+ throw new VictStoreError('VICT_STORE_INVALID_RECORD', 'A stored event payload disagrees with its columns.', { operation: 'execution.readEvent', runId: event.runId });
1280
+ }
1281
+ return parsed;
1282
+ }
1283
+ function toRunResult(output) {
1284
+ const result = {
1285
+ runId: output.runId,
1286
+ graphId: output.graphId,
1287
+ graphVersion: output.graphVersion,
1288
+ capabilitySetVersion: output.capabilitySetVersion,
1289
+ activationVersion: output.activationVersion,
1290
+ status: output.status,
1291
+ trace: output.events,
1292
+ };
1293
+ if (output.output !== undefined) {
1294
+ result.output = output.output;
1295
+ }
1296
+ if (output.error !== undefined) {
1297
+ result.error = output.error;
1298
+ }
1299
+ return result;
1300
+ }
1301
+ function randomId() {
1302
+ const bytes = new Uint8Array(8);
1303
+ globalThis.crypto.getRandomValues(bytes);
1304
+ return Array.from(bytes, (byte) => byte.toString(16).padStart(2, '0')).join('');
1305
+ }
1306
+ /** Create a runtime instance. Application code should use the `@victframework/sdk` facade instead. */
1307
+ export function createRuntime(options = {}) {
1308
+ return new VictRuntime(options);
1309
+ }
1310
+ /**
1311
+ * Snapshot the execution-affecting authority declarations of a live
1312
+ * capability definition (declared names only — never resolved values and
1313
+ * never runtime grants). Used by activation snapshots so runNode's isolated
1314
+ * compile reproduces the exact capability-set identity.
1315
+ */
1316
+ function descriptorAuthorityOf(definition) {
1317
+ const has = (names) => names !== undefined && names.length > 0;
1318
+ if (!has(definition.permissions) &&
1319
+ !has(definition.configuration) &&
1320
+ !has(definition.requiredConfiguration) &&
1321
+ !has(definition.secrets) &&
1322
+ !has(definition.requiredSecrets)) {
1323
+ return undefined;
1324
+ }
1325
+ return {
1326
+ ...(has(definition.permissions)
1327
+ ? { permissions: Object.freeze([...(definition.permissions ?? [])]) }
1328
+ : {}),
1329
+ ...(has(definition.configuration)
1330
+ ? { configuration: Object.freeze([...(definition.configuration ?? [])]) }
1331
+ : {}),
1332
+ ...(has(definition.requiredConfiguration)
1333
+ ? { requiredConfiguration: Object.freeze([...(definition.requiredConfiguration ?? [])]) }
1334
+ : {}),
1335
+ ...(has(definition.secrets) ? { secrets: Object.freeze([...(definition.secrets ?? [])]) } : {}),
1336
+ ...(has(definition.requiredSecrets)
1337
+ ? { requiredSecrets: Object.freeze([...(definition.requiredSecrets ?? [])]) }
1338
+ : {}),
1339
+ };
1340
+ }
1341
+ //# sourceMappingURL=runtime.js.map