@usearazzo/runner 1.0.1-alpha.20 → 1.0.1-alpha.22

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.
@@ -37,11 +37,27 @@ export const STEP_TARGET_FIELDS = ['operationId', 'operationPath', 'workflowId']
37
37
  * @public
38
38
  */
39
39
 
40
+ /**
41
+ * The outcome of one criterion a step evaluated: one of its `successCriteria`,
42
+ * or one of the `criteria` of an action it considered.
43
+ * @public
44
+ */
45
+
40
46
  /**
41
47
  * The outcome of executing a single step.
42
48
  * @public
43
49
  */
44
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
+
45
61
  /**
46
62
  * Executes a single Arazzo step that invokes an OpenAPI operation.
47
63
  *
@@ -183,16 +199,22 @@ class StepExecutor {
183
199
  // evaluate criteria, outputs, and the next action against the post-request
184
200
  // context (with $request / $url / $method and $response / $statusCode).
185
201
  const postContext = state.toContext(this.#requestContext(response), this.#responseContext(response));
186
- const successful = this.evaluateCriteria(step, postContext);
202
+ const successCriteria = this.evaluateCriteria(step, postContext);
203
+ const successful = successCriteria.every(criterion => criterion.met);
187
204
  const outputs = this.#outputResolver.resolve(step.outputs, expression => this.#evaluate(postContext, expression), scope);
188
- const matchedActions = this.selectActions(step, successful, postContext);
205
+ const {
206
+ matchedActions,
207
+ actionCriteria
208
+ } = this.selectActions(step, successful, postContext);
189
209
  return {
190
210
  stepId,
191
211
  response,
192
212
  successful,
193
213
  outputs,
194
214
  action: matchedActions[0],
195
- matchedActions
215
+ matchedActions,
216
+ successCriteria,
217
+ actionCriteria
196
218
  };
197
219
  }
198
220
 
@@ -265,9 +287,12 @@ class StepExecutor {
265
287
  }
266
288
 
267
289
  /**
268
- * Evaluates the step's `successCriteria`; all criteria must pass. A step with
269
- * no criteria succeeds on a received response. An unreadable
270
- * `successCriteria` throws, see {@link StepExecutor.assertSuccessCriteria}.
290
+ * Evaluates the step's `successCriteria`, returning one record per criterion
291
+ * in list order; the step succeeds when every record is met. Every criterion
292
+ * is evaluated, so the records explain each unmet one, not only the first. A
293
+ * step with no criteria returns no records and succeeds on a received
294
+ * response. An unreadable `successCriteria` throws, see
295
+ * {@link StepExecutor.assertSuccessCriteria}.
271
296
  *
272
297
  * Public for the same reason as {@link StepExecutor.selectActions}: a step
273
298
  * targeting a `workflowId` is run by the workflow executor and never reaches
@@ -277,15 +302,16 @@ class StepExecutor {
277
302
  evaluateCriteria(step, context) {
278
303
  this.assertSuccessCriteria(step);
279
304
  const criteria = step.successCriteria;
280
- if (criteria === undefined) return true;
305
+ if (criteria === undefined) return [];
281
306
 
282
307
  // assertSuccessCriteria above guarantees every entry is a Criterion Object
283
- return [...criteria].every(criterion => this.#criterionEvaluator.evaluate(criterion, expression => this.#evaluate(context, expression)));
308
+ return [...criteria].map(criterion => this.#evaluateCriterion(criterion, context));
284
309
  }
285
310
 
286
311
  /**
287
312
  * Selects every matching `onSuccess` / `onFailure` action for the outcome, in
288
- * list order, gating each action's criteria against the context.
313
+ * list order, gating each action's criteria against the context, and records
314
+ * the outcome of every action criterion evaluated.
289
315
  *
290
316
  * The step's own list is the whole story here: a step that declares none of
291
317
  * its own already carries the workflow-level defaults, inherited by
@@ -304,25 +330,65 @@ class StepExecutor {
304
330
  * the expression context through this one method rather than wiring their own.
305
331
  */
306
332
  selectActions(step, successful, context) {
307
- const isCriterionMet = criterion => {
308
- try {
309
- return this.#criterionEvaluator.evaluate(criterion, expression => this.#evaluate(context, expression));
310
- } catch (error) {
311
- // an action's criteria decide whether it applies to this outcome, and
312
- // actions are shared — inherited from the workflow, reused from
313
- // components. A condition on an HTTP exchange cannot apply to a step
314
- // that calls a workflow, so the action does not match; the run does not
315
- // abort over a default written for the operation steps.
316
- if (error instanceof RuntimeExpressionError && error.reason === 'unavailable-expression') {
317
- return false;
318
- }
319
- throw error;
320
- }
333
+ const actionCriteria = [];
334
+ const isCriterionMet = (criterion, action) => {
335
+ const record = {
336
+ ...this.#evaluateCriterion(criterion, context),
337
+ actionName: toValue(action.name)
338
+ };
339
+ actionCriteria.push(record);
340
+ return record.met;
321
341
  };
322
342
  const scope = {
323
343
  stepId: toValue(step.stepId)
324
344
  };
325
- return successful ? this.#actionResolver.resolveOnSuccess(step.onSuccess, isCriterionMet, scope) : this.#actionResolver.resolveOnFailure(step.onFailure, isCriterionMet, scope);
345
+ const matchedActions = successful ? this.#actionResolver.resolveOnSuccess(step.onSuccess, isCriterionMet, scope) : this.#actionResolver.resolveOnFailure(step.onFailure, isCriterionMet, scope);
346
+ return {
347
+ matchedActions,
348
+ actionCriteria
349
+ };
350
+ }
351
+
352
+ /**
353
+ * Evaluates one criterion into a {@link CriterionEvaluationRecord}. A
354
+ * condition that cannot be evaluated is unmet and records why; any other
355
+ * error propagates.
356
+ */
357
+ #evaluateCriterion(criterion, context) {
358
+ const condition = toValue(criterion.condition);
359
+ try {
360
+ const {
361
+ met,
362
+ error
363
+ } = this.#criterionEvaluator.evaluate(criterion, expression => this.#evaluate(context, expression));
364
+ if (error === undefined) return {
365
+ condition,
366
+ met
367
+ };
368
+ return {
369
+ condition,
370
+ met,
371
+ diagnostic: {
372
+ code: error.reason,
373
+ message: error.message
374
+ }
375
+ };
376
+ } catch (error) {
377
+ // a step that calls a workflow made no HTTP call, so a condition on one
378
+ // fails like any unevaluable condition; the diagnostic keeps the
379
+ // authoring mistake visible.
380
+ if (error instanceof RuntimeExpressionError && error.reason === 'unavailable-expression') {
381
+ return {
382
+ condition,
383
+ met: false,
384
+ diagnostic: {
385
+ code: error.reason,
386
+ message: error.message
387
+ }
388
+ };
389
+ }
390
+ throw error;
391
+ }
326
392
  }
327
393
 
328
394
  /**
@@ -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
@@ -359,6 +359,8 @@ class WorkflowExecutor {
359
359
  action,
360
360
  attempts,
361
361
  durationMs: this.#now() - stepStartedAt,
362
+ successCriteria: outcome.successCriteria,
363
+ actionCriteria: outcome.actionCriteria,
362
364
  ...(subWorkflows.length > 0 ? {
363
365
  subWorkflows
364
366
  } : {}),
@@ -776,12 +778,6 @@ class WorkflowExecutor {
776
778
  const locator = await this.#workflowLocatorNormalizer.normalize(reference, document, {
777
779
  workflowId
778
780
  });
779
- if (!locator.document.workflowIndex.has(locator.workflowId)) {
780
- throw new _ExecutionError.default(`workflow "${workflowId}" depends on "${reference}", which the Arazzo document at "${locator.document.uri}" does not define`, {
781
- workflowId,
782
- reason: 'workflow-not-found'
783
- });
784
- }
785
781
  refs.push({
786
782
  locator,
787
783
  inputsKey: reference
@@ -889,19 +885,26 @@ class WorkflowExecutor {
889
885
  // apply on top — they are the author's assertion about this step, and
890
886
  // dropping them because the step happens to target a workflow would
891
887
  // silently discard it. They see the sub-run's own outputs (`{}` when it
892
- // transferred) through `$outputs` and `$workflows`.
888
+ // transferred) through `$outputs` and `$workflows`, and are evaluated
889
+ // even when the sub-run failed, so the record explains them either way.
893
890
  // the criteria and actions are the *calling* step's, so they are
894
891
  // evaluated by the parent document's step executor — only the sub-run
895
892
  // itself switched documents.
896
893
  const stepExecutor = this.#stepExecutor.forDocument(document);
897
- const successful = !this.#settledFailed(result) && stepExecutor.evaluateCriteria(step, context);
898
- const matchedActions = stepExecutor.selectActions(step, successful, context);
894
+ const successCriteria = stepExecutor.evaluateCriteria(step, context);
895
+ const successful = !this.#settledFailed(result) && successCriteria.every(criterion => criterion.met);
896
+ const {
897
+ matchedActions,
898
+ actionCriteria
899
+ } = stepExecutor.selectActions(step, successful, context);
899
900
  return {
900
901
  stepId,
901
902
  successful,
902
903
  outputs,
903
904
  action: matchedActions[0],
904
- matchedActions
905
+ matchedActions,
906
+ successCriteria,
907
+ actionCriteria
905
908
  };
906
909
  };
907
910
  }
@@ -963,24 +966,14 @@ class WorkflowExecutor {
963
966
  *
964
967
  * Shared rather than duplicated by its two callers, which otherwise repeat
965
968
  * the identical "resolve the reference, then run with no inputs" sequence.
966
- * Rejects a target its document does not define with the calling `stepId`
967
- * attached, unlike {@link WorkflowExecutor.#resolveWorkflow}'s own
968
- * `workflow-not-found` (reached only once `#run` itself resolves the
969
- * target) — which carries no caller context at all, since it is reached
970
- * generically from every kind of nested call.
969
+ * A target its document does not define is rejected by the normalizer,
970
+ * attributed to the calling workflow and `stepId`.
971
971
  */
972
972
  async #runReferencedWorkflow(reference, stepId, invocation, scope, via) {
973
973
  const locator = await this.#workflowLocatorNormalizer.normalize(reference, invocation.document, {
974
974
  workflowId: invocation.workflowId,
975
975
  stepId
976
976
  });
977
- if (!locator.document.workflowIndex.has(locator.workflowId)) {
978
- throw new _ExecutionError.default(`workflow "${locator.workflowId}" not found in Arazzo document at "${locator.document.uri}"`, {
979
- workflowId: locator.workflowId,
980
- stepId,
981
- reason: 'workflow-not-found'
982
- });
983
- }
984
977
  return this.#run(locator, {}, scope, invocation.callStack, via);
985
978
  }
986
979
 
@@ -988,7 +981,11 @@ class WorkflowExecutor {
988
981
  * Extracts and normalizes the workflow a locator names. A workflow its
989
982
  * document does not define is the executor-level `workflow-not-found`
990
983
  * authoring error, raised here rather than leaking the extractor's
991
- * `ExtractionError`.
984
+ * `ExtractionError`. Only the root {@link WorkflowExecutor.execute} id
985
+ * reaches this check unvalidated — every reference position goes through
986
+ * the locator normalizer, which already rejects a missing workflow with
987
+ * the referencing context attached — so the error's `workflowId` is the
988
+ * missing id itself.
992
989
  */
993
990
  async #resolveWorkflow(locator, scope) {
994
991
  const {
@@ -345,6 +345,8 @@ class WorkflowExecutor {
345
345
  action,
346
346
  attempts,
347
347
  durationMs: this.#now() - stepStartedAt,
348
+ successCriteria: outcome.successCriteria,
349
+ actionCriteria: outcome.actionCriteria,
348
350
  ...(subWorkflows.length > 0 ? {
349
351
  subWorkflows
350
352
  } : {}),
@@ -762,12 +764,6 @@ class WorkflowExecutor {
762
764
  const locator = await this.#workflowLocatorNormalizer.normalize(reference, document, {
763
765
  workflowId
764
766
  });
765
- if (!locator.document.workflowIndex.has(locator.workflowId)) {
766
- throw new ExecutionError(`workflow "${workflowId}" depends on "${reference}", which the Arazzo document at "${locator.document.uri}" does not define`, {
767
- workflowId,
768
- reason: 'workflow-not-found'
769
- });
770
- }
771
767
  refs.push({
772
768
  locator,
773
769
  inputsKey: reference
@@ -875,19 +871,26 @@ class WorkflowExecutor {
875
871
  // apply on top — they are the author's assertion about this step, and
876
872
  // dropping them because the step happens to target a workflow would
877
873
  // silently discard it. They see the sub-run's own outputs (`{}` when it
878
- // transferred) through `$outputs` and `$workflows`.
874
+ // transferred) through `$outputs` and `$workflows`, and are evaluated
875
+ // even when the sub-run failed, so the record explains them either way.
879
876
  // the criteria and actions are the *calling* step's, so they are
880
877
  // evaluated by the parent document's step executor — only the sub-run
881
878
  // itself switched documents.
882
879
  const stepExecutor = this.#stepExecutor.forDocument(document);
883
- const successful = !this.#settledFailed(result) && stepExecutor.evaluateCriteria(step, context);
884
- const matchedActions = stepExecutor.selectActions(step, successful, context);
880
+ const successCriteria = stepExecutor.evaluateCriteria(step, context);
881
+ const successful = !this.#settledFailed(result) && successCriteria.every(criterion => criterion.met);
882
+ const {
883
+ matchedActions,
884
+ actionCriteria
885
+ } = stepExecutor.selectActions(step, successful, context);
885
886
  return {
886
887
  stepId,
887
888
  successful,
888
889
  outputs,
889
890
  action: matchedActions[0],
890
- matchedActions
891
+ matchedActions,
892
+ successCriteria,
893
+ actionCriteria
891
894
  };
892
895
  };
893
896
  }
@@ -949,24 +952,14 @@ class WorkflowExecutor {
949
952
  *
950
953
  * Shared rather than duplicated by its two callers, which otherwise repeat
951
954
  * the identical "resolve the reference, then run with no inputs" sequence.
952
- * Rejects a target its document does not define with the calling `stepId`
953
- * attached, unlike {@link WorkflowExecutor.#resolveWorkflow}'s own
954
- * `workflow-not-found` (reached only once `#run` itself resolves the
955
- * target) — which carries no caller context at all, since it is reached
956
- * generically from every kind of nested call.
955
+ * A target its document does not define is rejected by the normalizer,
956
+ * attributed to the calling workflow and `stepId`.
957
957
  */
958
958
  async #runReferencedWorkflow(reference, stepId, invocation, scope, via) {
959
959
  const locator = await this.#workflowLocatorNormalizer.normalize(reference, invocation.document, {
960
960
  workflowId: invocation.workflowId,
961
961
  stepId
962
962
  });
963
- if (!locator.document.workflowIndex.has(locator.workflowId)) {
964
- throw new ExecutionError(`workflow "${locator.workflowId}" not found in Arazzo document at "${locator.document.uri}"`, {
965
- workflowId: locator.workflowId,
966
- stepId,
967
- reason: 'workflow-not-found'
968
- });
969
- }
970
963
  return this.#run(locator, {}, scope, invocation.callStack, via);
971
964
  }
972
965
 
@@ -974,7 +967,11 @@ class WorkflowExecutor {
974
967
  * Extracts and normalizes the workflow a locator names. A workflow its
975
968
  * document does not define is the executor-level `workflow-not-found`
976
969
  * authoring error, raised here rather than leaking the extractor's
977
- * `ExtractionError`.
970
+ * `ExtractionError`. Only the root {@link WorkflowExecutor.execute} id
971
+ * reaches this check unvalidated — every reference position goes through
972
+ * the locator normalizer, which already rejects a missing workflow with
973
+ * the referencing context attached — so the error's `workflowId` is the
974
+ * missing id itself.
978
975
  */
979
976
  async #resolveWorkflow(locator, scope) {
980
977
  const {