@usearazzo/runner 1.0.1-alpha.21 → 1.0.1-alpha.23

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.
@@ -55,6 +55,16 @@ const STEP_TARGET_FIELDS = exports.STEP_TARGET_FIELDS = ['operationId', 'operati
55
55
  * @public
56
56
  */
57
57
 
58
+ /**
59
+ * What one attempt at a step produced.
60
+ *
61
+ * Both kinds of step reduce to this — an operation step (whose richer
62
+ * {@link StepExecutionResult} satisfies this shape) and a sub-workflow step
63
+ * (synthesized from the sub-run's outcome) — so the retry loop drives them
64
+ * identically and knows nothing about either.
65
+ * @internal
66
+ */
67
+
58
68
  /**
59
69
  * Executes a single Arazzo step that invokes an OpenAPI operation.
60
70
  *
@@ -124,38 +134,45 @@ class StepExecutor {
124
134
  * request nobody is waiting for, whose wire-level cancellation would surface
125
135
  * as a transport {@link ClientError} — a request that failed on its own terms
126
136
  * — rather than as the withdrawal it is.
137
+ *
138
+ * `workflowId` names the workflow the step belongs to; every error raised
139
+ * here then carries it alongside the `stepId`, since step ids are only
140
+ * unique within a workflow. The workflow executor always passes it.
127
141
  */
128
- async execute(step, state, executeOptions = {}) {
142
+ async execute(step, state, executeOptions = {}, workflowId) {
129
143
  const stepId = (0, _apidomCore.toValue)(step.stepId);
144
+ const scope = {
145
+ ...(workflowId === undefined ? {} : {
146
+ workflowId
147
+ }),
148
+ stepId
149
+ };
130
150
 
131
151
  // a step's target fields are mutually exclusive; declaring more than one is
132
152
  // malformed and has no defined resolution.
133
153
  const declaredTargets = STEP_TARGET_FIELDS.filter(field => (0, _apidomDatamodel.isStringElement)(step[field]));
134
154
  if (declaredTargets.length > 1) {
135
155
  throw new _ExecutionError.default(`Step "${stepId}" declares more than one of ${STEP_TARGET_FIELDS.join(', ')} (mutually exclusive)`, {
136
- stepId,
156
+ ...scope,
137
157
  reason: 'ambiguous-target'
138
158
  });
139
159
  }
140
160
  if ((0, _apidomDatamodel.isStringElement)(step.workflowId)) {
141
161
  throw new _ExecutionError.default(`Step "${stepId}" targets a workflowId; use the workflow executor`, {
142
- stepId,
162
+ ...scope,
143
163
  reason: 'workflow-step'
144
164
  });
145
165
  }
146
- this.assertSuccessCriteria(step);
166
+ this.assertSuccessCriteria(step, workflowId);
147
167
 
148
168
  // locate which operation the step invokes — reduced to a (document, JSON
149
169
  // Pointer) locator that the operation executor extracts, normalizes,
150
170
  // assembles into a standalone doc, and runs.
151
- const locator = await this.#locateOperation(step, stepId);
171
+ const locator = await this.#locateOperation(step, scope);
152
172
 
153
173
  // resolve parameters and request body against the pre-request context.
154
174
  // keyed by '{in}.{name}' rather than bare name, so two parameters that
155
175
  // legally differ only in their location each reach their own place.
156
- const scope = {
157
- stepId
158
- };
159
176
  const preContext = state.toContext();
160
177
  const parameters = this.#parameterResolver.resolve(step.parameters, expression => this.#evaluate(preContext, expression), scope);
161
178
  const requestBody = this.#requestBodyResolver.resolve(step.requestBody, expression => this.#evaluate(preContext, expression), scope);
@@ -166,9 +183,7 @@ class StepExecutor {
166
183
  // live side effect. The signal stays in the bag, so a request already in
167
184
  // flight is aborted by the transport as before.
168
185
  const signal = (0, _abort.readAbortSignal)(executeOptions);
169
- (0, _abort.throwIfAborted)(signal, {
170
- stepId
171
- });
186
+ (0, _abort.throwIfAborted)(signal, scope);
172
187
 
173
188
  // the Arazzo-derived parameters and request body are spread after the opaque
174
189
  // executeOptions bag so they take precedence over it. the operation executor
@@ -187,22 +202,20 @@ class StepExecutor {
187
202
  // cancelled, it did not: reporting it as the abort keeps one error shape
188
203
  // whether the run was withdrawn between two steps or mid-flight, and keeps
189
204
  // the whole feature from reading differently per transport.
190
- (0, _abort.throwIfAborted)(signal, {
191
- stepId
192
- });
205
+ (0, _abort.throwIfAborted)(signal, scope);
193
206
  throw error;
194
207
  }
195
208
 
196
209
  // evaluate criteria, outputs, and the next action against the post-request
197
210
  // context (with $request / $url / $method and $response / $statusCode).
198
211
  const postContext = state.toContext(this.#requestContext(response), this.#responseContext(response));
199
- const successCriteria = this.evaluateCriteria(step, postContext);
212
+ const successCriteria = this.evaluateCriteria(step, postContext, workflowId);
200
213
  const successful = successCriteria.every(criterion => criterion.met);
201
214
  const outputs = this.#outputResolver.resolve(step.outputs, expression => this.#evaluate(postContext, expression), scope);
202
215
  const {
203
216
  matchedActions,
204
217
  actionCriteria
205
- } = this.selectActions(step, successful, postContext);
218
+ } = this.selectActions(step, successful, postContext, workflowId);
206
219
  return {
207
220
  stepId,
208
221
  response,
@@ -232,15 +245,15 @@ class StepExecutor {
232
245
  requestContentType: requestBody.contentType
233
246
  };
234
247
  }
235
- async #locateOperation(step, stepId) {
248
+ async #locateOperation(step, scope) {
236
249
  if ((0, _apidomDatamodel.isStringElement)(step.operationId)) {
237
- return this.#locatorNormalizer.normalizeOperationId((0, _apidomCore.toValue)(step.operationId), this.#document);
250
+ return this.#locatorNormalizer.normalizeOperationId((0, _apidomCore.toValue)(step.operationId), this.#document, scope);
238
251
  }
239
252
  if ((0, _apidomDatamodel.isStringElement)(step.operationPath)) {
240
- return this.#locatorNormalizer.normalizeOperationPath((0, _apidomCore.toValue)(step.operationPath), this.#document);
253
+ return this.#locatorNormalizer.normalizeOperationPath((0, _apidomCore.toValue)(step.operationPath), this.#document, scope);
241
254
  }
242
- throw new _ExecutionError.default(`Step "${stepId}" has no operationId or operationPath`, {
243
- stepId,
255
+ throw new _ExecutionError.default(`Step "${scope.stepId}" has no operationId or operationPath`, {
256
+ ...scope,
244
257
  reason: 'missing-operation'
245
258
  });
246
259
  }
@@ -294,10 +307,12 @@ class StepExecutor {
294
307
  * Public for the same reason as {@link StepExecutor.selectActions}: a step
295
308
  * targeting a `workflowId` is run by the workflow executor and never reaches
296
309
  * {@link StepExecutor.execute}, but its criteria are still the author's
297
- * assertion about the step and must be evaluated somewhere.
310
+ * assertion about the step and must be evaluated somewhere. `workflowId`
311
+ * names the step's workflow on a thrown error, as for
312
+ * {@link StepExecutor.execute}.
298
313
  */
299
- evaluateCriteria(step, context) {
300
- this.assertSuccessCriteria(step);
314
+ evaluateCriteria(step, context, workflowId) {
315
+ this.assertSuccessCriteria(step, workflowId);
301
316
  const criteria = step.successCriteria;
302
317
  if (criteria === undefined) return [];
303
318
 
@@ -325,8 +340,10 @@ class StepExecutor {
325
340
  * it runs itself and which therefore never reaches
326
341
  * {@link StepExecutor.execute}. Both callers reach the criteria evaluator and
327
342
  * the expression context through this one method rather than wiring their own.
343
+ * `workflowId` names the step's workflow on a thrown error, as for
344
+ * {@link StepExecutor.execute}.
328
345
  */
329
- selectActions(step, successful, context) {
346
+ selectActions(step, successful, context, workflowId) {
330
347
  const actionCriteria = [];
331
348
  const isCriterionMet = (criterion, action) => {
332
349
  const record = {
@@ -337,6 +354,9 @@ class StepExecutor {
337
354
  return record.met;
338
355
  };
339
356
  const scope = {
357
+ ...(workflowId === undefined ? {} : {
358
+ workflowId
359
+ }),
340
360
  stepId: (0, _apidomCore.toValue)(step.stepId)
341
361
  };
342
362
  const matchedActions = successful ? this.#actionResolver.resolveOnSuccess(step.onSuccess, isCriterionMet, scope) : this.#actionResolver.resolveOnFailure(step.onFailure, isCriterionMet, scope);
@@ -48,6 +48,16 @@ export const STEP_TARGET_FIELDS = ['operationId', 'operationPath', 'workflowId']
48
48
  * @public
49
49
  */
50
50
 
51
+ /**
52
+ * What one attempt at a step produced.
53
+ *
54
+ * Both kinds of step reduce to this — an operation step (whose richer
55
+ * {@link StepExecutionResult} satisfies this shape) and a sub-workflow step
56
+ * (synthesized from the sub-run's outcome) — so the retry loop drives them
57
+ * identically and knows nothing about either.
58
+ * @internal
59
+ */
60
+
51
61
  /**
52
62
  * Executes a single Arazzo step that invokes an OpenAPI operation.
53
63
  *
@@ -117,38 +127,45 @@ class StepExecutor {
117
127
  * request nobody is waiting for, whose wire-level cancellation would surface
118
128
  * as a transport {@link ClientError} — a request that failed on its own terms
119
129
  * — rather than as the withdrawal it is.
130
+ *
131
+ * `workflowId` names the workflow the step belongs to; every error raised
132
+ * here then carries it alongside the `stepId`, since step ids are only
133
+ * unique within a workflow. The workflow executor always passes it.
120
134
  */
121
- async execute(step, state, executeOptions = {}) {
135
+ async execute(step, state, executeOptions = {}, workflowId) {
122
136
  const stepId = toValue(step.stepId);
137
+ const scope = {
138
+ ...(workflowId === undefined ? {} : {
139
+ workflowId
140
+ }),
141
+ stepId
142
+ };
123
143
 
124
144
  // a step's target fields are mutually exclusive; declaring more than one is
125
145
  // malformed and has no defined resolution.
126
146
  const declaredTargets = STEP_TARGET_FIELDS.filter(field => isStringElement(step[field]));
127
147
  if (declaredTargets.length > 1) {
128
148
  throw new ExecutionError(`Step "${stepId}" declares more than one of ${STEP_TARGET_FIELDS.join(', ')} (mutually exclusive)`, {
129
- stepId,
149
+ ...scope,
130
150
  reason: 'ambiguous-target'
131
151
  });
132
152
  }
133
153
  if (isStringElement(step.workflowId)) {
134
154
  throw new ExecutionError(`Step "${stepId}" targets a workflowId; use the workflow executor`, {
135
- stepId,
155
+ ...scope,
136
156
  reason: 'workflow-step'
137
157
  });
138
158
  }
139
- this.assertSuccessCriteria(step);
159
+ this.assertSuccessCriteria(step, workflowId);
140
160
 
141
161
  // locate which operation the step invokes — reduced to a (document, JSON
142
162
  // Pointer) locator that the operation executor extracts, normalizes,
143
163
  // assembles into a standalone doc, and runs.
144
- const locator = await this.#locateOperation(step, stepId);
164
+ const locator = await this.#locateOperation(step, scope);
145
165
 
146
166
  // resolve parameters and request body against the pre-request context.
147
167
  // keyed by '{in}.{name}' rather than bare name, so two parameters that
148
168
  // legally differ only in their location each reach their own place.
149
- const scope = {
150
- stepId
151
- };
152
169
  const preContext = state.toContext();
153
170
  const parameters = this.#parameterResolver.resolve(step.parameters, expression => this.#evaluate(preContext, expression), scope);
154
171
  const requestBody = this.#requestBodyResolver.resolve(step.requestBody, expression => this.#evaluate(preContext, expression), scope);
@@ -159,9 +176,7 @@ class StepExecutor {
159
176
  // live side effect. The signal stays in the bag, so a request already in
160
177
  // flight is aborted by the transport as before.
161
178
  const signal = readAbortSignal(executeOptions);
162
- throwIfAborted(signal, {
163
- stepId
164
- });
179
+ throwIfAborted(signal, scope);
165
180
 
166
181
  // the Arazzo-derived parameters and request body are spread after the opaque
167
182
  // executeOptions bag so they take precedence over it. the operation executor
@@ -180,22 +195,20 @@ class StepExecutor {
180
195
  // cancelled, it did not: reporting it as the abort keeps one error shape
181
196
  // whether the run was withdrawn between two steps or mid-flight, and keeps
182
197
  // the whole feature from reading differently per transport.
183
- throwIfAborted(signal, {
184
- stepId
185
- });
198
+ throwIfAborted(signal, scope);
186
199
  throw error;
187
200
  }
188
201
 
189
202
  // evaluate criteria, outputs, and the next action against the post-request
190
203
  // context (with $request / $url / $method and $response / $statusCode).
191
204
  const postContext = state.toContext(this.#requestContext(response), this.#responseContext(response));
192
- const successCriteria = this.evaluateCriteria(step, postContext);
205
+ const successCriteria = this.evaluateCriteria(step, postContext, workflowId);
193
206
  const successful = successCriteria.every(criterion => criterion.met);
194
207
  const outputs = this.#outputResolver.resolve(step.outputs, expression => this.#evaluate(postContext, expression), scope);
195
208
  const {
196
209
  matchedActions,
197
210
  actionCriteria
198
- } = this.selectActions(step, successful, postContext);
211
+ } = this.selectActions(step, successful, postContext, workflowId);
199
212
  return {
200
213
  stepId,
201
214
  response,
@@ -225,15 +238,15 @@ class StepExecutor {
225
238
  requestContentType: requestBody.contentType
226
239
  };
227
240
  }
228
- async #locateOperation(step, stepId) {
241
+ async #locateOperation(step, scope) {
229
242
  if (isStringElement(step.operationId)) {
230
- return this.#locatorNormalizer.normalizeOperationId(toValue(step.operationId), this.#document);
243
+ return this.#locatorNormalizer.normalizeOperationId(toValue(step.operationId), this.#document, scope);
231
244
  }
232
245
  if (isStringElement(step.operationPath)) {
233
- return this.#locatorNormalizer.normalizeOperationPath(toValue(step.operationPath), this.#document);
246
+ return this.#locatorNormalizer.normalizeOperationPath(toValue(step.operationPath), this.#document, scope);
234
247
  }
235
- throw new ExecutionError(`Step "${stepId}" has no operationId or operationPath`, {
236
- stepId,
248
+ throw new ExecutionError(`Step "${scope.stepId}" has no operationId or operationPath`, {
249
+ ...scope,
237
250
  reason: 'missing-operation'
238
251
  });
239
252
  }
@@ -287,10 +300,12 @@ class StepExecutor {
287
300
  * Public for the same reason as {@link StepExecutor.selectActions}: a step
288
301
  * targeting a `workflowId` is run by the workflow executor and never reaches
289
302
  * {@link StepExecutor.execute}, but its criteria are still the author's
290
- * assertion about the step and must be evaluated somewhere.
303
+ * assertion about the step and must be evaluated somewhere. `workflowId`
304
+ * names the step's workflow on a thrown error, as for
305
+ * {@link StepExecutor.execute}.
291
306
  */
292
- evaluateCriteria(step, context) {
293
- this.assertSuccessCriteria(step);
307
+ evaluateCriteria(step, context, workflowId) {
308
+ this.assertSuccessCriteria(step, workflowId);
294
309
  const criteria = step.successCriteria;
295
310
  if (criteria === undefined) return [];
296
311
 
@@ -318,8 +333,10 @@ class StepExecutor {
318
333
  * it runs itself and which therefore never reaches
319
334
  * {@link StepExecutor.execute}. Both callers reach the criteria evaluator and
320
335
  * the expression context through this one method rather than wiring their own.
336
+ * `workflowId` names the step's workflow on a thrown error, as for
337
+ * {@link StepExecutor.execute}.
321
338
  */
322
- selectActions(step, successful, context) {
339
+ selectActions(step, successful, context, workflowId) {
323
340
  const actionCriteria = [];
324
341
  const isCriterionMet = (criterion, action) => {
325
342
  const record = {
@@ -330,6 +347,9 @@ class StepExecutor {
330
347
  return record.met;
331
348
  };
332
349
  const scope = {
350
+ ...(workflowId === undefined ? {} : {
351
+ workflowId
352
+ }),
333
353
  stepId: toValue(step.stepId)
334
354
  };
335
355
  const matchedActions = successful ? this.#actionResolver.resolveOnSuccess(step.onSuccess, isCriterionMet, scope) : this.#actionResolver.resolveOnFailure(step.onFailure, isCriterionMet, scope);
@@ -9,16 +9,6 @@ var _apidomCore = require("@speclynx/apidom-core");
9
9
  var _apidomDatamodel = require("@speclynx/apidom-datamodel");
10
10
  var _apidomNsArazzo = require("@speclynx/apidom-ns-arazzo-1");
11
11
  var _ExecutionError = _interopRequireDefault(require("../errors/ExecutionError.cjs"));
12
- /**
13
- * What one attempt at a step produced.
14
- *
15
- * Both kinds of step reduce to this — an operation step (whose richer
16
- * `StepExecutionResult` satisfies this shape) and a sub-workflow step
17
- * (synthesized from the sub-run's outcome) — so the retry loop drives them
18
- * identically and knows nothing about either.
19
- * @internal
20
- */
21
-
22
12
  /**
23
13
  * The terminal outcome of a step once its retries are settled.
24
14
  * @internal
@@ -2,15 +2,6 @@ import { toValue } from '@speclynx/apidom-core';
2
2
  import { isNumberElement, isStringElement } from '@speclynx/apidom-datamodel';
3
3
  import { isFailureActionElement } from '@speclynx/apidom-ns-arazzo-1';
4
4
  import ExecutionError from "../errors/ExecutionError.mjs";
5
- /**
6
- * What one attempt at a step produced.
7
- *
8
- * Both kinds of step reduce to this — an operation step (whose richer
9
- * `StepExecutionResult` satisfies this shape) and a sub-workflow step
10
- * (synthesized from the sub-run's outcome) — so the retry loop drives them
11
- * identically and knows nothing about either.
12
- * @internal
13
- */
14
5
  /**
15
6
  * The terminal outcome of a step once its retries are settled.
16
7
  * @internal
@@ -778,12 +778,6 @@ class WorkflowExecutor {
778
778
  const locator = await this.#workflowLocatorNormalizer.normalize(reference, document, {
779
779
  workflowId
780
780
  });
781
- if (!locator.document.workflowIndex.has(locator.workflowId)) {
782
- throw new _ExecutionError.default(`workflow "${workflowId}" depends on "${reference}", which the Arazzo document at "${locator.document.uri}" does not define`, {
783
- workflowId,
784
- reason: 'workflow-not-found'
785
- });
786
- }
787
781
  refs.push({
788
782
  locator,
789
783
  inputsKey: reference
@@ -816,10 +810,11 @@ class WorkflowExecutor {
816
810
  callStack
817
811
  } = invocation;
818
812
  if (!(0, _apidomDatamodel.isStringElement)(step.workflowId)) {
819
- return () => this.#stepExecutor.forDocument(document).execute(step, state, scope.executeOptions);
813
+ return () => this.#stepExecutor.forDocument(document).execute(step, state, scope.executeOptions, workflowId);
820
814
  }
821
815
  const reference = this.#subWorkflowReference(step, stepId, workflowId);
822
816
  const stepScope = {
817
+ workflowId,
823
818
  stepId
824
819
  };
825
820
  // the reference resolves to the same locator on every attempt, so it is
@@ -897,12 +892,12 @@ class WorkflowExecutor {
897
892
  // evaluated by the parent document's step executor — only the sub-run
898
893
  // itself switched documents.
899
894
  const stepExecutor = this.#stepExecutor.forDocument(document);
900
- const successCriteria = stepExecutor.evaluateCriteria(step, context);
895
+ const successCriteria = stepExecutor.evaluateCriteria(step, context, workflowId);
901
896
  const successful = !this.#settledFailed(result) && successCriteria.every(criterion => criterion.met);
902
897
  const {
903
898
  matchedActions,
904
899
  actionCriteria
905
- } = stepExecutor.selectActions(step, successful, context);
900
+ } = stepExecutor.selectActions(step, successful, context, workflowId);
906
901
  return {
907
902
  stepId,
908
903
  successful,
@@ -972,24 +967,14 @@ class WorkflowExecutor {
972
967
  *
973
968
  * Shared rather than duplicated by its two callers, which otherwise repeat
974
969
  * the identical "resolve the reference, then run with no inputs" sequence.
975
- * Rejects a target its document does not define with the calling `stepId`
976
- * attached, unlike {@link WorkflowExecutor.#resolveWorkflow}'s own
977
- * `workflow-not-found` (reached only once `#run` itself resolves the
978
- * target) — which carries no caller context at all, since it is reached
979
- * generically from every kind of nested call.
970
+ * A target its document does not define is rejected by the normalizer,
971
+ * attributed to the calling workflow and `stepId`.
980
972
  */
981
973
  async #runReferencedWorkflow(reference, stepId, invocation, scope, via) {
982
974
  const locator = await this.#workflowLocatorNormalizer.normalize(reference, invocation.document, {
983
975
  workflowId: invocation.workflowId,
984
976
  stepId
985
977
  });
986
- if (!locator.document.workflowIndex.has(locator.workflowId)) {
987
- throw new _ExecutionError.default(`workflow "${locator.workflowId}" not found in Arazzo document at "${locator.document.uri}"`, {
988
- workflowId: locator.workflowId,
989
- stepId,
990
- reason: 'workflow-not-found'
991
- });
992
- }
993
978
  return this.#run(locator, {}, scope, invocation.callStack, via);
994
979
  }
995
980
 
@@ -997,7 +982,11 @@ class WorkflowExecutor {
997
982
  * Extracts and normalizes the workflow a locator names. A workflow its
998
983
  * document does not define is the executor-level `workflow-not-found`
999
984
  * authoring error, raised here rather than leaking the extractor's
1000
- * `ExtractionError`.
985
+ * `ExtractionError`. Only the root {@link WorkflowExecutor.execute} id
986
+ * reaches this check unvalidated — every reference position goes through
987
+ * the locator normalizer, which already rejects a missing workflow with
988
+ * the referencing context attached — so the error's `workflowId` is the
989
+ * missing id itself.
1001
990
  */
1002
991
  async #resolveWorkflow(locator, scope) {
1003
992
  const {
@@ -764,12 +764,6 @@ class WorkflowExecutor {
764
764
  const locator = await this.#workflowLocatorNormalizer.normalize(reference, document, {
765
765
  workflowId
766
766
  });
767
- if (!locator.document.workflowIndex.has(locator.workflowId)) {
768
- throw new ExecutionError(`workflow "${workflowId}" depends on "${reference}", which the Arazzo document at "${locator.document.uri}" does not define`, {
769
- workflowId,
770
- reason: 'workflow-not-found'
771
- });
772
- }
773
767
  refs.push({
774
768
  locator,
775
769
  inputsKey: reference
@@ -802,10 +796,11 @@ class WorkflowExecutor {
802
796
  callStack
803
797
  } = invocation;
804
798
  if (!isStringElement(step.workflowId)) {
805
- return () => this.#stepExecutor.forDocument(document).execute(step, state, scope.executeOptions);
799
+ return () => this.#stepExecutor.forDocument(document).execute(step, state, scope.executeOptions, workflowId);
806
800
  }
807
801
  const reference = this.#subWorkflowReference(step, stepId, workflowId);
808
802
  const stepScope = {
803
+ workflowId,
809
804
  stepId
810
805
  };
811
806
  // the reference resolves to the same locator on every attempt, so it is
@@ -883,12 +878,12 @@ class WorkflowExecutor {
883
878
  // evaluated by the parent document's step executor — only the sub-run
884
879
  // itself switched documents.
885
880
  const stepExecutor = this.#stepExecutor.forDocument(document);
886
- const successCriteria = stepExecutor.evaluateCriteria(step, context);
881
+ const successCriteria = stepExecutor.evaluateCriteria(step, context, workflowId);
887
882
  const successful = !this.#settledFailed(result) && successCriteria.every(criterion => criterion.met);
888
883
  const {
889
884
  matchedActions,
890
885
  actionCriteria
891
- } = stepExecutor.selectActions(step, successful, context);
886
+ } = stepExecutor.selectActions(step, successful, context, workflowId);
892
887
  return {
893
888
  stepId,
894
889
  successful,
@@ -958,24 +953,14 @@ class WorkflowExecutor {
958
953
  *
959
954
  * Shared rather than duplicated by its two callers, which otherwise repeat
960
955
  * the identical "resolve the reference, then run with no inputs" sequence.
961
- * Rejects a target its document does not define with the calling `stepId`
962
- * attached, unlike {@link WorkflowExecutor.#resolveWorkflow}'s own
963
- * `workflow-not-found` (reached only once `#run` itself resolves the
964
- * target) — which carries no caller context at all, since it is reached
965
- * generically from every kind of nested call.
956
+ * A target its document does not define is rejected by the normalizer,
957
+ * attributed to the calling workflow and `stepId`.
966
958
  */
967
959
  async #runReferencedWorkflow(reference, stepId, invocation, scope, via) {
968
960
  const locator = await this.#workflowLocatorNormalizer.normalize(reference, invocation.document, {
969
961
  workflowId: invocation.workflowId,
970
962
  stepId
971
963
  });
972
- if (!locator.document.workflowIndex.has(locator.workflowId)) {
973
- throw new ExecutionError(`workflow "${locator.workflowId}" not found in Arazzo document at "${locator.document.uri}"`, {
974
- workflowId: locator.workflowId,
975
- stepId,
976
- reason: 'workflow-not-found'
977
- });
978
- }
979
964
  return this.#run(locator, {}, scope, invocation.callStack, via);
980
965
  }
981
966
 
@@ -983,7 +968,11 @@ class WorkflowExecutor {
983
968
  * Extracts and normalizes the workflow a locator names. A workflow its
984
969
  * document does not define is the executor-level `workflow-not-found`
985
970
  * authoring error, raised here rather than leaking the extractor's
986
- * `ExtractionError`.
971
+ * `ExtractionError`. Only the root {@link WorkflowExecutor.execute} id
972
+ * reaches this check unvalidated — every reference position goes through
973
+ * the locator normalizer, which already rejects a missing workflow with
974
+ * the referencing context attached — so the error's `workflowId` is the
975
+ * missing id itself.
987
976
  */
988
977
  async #resolveWorkflow(locator, scope) {
989
978
  const {