@qaick/executor 0.2.6 → 0.3.1

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.
@@ -0,0 +1,63 @@
1
+ import type { NormalizedExecutionDocument, ExecutionPackageV7, ExecutableNodeV8, ControlV8, ConditionV8, ContextNode, ExecutionBindingV8, HierarchyAddress, ScopeIteration, IterationSelection, ExecutableInput } from '@qaick/contract';
2
+ export declare const addressKey: (a: HierarchyAddress) => string;
3
+ export type RunIteration = ScopeIteration | IterationSelection;
4
+ export type PlanStep = Omit<ExecutableNodeV8, 'children' | 'control'> & {
5
+ structuralAddress?: HierarchyAddress;
6
+ children?: PlanStep[];
7
+ control?: Omit<ControlV8, 'branches'> & {
8
+ branches?: Array<{
9
+ id: string;
10
+ occurrence: string;
11
+ condition: boolean | ConditionV8;
12
+ children: PlanStep[];
13
+ }>;
14
+ };
15
+ };
16
+ export type PlanFlow = {
17
+ id: string;
18
+ name: string;
19
+ occurrence: string;
20
+ steps: PlanStep[];
21
+ inputDeclarations?: ExecutableInput[];
22
+ requiredInputs?: string[];
23
+ outputBindings?: Record<string, ExecutionBindingV8>;
24
+ };
25
+ export type PlanIssue = {
26
+ code: string;
27
+ message: string;
28
+ stepId?: string;
29
+ };
30
+ /** References into the immutable canonical document, not a second executable package. */
31
+ export declare class ExecutionPlan {
32
+ readonly document: NormalizedExecutionDocument;
33
+ readonly legacy?: ExecutionPackageV7 | undefined;
34
+ readonly context: Map<string, ContextNode>;
35
+ readonly nodes: Map<string, PlanStep>;
36
+ readonly ordinals: Map<string, string>;
37
+ readonly inScope: Set<string>;
38
+ readonly steps: PlanStep[];
39
+ readonly flows: PlanFlow[];
40
+ readonly target: {
41
+ kind: "flow" | "project";
42
+ id: string;
43
+ name: string;
44
+ };
45
+ readonly packageId: string;
46
+ readonly inputDeclarations: ExecutableInput[] | undefined;
47
+ readonly requiredInputs: string[] | undefined;
48
+ readonly runtimeRequirements: string[] | undefined;
49
+ readonly project: boolean;
50
+ constructor(document: NormalizedExecutionDocument, legacy?: ExecutionPackageV7 | undefined);
51
+ ancestors(key: string): ContextNode[];
52
+ loops(key: string): string[];
53
+ dependency(operand: ExecutionBindingV8, consumer: PlanStep, phase: 'entry' | 'completion', actual?: RunIteration[]): PlanIssue[];
54
+ source(operand: Exclude<ExecutionBindingV8, {
55
+ type: 'input' | 'unresolved-output';
56
+ }>): {
57
+ step: PlanStep;
58
+ iterations: RunIteration[];
59
+ };
60
+ /** Prove unavailable dependencies, never evaluate a future Action's output value. */
61
+ preflight(inputs: Record<string, unknown>, runtime: Record<string, unknown>, supports: (action: string) => boolean): PlanIssue[];
62
+ }
63
+ export declare function journalKey(step: Pick<PlanStep, 'occurrence' | 'structuralAddress'>, iterations: RunIteration[], version: 7 | 8): string;
@@ -0,0 +1,241 @@
1
+ import { executionOperandDependencyIssues } from '@qaick/contract';
2
+ import { resolveDeclaredInputs } from './declared-inputs.js';
3
+ export const addressKey = (a) => JSON.stringify([a.scope, a.segments.map(s => s.kind === 'body' ? ['body'] : [s.kind, s.position])]);
4
+ /** References into the immutable canonical document, not a second executable package. */
5
+ export class ExecutionPlan {
6
+ document;
7
+ legacy;
8
+ context;
9
+ nodes = new Map();
10
+ ordinals = new Map();
11
+ inScope = new Set();
12
+ steps;
13
+ flows;
14
+ target;
15
+ packageId;
16
+ inputDeclarations;
17
+ requiredInputs;
18
+ runtimeRequirements;
19
+ project;
20
+ constructor(document, legacy) {
21
+ this.document = document;
22
+ this.legacy = legacy;
23
+ this.context = new Map(document.context.nodes.map(n => [n.key, n]));
24
+ this.target = document.target;
25
+ this.packageId = document.packageId;
26
+ this.runtimeRequirements = document.selection.runtimeRequirements;
27
+ for (const n of this.context.values())
28
+ if (n.originalAddress)
29
+ this.ordinals.set(addressKey(n.originalAddress), n.key);
30
+ for (const n of document.nodes)
31
+ this.nodes.set(n.occurrence, { ...n, children: undefined, control: undefined, structuralAddress: this.context.get(n.occurrence)?.originalAddress });
32
+ const list = (keys) => keys.map(k => this.nodes.get(k));
33
+ for (const n of document.nodes) {
34
+ const step = this.nodes.get(n.occurrence);
35
+ if (n.children)
36
+ step.children = list(n.children);
37
+ if (n.control)
38
+ step.control = { ...n.control, branches: n.control.branches?.map(b => {
39
+ const locator = this.context.get(b.occurrence).locator;
40
+ const last = locator.kind === 'source' ? locator.path.at(-1) : undefined;
41
+ return { ...b, id: last && 'id' in last ? last.id : b.occurrence, children: list(b.children) };
42
+ }) };
43
+ }
44
+ const flow = (key) => {
45
+ const f = document.flows.find(f => f.occurrence === key);
46
+ const c = this.context.get(key);
47
+ return { ...f, id: c.locator.kind === 'source' ? c.locator.path.at(-1)?.kind === 'flow' ? c.locator.path.at(-1).id : c.locator.root.id : key, name: c.name, steps: list(f.children) };
48
+ };
49
+ const root = document.selection.root.kind === 'occurrence' ? document.selection.root.key : document.context.root;
50
+ const selected = this.context.get(root);
51
+ const visit = (key) => {
52
+ this.inScope.add(key);
53
+ // Branch Controls retain their shadowed direct body as context, not executable scope.
54
+ const branches = this.nodes.get(key)?.control?.branches;
55
+ const children = branches ? branches.map(branch => branch.occurrence) : this.context.get(key).children;
56
+ for (const child of children)
57
+ visit(child);
58
+ };
59
+ visit(root);
60
+ const entryPayload = selected.kind === 'flow' ? document.flows.find(f => f.occurrence === root) : document.nodes.find(n => n.occurrence === root);
61
+ this.inputDeclarations = entryPayload?.inputDeclarations ?? document.selection.inputDeclarations;
62
+ this.requiredInputs = selected.kind === 'flow' ? [...new Set([...(document.flows.find(f => f.occurrence === root)?.requiredInputs ?? []), ...(document.selection.requiredInputs ?? [])])] : document.selection.requiredInputs;
63
+ this.project = selected.kind === 'project';
64
+ this.flows = !document.selection.executable ? [] : this.project ? (selected.sequence ?? []).map(flow) : [];
65
+ this.steps = !document.selection.executable ? [] : selected.kind === 'flow' ? flow(root).steps : this.project ? [] : [this.nodes.get(root)];
66
+ if (document.inputVersion === 8) {
67
+ const freeze = (v) => { if (v && typeof v === 'object') {
68
+ for (const child of Object.values(v))
69
+ freeze(child);
70
+ Object.freeze(v);
71
+ } };
72
+ for (const node of this.nodes.values())
73
+ freeze(node);
74
+ }
75
+ // Only the selected entry is dispatched; full unselected context is retained unchanged.
76
+ }
77
+ ancestors(key) {
78
+ const path = [];
79
+ for (let n = this.context.get(key); n; n = n.parent === null ? undefined : this.context.get(n.parent))
80
+ path.unshift(n);
81
+ return path;
82
+ }
83
+ loops(key) { return this.ancestors(key).slice(0, -1).filter(n => this.inScope.has(n.key) && this.nodes.get(n.key)?.control?.type === 'repeat').map(n => n.key); }
84
+ dependency(operand, consumer, phase, actual) {
85
+ if (this.document.inputVersion === 7)
86
+ return executionOperandDependencyIssues(this.legacy, operand, phase === 'entry' ? { phase, address: consumer.structuralAddress } : { phase, address: consumer.structuralAddress, iterations: actual }).map(i => ({ code: i.code, message: i.reason }));
87
+ const fail = (code, message) => [{ code, message: `${consumer.name}: ${message}`, stepId: consumer.id }];
88
+ if (operand.type === 'input')
89
+ return [];
90
+ if (operand.type === 'unresolved-output')
91
+ return fail('UNRESOLVED_SCOPE_BINDING', 'The output reference requires repair.');
92
+ const producer = operand.type === 'occurrence-output' ? operand.producer : this.ordinals.get(addressKey(operand.address));
93
+ if (!producer || !this.inScope.has(producer))
94
+ return fail('UNAVAILABLE_SCOPE_DEPENDENCY', 'The required output is outside this execution scope or has no original structural address.');
95
+ const source = this.nodes.get(producer);
96
+ if (!source || source.kind !== 'action' || !Object.hasOwn(source.output, operand.outputName))
97
+ return fail('UNRESOLVED_SCOPE_BINDING', 'The producer does not declare the required Action output.');
98
+ const a = this.ancestors(producer), b = this.ancestors(consumer.occurrence);
99
+ const descendant = a.some(n => n.key === consumer.occurrence) && producer !== consumer.occurrence;
100
+ if (phase === 'completion' && descendant) {
101
+ const path = a.slice(a.findIndex(n => n.key === consumer.occurrence));
102
+ if (path.some(n => n.kind === 'branch' || ['condition', 'parallel', 'branch'].includes(this.nodes.get(n.key)?.control?.type ?? '')))
103
+ return fail('UNAVAILABLE_SCOPE_CONTEXT', 'Conditional or parallel output exposure is not proven.');
104
+ }
105
+ else {
106
+ let i = 0;
107
+ while (i < a.length && i < b.length && a[i].key === b[i].key)
108
+ i++;
109
+ const parent = a[i - 1], left = a[i], right = b[i];
110
+ if (!parent || !left || !right || left.kind === 'branch' || right.kind === 'branch' || !parent.sequence || parent.sequence.indexOf(left.key) >= parent.sequence.indexOf(right.key))
111
+ return fail('UNAVAILABLE_SCOPE_DEPENDENCY', 'The producer does not precede this consumer on a proven sequential path.');
112
+ }
113
+ const loops = this.loops(producer);
114
+ const selected = operand.type === 'occurrence-output' ? operand.iterations : operand.iterations.map(i => ({ loop: this.ordinals.get(addressKey(i.loopAddress)) ?? '', iteration: i.iteration }));
115
+ if (selected.length !== loops.length || loops.some(k => !selected.some(i => i.loop === k)))
116
+ return fail('UNAVAILABLE_SCOPE_CONTEXT', 'Select every actual in-scope producer loop; enclosing unexecuted loops cannot supply an iteration.');
117
+ const shared = this.loops(consumer.occurrence).filter(k => loops.includes(k));
118
+ if (phase === 'entry' && shared.length)
119
+ return fail('UNAVAILABLE_SCOPE_CONTEXT', 'A fixed iteration cannot precede every consumer iteration.');
120
+ for (const i of selected) {
121
+ const count = this.nodes.get(i.loop)?.control?.iterations;
122
+ if (count !== undefined && i.iteration > count)
123
+ return fail('UNAVAILABLE_SCOPE_CONTEXT', 'The selected iteration exceeds the configured repeat count.');
124
+ if (phase === 'completion' && shared.includes(i.loop) && actual && !actual.some(a => 'loop' in a && a.loop === i.loop && a.iteration === i.iteration))
125
+ return fail('UNAVAILABLE_SCOPE_CONTEXT', 'The selected iteration differs from the completing invocation.');
126
+ }
127
+ return [];
128
+ }
129
+ source(operand) {
130
+ const key = operand.type === 'occurrence-output' ? operand.producer : this.ordinals.get(addressKey(operand.address));
131
+ const step = this.nodes.get(key);
132
+ if (this.document.inputVersion === 7)
133
+ return { step, iterations: this.loops(key).map(loop => ({ loopAddress: this.context.get(loop).originalAddress, iteration: operand.iterations.find(i => addressKey(i.loopAddress) === addressKey(this.context.get(loop).originalAddress)).iteration })) };
134
+ const selected = operand.type === 'occurrence-output' ? operand.iterations : operand.iterations.map(i => ({ loop: this.ordinals.get(addressKey(i.loopAddress)), iteration: i.iteration }));
135
+ return { step, iterations: this.loops(key).map(loop => ({ loop, iteration: selected.find(i => i.loop === loop).iteration })) };
136
+ }
137
+ /** Prove unavailable dependencies, never evaluate a future Action's output value. */
138
+ preflight(inputs, runtime, supports) {
139
+ if (this.document.inputVersion === 7)
140
+ return [];
141
+ const issues = [];
142
+ if (!this.document.selection.executable)
143
+ return this.document.selection.issues.map(issue => ({ code: 'INCOMPLETE_SCOPE', message: `${this.context.get(issue.at)?.name ?? 'Selected scope'}: This content is not ready to run.` }));
144
+ for (const key of this.inScope) {
145
+ const step = this.nodes.get(key);
146
+ if (!step)
147
+ continue;
148
+ if (step.kind === 'action' && !supports(step.action))
149
+ issues.push({ code: 'UNSUPPORTED_SCOPE_CAPABILITY', message: `${step.name}: No Action Executor supports this Action.`, stepId: step.id });
150
+ // Existing engine intentionally does not implement these completion projections.
151
+ if (step.control?.mergeBindings)
152
+ issues.push({ code: 'UNSUPPORTED_SCOPE_CAPABILITY', message: `${step.name}: Parallel output projections are unsupported.`, stepId: step.id });
153
+ }
154
+ const rootKey = this.document.selection.root.kind === 'occurrence' ? this.document.selection.root.key : this.document.context.root;
155
+ const relevantFlows = this.document.flows.filter(f => this.inScope.has(f.occurrence));
156
+ for (const flow of relevantFlows)
157
+ if (Object.keys(flow.outputBindings ?? {}).length)
158
+ issues.push({ code: 'UNSUPPORTED_SCOPE_CAPABILITY', message: `${this.context.get(flow.occurrence).name}: Flow completion exports are unsupported.` });
159
+ const scopeFrom = (values) => new Map(Object.entries(values).map(([k, value]) => [k, { known: true, value }]));
160
+ const binding = (op, step, scope, phase) => {
161
+ issues.push(...this.dependency(op, step, phase));
162
+ if (op.type !== 'input')
163
+ return { known: false };
164
+ let v = scope.get(op.path[0]);
165
+ if (!v) {
166
+ issues.push({ code: 'UNAVAILABLE_SCOPE_CONTEXT', message: `${step.name}: Required input ${op.path.join('.')} is unavailable in this scope.`, stepId: step.id });
167
+ return { known: false };
168
+ }
169
+ if (v.known) {
170
+ let value = v.value;
171
+ for (const part of op.path.slice(1)) {
172
+ if (value === null || typeof value !== 'object' || !Object.hasOwn(value, part)) {
173
+ issues.push({ code: 'UNAVAILABLE_SCOPE_CONTEXT', message: `${step.name}: Required input path ${op.path.join('.')} is unavailable.`, stepId: step.id });
174
+ return { known: false };
175
+ }
176
+ value = value[part];
177
+ }
178
+ if (value === undefined) {
179
+ issues.push({ code: 'UNAVAILABLE_SCOPE_CONTEXT', message: `${step.name}: Required input ${op.path.join('.')} is unavailable.`, stepId: step.id });
180
+ return { known: false };
181
+ }
182
+ return { known: true, value };
183
+ }
184
+ return v;
185
+ };
186
+ const condition = (c, s, scope) => { if (c !== undefined && typeof c !== 'boolean')
187
+ binding(c.binding, s, scope, 'entry'); };
188
+ const visit = (s, scope) => {
189
+ const args = scopeFrom(s.occurrence === rootKey ? { ...inputs, ...s.staticInputs } : s.staticInputs ?? {});
190
+ for (const [name, op] of Object.entries(s.bindings ?? {}))
191
+ args.set(name, binding(op, s, scope, 'entry'));
192
+ const child = new Map(scope);
193
+ if (s.kind === 'operation') {
194
+ const known = Object.fromEntries([...args].filter(([, v]) => v.known).map(([k, v]) => [k, v.value]));
195
+ const resolved = resolveDeclaredInputs((s.inputDeclarations ?? []).filter(d => args.get(d.name)?.known !== false), known);
196
+ issues.push(...resolved.issues.map(i => ({ ...i, message: `${s.name}: ${i.message}`, stepId: s.id })));
197
+ for (const d of s.inputDeclarations ?? [])
198
+ child.delete(d.name);
199
+ for (const [k, v] of Object.entries(resolved.inputs))
200
+ child.set(k, { known: true, value: v });
201
+ for (const [k, v] of args)
202
+ if (!v.known)
203
+ child.set(k, v);
204
+ }
205
+ else
206
+ for (const [k, v] of args)
207
+ child.set(k, v);
208
+ condition(s.control?.condition, s, child);
209
+ for (const branch of s.control?.branches ?? []) {
210
+ condition(branch.condition, s, child);
211
+ for (const c of branch.children)
212
+ visit(c, child);
213
+ }
214
+ if (!s.control?.branches)
215
+ for (const c of s.children ?? [])
216
+ visit(c, child);
217
+ for (const op of Object.values(s.outputBindings ?? {}))
218
+ binding(op, s, child, 'completion');
219
+ for (const op of Object.values(s.control?.mergeBindings ?? {}))
220
+ binding(op, s, child, 'completion');
221
+ };
222
+ if (this.project) {
223
+ for (const flow of this.flows) {
224
+ const resolved = resolveDeclaredInputs(flow.inputDeclarations, inputs);
225
+ issues.push(...resolved.issues);
226
+ for (const name of flow.requiredInputs ?? [])
227
+ if (resolved.inputs[name] === undefined)
228
+ issues.push({ code: 'MISSING_INPUT', message: `${flow.name}: Required input ${name} is missing.` });
229
+ for (const s of flow.steps)
230
+ visit(s, scopeFrom(resolved.inputs));
231
+ }
232
+ }
233
+ else
234
+ for (const s of this.steps)
235
+ visit(s, scopeFrom(inputs));
236
+ return issues;
237
+ }
238
+ }
239
+ export function journalKey(step, iterations, version) {
240
+ return version === 7 ? JSON.stringify([addressKey(step.structuralAddress), iterations.map(i => [addressKey(i.loopAddress), i.iteration])]) : JSON.stringify([step.occurrence, iterations.map(i => [i.loop, i.iteration])]);
241
+ }
@@ -1,4 +1,5 @@
1
- import type { ActReplayTarget, ActReplayHandle, OperationReplayHandle, ActionExecutor, ControlCommand, ExecutionControlState, Executor, ExecutorConfiguration, ExecutorEvent, ExecutorRequest, ExecutorResult, ExecutorRun } from './types.js';
1
+ import type { AcceptedExecutorRequest, AcceptedExecutorResult, AcceptedExecutorEvent, ExecutorRequestV8, ExecutorRunV8 } from './types.js';
2
+ import type { ActReplayTarget, ActReplayHandle, OperationReplayHandle, ActionExecutor, ControlCommand, ExecutionControlState, Executor, ExecutorConfiguration, ExecutorRequest, ExecutorRun } from './types.js';
2
3
  export declare function createExecutor(configuration?: ExecutorConfiguration): Executor;
3
4
  export declare class DefaultExecutor implements Executor {
4
5
  private readonly runs;
@@ -7,12 +8,14 @@ export declare class DefaultExecutor implements Executor {
7
8
  constructor(configuration?: ExecutorConfiguration);
8
9
  registerActionExecutor(executor: ActionExecutor): void;
9
10
  execute(request: ExecutorRequest): Promise<ExecutorRun>;
11
+ execute(request: ExecutorRequestV8): Promise<ExecutorRunV8>;
12
+ execute(request: AcceptedExecutorRequest): Promise<ExecutorRun | ExecutorRunV8>;
10
13
  private startRun;
11
14
  cancel(requestId: string): Promise<void>;
12
15
  control(requestId: string, command: ControlCommand): Promise<void>;
13
16
  controlState(requestId: string): ExecutionControlState | undefined;
14
- events(requestId: string): AsyncIterable<ExecutorEvent>;
15
- result(requestId: string): Promise<ExecutorResult>;
17
+ events(requestId: string): AsyncIterable<AcceptedExecutorEvent>;
18
+ result(requestId: string): Promise<AcceptedExecutorResult>;
16
19
  private replayTarget;
17
20
  retryAction(requestId: string, target: ActReplayTarget): Promise<ActReplayHandle>;
18
21
  private replayEvents;
@@ -39,5 +42,6 @@ export declare class DefaultExecutor implements Executor {
39
42
  private emitReplayOccurrence;
40
43
  private finish;
41
44
  private baseResult;
45
+ private versionedResult;
42
46
  private failure;
43
47
  }
package/dist/executor.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { ExecutionPlan, journalKey } from './execution-plan.js';
2
+ import { normalizeV7, normalizeV8, hashCanonicalJson } from '@qaick/contract';
1
3
  import { resolveDeclaredInputs } from './declared-inputs.js';
2
4
  import { isDeclaredInputNoValue, executionOccurrences, executionOperandDependencyIssues, hierarchyAddressIssues } from '@qaick/contract';
3
5
  const addressKey = (address) => JSON.stringify([address.scope, address.segments.map(s => s.kind === 'body' ? ['body'] : [s.kind, s.position])]);
@@ -8,6 +10,11 @@ function freezePlan(value) {
8
10
  Object.freeze(value);
9
11
  }
10
12
  }
13
+ function legacyPackage(run) {
14
+ if (run.request.executionPackage.schemaVersion !== 7)
15
+ throw failure('UNSUPPORTED_SCOPE_CAPABILITY', 'Replay of Execution v8 is unsupported.');
16
+ return run.request.executionPackage;
17
+ }
11
18
  const MAX_TIMEOUT_MS = 300_000;
12
19
  export function createExecutor(configuration = {}) {
13
20
  return new DefaultExecutor(configuration);
@@ -27,26 +34,48 @@ export class DefaultExecutor {
27
34
  async startRun(request, debugSeed, debugProvenance) {
28
35
  if (this.runs.has(request.requestId))
29
36
  throw failure('PREFLIGHT_ERROR', 'Execution request ID already belongs to a Run.');
30
- const requestSnapshot = structuredClone(request);
31
- const validation = [];
32
- let plan = new Map();
33
- try {
34
- // Shared traversal validates the projection before constructing the immutable plan.
35
- plan = new Map(executionOccurrences(requestSnapshot.executionPackage).map(o => [addressKey(o.address), o]));
36
- freezePlan(requestSnapshot.executionPackage);
37
- }
38
- catch (cause) {
39
- validation.push({ code: 'invalid-execution-package', message: cause instanceof Error ? cause.message : 'Invalid Execution v7 projection.' });
37
+ // Register before awaiting native hashing, so concurrent duplicate IDs cannot race admission.
38
+ // Validate the original v8 JSON boundary before structuredClone could invoke a getter.
39
+ let preparedV8;
40
+ if (request.executionPackage.schemaVersion === 8) {
41
+ try {
42
+ preparedV8 = normalizeV8(request.executionPackage);
43
+ }
44
+ catch (cause) {
45
+ const issues = cause.issues;
46
+ throw { code: issues?.[0]?.code ?? 'INVALID_EXECUTION_DOCUMENT', message: 'Execution Document v8 could not be admitted.', ...(issues?.length ? { details: { issues } } : {}) };
47
+ }
40
48
  }
49
+ const requestSnapshot = structuredClone(request);
50
+ freezePlan(requestSnapshot.executionPackage);
41
51
  let resolve;
42
- const result = new Promise(promiseResolve => { resolve = promiseResolve; });
52
+ const result = new Promise(r => { resolve = r; });
43
53
  const run = {
44
- debugSeed, debugProvenance, request: requestSnapshot, inputSelections: structuredClone(requestSnapshot.inputs), plan, journal: new Map(), replayCaptures: new Map(), resolve, events: [], listeners: [], abort: new AbortController(), cancelled: false,
54
+ debugSeed, debugProvenance, request: requestSnapshot, inputSelections: structuredClone(requestSnapshot.inputs), plan: new Map(), journal: new Map(), replayCaptures: new Map(), resolve, events: [], listeners: [], abort: new AbortController(), cancelled: false,
45
55
  timedOut: false, controlState: 'idle', pauseRequested: false, stepRequested: false,
46
- controlWaiters: [], hitBreakpoints: new Set(), stopAfter: request.debugSession?.stopAfterStepId,
56
+ controlWaiters: [], hitBreakpoints: new Set(), stopAfter: requestSnapshot.debugSession && ('stopAfter' in requestSnapshot.debugSession ? requestSnapshot.debugSession.stopAfter : ('stopAfterStepId' in requestSnapshot.debugSession ? requestSnapshot.debugSession.stopAfterStepId : undefined)),
47
57
  startedMs: Date.now(), pausedDurationMs: 0,
48
58
  };
49
59
  this.runs.set(request.requestId, run);
60
+ const validation = [];
61
+ try {
62
+ const pkg = requestSnapshot.executionPackage;
63
+ const document = pkg.schemaVersion === 8 ? preparedV8 : await normalizeV7(pkg);
64
+ if (pkg.schemaVersion === 8)
65
+ run.documentHash = await hashCanonicalJson(pkg);
66
+ run.execution = new ExecutionPlan(document, pkg.schemaVersion === 7 ? pkg : undefined);
67
+ if (pkg.schemaVersion === 7)
68
+ run.plan = new Map(executionOccurrences(pkg).map(o => [addressKey(o.address), o]));
69
+ }
70
+ catch (cause) {
71
+ const supplied = cause?.issues;
72
+ if (requestSnapshot.executionPackage.schemaVersion === 8) {
73
+ // No accepted v8 Run may pretend to have a hash/source envelope when admission failed.
74
+ this.runs.delete(request.requestId);
75
+ throw { code: supplied?.[0]?.code ?? 'INVALID_EXECUTION_DOCUMENT', message: 'Execution Document v8 could not be admitted.', ...(supplied?.length ? { details: { issues: supplied } } : {}) };
76
+ }
77
+ validation.push(...(supplied?.length ? supplied : [{ code: 'invalid-execution-package', message: cause instanceof Error ? cause.message : 'Execution admission failed.' }]));
78
+ }
50
79
  void this.run(run, validation);
51
80
  return { requestId: request.requestId, result, ...(debugProvenance ? { debugProvenance } : {}) };
52
81
  }
@@ -67,6 +96,8 @@ export class DefaultExecutor {
67
96
  if (command.type === 'stop')
68
97
  return this.cancel(requestId);
69
98
  if (command.type === 'stop-after') {
99
+ if (run.execution?.document.inputVersion === 8 && (!run.execution.inScope.has(command.stepId) || !run.execution.nodes.has(command.stepId)))
100
+ throw failure('INVALID_SCOPE', 'Stop After target is outside the selected execution scope.');
70
101
  run.stopAfter = command.stepId;
71
102
  return;
72
103
  }
@@ -124,6 +155,7 @@ export class DefaultExecutor {
124
155
  if (!parent?.result)
125
156
  reject('Act replay requires a terminal original Run.');
126
157
  const run = parent;
158
+ legacyPackage(run);
127
159
  if (run.cancelled || run.timedOut)
128
160
  reject('Interrupted original Runs cannot be replayed.');
129
161
  if (run.activeReplay)
@@ -262,7 +294,7 @@ export class DefaultExecutor {
262
294
  // Separate structural state, not a new Run: no registration, root result, resolver,
263
295
  // historical journal, controls, captures or original event stream are reused.
264
296
  const context = {
265
- request: session.request, inputSelections: structuredClone(parent.inputSelections), plan: parent.plan, journal: new Map(), replayCaptures: new Map(),
297
+ request: session.request, execution: parent.execution, inputSelections: structuredClone(parent.inputSelections), plan: parent.plan, journal: new Map(), replayCaptures: new Map(),
266
298
  replay: { session, capture, root: capture.structuralAddress },
267
299
  events: [], listeners: [], abort: session.abort,
268
300
  get cancelled() { return session.cancelled; }, get timedOut() { return session.timedOut; },
@@ -282,7 +314,7 @@ export class DefaultExecutor {
282
314
  session.abort.abort();
283
315
  }, timeoutMs);
284
316
  this.emitReplay(session, 'replay_started', 'running');
285
- operation = await this.executeStep(context, step, capture.lexicalScope, [], session.abort.signal, capture.iterations);
317
+ operation = await this.executeStep(context, parent.execution.nodes.get(parent.execution.ordinals.get(addressKey(step.structuralAddress))), capture.lexicalScope, [], session.abort.signal, capture.iterations);
286
318
  this.assertActive(session, session.abort.signal);
287
319
  if (operation.state !== 'finished') {
288
320
  state = 'failed';
@@ -315,6 +347,8 @@ export class DefaultExecutor {
315
347
  async runFromHere(sourceRequestId, target) {
316
348
  const source = this.runs.get(sourceRequestId);
317
349
  const reject = (message) => { throw failure('unsupported-execution-context', message); };
350
+ if (source)
351
+ legacyPackage(source);
318
352
  if (!source?.result || source.cancelled || source.timedOut)
319
353
  reject('Run From Here requires a terminal uninterrupted source Run.');
320
354
  if (!isRecord(target) || Object.keys(target).some(k => !['structuralAddress', 'iterations'].includes(k))
@@ -327,10 +361,11 @@ export class DefaultExecutor {
327
361
  if (!topLevel)
328
362
  reject('Nested/control continuation is not supported by Run From Here.');
329
363
  const parent = source;
364
+ legacyPackage(parent);
330
365
  const capture = parent.replayCaptures.get(invocationKey(address, []));
331
366
  if (!capture || (capture.kind !== 'action' && capture.kind !== 'operation'))
332
367
  reject('No eligible private original input capture exists.');
333
- const package_ = structuredClone(parent.request.executionPackage);
368
+ const package_ = structuredClone(legacyPackage(parent));
334
369
  const position = segments.at(-1);
335
370
  if (position.kind !== 'step')
336
371
  throw failure('unsupported-execution-context', 'Expected a top-level step.');
@@ -390,7 +425,7 @@ export class DefaultExecutor {
390
425
  freezePlan(seed);
391
426
  freezePlan(provenance);
392
427
  // Normal Run creation is independent of replay admission and has no external evidence view.
393
- return this.startRun({ ...structuredClone(parent.request), requestId: crypto.randomUUID(), executionPackage: package_, inputs: structuredClone(parent.inputSelections) }, seed, provenance);
428
+ return this.startRun({ ...structuredClone(parent.request), debugSession: parent.request.debugSession, requestId: crypto.randomUUID(), executionPackage: package_, inputs: structuredClone(parent.inputSelections) }, seed, provenance);
394
429
  }
395
430
  async run(run, validation) {
396
431
  const startedAt = new Date().toISOString();
@@ -406,12 +441,15 @@ export class DefaultExecutor {
406
441
  let timeout;
407
442
  if (timeoutMs !== undefined)
408
443
  timeout = setTimeout(() => { if (run.abort.signal.aborted)
409
- return; run.timedOut = true; run.abort.abort(); }, timeoutMs);
444
+ return; run.timedOut = true; run.abort.abort(); if (run.request.executionPackage.schemaVersion === 8)
445
+ run.controlWaiters.splice(0).forEach(resolve => resolve()); }, timeoutMs);
410
446
  this.emit(run, 'run_started', 'created', 'Execution accepted.');
411
447
  this.emit(run, 'preflight_started', 'preflighting', 'Validating execution package.');
412
- const arguments_ = resolveDeclaredInputs(run.request.executionPackage.inputDeclarations, run.request.inputs);
448
+ const arguments_ = resolveDeclaredInputs(run.execution.inputDeclarations, run.request.inputs);
413
449
  run.request = { ...run.request, inputs: arguments_.inputs };
414
- const issues = [...arguments_.issues, ...preflight(run.request.executionPackage, arguments_.inputs, run.request.runtimeValues ?? {})];
450
+ const debug = run.request.debugSession;
451
+ const debugIssues = run.execution.document.inputVersion === 8 && debug && [...debug.breakpoints, ...(debug.stopAfter ? [debug.stopAfter] : [])].some(k => !run.execution.inScope.has(k) || !run.execution.nodes.has(k)) ? [{ code: 'INVALID_SCOPE', message: 'Debug target is outside the selected executable scope.' }] : [];
452
+ const issues = [...debugIssues, ...arguments_.issues, ...preflight(run.execution, arguments_.inputs, run.request.runtimeValues ?? {}), ...run.execution.preflight(arguments_.inputs, run.request.runtimeValues ?? {}, action => this.actionExecutors.some(e => e.supports(action)))];
415
453
  if (issues.length > 0 || run.request.options?.dryRun) {
416
454
  const result = this.baseResult(run, startedAt, issues.length > 0 ? 'failed' : 'completed', undefined, issues);
417
455
  if (timeout)
@@ -419,11 +457,17 @@ export class DefaultExecutor {
419
457
  this.finish(run, result);
420
458
  return;
421
459
  }
460
+ if (run.cancelled || run.abort.signal.aborted) {
461
+ if (timeout)
462
+ clearTimeout(timeout);
463
+ this.finish(run, this.baseResult(run, startedAt, 'cancelled'));
464
+ return;
465
+ }
422
466
  run.controlState = 'running';
423
467
  try {
424
- const result = run.request.executionPackage.target.kind === 'project'
468
+ const result = run.execution.project
425
469
  ? await this.executeProject(run, startedAt)
426
- : await this.executeFlow(run, run.request.executionPackage.steps, run.request.inputs, [], startedAt);
470
+ : await this.executeFlow(run, run.execution.steps, run.request.inputs, [], startedAt);
427
471
  if (timeout)
428
472
  clearTimeout(timeout);
429
473
  this.finish(run, result);
@@ -436,7 +480,7 @@ export class DefaultExecutor {
436
480
  }
437
481
  }
438
482
  async executeProject(run, startedAt) {
439
- const pkg = run.request.executionPackage;
483
+ const pkg = run.execution;
440
484
  const children = [];
441
485
  let inputs = { ...run.request.inputs };
442
486
  for (const flow of pkg.flows ?? []) {
@@ -461,18 +505,31 @@ export class DefaultExecutor {
461
505
  }
462
506
  return this.baseResult(run, startedAt, 'completed', undefined, undefined, children);
463
507
  }
464
- async executeFlow(run, steps, initialInputs, path, startedAt, target = run.request.executionPackage.target, signal = run.abort.signal, iterations = [], bodyFailurePolicy = 'stop') {
508
+ async executeFlow(run, steps, initialInputs, path, startedAt, target = run.execution.target, signal = run.abort.signal, iterations = [], bodyFailurePolicy = 'stop') {
465
509
  const results = [];
466
510
  let state = 'completed';
467
511
  let error;
468
512
  for (const step of steps) {
469
- if (run.cancelled || signal.aborted)
470
- throw new Error('Execution cancelled.');
471
- await this.boundary(run, step, [...path, step.id], signal, iterations);
472
- if (run.cancelled || signal.aborted)
473
- throw new Error('Execution cancelled.');
513
+ try {
514
+ if (run.cancelled || signal.aborted)
515
+ throw new Error('Execution cancelled.');
516
+ await this.boundary(run, step, [...path, step.id], signal, iterations);
517
+ if (run.cancelled || signal.aborted)
518
+ throw new Error('Execution cancelled.');
519
+ }
520
+ catch (cause) {
521
+ if (run.execution.document.inputVersion === 7)
522
+ throw cause;
523
+ state = run.timedOut ? 'failed' : 'cancelled';
524
+ error = this.failure(cause, run, signal);
525
+ break;
526
+ }
474
527
  const result = await this.executeStep(run, step, initialInputs, path, signal, iterations);
475
528
  results.push(result);
529
+ if (run.execution.document.inputVersion === 8 && step.kind !== 'action' && run.stopAfter === step.occurrence) {
530
+ run.stopAfter = undefined;
531
+ await this.pauseAtBoundary(run, step, iterations);
532
+ }
476
533
  if (result.state === 'failed' || result.state === 'cancelled') {
477
534
  const interrupted = result.state === 'cancelled' || run.cancelled || run.timedOut || signal.aborted
478
535
  || result.terminalReason === 'timeout' || result.error?.category === 'ACTION_TIMEOUT'
@@ -496,20 +553,25 @@ export class DefaultExecutor {
496
553
  async executeStep(run, step, scope, path, signal = run.abort.signal, iterations = []) {
497
554
  const startedMs = Date.now();
498
555
  const invocationPath = [...path, step.id];
499
- const base = { id: step.id, name: step.name, kind: step.kind ?? 'action', inputs: {}, outputs: {}, structuralAddress: structuredClone(step.structuralAddress), iterations: structuredClone(iterations), invocationId: invocationPath.join('/'), invocationPath };
556
+ const base = { ...(run.execution.document.inputVersion === 8 ? { occurrence: step.occurrence } : {}), id: step.id, name: step.name, kind: step.kind ?? 'action', inputs: {}, outputs: {}, ...(step.structuralAddress ? { structuralAddress: structuredClone(step.structuralAddress) } : {}), iterations: structuredClone(iterations), invocationId: invocationPath.join('/'), invocationPath };
500
557
  let inputs = {};
501
558
  const attempts = [];
559
+ let actualChildren;
502
560
  this.emit(run, 'step_started', 'running', `${step.name} started.`, step, undefined, iterations);
503
561
  try {
504
562
  const debugSeed = run.debugSeed && addressKey(run.debugSeed.structuralAddress) === addressKey(step.structuralAddress) ? run.debugSeed : undefined;
505
563
  const rootCapture = run.replay && addressKey(run.replay.root) === addressKey(step.structuralAddress) ? run.replay.capture : debugSeed?.kind === 'operation' ? debugSeed : undefined;
506
- inputs = debugSeed ? structuredClone(debugSeed.inputs) : rootCapture ? structuredClone(rootCapture.inputs) : { ...structuredClone(step.staticInputs ?? {}), ...this.resolveBindings(run, step.bindings ?? {}, scope, step.structuralAddress) };
564
+ const selected = run.execution.document.selection.root;
565
+ const entryInputs = run.execution.document.inputVersion === 8 && (selected.kind === 'occurrence' ? selected.key : run.execution.document.context.root) === step.occurrence ? scope : {};
566
+ inputs = debugSeed ? structuredClone(debugSeed.inputs) : rootCapture ? structuredClone(rootCapture.inputs) : { ...entryInputs, ...structuredClone(step.staticInputs ?? {}), ...this.resolveBindings(run, step.bindings ?? {}, scope, step) };
507
567
  if (step.delayMs) {
508
568
  this.emit(run, 'waiting', 'waiting', `${step.name} is waiting for ${step.delayMs} ms.`, step, undefined, iterations);
509
569
  await delay(step.delayMs, signal);
510
570
  }
511
571
  if (step.control) {
512
572
  const controlResult = await this.executeControl(run, step, inputs, scope, path, signal, iterations);
573
+ if (run.execution.document.inputVersion === 8)
574
+ this.emit(run, 'step_finished', controlResult.state, `${step.name} ${controlResult.state}.`, step, undefined, iterations);
513
575
  return { ...base, ...controlResult, inputs, durationMs: Date.now() - startedMs };
514
576
  }
515
577
  if (step.kind === 'operation' && step.children) {
@@ -521,29 +583,30 @@ export class DefaultExecutor {
521
583
  for (const declaration of step.inputDeclarations ?? [])
522
584
  if (!Object.hasOwn(arguments_.inputs, declaration.name))
523
585
  delete childScope[declaration.name];
524
- if (!run.replay)
586
+ if (!run.replay && run.execution.document.inputVersion === 7)
525
587
  this.capture(run, step, iterations, arguments_.inputs, {
526
588
  kind: 'operation', lexicalScope: structuredClone(childScope),
527
589
  externalEvidence: this.captureExternalEvidence(run, step, iterations),
528
590
  });
529
- const nested = await this.executeFlow(run, step.children, childScope, [...path, step.id], new Date().toISOString(), run.request.executionPackage.target, signal, iterations);
591
+ const nested = await this.executeFlow(run, step.children, childScope, [...path, step.id], new Date().toISOString(), run.execution.target, signal, iterations);
592
+ actualChildren = nested.steps;
530
593
  const outputs = nested.state === 'completed'
531
- ? { ...structuredClone(step.staticOutputs ?? {}), ...this.resolveBindings(run, step.outputBindings ?? {}, childScope, step.structuralAddress, { phase: 'completion', address: step.structuralAddress, iterations }) }
594
+ ? { ...structuredClone(step.staticOutputs ?? {}), ...this.resolveBindings(run, step.outputBindings ?? {}, childScope, step, { phase: 'completion', iterations }) }
532
595
  : {};
533
596
  const result = { ...base, state: nested.state === 'completed' ? 'finished' : nested.state, inputs, outputs, durationMs: Date.now() - startedMs, children: nested.steps, terminalReason: nested.terminalReason ?? (nested.state === 'completed' ? 'completed' : 'failed'), error: nested.error };
534
597
  this.emit(run, 'step_finished', result.state, `${step.name} ${result.state}.`, step, undefined, iterations);
535
598
  return result;
536
599
  }
537
- if (!run.replay)
600
+ if (!run.replay && run.execution.document.inputVersion === 7)
538
601
  this.capture(run, step, iterations, inputs, { kind: 'action' });
539
602
  const actionResult = await this.executeAction(run, step, inputs, attempts, signal, run.replay ? (type, attempt, state) => this.emitReplayOccurrence(run.replay.session, type, state, step, iterations, attempt) : undefined);
540
603
  if (signal.aborted || run.cancelled)
541
604
  throw failure(run.timedOut ? 'ACTION_TIMEOUT' : 'ACTION_CANCELLED', 'Execution ended before evidence publication.');
542
- run.journal.set(invocationKey(step.structuralAddress, iterations), { outputs: structuredClone(actionResult.outputs), ...(actionResult.evidence ? { evidence: structuredClone(actionResult.evidence) } : {}) });
605
+ run.journal.set(journalKey(step, iterations, run.execution.document.inputVersion), { outputs: structuredClone(actionResult.outputs), ...(actionResult.evidence ? { evidence: structuredClone(actionResult.evidence) } : {}) });
543
606
  freezePlan(attempts);
544
607
  const result = { ...base, state: 'finished', inputs, outputs: actionResult.outputs, durationMs: Date.now() - startedMs, terminalReason: 'completed', attempts: actionResult.attempts, evidence: actionResult.evidence };
545
608
  this.emit(run, 'step_finished', 'finished', `${step.name} finished.`, step, undefined, iterations);
546
- if (run.stopAfter === step.id) {
609
+ if (run.stopAfter === (run.execution.document.inputVersion === 8 ? step.occurrence : step.id)) {
547
610
  run.stopAfter = undefined;
548
611
  await this.pauseAtBoundary(run, step, iterations);
549
612
  }
@@ -553,14 +616,14 @@ export class DefaultExecutor {
553
616
  const failure = this.failure(cause, run, signal);
554
617
  const state = run.timedOut ? 'failed' : run.cancelled || signal.aborted ? 'cancelled' : 'failed';
555
618
  freezePlan(attempts);
556
- const result = { ...base, state, inputs, durationMs: Date.now() - startedMs, ...(attempts.length ? { attempts } : {}), terminalReason: run.timedOut ? 'timeout' : state === 'cancelled' ? 'cancelled' : 'failed', error: failure };
619
+ const result = { ...base, ...(run.execution.document.inputVersion === 8 && actualChildren ? { children: actualChildren } : {}), state, inputs, durationMs: Date.now() - startedMs, ...(attempts.length ? { attempts } : {}), terminalReason: run.timedOut ? 'timeout' : state === 'cancelled' ? 'cancelled' : 'failed', error: failure };
557
620
  this.emit(run, 'step_finished', state, `${step.name} ${state}.`, step, undefined, iterations);
558
621
  return result;
559
622
  }
560
623
  }
561
624
  async executeControl(run, step, inputs, scope, path, signal = run.abort.signal, iterations = []) {
562
625
  const control = step.control;
563
- if (control.condition !== undefined && !this.evaluateCondition(run, control.condition, { ...scope, ...inputs }, step.structuralAddress))
626
+ if (control.condition !== undefined && !this.evaluateCondition(run, control.condition, { ...scope, ...inputs }, step))
564
627
  return { state: 'skipped', outputs: {}, terminalReason: 'condition-skipped' };
565
628
  if (control.type === 'repeat') {
566
629
  const total = Math.max(1, Math.min(control.iterations ?? (control.durationMs !== undefined ? 32 : 1), 32));
@@ -569,8 +632,8 @@ export class DefaultExecutor {
569
632
  for (let current = 1; current <= total; current++) {
570
633
  if (control.durationMs !== undefined && Date.now() - loopStarted >= control.durationMs)
571
634
  break;
572
- const child = await this.executeFlow(run, step.children ?? [], { ...scope, ...inputs }, [...path, step.id, String(current)], new Date().toISOString(), run.request.executionPackage.target, signal, [...iterations, { loopAddress: step.structuralAddress, iteration: current }]);
573
- children.push(...child.steps.map(item => ({ ...item, iteration: { loopId: step.id, current, total } })));
635
+ const child = await this.executeFlow(run, step.children ?? [], { ...scope, ...inputs }, [...path, step.id, String(current)], new Date().toISOString(), run.execution.target, signal, [...iterations, (run.execution.document.inputVersion === 8 ? { loop: step.occurrence, iteration: current } : { loopAddress: step.structuralAddress, iteration: current })]);
636
+ children.push(...child.steps.map(item => ({ ...item, iteration: run.execution.document.inputVersion === 8 ? { loop: step.occurrence, current, total } : { loopId: step.id, current, total } })));
574
637
  if (child.state !== 'completed' && control.onError !== 'continue')
575
638
  return { state: child.state, outputs: child.steps.at(-1)?.outputs ?? {}, children, terminalReason: child.terminalReason, error: child.error };
576
639
  if (control.iterationDelayMs)
@@ -581,9 +644,9 @@ export class DefaultExecutor {
581
644
  if (control.type === 'branch') {
582
645
  const children = [];
583
646
  for (const branch of control.branches ?? []) {
584
- if (!this.evaluateCondition(run, branch.condition, { ...scope, ...inputs }, step.structuralAddress))
647
+ if (!this.evaluateCondition(run, branch.condition, { ...scope, ...inputs }, step))
585
648
  continue;
586
- const nested = await this.executeFlow(run, branch.children, { ...scope, ...inputs }, [...path, step.id, branch.id], new Date().toISOString(), run.request.executionPackage.target, signal, iterations);
649
+ const nested = await this.executeFlow(run, branch.children, { ...scope, ...inputs }, [...path, step.id, branch.id], new Date().toISOString(), run.execution.target, signal, iterations);
587
650
  children.push(...nested.steps);
588
651
  if (nested.state !== 'completed' && control.onError !== 'continue')
589
652
  return { state: nested.state, outputs: nested.steps.at(-1)?.outputs ?? {}, children, terminalReason: nested.terminalReason, error: nested.error };
@@ -592,7 +655,7 @@ export class DefaultExecutor {
592
655
  }
593
656
  if (control.type === 'parallel') {
594
657
  const branches = control.branches ?? [];
595
- const selected = branches.filter(branch => this.evaluateCondition(run, branch.condition, { ...scope, ...inputs }, step.structuralAddress));
658
+ const selected = branches.filter(branch => this.evaluateCondition(run, branch.condition, { ...scope, ...inputs }, step));
596
659
  const controllers = selected.map(() => new AbortController());
597
660
  const abortBranches = () => controllers.forEach(controller => controller.abort());
598
661
  signal.addEventListener('abort', abortBranches, { once: true });
@@ -600,7 +663,7 @@ export class DefaultExecutor {
600
663
  abortBranches();
601
664
  const branchRuns = await Promise.all(selected.map(async (branch, index) => {
602
665
  try {
603
- const result = await this.executeFlow(run, branch.children, { ...scope, ...inputs }, [...path, step.id, branch.id], new Date().toISOString(), run.request.executionPackage.target, controllers[index].signal, iterations);
666
+ const result = await this.executeFlow(run, branch.children, { ...scope, ...inputs }, [...path, step.id, branch.id], new Date().toISOString(), run.execution.target, controllers[index].signal, iterations);
604
667
  return { branch, result };
605
668
  }
606
669
  catch (cause) {
@@ -615,16 +678,16 @@ export class DefaultExecutor {
615
678
  throw failure('unsupported-context', 'Parallel output projections are outside this foundation slice.');
616
679
  return { state: failed ? invocationState(failed.result.state) : 'finished', outputs: {}, children, terminalReason: failed?.result.terminalReason ?? 'parallel-joined', error: failed?.result.error };
617
680
  }
618
- const nested = await this.executeFlow(run, step.children ?? [], { ...scope, ...inputs }, [...path, step.id], new Date().toISOString(), run.request.executionPackage.target, signal, iterations, control.type === 'sequence' ? control.onError ?? 'stop' : 'stop');
681
+ const nested = await this.executeFlow(run, step.children ?? [], { ...scope, ...inputs }, [...path, step.id], new Date().toISOString(), run.execution.target, signal, iterations, control.type === 'sequence' ? control.onError ?? 'stop' : 'stop');
619
682
  return { state: nested.state === 'completed' ? 'finished' : nested.state, outputs: nested.steps.at(-1)?.outputs ?? {}, children: nested.steps, terminalReason: nested.terminalReason, error: nested.error };
620
683
  }
621
- resolveBindings(run, bindings, scope, consumer, context = { phase: 'entry', address: consumer }) {
684
+ resolveBindings(run, bindings, scope, consumer, context = { phase: 'entry' }) {
622
685
  return Object.fromEntries(Object.entries(bindings).map(([name, operand]) => [name, this.resolveOperand(run, operand, scope, consumer, context)]));
623
686
  }
624
- resolveOperand(run, operand, scope, consumer, context = { phase: 'entry', address: consumer }) {
625
- const issues = executionOperandDependencyIssues(run.request.executionPackage, operand, context);
687
+ resolveOperand(run, operand, scope, consumer, context = { phase: 'entry' }) {
688
+ const issues = run.execution.dependency(operand, consumer, context.phase, context.iterations);
626
689
  if (issues.length)
627
- throw { code: issues[0].code, message: issues[0].reason, details: { consumer, operand } };
690
+ throw { code: issues[0].code, message: issues[0].message, ...(run.execution.document.inputVersion === 7 ? { details: { consumer: consumer.structuralAddress, operand } } : {}) };
628
691
  if (operand.type === 'input') {
629
692
  let value = scope;
630
693
  for (const part of operand.path) {
@@ -638,13 +701,11 @@ export class DefaultExecutor {
638
701
  }
639
702
  if (operand.type === 'unresolved-output')
640
703
  throw failure(operand.code, operand.reason);
641
- const source = run.plan.get(addressKey(operand.address));
642
- const selectors = source.loops.map(loop => ({ loopAddress: loop, iteration: operand.iterations.find(i => addressKey(i.loopAddress) === addressKey(loop)).iteration }));
643
- const key = invocationKey(source.address, selectors);
644
- const invocation = run.replay && !ownsAddress(run.replay.root, source.address)
645
- ? run.replay.capture.externalEvidence.get(key) : run.journal.get(key);
704
+ const source = run.execution.source(operand);
705
+ const key = journalKey(source.step, source.iterations, run.execution.document.inputVersion);
706
+ const invocation = run.replay && !ownsAddress(run.replay.root, source.step.structuralAddress) ? run.replay.capture.externalEvidence.get(key) : run.journal.get(key);
646
707
  if (!invocation || !Object.hasOwn(invocation.outputs, operand.outputName) || invocation.outputs[operand.outputName] === undefined)
647
- throw { code: 'output-evidence-unavailable', message: run.replay ? 'Required successful Act output is unavailable in this replay evidence view.' : 'Required successful Act output is unavailable in this Run.', details: { missing: invocation ? 'output' : 'invocation', source: operand.address, iterations: selectors, outputName: operand.outputName, consumer } };
708
+ throw { code: 'output-evidence-unavailable', message: run.execution.document.inputVersion === 7 ? run.replay ? 'Required successful Act output is unavailable in this replay evidence view.' : 'Required successful Act output is unavailable in this Run.' : 'Required successful Action output is unavailable in this Run.', details: { missing: invocation ? 'output' : 'invocation', source: source.step.structuralAddress, iterations: source.iterations, outputName: operand.outputName, consumer: consumer.structuralAddress } };
648
709
  return structuredClone(invocation.outputs[operand.outputName]);
649
710
  }
650
711
  evaluateCondition(run, condition, scope, consumer) {
@@ -673,7 +734,7 @@ export class DefaultExecutor {
673
734
  return;
674
735
  // An exact external reference must already be causal at this boundary. No journal scan,
675
736
  // legacy ID lookup, or inference of selectors from the current invocation is permitted.
676
- if (executionOperandDependencyIssues(run.request.executionPackage, operand, { phase: 'completion', address: operation.structuralAddress, iterations }).length)
737
+ if (executionOperandDependencyIssues(legacyPackage(run), operand, { phase: 'completion', address: operation.structuralAddress, iterations }).length)
677
738
  return;
678
739
  const source = run.plan.get(addressKey(operand.address));
679
740
  const selectors = source.loops.map(loop => ({ loopAddress: loop, iteration: operand.iterations.find(i => addressKey(i.loopAddress) === addressKey(loop)).iteration }));
@@ -756,7 +817,7 @@ export class DefaultExecutor {
756
817
  }
757
818
  const breakpointKey = stepPathForBoundary(step, run, path);
758
819
  const breakpoints = run.request.debugSession?.breakpoints ?? [];
759
- if (breakpoints.includes(step.id) && !run.hitBreakpoints.has(breakpointKey)) {
820
+ if (breakpoints.includes(run.execution.document.inputVersion === 8 ? step.occurrence : step.id) && !run.hitBreakpoints.has(breakpointKey)) {
760
821
  run.hitBreakpoints.add(breakpointKey);
761
822
  run.pauseRequested = true;
762
823
  this.emit(run, 'waiting', 'waiting', `Breakpoint reached before ${step.name}.`, step, { type: 'pause', stepId: step.id }, iterations);
@@ -784,7 +845,12 @@ export class DefaultExecutor {
784
845
  this.emitReplayOccurrence(run.replay.session, type, state, step, iterations);
785
846
  return;
786
847
  }
787
- run.events.push({ requestId: run.request.requestId, sequence: run.events.length + 1, type, state, message, timestamp: new Date().toISOString(), ...(step ? { stepId: step.id, stepName: step.name, structuralAddress: structuredClone(step.structuralAddress), iterations: structuredClone(iterations), kind: step.kind ?? 'action' } : {}), ...(control ? { control } : {}) });
848
+ const base = { requestId: run.request.requestId, sequence: run.events.length + 1, type, state, message, timestamp: new Date().toISOString(), ...(step ? { stepId: step.id, stepName: step.name, ...(step.structuralAddress ? { structuralAddress: structuredClone(step.structuralAddress) } : {}), kind: step.kind ?? 'action' } : {}) };
849
+ const event = run.request.executionPackage.schemaVersion === 8
850
+ ? { ...base, eventVersion: 2, ...(step ? { occurrence: step.occurrence, iterations: structuredClone(iterations) } : {}), ...(control ? { control: { type: control.type, ...(step ? { occurrence: step.occurrence } : {}) } } : {}) }
851
+ : { ...base, ...(step ? { iterations: structuredClone(iterations) } : {}), ...(control ? { control } : {}) };
852
+ freezePlan(event);
853
+ run.events.push(event);
788
854
  run.listeners.splice(0).forEach(resolve => resolve());
789
855
  }
790
856
  emitReplayOccurrence(session, type, state, step, iterations, attempt) {
@@ -801,15 +867,21 @@ export class DefaultExecutor {
801
867
  finish(run, result) {
802
868
  const finishedAt = new Date().toISOString();
803
869
  const finalResult = { ...result, finishedAt, executionTiming: { wallDurationMs: Date.now() - run.startedMs, activeDurationMs: Date.now() - run.startedMs - run.pausedDurationMs, pausedDurationMs: run.pausedDurationMs } };
804
- run.result = finalResult;
870
+ const evidence = run.request.executionPackage.schemaVersion === 8 ? this.versionedResult(run, finalResult) : finalResult;
871
+ if (run.request.executionPackage.schemaVersion === 8)
872
+ freezePlan(evidence);
873
+ run.result = evidence;
805
874
  run.controlState = finalResult.state === 'completed' ? 'completed' : finalResult.state === 'cancelled' ? 'stopped' : 'failed';
806
875
  this.emit(run, 'run_finished', finalResult.state, finalResult.error?.message ?? `Execution ${finalResult.state}.`);
807
- run.resolve(finalResult);
876
+ run.resolve(evidence);
808
877
  run.listeners.splice(0).forEach(resolve => resolve());
809
878
  }
810
879
  baseResult(run, startedAt, state, error, issues, children) {
811
880
  return { ...(run.debugProvenance ? { debugProvenance: run.debugProvenance } : {}), requestId: run.request.requestId, state, target: run.request.executionPackage.target, environment: run.request.environment, startedAt, finishedAt: new Date().toISOString(), steps: [], ...(children ? { children } : {}), ...(error ? { error } : {}), ...(issues ? { issues } : {}) };
812
881
  }
882
+ versionedResult(run, result) {
883
+ return { ...result, resultVersion: 2, documentHash: run.documentHash, source: run.execution.document.source, scope: run.execution.document.selection.root, steps: result.steps, ...(result.children ? { children: result.children.map(r => this.versionedResult(run, r)) } : {}) };
884
+ }
813
885
  failure(cause, run, signal = run.abort.signal) {
814
886
  if (run.timedOut)
815
887
  return failure('ACTION_TIMEOUT', 'Execution exceeded its timeout.');
@@ -827,9 +899,9 @@ function preflight(pkg, inputs, runtimeValues) {
827
899
  const issues = [];
828
900
  if (!pkg.packageId || !pkg.target?.id)
829
901
  issues.push({ code: 'INVALID_PACKAGE', message: 'Execution package identity is incomplete.' });
830
- const flowInputNames = new Set(pkg.target.kind === 'project' ? (pkg.flows ?? []).flatMap(flow => Array.isArray(flow.inputDeclarations) ? flow.inputDeclarations.flatMap(input => typeof input?.name === 'string' ? [input.name] : []) : []) : []);
902
+ const flowInputNames = new Set(pkg.project ? (pkg.flows ?? []).flatMap(flow => Array.isArray(flow.inputDeclarations) ? flow.inputDeclarations.flatMap(input => typeof input?.name === 'string' ? [input.name] : []) : []) : []);
831
903
  for (const name of pkg.requiredInputs ?? [])
832
- if (inputs[name] === undefined && !flowInputNames.has(name))
904
+ if ((pkg instanceof ExecutionPlan && pkg.document.inputVersion === 8 ? isDeclaredInputNoValue(inputs[name]) : inputs[name] === undefined) && !flowInputNames.has(name))
833
905
  issues.push({ code: 'MISSING_INPUT', message: `Required input '${name}' is missing.` });
834
906
  for (const name of pkg.runtimeRequirements ?? [])
835
907
  if (runtimeValues[name] === undefined)
@@ -1,4 +1,4 @@
1
- import type { ActReplayTarget, ActReplayHandle, OperationReplayHandle, ActionExecutor, Executor, ExecutorConfiguration, ExecutorEvent, ExecutorRequest, ExecutorResult, ExecutorRun, ControlCommand, ExecutionControlState } from './types.js';
1
+ import type { AcceptedExecutorRequest, AcceptedExecutorResult, AcceptedExecutorEvent, ExecutorRequestV8, ExecutorRunV8, ActReplayTarget, ActReplayHandle, OperationReplayHandle, ActionExecutor, Executor, ExecutorConfiguration, ExecutorRequest, ExecutorRun, ControlCommand, ExecutionControlState } from './types.js';
2
2
  export declare function createMigratedExecutor(configuration?: ExecutorConfiguration): Executor;
3
3
  /**
4
4
  * Compatibility facade over the extracted Runner engine. The facade owns the
@@ -13,11 +13,13 @@ export declare class MigratedExecutor implements Executor {
13
13
  constructor(configuration?: ExecutorConfiguration);
14
14
  registerActionExecutor(executor: ActionExecutor): void;
15
15
  execute(request: ExecutorRequest): Promise<ExecutorRun>;
16
+ execute(request: ExecutorRequestV8): Promise<ExecutorRunV8>;
17
+ execute(request: AcceptedExecutorRequest): Promise<ExecutorRun | ExecutorRunV8>;
16
18
  cancel(requestId: string): Promise<void>;
17
19
  control(requestId: string, command: ControlCommand): Promise<void>;
18
20
  controlState(requestId: string): ExecutionControlState | undefined;
19
- events(requestId: string): AsyncIterable<ExecutorEvent>;
20
- result(requestId: string): Promise<ExecutorResult>;
21
+ events(requestId: string): AsyncIterable<AcceptedExecutorEvent>;
22
+ result(requestId: string): Promise<AcceptedExecutorResult>;
21
23
  retryAction(requestId: string, target: ActReplayTarget): Promise<ActReplayHandle>;
22
24
  retryOperation(requestId: string, target: ActReplayTarget): Promise<OperationReplayHandle>;
23
25
  runFromHere(requestId: string, target: ActReplayTarget): Promise<ExecutorRun>;
@@ -21,6 +21,10 @@ export class MigratedExecutor {
21
21
  }
22
22
  registerActionExecutor(executor) { this.routine.registerActionExecutor(executor); this.adapters.push(toActionAdapter(executor)); }
23
23
  async execute(request) {
24
+ if (request.executionPackage.schemaVersion === 8) {
25
+ this.routineRuns.add(request.requestId);
26
+ return this.routine.execute(request);
27
+ }
24
28
  if (request.executionPackage.ordering === 'routine') {
25
29
  this.routineRuns.add(request.requestId);
26
30
  return this.routine.execute(request);
package/dist/types.d.ts CHANGED
@@ -223,13 +223,72 @@ export interface ExecutorConfiguration {
223
223
  }
224
224
  export interface Executor {
225
225
  execute(request: ExecutorRequest): Promise<ExecutorRun>;
226
+ execute(request: ExecutorRequestV8): Promise<ExecutorRunV8>;
227
+ execute(request: AcceptedExecutorRequest): Promise<ExecutorRun | ExecutorRunV8>;
226
228
  cancel(requestId: string): Promise<void>;
227
229
  control(requestId: string, command: ControlCommand): Promise<void>;
228
230
  controlState(requestId: string): ExecutionControlState | undefined;
229
- events(requestId: string): AsyncIterable<ExecutorEvent>;
230
- result(requestId: string): Promise<ExecutorResult>;
231
+ events(requestId: string): AsyncIterable<AcceptedExecutorEvent>;
232
+ result(requestId: string): Promise<AcceptedExecutorResult>;
231
233
  retryAction(requestId: string, target: ActReplayTarget): Promise<ActReplayHandle>;
232
234
  retryOperation(requestId: string, target: ActReplayTarget): Promise<OperationReplayHandle>;
233
235
  runFromHere(requestId: string, target: ActReplayTarget): Promise<ExecutorRun>;
234
236
  registerActionExecutor(executor: ActionExecutor): void;
235
237
  }
238
+ /** Execution v8 admission shares the existing lifecycle; only identity/evidence is versioned. */
239
+ export interface ExecutorRequestV8 extends Omit<ExecutorRequest, 'executionPackage' | 'debugSession'> {
240
+ executionPackage: import('@qaick/contract').ExecutionDocumentV8;
241
+ debugSession?: {
242
+ breakpoints: string[];
243
+ stopAfter?: string;
244
+ };
245
+ }
246
+ export type AcceptedExecutorRequest = ExecutorRequest | ExecutorRequestV8;
247
+ export interface StepResultV8 extends Omit<ExecutorStepResult, 'iterations' | 'children' | 'attempts' | 'iteration'> {
248
+ occurrence: string;
249
+ iterations: import('@qaick/contract').ScopeIteration[];
250
+ children?: StepResultV8[];
251
+ attempts?: AttemptV8[];
252
+ iteration?: {
253
+ loop: string;
254
+ current: number;
255
+ total: number;
256
+ };
257
+ }
258
+ export interface AttemptV8 extends Omit<ExecutionAttempt, 'children'> {
259
+ children?: StepResultV8[];
260
+ }
261
+ export interface ExecutionResultV8 extends Omit<ExecutorResult, 'steps' | 'children'> {
262
+ resultVersion: 2;
263
+ documentHash: string;
264
+ source: import('@qaick/contract').DocumentSource;
265
+ scope: import('@qaick/contract').ExecutionRoot;
266
+ steps: StepResultV8[];
267
+ children?: ExecutionResultV8[];
268
+ }
269
+ export interface ExecutionEventV8 extends Omit<ExecutorEvent, 'iterations' | 'iteration' | 'parallel' | 'control'> {
270
+ eventVersion: 2;
271
+ occurrence?: string;
272
+ iterations?: import('@qaick/contract').ScopeIteration[];
273
+ iteration?: {
274
+ loop: string;
275
+ current: number;
276
+ total: number;
277
+ };
278
+ parallel?: {
279
+ group: string;
280
+ completed: number;
281
+ total: number;
282
+ join: 'all' | 'first';
283
+ };
284
+ control?: {
285
+ type: ControlCommand['type'];
286
+ occurrence?: string;
287
+ };
288
+ }
289
+ export type AcceptedExecutorResult = ExecutorResult | ExecutionResultV8;
290
+ export type AcceptedExecutorEvent = ExecutorEvent | ExecutionEventV8;
291
+ export interface ExecutorRunV8 {
292
+ requestId: string;
293
+ result: Promise<ExecutionResultV8>;
294
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qaick/executor",
3
- "version": "0.2.6",
3
+ "version": "0.3.1",
4
4
  "description": "Host-neutral QAick action execution primitives.",
5
5
  "files": [
6
6
  "dist",
@@ -34,7 +34,7 @@
34
34
  "prepublishOnly": "npm run typecheck && npm test && npm run build"
35
35
  },
36
36
  "dependencies": {
37
- "@qaick/contract": "0.5.0"
37
+ "@qaick/contract": "0.6.2"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@types/node": "^24.13.3",