@uipath/maestro-builder-sdk 6.60.0 → 6.61.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +10 -12
  2. package/dist/api-index.md +5 -4
  3. package/dist/check-cli.js +8 -7
  4. package/dist/check.d.ts +11 -0
  5. package/dist/check.js +192 -783
  6. package/dist/cli-library.d.ts +2 -1
  7. package/dist/cli-library.js +7 -2
  8. package/dist/cli-result.d.ts +14 -0
  9. package/dist/cli-result.js +18 -1
  10. package/dist/compile-cli.js +2 -2
  11. package/dist/core/compile-refusal.d.ts +7 -1
  12. package/dist/core/compile-refusal.js +9 -1
  13. package/dist/core/connector-checks.d.ts +8 -30
  14. package/dist/core/connector-checks.js +11 -60
  15. package/dist/core/connector-inputs.js +9 -1
  16. package/dist/core/event-checks.d.ts +15 -2
  17. package/dist/core/event-checks.js +28 -13
  18. package/dist/core/hitl-routing.d.ts +10 -1
  19. package/dist/core/hitl-routing.js +20 -5
  20. package/dist/core/lookups.d.ts +13 -0
  21. package/dist/core/lookups.js +26 -0
  22. package/dist/core/refusals.d.ts +2 -0
  23. package/dist/core/refusals.js +5 -1
  24. package/dist/flow-node-type.d.ts +35 -21
  25. package/dist/flow-parallel-hitl.d.ts +105 -0
  26. package/dist/flow-parallel-hitl.js +214 -0
  27. package/dist/flow-sdk.d.ts +4 -4
  28. package/dist/flow-sdk.js +2 -2
  29. package/dist/flow-terminate-names.d.ts +2 -2
  30. package/dist/flow-terminate-names.js +2 -2
  31. package/dist/merge-compile.js +2 -1
  32. package/dist/nodes/agent/flow/check.js +13 -88
  33. package/dist/nodes/agent/flow/inline-agent.js +41 -14
  34. package/dist/nodes/connector/flow/node-type.js +45 -18
  35. package/dist/nodes/document/flow/ixp.d.ts +1 -1
  36. package/dist/nodes/document/flow/ixp.js +22 -26
  37. package/dist/nodes/hitl/flow/check.js +4 -11
  38. package/dist/nodes/hitl/flow/node-type.js +16 -7
  39. package/dist/nodes/http/flow/check.js +5 -31
  40. package/dist/nodes/http/flow/node-type.js +7 -4
  41. package/dist/nodes/queue/flow/check.d.ts +4 -3
  42. package/dist/nodes/queue/flow/check.js +7 -6
  43. package/dist/nodes/queue/flow/node-type.js +1 -1
  44. package/dist/nodes/transform/flow/check.d.ts +2 -0
  45. package/dist/nodes/transform/flow/check.js +3 -6
  46. package/dist/nodes/transform/flow/node-type.js +6 -6
  47. package/dist/raw-node.js +8 -8
  48. package/dist/serialize.d.ts +38 -3
  49. package/dist/serialize.js +757 -322
  50. package/package.json +2 -2
@@ -6,7 +6,8 @@
6
6
  * stops `compile` from writing. A serializer warning (`CONNECTION_STUB`) is
7
7
  * `lint` too: it never blocks.
8
8
  * - `lowering`: `compile` cannot write a correct `.flow`. It always stops
9
- * `compile`; `check` may report it early.
9
+ * `compile`; `check` reports it too, from the serializer it runs in collect
10
+ * mode (`SerializeOpts.collect`), so the rule lives in one place.
10
11
  * - `draft`: an unfinished canvas node (no connection yet, a required input
11
12
  * left empty, a generic operation with no object). These are the codes a
12
13
  * draft-tolerant compile collects instead of throwing (`DraftCode` in
@@ -92,6 +93,8 @@ export const REFUSALS = {
92
93
  CARRIED_DEFINITION_CONFLICT: 'lowering',
93
94
  CARRIED_DEFINITION_INVALID: 'lowering',
94
95
  CEQL_UNPARSEABLE: 'lowering',
96
+ // A throw that is no refusal, which `compile` cannot get past: check reports it on its step.
97
+ COMPILE_FAILED: 'lowering',
95
98
  CONNECTION_CONNECTOR_CONFLICT: 'draft',
96
99
  CONNECTION_STUB: 'lint',
97
100
  CONNECTOR_IMPORT_NOT_A_FILE: 'lowering',
@@ -246,6 +249,7 @@ export const REFUSALS = {
246
249
  HITL_OUTCOME_ACTION: 'lint',
247
250
  HITL_OUTCOME_BAD_ID: 'lint',
248
251
  HITL_OUTCOME_NO_NAME: 'lint',
252
+ HITL_OUTCOME_PORTS_NONE: 'lowering',
249
253
  HITL_OUTCOME_PORTS_OFF: 'lowering',
250
254
  HITL_OUTCOME_PORTS_VARIANT: 'lowering',
251
255
  HITL_OUTCOME_PORT_MISPLACED: 'lint',
@@ -59,6 +59,17 @@ export interface FlowNodeContribution extends Omit<NodeContribution, 'definition
59
59
  * guard that refuses a `completed` edge on such a task (`hitlExitViolation`).
60
60
  */
61
61
  legacyCompleted?: boolean;
62
+ /**
63
+ * The node is a human task with no per-outcome exits (no outcomes, an
64
+ * app-backed variant, `legacyCompleted: true`): why, as a clause ("it declares
65
+ * no outcomes"), and what to write instead. An `outcome-<id>` edge out of it
66
+ * would leave a handle it does not have, so it is refused
67
+ * (HITL_OUTCOME_PORTS_NONE).
68
+ */
69
+ noOutcomePorts?: {
70
+ readonly why: string;
71
+ readonly suggestion: string;
72
+ };
62
73
  /**
63
74
  * `outputs` REPLACES the definition's `outputDefinition` on the node instead of
64
75
  * being merged over it — for an instance whose output slots are its own (a
@@ -195,13 +206,16 @@ export interface FlowSerializeContext {
195
206
  * Write a file beside the `.flow` (an agent's `agent.json`), at a path relative
196
207
  * to it. The same file twice is not a clash (a subflow body emitted for two
197
208
  * callers); two different contents at a path an explicit `source` chose are
198
- * refused (`INLINE_AGENT_RESOURCE_SOURCE_DUP`). The `json` object is kept, not
199
- * copied, so `typeAgentInputs` can still fill it in.
209
+ * refused (`INLINE_AGENT_RESOURCE_SOURCE_DUP`). `owner` names whoever chose
210
+ * that `source`, as the refusal names it (`inline agent step "answer"'s
211
+ * context index "SOP"`); omit it for a derived one. `true` (the older form)
212
+ * marks an explicit `source` without naming its owner. The `json` object is
213
+ * kept, not copied, so `typeAgentInputs` can still fill it in.
200
214
  */
201
215
  sidecar(file: {
202
216
  path: string;
203
217
  json: unknown;
204
- }, explicitSource?: boolean): void;
218
+ }, owner?: string | boolean): void;
205
219
  /**
206
220
  * Type an agent sidecar's `inputSchema` once this scope's variables are known:
207
221
  * each ref's `flatName` property gets the JSON Schema of the variable its
@@ -375,10 +389,12 @@ export interface FlowCheckContext {
375
389
  readonly options: CheckOpts;
376
390
  /**
377
391
  * Check a step's symbolic `connection` / `folder` labels against bindings.json,
378
- * as a connector step's are, reporting `BINDING_SELF_NAME` / `BINDING_UNDECLARED`.
379
- * Those codes are the pipeline's, shared by every family that binds a
380
- * connection, so they belong to no family catalog. `use` names the connector
381
- * the connection must belong to, for the cross-step connection-owner rule.
392
+ * reporting `BINDING_SELF_NAME` / `BINDING_UNDECLARED`.
393
+ *
394
+ * @deprecated `check` runs the serializer, which refuses a label passed to
395
+ * `FlowSerializeContext.resolveBinding` under the same codes; a node type that
396
+ * resolves its labels there needs no call here, and a call here reports the
397
+ * label twice. Kept so a node-type package written against it still builds.
382
398
  */
383
399
  bindingLabels(where: string, labels: {
384
400
  connection?: unknown;
@@ -393,17 +409,12 @@ export interface FlowCheckContext {
393
409
  action?: string;
394
410
  }): void;
395
411
  /**
396
- * The node ids and folders this step's attached resources take, recorded
397
- * file-wide — across the root flow and every subflow, as compile claims them.
398
- * An explicit id equal to one another node gets is reported once the whole
399
- * file is checked (`INLINE_AGENT_RESOURCE_NODE_ID`).
400
- */
401
- /**
402
- * Run the rules every Integration Service operation shares — its lookups, its
403
- * schema readiness, its contract against the library facts check was given
404
- * (`options`), and BINDING_MISSING — on this step. Their codes are the
405
- * pipeline's, shared with an event subscription's, so they belong to no
406
- * family catalog; each is reported at its own severity.
412
+ * Run the rules every Integration Service operation shares that compile does
413
+ * not: its lookups and its schema readiness, against the library facts check
414
+ * was given (`options`). The operation, its field contract and its binding
415
+ * labels are the serializer's refusals, reported from `check`'s serializer
416
+ * run. Their codes are the pipeline's, shared with an event subscription's, so
417
+ * they belong to no family catalog; each is reported at its own severity.
407
418
  */
408
419
  connector(spec: {
409
420
  key: string;
@@ -414,15 +425,18 @@ export interface FlowCheckContext {
414
425
  folder?: unknown;
415
426
  inputs?: unknown;
416
427
  }): void;
428
+ /**
429
+ * @deprecated Compile claims node ids and folders file-wide and refuses a clash
430
+ * (INLINE_AGENT_RESOURCE_NODE_ID / INLINE_AGENT_RESOURCE_SOURCE_DUP), which
431
+ * `check` reports from its serializer run. These record nothing and answer
432
+ * `undefined`; kept so a node-type package written against them still builds.
433
+ */
417
434
  readonly resources: {
418
- /** An id compile derives for one of this step's resources, and what it is, for that report. */
419
435
  derivedNodeId(id: string, what: string): void;
420
- /** Claim an explicit node id: the earlier claimant when one already has it (and nothing is recorded), else undefined. */
421
436
  claimNodeId(id: string, what: string): {
422
437
  step: string;
423
438
  what: string;
424
439
  } | undefined;
425
- /** Claim an agent's folder (its `source`, compared lower-cased): the other step that has it, else undefined. */
426
440
  claimFolder(source: string): string | undefined;
427
441
  };
428
442
  }
@@ -0,0 +1,105 @@
1
+ /**
2
+ * How a `.parallel` arm hands its join its edges, read from the step tree the
3
+ * way serialize's `emit` wires them, and the two refusals a human task inside
4
+ * an arm can earn: alternative outcome edges that each enter the join
5
+ * (HITL_FAN_OUT_INTO_JOIN), and an End outcome beside a continuing one
6
+ * (HITL_END_OUTCOME_IN_PARALLEL). serialize refuses both on the graph it
7
+ * builds and words them here, naming the outcomes; `check` reads `armTails` for
8
+ * its own join rules.
9
+ */
10
+ import type { Step } from './flow-sdk.js';
11
+ import { type HitlRoutingInputs } from './core/hitl-routing.js';
12
+ /**
13
+ * Whether a `.parallel` arm hands its join an edge, as serialize wires it: it
14
+ * has a step, and `armTails` (serialize's `emit` tail for tail) leaves it at
15
+ * least one tail. So an arm that ends by `.return`, `.terminate`, `.break`,
16
+ * `.continue` or a `.stepToRef` on the default port does not, and neither does
17
+ * one that ends in a `.branch` / `.switch` / `.stepSwitch` every arm of which
18
+ * ends, or in a nested `.parallel` none of whose arms reaches its own join.
19
+ * When no arm hands on a tail, serialize emits no Merge at all (#1267), so the
20
+ * join rules (PARALLEL_ARM_RETURNS, REF_INTO_PARALLEL_ARM,
21
+ * REF_OUT_OF_PARALLEL_ARM) have no join to protect.
22
+ */
23
+ export declare function armReachesJoin(arm: Step[]): boolean;
24
+ /**
25
+ * One edge a `.parallel` arm hands its join, as `emit` wires it: the step it
26
+ * leaves from, and the human-task outcome it leaves on, if it is one.
27
+ */
28
+ export interface ArmTail {
29
+ from: string;
30
+ outcome?: string;
31
+ /**
32
+ * The human task whose `.stepSwitch` arm this path runs on, and the outcome
33
+ * of that arm. Set on every tail a human-task `.stepSwitch` hands on, body
34
+ * arm or empty arm, so an arm that ends on two of them is seen for what it
35
+ * is: alternatives, of which only one fires.
36
+ */
37
+ humanSwitch?: {
38
+ step: string;
39
+ outcome: string;
40
+ };
41
+ }
42
+ /**
43
+ * The edges a step list hands on, mirroring serialize's `emit` tail for tail.
44
+ *
45
+ * `.branch` / `.switch` / `.stepSwitch` hand on every arm's tails (an empty arm
46
+ * hands on the port it started from), a `.parallel` its Merge when an arm
47
+ * reaches it and nothing otherwise, a fan-out human task one per continuing
48
+ * outcome, a routed one its primary outcome; `.return` / `.terminate` / `.break`
49
+ * and a `stepToRef('output', …)` hand on none, `.stepToList` leaves the path as
50
+ * it was, and every other step hands on its own single edge.
51
+ */
52
+ export declare function armTails(steps: readonly Step[], incoming: ArmTail[]): ArmTail[];
53
+ export declare function actionTails(name: string, spec: {
54
+ kind?: string;
55
+ inputs?: HitlRoutingInputs;
56
+ }, version?: string): ArmTail[];
57
+ /**
58
+ * The tails of a `.parallel` arm when they would starve its join: two or more
59
+ * alternative edges — only one fires — at least one of which is a human task's
60
+ * outcome edge, or the end of a human-task `.stepSwitch` arm. The join waits
61
+ * for a token on every incoming flow.
62
+ *
63
+ * However the task got there, it is the same graph: a fan-out task at the arm's
64
+ * end (its outcomes are the alternatives), a `.stepSwitch` with an empty arm
65
+ * beside a body arm, a complete `.stepSwitch` with nothing after it (one edge
66
+ * per arm's last step), a task on one side of a `.branch`. serialize refuses the
67
+ * same condition, edge for edge. Rejoining arms with NO human task in them are
68
+ * the older, general case, and not refused here.
69
+ */
70
+ export declare function hitlFanInto(steps: readonly Step[]): ArmTail[] | undefined;
71
+ /**
72
+ * The HITL_FAN_OUT_INTO_JOIN refusal for arm `arm` of `.parallel` `join`, whose
73
+ * `tails` ({@link hitlFanInto}) would each enter the join, and the human task it
74
+ * is about.
75
+ */
76
+ export declare function fanInRefusal(join: string, arm: number, steps: readonly Step[], tails: ArmTail[]): {
77
+ message: string;
78
+ suggestion: string;
79
+ task: string;
80
+ };
81
+ /**
82
+ * A human task inside a `.parallel` arm with an outcome whose path stops at an
83
+ * End, which would starve the join: an `action: 'End'` outcome of a fan-out
84
+ * task, or a `.stepSwitch` outcome with no arm or whose arm ends in `.return()`.
85
+ */
86
+ export interface HitlEndHazard {
87
+ step: string;
88
+ outcomes: string[];
89
+ via: 'action-end' | 'step-switch';
90
+ }
91
+ /**
92
+ * Human tasks in one `.parallel` arm that continue every outcome while some of
93
+ * them are `action: 'End'`. The End outcome's path stops at its End node, so the
94
+ * join waits for the continuing path that did not run.
95
+ *
96
+ * Recurses into `.branch` / `.switch` / `.stepSwitch` bodies, which reach the
97
+ * join too. Not into a loop body (the loop node is what reaches the join) or a
98
+ * nested `.parallel` (its own join is checked on its own).
99
+ */
100
+ export declare function hitlEndHazards(steps: readonly Step[]): HitlEndHazard[];
101
+ /** The HITL_END_OUTCOME_IN_PARALLEL refusal for hazard `h` ({@link hitlEndHazards}) inside `.parallel` `join`. */
102
+ export declare function endOutcomeRefusal(join: string, h: HitlEndHazard): {
103
+ message: string;
104
+ suggestion?: string;
105
+ };
@@ -0,0 +1,214 @@
1
+ import { commentText } from './core/comment-text.js';
2
+ import { OUTCOME_HANDLE_NODE_TYPES, hitlExitModel } from './core/hitl-routing.js';
3
+ import { FLOW_NODE_TYPES } from './flow-node-registry.js';
4
+ import { hasExits } from './core/step-ports.js';
5
+ import { stopsAtEnd } from './flow-step-tree.js';
6
+ import { stepExits } from './flow-step-exits.js';
7
+ /**
8
+ * Whether a `.parallel` arm hands its join an edge, as serialize wires it: it
9
+ * has a step, and `armTails` (serialize's `emit` tail for tail) leaves it at
10
+ * least one tail. So an arm that ends by `.return`, `.terminate`, `.break`,
11
+ * `.continue` or a `.stepToRef` on the default port does not, and neither does
12
+ * one that ends in a `.branch` / `.switch` / `.stepSwitch` every arm of which
13
+ * ends, or in a nested `.parallel` none of whose arms reaches its own join.
14
+ * When no arm hands on a tail, serialize emits no Merge at all (#1267), so the
15
+ * join rules (PARALLEL_ARM_RETURNS, REF_INTO_PARALLEL_ARM,
16
+ * REF_OUT_OF_PARALLEL_ARM) have no join to protect.
17
+ */
18
+ export function armReachesJoin(arm) {
19
+ return arm.length > 0 && armTails(arm, [{ from: '' }]).length > 0;
20
+ }
21
+ /**
22
+ * The edges a step list hands on, mirroring serialize's `emit` tail for tail.
23
+ *
24
+ * `.branch` / `.switch` / `.stepSwitch` hand on every arm's tails (an empty arm
25
+ * hands on the port it started from), a `.parallel` its Merge when an arm
26
+ * reaches it and nothing otherwise, a fan-out human task one per continuing
27
+ * outcome, a routed one its primary outcome; `.return` / `.terminate` / `.break`
28
+ * and a `stepToRef('output', …)` hand on none, `.stepToList` leaves the path as
29
+ * it was, and every other step hands on its own single edge.
30
+ */
31
+ export function armTails(steps, incoming) {
32
+ let tails = incoming;
33
+ for (const st of steps) {
34
+ if (st.kind === 'stepToList')
35
+ continue;
36
+ if (st.kind === 'stepToRef') {
37
+ if (st.port === 'output')
38
+ tails = [];
39
+ continue;
40
+ }
41
+ if (st.kind === 'return' || st.kind === 'terminate' || st.kind === 'break' || st.kind === 'continue')
42
+ return [];
43
+ if (st.kind === 'action') {
44
+ tails = actionTails(st.name, st.spec, st.options?.version);
45
+ }
46
+ else if (st.kind === 'stepSwitch') {
47
+ const spec = st.spec;
48
+ const outcomes = outcomeHandleTask(spec);
49
+ tails = st.cases.flatMap((c) => armTails(c.body, [{ from: st.name, ...(outcomes ? { outcome: String(c.value) } : {}) }])
50
+ .map((t) => (outcomes && !t.humanSwitch ? { ...t, humanSwitch: { step: st.name, outcome: String(c.value) } } : t)));
51
+ }
52
+ else if (st.kind === 'branch') {
53
+ tails = [...armTails(st.then, [{ from: st.name }]), ...armTails(st.otherwise, [{ from: st.name }])];
54
+ }
55
+ else if (st.kind === 'switch') {
56
+ tails = [
57
+ ...st.cases.flatMap((c) => armTails(c.body, [{ from: st.name }])),
58
+ ...(st.default !== undefined ? armTails(st.default, [{ from: st.name }]) : []),
59
+ ];
60
+ }
61
+ else if (st.kind === 'parallel') {
62
+ // serialize joins a nested `.parallel` at its Merge, the one edge it hands
63
+ // on, and emits no Merge, so no edge, when no arm hands on a tail (#1267).
64
+ // An empty arm hands on the path it started from, which the Merge joins.
65
+ const arms = Array.isArray(st.arms) ? st.arms : [];
66
+ tails = arms.some((arm) => armTails(arm, tails).length > 0) ? [{ from: st.name }] : [];
67
+ }
68
+ else {
69
+ tails = [{ from: st.name ?? st.kind }];
70
+ }
71
+ }
72
+ return tails;
73
+ }
74
+ /**
75
+ * Whether `spec` is a human task the product draws one handle per outcome on
76
+ * (the base node type and quick-form), as its registered node type names it.
77
+ */
78
+ function outcomeHandleTask(spec) {
79
+ const kind = spec?.kind;
80
+ const entry = typeof kind === 'string' ? FLOW_NODE_TYPES.byKind(kind, spec) : undefined;
81
+ return entry !== undefined && OUTCOME_HANDLE_NODE_TYPES.has(entry.type.typeName);
82
+ }
83
+ export function actionTails(name, spec, version) {
84
+ if (!outcomeHandleTask(spec))
85
+ return [{ from: name }];
86
+ const model = hitlExitModel(spec.inputs, version);
87
+ if (model.shape === 'placeholder')
88
+ return [{ from: name }];
89
+ if (model.shape === 'per-outcome')
90
+ return model.exits.slice(0, 1).map((e) => ({ from: name, outcome: e.value }));
91
+ return model.exits.filter((e) => !e.endsProcess).map((e) => ({ from: name, outcome: e.value }));
92
+ }
93
+ /**
94
+ * The tails of a `.parallel` arm when they would starve its join: two or more
95
+ * alternative edges — only one fires — at least one of which is a human task's
96
+ * outcome edge, or the end of a human-task `.stepSwitch` arm. The join waits
97
+ * for a token on every incoming flow.
98
+ *
99
+ * However the task got there, it is the same graph: a fan-out task at the arm's
100
+ * end (its outcomes are the alternatives), a `.stepSwitch` with an empty arm
101
+ * beside a body arm, a complete `.stepSwitch` with nothing after it (one edge
102
+ * per arm's last step), a task on one side of a `.branch`. serialize refuses the
103
+ * same condition, edge for edge. Rejoining arms with NO human task in them are
104
+ * the older, general case, and not refused here.
105
+ */
106
+ export function hitlFanInto(steps) {
107
+ const tails = armTails(steps, [{ from: '(fork)' }]);
108
+ return tails.length >= 2 && tails.some((t) => t.outcome !== undefined || t.humanSwitch !== undefined) ? tails : undefined;
109
+ }
110
+ /**
111
+ * The HITL_FAN_OUT_INTO_JOIN refusal for arm `arm` of `.parallel` `join`, whose
112
+ * `tails` ({@link hitlFanInto}) would each enter the join, and the human task it
113
+ * is about.
114
+ */
115
+ export function fanInRefusal(join, arm, steps, tails) {
116
+ const tagged = tails.find((t) => t.outcome !== undefined || t.humanSwitch !== undefined);
117
+ const task = tagged.outcome !== undefined ? tagged.from : tagged.humanSwitch.step;
118
+ if (tails.every((t) => t.from === task && t.outcome !== undefined)) {
119
+ return {
120
+ task,
121
+ message: `Human task "${task}" ends its path inside parallel "${join}", so its outcomes `
122
+ + `${tails.map((t) => t.outcome).join(', ')} each get their own edge into the join. Only one outcome ever `
123
+ + `fires, and the join waits for a token on every incoming edge, so the run never gets past "${join}" — with `
124
+ + `no incident, and \`flow validate\` answers Valid. Add a step after "${task}" inside the arm, so every `
125
+ + `outcome continues to that step and only its one edge reaches the join (the decision is `
126
+ + `out('${task}', 'Action')), or move the task out of the \`.parallel(…)\`.`,
127
+ suggestion: `a.step('${task}', hitl(…)).step('record', …)`,
128
+ };
129
+ }
130
+ const edges = tails.map((t) => (t.outcome !== undefined ? `outcome ${t.outcome} of "${t.from}"`
131
+ : t.humanSwitch ? `"${t.from}" (on outcome ${t.humanSwitch.outcome} of "${t.humanSwitch.step}")` : `"${t.from}"`));
132
+ const last = [...steps].reverse().find((x) => x.kind !== 'stepToList' && x.kind !== 'stepToRef');
133
+ return {
134
+ task,
135
+ message: `Parallel "${join}" arm ${arm + 1} ends on ${tails.length} alternative paths, and each gets its own edge `
136
+ + `into the join: ${edges.join(', ')}. Only one of them runs, and the join waits for a token on every incoming `
137
+ + `edge, so the run never gets past "${join}" — with no incident, and \`flow validate\` answers Valid. Add a `
138
+ + `step at the end of the arm${last?.name ? `, after "${last.name}"` : ''}, so the paths meet on that step and `
139
+ + `only its one edge reaches the join, or move human task "${task}" out of the \`.parallel(…)\`.`,
140
+ suggestion: `….step('record', …) // last step of arm ${commentText(arm + 1)}`,
141
+ };
142
+ }
143
+ /**
144
+ * Human tasks in one `.parallel` arm that continue every outcome while some of
145
+ * them are `action: 'End'`. The End outcome's path stops at its End node, so the
146
+ * join waits for the continuing path that did not run.
147
+ *
148
+ * Recurses into `.branch` / `.switch` / `.stepSwitch` bodies, which reach the
149
+ * join too. Not into a loop body (the loop node is what reaches the join) or a
150
+ * nested `.parallel` (its own join is checked on its own).
151
+ */
152
+ export function hitlEndHazards(steps) {
153
+ const found = [];
154
+ for (const st of steps) {
155
+ if (st.kind === 'action' && outcomeHandleTask(st.spec)) {
156
+ const model = hitlExitModel(st.spec.inputs, st.options?.version);
157
+ if (model.shape !== 'every-outcome')
158
+ continue;
159
+ const ending = model.exits.filter((e) => e.endsProcess).map((e) => e.value);
160
+ if (ending.length > 0 && ending.length < model.exits.length)
161
+ found.push({ step: st.name, outcomes: ending, via: 'action-end' });
162
+ }
163
+ else if (st.kind === 'branch') {
164
+ found.push(...hitlEndHazards(st.then), ...hitlEndHazards(st.otherwise));
165
+ }
166
+ else if (st.kind === 'switch') {
167
+ for (const c of st.cases)
168
+ found.push(...hitlEndHazards(c.body));
169
+ if (st.default)
170
+ found.push(...hitlEndHazards(st.default));
171
+ }
172
+ else if (st.kind === 'stepSwitch') {
173
+ // An outcome with no arm gets an End (serialize's `endExit`), and so does
174
+ // an arm that ends in `.return()`. Beside an arm that continues, that End
175
+ // is where this arm's token stops on that outcome.
176
+ const spec = st.spec;
177
+ const exits = outcomeHandleTask(spec) ? stepExits(st.spec, st.options?.version) : undefined;
178
+ if (exits && hasExits(exits)) {
179
+ const stopped = exits.filter((e) => {
180
+ const c = st.cases.find((x) => x.value === e.value);
181
+ return !c || (armTails(c.body, [{ from: st.name }]).length === 0 && stopsAtEnd(c.body));
182
+ }).map((e) => e.value);
183
+ if (stopped.length > 0 && armTails([st], [{ from: '(fork)' }]).length > 0) {
184
+ found.push({ step: st.name, outcomes: stopped, via: 'step-switch' });
185
+ }
186
+ }
187
+ for (const c of st.cases)
188
+ found.push(...hitlEndHazards(c.body));
189
+ }
190
+ }
191
+ return found;
192
+ }
193
+ /** The HITL_END_OUTCOME_IN_PARALLEL refusal for hazard `h` ({@link hitlEndHazards}) inside `.parallel` `join`. */
194
+ export function endOutcomeRefusal(join, h) {
195
+ const list = h.outcomes.join(', ');
196
+ if (h.via === 'step-switch') {
197
+ return {
198
+ message: `Human task "${h.step}" is a .stepSwitch inside parallel "${join}", and ${h.outcomes.length === 1
199
+ ? `outcome ${list} has` : `outcomes ${list} have`} no arm, or an arm that ends in .return(). That path `
200
+ + `stops at an End node, while the join waits for a token from this arm on the path that continues, so the `
201
+ + `run never gets past "${join}" — with no incident, and \`flow validate\` answers Valid. Give every outcome `
202
+ + `an arm that continues (and move a .return() after the \`.parallel(…)\`), or move the task out of the `
203
+ + `\`.parallel(…)\`.`,
204
+ suggestion: `{ value: '${h.outcomes[0]}', body: (b) => b.step(…) }`,
205
+ };
206
+ }
207
+ return {
208
+ message: `Human task "${h.step}" is inside parallel "${join}", and ${h.outcomes.length === 1
209
+ ? `outcome ${list} is` : `outcomes ${list} are`} action: 'End'. `
210
+ + `That path stops at its own End node, while the join waits for a token from this arm on its `
211
+ + `continuing path, so the run never gets past "${join}". Drop action: 'End' there and act on `
212
+ + `out('${h.step}', 'Action') after the join, or move the task out of the \`.parallel(…)\`.`,
213
+ };
214
+ }
@@ -501,8 +501,8 @@ export interface ReturnOptions extends NodeOptions {
501
501
  * for what the canvas shows. Two Ends in one scope asking for `done` with the
502
502
  * same values, label, version and updates are one End reached from both paths.
503
503
  * Two that differ in any of these, or that sit in different scopes (a loop body
504
- * and the main path), are refused (`check`: DUP_END_NAME; `compile --no-check`
505
- * throws) rather than renamed `done2`. The name is never renamed to make it
504
+ * and the main path), are refused (DUP_END_NAME, by `compile` and so by
505
+ * `check`) rather than renamed `done2`. The name is never renamed to make it
506
506
  * unique: a name that gives the node id of a step, the trigger or an entry
507
507
  * point is refused too (STEP_ID_COLLISION), as is a second End whose name gives
508
508
  * the same id (`a b` and `a_b`, DUP_END_NAME).
@@ -1265,8 +1265,8 @@ declare class StepList {
1265
1265
  * own, from anywhere in the flow, including another port's path. To stop
1266
1266
  * several paths at one Terminate, spell its `.terminate` once and end the
1267
1267
  * other paths with `.stepToRef('<name>')`. Two `.terminate`s asking for the
1268
- * same name are refused (`check`: DUP_STEP; `compile --no-check` throws)
1269
- * rather than renamed `<name>2`.
1268
+ * same name are refused (DUP_STEP, by `compile` and so by `check`) rather than
1269
+ * renamed `<name>2`.
1270
1270
  *
1271
1271
  * @example
1272
1272
  * **Abort the sibling arm from inside a parallel**
package/dist/flow-sdk.js CHANGED
@@ -899,8 +899,8 @@ class StepList {
899
899
  * own, from anywhere in the flow, including another port's path. To stop
900
900
  * several paths at one Terminate, spell its `.terminate` once and end the
901
901
  * other paths with `.stepToRef('<name>')`. Two `.terminate`s asking for the
902
- * same name are refused (`check`: DUP_STEP; `compile --no-check` throws)
903
- * rather than renamed `<name>2`.
902
+ * same name are refused (DUP_STEP, by `compile` and so by `check`) rather than
903
+ * renamed `<name>2`.
904
904
  *
905
905
  * @example
906
906
  * **Abort the sibling arm from inside a parallel**
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * The words for a `.terminate('<name>')` whose name is already taken (#1239).
3
- * `check` (DUP_STEP) and `serialize` (the `--no-check` backstop) both use them,
4
- * so one clash gets one explanation whichever reports it.
3
+ * The serializer refuses the clash with them (DUP_STEP), and `check` reports
4
+ * that refusal from its serializer run.
5
5
  */
6
6
  /** Two `.terminate('<name>')` calls: keep one and end the other paths at it. */
7
7
  export declare function terminateTwiceMessage(name: string): string;
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * The words for a `.terminate('<name>')` whose name is already taken (#1239).
3
- * `check` (DUP_STEP) and `serialize` (the `--no-check` backstop) both use them,
4
- * so one clash gets one explanation whichever reports it.
3
+ * The serializer refuses the clash with them (DUP_STEP), and `check` reports
4
+ * that refusal from its serializer run.
5
5
  */
6
6
  /** Two `.terminate('<name>')` calls: keep one and end the other paths at it. */
7
7
  export function terminateTwiceMessage(name) {
@@ -81,5 +81,6 @@ function baselineDirectory(originalPath) {
81
81
  * load-time cycle.
82
82
  */
83
83
  export function checkEdited(built, editedFlow, opts) {
84
- return placeDiagnostics(check(built, opts), editedFlow);
84
+ // Merge compiles the flow itself (draft-tolerant), so check's serializer pass is skipped.
85
+ return placeDiagnostics(check(built, { ...opts, serializer: false }), editedFlow);
85
86
  }