@uipath/maestro-builder-sdk 5.4.1 → 6.0.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.
- package/dist/check.js +205 -57
- package/dist/core/actions.d.ts +21 -17
- package/dist/core/hitl-routing.d.ts +47 -0
- package/dist/core/hitl-routing.js +33 -0
- package/dist/core/step-ports.d.ts +37 -0
- package/dist/core/step-ports.js +102 -0
- package/dist/decompile.js +91 -2
- package/dist/flow-expr-check.js +11 -0
- package/dist/flow-sdk.d.ts +61 -0
- package/dist/flow-sdk.js +61 -0
- package/dist/serialize.js +83 -6
- package/package.json +2 -2
package/dist/check.js
CHANGED
|
@@ -11,6 +11,8 @@ import { isLookupToken, resolvedValue, unresolvedLookupMessage, } from './core/l
|
|
|
11
11
|
import { EVENT_FILTER_OPERATORS, readEventFilter, eventFilterProblem } from './event-filters.js';
|
|
12
12
|
import { extractRefs } from './core/expr-check.js';
|
|
13
13
|
import { bindingSelfNameMessage } from './core/binding-messages.js';
|
|
14
|
+
import { hitlRoutesPerOutcome } from './core/hitl-routing.js';
|
|
15
|
+
import { declaredExits, hasExits } from './core/step-ports.js';
|
|
14
16
|
import { FLOW_SCHEMA_VERSIONS } from './format-profile.js';
|
|
15
17
|
import { compareSchemaVersions, schemaVersionRefusal } from './schema-version.js';
|
|
16
18
|
import { prepareCommand } from './core/cli-spelling.js';
|
|
@@ -105,11 +107,14 @@ function checkFlow(built, ancestors, opts = {}) {
|
|
|
105
107
|
const hitlOutcomeSteps = new Map();
|
|
106
108
|
const hitlOutcomeRouted = new Map();
|
|
107
109
|
// The other half of that bookkeeping: hitl steps that declare a CHOICE (more
|
|
108
|
-
// than one outcome) and
|
|
109
|
-
//
|
|
110
|
-
//
|
|
111
|
-
//
|
|
112
|
-
//
|
|
110
|
+
// than one outcome) and still leave on ONE exit, so every outcome leaves the
|
|
111
|
+
// node the same way. Since more than one outcome infers per-outcome routing,
|
|
112
|
+
// reaching this map means the single exit was chosen — by a variant, a pinned
|
|
113
|
+
// version, or `outcomePorts: false` — and WHICH of the three decides what the
|
|
114
|
+
// remedy can be. Whether it is a mistake at all depends on something no single
|
|
115
|
+
// step can see — whether anything downstream reads the chosen outcome — so the
|
|
116
|
+
// decision waits until the expressions are collected (see
|
|
117
|
+
// HITL_OUTCOMES_UNREACHABLE below).
|
|
113
118
|
const hitlChoiceNoPorts = new Map();
|
|
114
119
|
const dataFabricUpdates = [];
|
|
115
120
|
// Placeholder (`mock()`) step names. The platform's `core.logic.mock` declares
|
|
@@ -174,7 +179,16 @@ function checkFlow(built, ancestors, opts = {}) {
|
|
|
174
179
|
let prevPortSource;
|
|
175
180
|
/** Collect an expression, stamped with the error handler it sits in (if any). */
|
|
176
181
|
const addExpr = (js, step) => { exprs.push({ js, step, errorSource }); };
|
|
177
|
-
for (const
|
|
182
|
+
for (const s0 of steps) {
|
|
183
|
+
// A `.stepSwitch` IS an action plus its arms, so every action rule below —
|
|
184
|
+
// name registration, the per-family `check*`, expression collection, the
|
|
185
|
+
// read maps — has to see it as one. Normalising here rather than
|
|
186
|
+
// duplicating forty lines is what keeps a new family rule from silently
|
|
187
|
+
// applying to `.step()` and not to `.stepSwitch()`. The arms and the
|
|
188
|
+
// stepSwitch-only diagnostics are handled after the loop body.
|
|
189
|
+
const s = s0.kind === 'stepSwitch'
|
|
190
|
+
? { kind: 'action', name: s0.name, spec: s0.spec, ...(s0.options ? { options: s0.options } : {}) }
|
|
191
|
+
: s0;
|
|
178
192
|
// `return` carries a synthetic `name` that names no node and is not
|
|
179
193
|
// referenceable — registering it would report DUP_STEP the second time one
|
|
180
194
|
// appears. The port-edge kinds carry no name at all.
|
|
@@ -201,32 +215,40 @@ function checkFlow(built, ancestors, opts = {}) {
|
|
|
201
215
|
}
|
|
202
216
|
}
|
|
203
217
|
// The mirror of HITL_OUTCOME_PORTS_OFF below: `completed` on a human task
|
|
204
|
-
// that
|
|
218
|
+
// that routes per outcome.
|
|
219
|
+
//
|
|
220
|
+
// Per-outcome routing moves the step to the node's 1.1/1.2 definition, and
|
|
221
|
+
// those declare ONE source handle — `outcome-{item.id}`, repeated over
|
|
222
|
+
// `inputs.schema.outcomes`. There is no `completed` handle to leave from,
|
|
223
|
+
// so this edge is refused by the product: `flow validate` answers "Edge
|
|
224
|
+
// references undeclared source handle \"completed\" on node \"<step>\""
|
|
225
|
+
// (measured). Without this rule `check` was silent, `compile` emitted it,
|
|
226
|
+
// and the author found out from validate — or from a consumer that
|
|
227
|
+
// inspects ports.
|
|
205
228
|
//
|
|
206
|
-
//
|
|
207
|
-
//
|
|
208
|
-
//
|
|
209
|
-
// There is no `completed` handle to leave from, so this edge is refused by
|
|
210
|
-
// the product: `flow validate` answers "Edge references undeclared source
|
|
211
|
-
// handle \"completed\" on node \"<step>\"" (measured). Without this rule
|
|
212
|
-
// `check` was silent, `compile` emitted it, and the author found out from
|
|
213
|
-
// validate — or from a consumer that inspects ports.
|
|
229
|
+
// Since more than one outcome INFERS the routing, this is now also the
|
|
230
|
+
// rule an author meets when they write the old shape on a multi-outcome
|
|
231
|
+
// task, so the message names the two ways back to a `completed` exit.
|
|
214
232
|
if ((s.kind === 'stepToList' || s.kind === 'stepToRef') && s.port === 'completed') {
|
|
215
233
|
const prevSpec = prevAction?.spec;
|
|
216
234
|
const isHitl = !!prevSpec && 'kind' in prevSpec && prevSpec.kind === 'hitl';
|
|
217
|
-
const routed = isHitl
|
|
218
|
-
&& (prevSpec.inputs?.outcomePorts === true || prevSpec.inputs?.exposeError === true);
|
|
235
|
+
const routed = isHitl && hitlRoutesPerOutcome(prevSpec.inputs, prevAction?.options?.version);
|
|
219
236
|
if (routed) {
|
|
220
237
|
const first = prevSpec.inputs.outcomes?.[0];
|
|
221
238
|
const firstId = outcomeSlug(typeof first === 'string' ? first : first?.name, 0);
|
|
239
|
+
const why = prevSpec.inputs?.exposeError === true ? 'exposeError selects'
|
|
240
|
+
: prevSpec.inputs?.outcomePorts === true ? 'outcomePorts selects'
|
|
241
|
+
: `its ${(prevSpec.inputs?.outcomes ?? []).length} outcomes select`;
|
|
222
242
|
diags.push({
|
|
223
243
|
level: 'error', code: 'HITL_COMPLETED_PORT_GONE', step: prevAction?.name,
|
|
224
|
-
message: `Port "completed" does not exist on human task "${prevAction?.name}". `
|
|
225
|
-
+ `${prevSpec.inputs?.exposeError === true ? 'exposeError' : 'outcomePorts'} selects the node's `
|
|
244
|
+
message: `Port "completed" does not exist on human task "${prevAction?.name}". ${why} the node's `
|
|
226
245
|
+ `${prevSpec.inputs?.exposeError === true ? '1.2' : '1.1'} definition, whose only source handle is `
|
|
227
246
|
+ `"outcome-<id>" repeated over the outcomes — the 1.0 definition's single 'completed' exit is not `
|
|
228
247
|
+ `part of it. The FIRST outcome ("outcome-${firstId}") is what continues the main path.`,
|
|
229
|
-
suggestion:
|
|
248
|
+
suggestion: prevSpec.inputs?.outcomePorts === true || prevSpec.inputs?.exposeError === true
|
|
249
|
+
? `route the outcomes (.stepToList('outcome-${firstId}', …)), or drop outcomePorts to keep the 'completed' exit`
|
|
250
|
+
: `route the outcomes (.stepToList('outcome-${firstId}', …)), or keep the single exit with `
|
|
251
|
+
+ `outcomePorts: false`,
|
|
230
252
|
});
|
|
231
253
|
}
|
|
232
254
|
}
|
|
@@ -241,13 +263,24 @@ function checkFlow(built, ancestors, opts = {}) {
|
|
|
241
263
|
+ `${prevAction ? `"${prevAction.name}" ` : ''}is not a human task.`,
|
|
242
264
|
});
|
|
243
265
|
}
|
|
244
|
-
else if (prevSpec.inputs
|
|
266
|
+
else if (!hitlRoutesPerOutcome(prevSpec.inputs, prevAction?.options?.version)) {
|
|
267
|
+
// Reachable three ways now that more than one outcome infers the
|
|
268
|
+
// routing: `outcomePorts: false`, a pinned `{ version: '1.0' }`, or a
|
|
269
|
+
// variant that has no per-outcome definition to select.
|
|
270
|
+
const off = prevSpec.inputs?.outcomePorts === false ? 'outcomePorts: false turns them off'
|
|
271
|
+
: prevSpec.inputs?.variant !== undefined
|
|
272
|
+
? `the '${prevSpec.inputs.variant}' variant has no per-outcome definition version`
|
|
273
|
+
: prevAction?.options?.version !== undefined
|
|
274
|
+
? `{ version: '${prevAction.options.version}' } pins the definition that has none`
|
|
275
|
+
: 'it declares fewer than two outcomes';
|
|
245
276
|
diags.push({
|
|
246
277
|
level: 'error', code: 'HITL_OUTCOME_PORTS_OFF', step: prevAction?.name,
|
|
247
278
|
message: `Port "${s.port}" routes a per-outcome exit, but human task "${prevAction?.name}" `
|
|
248
|
-
+ `
|
|
279
|
+
+ `does not have them — ${off}, so its single exit is 'completed'. Per-outcome exits are the `
|
|
249
280
|
+ `node's 1.1 definition.`,
|
|
250
|
-
suggestion:
|
|
281
|
+
suggestion: prevSpec.inputs?.variant !== undefined
|
|
282
|
+
? `route on out('${prevAction?.name}', 'Action') downstream`
|
|
283
|
+
: `outcomePorts: true`,
|
|
251
284
|
});
|
|
252
285
|
}
|
|
253
286
|
else {
|
|
@@ -322,9 +355,15 @@ function checkFlow(built, ancestors, opts = {}) {
|
|
|
322
355
|
}
|
|
323
356
|
if (s.spec.kind === 'mock')
|
|
324
357
|
mockNames.add(s.name);
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
358
|
+
// `.stepSwitch` routes its exits as ARMS, so the `.stepToList` bookkeeping
|
|
359
|
+
// would report every one of them unrouted. STEP_SWITCH_EXIT_UNROUTED is
|
|
360
|
+
// that step's version of the same question.
|
|
361
|
+
if (s0.kind !== 'stepSwitch'
|
|
362
|
+
&& s.spec.kind === 'hitl' && hitlRoutesPerOutcome(s.spec.inputs, s.options?.version)) {
|
|
363
|
+
hitlOutcomeSteps.set(s.name, (s.spec.inputs.outcomes ?? []).map((o, i) => ({
|
|
364
|
+
id: outcomeSlug(typeof o === 'string' ? o : o?.name, i),
|
|
365
|
+
endsProcess: typeof o !== 'string' && o?.action === 'End',
|
|
366
|
+
})));
|
|
328
367
|
}
|
|
329
368
|
if (s.spec.kind === 'hitl') {
|
|
330
369
|
checkHitl(s.name, s.spec.inputs, diags);
|
|
@@ -336,10 +375,12 @@ function checkFlow(built, ancestors, opts = {}) {
|
|
|
336
375
|
const allEnd = outcomes.length > 0
|
|
337
376
|
&& outcomes.every((o) => typeof o !== 'string' && o?.action === 'End');
|
|
338
377
|
if (outcomes.length > 1 && !allEnd
|
|
339
|
-
&& s.spec.inputs
|
|
378
|
+
&& !hitlRoutesPerOutcome(s.spec.inputs, s.options?.version)) {
|
|
340
379
|
hitlChoiceNoPorts.set(s.name, {
|
|
341
380
|
names: outcomes.map((o) => String((typeof o === 'string' ? o : o?.name) ?? '').trim()),
|
|
342
381
|
variant: s.spec.inputs?.variant,
|
|
382
|
+
pinned: s.options?.version,
|
|
383
|
+
portsOff: s.spec.inputs?.outcomePorts === false,
|
|
343
384
|
});
|
|
344
385
|
}
|
|
345
386
|
}
|
|
@@ -579,6 +620,76 @@ function checkFlow(built, ancestors, opts = {}) {
|
|
|
579
620
|
checkOutputCategory(name, e, outputType, typeOf, diags, triggerId);
|
|
580
621
|
}
|
|
581
622
|
}
|
|
623
|
+
// AFTER the dispatch chain, not inside it: `s` was normalised to an action
|
|
624
|
+
// above, so an `else if` here would be unreachable — which it silently was
|
|
625
|
+
// on the first cut. The action rules have run by now; what is left is the
|
|
626
|
+
// arms and the three things only this step can get wrong.
|
|
627
|
+
if (s0.kind === 'stepSwitch') {
|
|
628
|
+
const exits = declaredExits(s0.spec, s0.options?.version);
|
|
629
|
+
if (!hasExits(exits)) {
|
|
630
|
+
diags.push({
|
|
631
|
+
level: 'error', code: 'STEP_SWITCH_NO_EXITS', step: s0.name,
|
|
632
|
+
message: `.stepSwitch("${s0.name}") routes this step's declared exits, but ${exits.reason}, `
|
|
633
|
+
+ `so there are none to route.`,
|
|
634
|
+
...(exits.suggestion ? { suggestion: exits.suggestion } : {}),
|
|
635
|
+
});
|
|
636
|
+
for (const c of s0.cases)
|
|
637
|
+
walk(c.body, inLoop);
|
|
638
|
+
}
|
|
639
|
+
else {
|
|
640
|
+
const seenExit = new Set();
|
|
641
|
+
for (const c of s0.cases) {
|
|
642
|
+
const exit = exits.find((e) => e.value === c.value);
|
|
643
|
+
if (!exit) {
|
|
644
|
+
const nearest = nearestName(c.value, exits.map((e) => e.value));
|
|
645
|
+
diags.push({
|
|
646
|
+
level: 'error', code: 'STEP_SWITCH_EXIT_UNKNOWN', step: s0.name,
|
|
647
|
+
message: `.stepSwitch("${s0.name}") has an arm for ${JSON.stringify(c.value)}, which is not `
|
|
648
|
+
+ `an exit it declares — those are ${exits.map((e) => JSON.stringify(e.value)).join(', ')}.`,
|
|
649
|
+
...(nearest !== undefined ? { suggestion: `value: '${nearest}'` } : {}),
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
else if (seenExit.has(exit.port)) {
|
|
653
|
+
diags.push({
|
|
654
|
+
level: 'error', code: 'STEP_SWITCH_EXIT_DUP', step: s0.name,
|
|
655
|
+
message: `.stepSwitch("${s0.name}") has two arms for ${JSON.stringify(c.value)}. A port takes `
|
|
656
|
+
+ `exactly one outgoing edge, so put everything that exit needs in one arm.`,
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
else {
|
|
660
|
+
seenExit.add(exit.port);
|
|
661
|
+
}
|
|
662
|
+
if (c.body.length === 0) {
|
|
663
|
+
diags.push({
|
|
664
|
+
level: 'warning', code: 'EMPTY_ARM', step: s0.name,
|
|
665
|
+
message: `.stepSwitch("${s0.name}") has an empty arm for ${JSON.stringify(c.value)}.`,
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
walk(c.body, inLoop);
|
|
669
|
+
}
|
|
670
|
+
// An exit with no arm compiles to an End node, so the run FINISHES
|
|
671
|
+
// there instead of reaching a port with no edge and stalling. Better
|
|
672
|
+
// than stalling, still probably not what the author meant — and the
|
|
673
|
+
// End carries none of the flow's output mappings, so the run answers
|
|
674
|
+
// with nothing (flow-builder-sdk#742). An `action: 'End'` outcome
|
|
675
|
+
// already ends the run and needs no arm.
|
|
676
|
+
const unrouted = exits.filter((e) => !seenExit.has(e.port) && e.endsProcess !== true);
|
|
677
|
+
if (unrouted.length > 0) {
|
|
678
|
+
const outs = built.outputs.map((o) => o.name);
|
|
679
|
+
diags.push({
|
|
680
|
+
level: 'warning', code: 'STEP_SWITCH_EXIT_UNROUTED', step: s0.name,
|
|
681
|
+
message: `.stepSwitch("${s0.name}") has no arm for `
|
|
682
|
+
+ `${unrouted.map((e) => JSON.stringify(e.value)).join(', ')}. Each compiles to an End node, `
|
|
683
|
+
+ `so the run finishes there`
|
|
684
|
+
+ (outs.length > 0
|
|
685
|
+
? ` without writing the flow's declared output${outs.length === 1 ? '' : 's'} `
|
|
686
|
+
+ `(${outs.join(', ')}).`
|
|
687
|
+
: '.'),
|
|
688
|
+
suggestion: `{ value: '${unrouted[0].value}', body: (b) => … }`,
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
}
|
|
582
693
|
}
|
|
583
694
|
};
|
|
584
695
|
walk(built.steps);
|
|
@@ -586,14 +697,34 @@ function checkFlow(built, ancestors, opts = {}) {
|
|
|
586
697
|
// The FIRST (primary) outcome continues the main path — only the rest need
|
|
587
698
|
// an explicit .stepToList route. An unrouted one deploys, then stalls the
|
|
588
699
|
// run when the reviewer picks it (the designer flags the same, as a warning).
|
|
700
|
+
//
|
|
701
|
+
// `action: 'End'` does NOT excuse a route here. It changes the CONSEQUENCE —
|
|
702
|
+
// the run ends rather than stalling — but the designer draws a handle for
|
|
703
|
+
// every entry in `inputs.schema.outcomes` whatever its action, so an
|
|
704
|
+
// unrouted one is a handle wired to nothing either way. UiPath/skills'
|
|
705
|
+
// `assert_outcome_wiring` requires an edge per outcome with no exemption,
|
|
706
|
+
// and its convention makes every non-primary outcome an `End`, so exempting
|
|
707
|
+
// them here produced artifacts that gate refuses. (`.stepSwitch` has no such
|
|
708
|
+
// gap: an omitted exit compiles to an End node, End action or not.)
|
|
589
709
|
const routed = hitlOutcomeRouted.get(step) ?? new Set();
|
|
590
|
-
const unrouted = ids.slice(1).filter((
|
|
710
|
+
const unrouted = ids.slice(1).filter((o) => !routed.has(o.id));
|
|
591
711
|
if (unrouted.length > 0) {
|
|
712
|
+
const names = unrouted.map((o) => `outcome-${o.id}`).join(', ');
|
|
713
|
+
const stalls = unrouted.filter((o) => !o.endsProcess);
|
|
714
|
+
const ends = unrouted.filter((o) => o.endsProcess);
|
|
592
715
|
diags.push({
|
|
593
716
|
level: 'warning', code: 'HITL_OUTCOME_UNROUTED', step,
|
|
594
|
-
message: `Human task "${step}" routes per outcome, but ${
|
|
595
|
-
+ `ha${unrouted.length === 1 ? 's' : 've'} no route
|
|
596
|
-
|
|
717
|
+
message: `Human task "${step}" routes per outcome, but ${names} `
|
|
718
|
+
+ `ha${unrouted.length === 1 ? 's' : 've'} no route.`
|
|
719
|
+
+ (stalls.length > 0
|
|
720
|
+
? ` The run stalls if the reviewer picks ${stalls.length === 1 ? 'it' : 'one of those'}.`
|
|
721
|
+
: '')
|
|
722
|
+
+ (ends.length > 0
|
|
723
|
+
? ` ${ends.map((o) => `outcome-${o.id}`).join(', ')} end${ends.length === 1 ? 's' : ''} the run `
|
|
724
|
+
+ `(action: 'End'), so nothing stalls — but the handle the canvas draws is still wired to `
|
|
725
|
+
+ `nothing. Route it to a .return() so the graph says where that outcome finishes.`
|
|
726
|
+
: ''),
|
|
727
|
+
suggestion: `.stepToList('outcome-${unrouted[0].id}', (b) => b.return({…}))`,
|
|
597
728
|
});
|
|
598
729
|
}
|
|
599
730
|
}
|
|
@@ -3846,24 +3977,30 @@ function outcomeSlug(name, index) {
|
|
|
3846
3977
|
* HITL_OUTCOMES_UNREACHABLE — a human task offers the reviewer a CHOICE that
|
|
3847
3978
|
* nothing downstream can act on.
|
|
3848
3979
|
*
|
|
3849
|
-
*
|
|
3850
|
-
*
|
|
3851
|
-
*
|
|
3852
|
-
*
|
|
3853
|
-
*
|
|
3854
|
-
*
|
|
3855
|
-
*
|
|
3856
|
-
*
|
|
3980
|
+
* The single-exit shape publishes the outcomes and routes none of them: the 1.0
|
|
3981
|
+
* definition's ONE source handle is `completed`, so the outcomes reach the
|
|
3982
|
+
* reviewer and `inputs.schema.outcomes`, and then every button the reviewer can
|
|
3983
|
+
* press leaves the node the same way. Two outcomes, one path — an approval whose
|
|
3984
|
+
* approve and reject arms are indistinguishable is a modelling error, not a
|
|
3985
|
+
* style choice, yet it compiles and `flow validate` answers `Valid`
|
|
3986
|
+
* (flow-builder-sdk#735).
|
|
3987
|
+
*
|
|
3988
|
+
* On a plain base task that is no longer reachable: more than one outcome now
|
|
3989
|
+
* INFERS per-outcome routing (`hitlRoutesPerOutcome`, #739), because a warning
|
|
3990
|
+
* alone did not change what agents emitted — #739 measured the diagnostic
|
|
3991
|
+
* firing three times and the unreachable artifact shipping anyway. What is left
|
|
3992
|
+
* for this rule is the cases where the single exit is CHOSEN and the choice may
|
|
3993
|
+
* still be a mistake: a `variant` (no per-outcome definition version exists to
|
|
3994
|
+
* select, #729), a pinned `{ version: '1.0' }`, and an explicit
|
|
3995
|
+
* `outcomePorts: false`.
|
|
3857
3996
|
*
|
|
3858
|
-
*
|
|
3859
|
-
*
|
|
3860
|
-
*
|
|
3861
|
-
*
|
|
3862
|
-
*
|
|
3863
|
-
*
|
|
3864
|
-
*
|
|
3865
|
-
* here, after `walk`, rather than inside `checkHitl`: the deciding evidence is
|
|
3866
|
-
* the rest of the flow.
|
|
3997
|
+
* In all three the shape is correct when something reads the chosen outcome:
|
|
3998
|
+
* `out('<step>', 'Action')` feeding a `.switch()` routes the decision as data,
|
|
3999
|
+
* and it is the ONLY shape the quick-form, action-app and document-validation
|
|
4000
|
+
* variants have. So the rule is not "outcomes without ports", which would
|
|
4001
|
+
* refuse that shape; it is "outcomes that no exit and no expression
|
|
4002
|
+
* distinguishes". That is why it runs here, after `walk`, rather than inside
|
|
4003
|
+
* `checkHitl`: the deciding evidence is the rest of the flow.
|
|
3867
4004
|
*
|
|
3868
4005
|
* A read counts when it can SEE the outcome — `.output.Action`, the whole
|
|
3869
4006
|
* answer object (`out('<step>')` / `$vars.<step>.output`, which carries
|
|
@@ -3893,23 +4030,34 @@ function reportUnreachableHitlOutcomes(choices, exprs, diags) {
|
|
|
3893
4030
|
for (const m of js.matchAll(/\$vars\.([A-Za-z0-9_]+)\s*\[/g))
|
|
3894
4031
|
readsOutcome.add(m[1]);
|
|
3895
4032
|
}
|
|
3896
|
-
for (const [step, { names, variant }] of choices) {
|
|
4033
|
+
for (const [step, { names, variant, pinned, portsOff }] of choices) {
|
|
3897
4034
|
if (readsOutcome.has(step))
|
|
3898
4035
|
continue;
|
|
3899
4036
|
const ids = names.map((n, i) => outcomeSlug(n, i));
|
|
4037
|
+
// Why this task kept the single exit decides which remedy is even legal:
|
|
4038
|
+
// `outcomePorts: true` is refused on a variant and conflicts with a pin, so
|
|
4039
|
+
// offering it there would trade a warning for a compile error.
|
|
4040
|
+
// On a base task with no pin, more than one outcome would have inferred the
|
|
4041
|
+
// routing — so the only way such a task reaches here is `outcomePorts:
|
|
4042
|
+
// false`, and it is the only case where offering the flag is legal.
|
|
4043
|
+
const canTakePorts = variant === undefined && pinned === undefined && portsOff === true;
|
|
4044
|
+
const kept = variant !== undefined
|
|
4045
|
+
? `The "${variant}" variant has no per-outcome definition version to select`
|
|
4046
|
+
: pinned !== undefined
|
|
4047
|
+
? `{ version: '${pinned}' } pins the definition whose only exit is 'completed'`
|
|
4048
|
+
: `outcomePorts: false keeps the single exit`;
|
|
3900
4049
|
diags.push({
|
|
3901
4050
|
level: 'warning', code: 'HITL_OUTCOMES_UNREACHABLE', step,
|
|
3902
4051
|
message: `Human task "${step}" declares ${names.length} outcomes (${names.join(', ')}), but nothing `
|
|
3903
4052
|
+ `downstream tells them apart: the node emits its single 'completed' exit and every outcome leaves `
|
|
3904
4053
|
+ `on it, and no expression reads out('${step}', 'Action') either. The outcomes reach the reviewer `
|
|
3905
|
-
+ `and the artifact; the decision does not reach the flow. `
|
|
3906
|
-
+ (
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
:
|
|
3910
|
-
|
|
3911
|
-
|
|
3912
|
-
? 'outcomePorts: true'
|
|
4054
|
+
+ `and the artifact; the decision does not reach the flow. ${kept}, so switch on `
|
|
4055
|
+
+ `out('${step}', 'Action') to route the decision as data`
|
|
4056
|
+
+ (canTakePorts
|
|
4057
|
+
? `, or drop outcomePorts: false for per-outcome exits (${ids.map((i) => `'outcome-${i}'`).join(', ')}).`
|
|
4058
|
+
: '.'),
|
|
4059
|
+
suggestion: canTakePorts
|
|
4060
|
+
? 'drop outcomePorts: false'
|
|
3913
4061
|
: `.switch('route', out('${step}', 'Action'), …)`,
|
|
3914
4062
|
});
|
|
3915
4063
|
}
|
package/dist/core/actions.d.ts
CHANGED
|
@@ -564,14 +564,16 @@ export interface HitlInputs {
|
|
|
564
564
|
/**
|
|
565
565
|
* The completion buttons, e.g. `['Approve', 'Reject']`. At least one, or the
|
|
566
566
|
* reviewer has no way to complete the task and the flow waits forever. The
|
|
567
|
-
* first is the primary (default) one.
|
|
568
|
-
*
|
|
569
|
-
*
|
|
570
|
-
*
|
|
571
|
-
*
|
|
567
|
+
* first is the primary (default) one. MORE THAN ONE gives each outcome its own
|
|
568
|
+
* exit, `outcome-<slug>`: the first continues the main path and the rest are
|
|
569
|
+
* routed with `.stepToList('outcome-<slug>', …)`. Only
|
|
570
|
+
* {@link HitlInputs.outcomePorts | outcomePorts}`: false`, a `variant`, an
|
|
571
|
+
* explicit `{ version }`, or every outcome ending the process keeps the older
|
|
572
|
+
* single `completed` exit.
|
|
572
573
|
*
|
|
573
|
-
* Which one they pressed is `out('<step>', 'Action')` — that is what the
|
|
574
|
-
* platform's own routing conditions compare against, so
|
|
574
|
+
* Which one they pressed is also `out('<step>', 'Action')` — that is what the
|
|
575
|
+
* platform's own routing conditions compare against, so a task on the single
|
|
576
|
+
* exit branches on it downstream.
|
|
575
577
|
*/
|
|
576
578
|
outcomes: Outcome[];
|
|
577
579
|
/** Who gets the task and how. Omit for the definition's default delivery. */
|
|
@@ -579,16 +581,18 @@ export interface HitlInputs {
|
|
|
579
581
|
/** Action Center labels, comma-separated (the platform stores one string). */
|
|
580
582
|
labels?: string;
|
|
581
583
|
/**
|
|
582
|
-
*
|
|
583
|
-
*
|
|
584
|
-
* `
|
|
585
|
-
*
|
|
586
|
-
*
|
|
587
|
-
*
|
|
588
|
-
*
|
|
589
|
-
*
|
|
590
|
-
*
|
|
591
|
-
*
|
|
584
|
+
* Whether each outcome exits from its OWN port (`outcome-<id>`) instead of the
|
|
585
|
+
* single `completed` one. **More than one outcome does this by default**, so
|
|
586
|
+
* the useful value is `false` — an opt back into the single exit, for a task
|
|
587
|
+
* that routes the decision as DATA by reading `out('<step>', 'Action')`.
|
|
588
|
+
*
|
|
589
|
+
* Per-outcome exits select the node's **1.1** definition, whose handles are
|
|
590
|
+
* `outcome-<id>` (ids are the outcome names slugified, e.g. `'Approve'` →
|
|
591
|
+
* `outcome-approve`). They REPLACE `completed` rather than adding to it. The
|
|
592
|
+
* FIRST (primary) outcome continues the main path; route the others with
|
|
593
|
+
* `.stepToList('outcome-<id>', …)`, or the run stalls when the reviewer picks
|
|
594
|
+
* one. Base variant only — the sub-typed nodes have no per-outcome definition
|
|
595
|
+
* version, so they keep the single exit whatever this says.
|
|
592
596
|
*/
|
|
593
597
|
outcomePorts?: boolean;
|
|
594
598
|
/**
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Does a human task route PER OUTCOME — one `outcome-<id>` exit each — or leave
|
|
3
|
+
* on the single `completed` handle?
|
|
4
|
+
*
|
|
5
|
+
* Shared by `serialize` (which picks the node's definition version and its tail
|
|
6
|
+
* port from this) and `check` (whose four outcome-port rules all key on it), so
|
|
7
|
+
* the two cannot drift. They did drift once, in the direction that matters
|
|
8
|
+
* least visibly: `check` keyed on the `outcomePorts` FLAG while `serialize` was
|
|
9
|
+
* about to infer it, which would have made `check` refuse per-outcome wiring
|
|
10
|
+
* that `compile` then emitted.
|
|
11
|
+
*
|
|
12
|
+
* **More than one outcome infers it.** Declaring outcomes creates no exits on
|
|
13
|
+
* the 1.0 definition — its one source handle is `completed`, so every button
|
|
14
|
+
* the reviewer can press leaves the node the same way and the decision never
|
|
15
|
+
* reaches the graph. That shape is only meaningful for a zero- or one-outcome
|
|
16
|
+
* acknowledgement, and at two or more it compiled, validated `Valid`, and
|
|
17
|
+
* shipped flows whose approve and reject paths were indistinguishable — first
|
|
18
|
+
* silently (flow-builder-sdk#735), then past the warning that #738 added
|
|
19
|
+
* (#739). So the flag is what an author reaches for to *override* the shape,
|
|
20
|
+
* not what they must remember to switch it on.
|
|
21
|
+
*
|
|
22
|
+
* Four things turn the inference off, in this order:
|
|
23
|
+
*
|
|
24
|
+
* 1. `outcomePorts: false` — an explicit opt back into the single exit.
|
|
25
|
+
* 2. A `variant`. The quick-form, action-app and document-validation node types
|
|
26
|
+
* carry no per-outcome definition version to select, so `completed` plus a
|
|
27
|
+
* `switch` on `out('<step>', 'Action')` is their only shape (#729).
|
|
28
|
+
* 3. An explicitly pinned `{ version }`. The shape of a deployed artifact is
|
|
29
|
+
* recorded in its `typeVersion`, and `decompile` emits that pin — so
|
|
30
|
+
* decompiling a 1.0 flow and recompiling it round-trips exactly instead of
|
|
31
|
+
* promoting the node to 1.1 and stranding its `completed` edge.
|
|
32
|
+
* 4. Every outcome carrying `action: 'End'`. The run stops AT the node, so no
|
|
33
|
+
* exit can distinguish anything and per-outcome handles would only invite
|
|
34
|
+
* routes that can never be taken. The same reasoning suppresses
|
|
35
|
+
* HITL_OUTCOMES_UNREACHABLE.
|
|
36
|
+
*/
|
|
37
|
+
import type { HitlInputs } from './actions.js';
|
|
38
|
+
/**
|
|
39
|
+
* An outcome's stable id — the slug that names its `outcome-<id>` port.
|
|
40
|
+
*
|
|
41
|
+
* One definition, because `serialize` emits these ids, `check` validates port
|
|
42
|
+
* names against them, and `.stepSwitch` resolves an arm's `value` through them.
|
|
43
|
+
* Three copies of one rule is three chances for a port name to be emitted that
|
|
44
|
+
* nothing else recognises.
|
|
45
|
+
*/
|
|
46
|
+
export declare function outcomeSlug(name: unknown, index: number): string;
|
|
47
|
+
export declare function hitlRoutesPerOutcome(inputs: HitlInputs | undefined, pinnedVersion?: string): boolean;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An outcome's stable id — the slug that names its `outcome-<id>` port.
|
|
3
|
+
*
|
|
4
|
+
* One definition, because `serialize` emits these ids, `check` validates port
|
|
5
|
+
* names against them, and `.stepSwitch` resolves an arm's `value` through them.
|
|
6
|
+
* Three copies of one rule is three chances for a port name to be emitted that
|
|
7
|
+
* nothing else recognises.
|
|
8
|
+
*/
|
|
9
|
+
export function outcomeSlug(name, index) {
|
|
10
|
+
const slug = String(name ?? '')
|
|
11
|
+
.trim()
|
|
12
|
+
.toLowerCase()
|
|
13
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
14
|
+
.replace(/^-+|-+$/g, '');
|
|
15
|
+
return slug || `outcome-${index + 1}`;
|
|
16
|
+
}
|
|
17
|
+
export function hitlRoutesPerOutcome(inputs, pinnedVersion) {
|
|
18
|
+
// `exposeError` selects 1.2, which routes per outcome too. Both flags are
|
|
19
|
+
// honoured on a variant so that `serialize` still refuses them there with its
|
|
20
|
+
// own message rather than silently ignoring them.
|
|
21
|
+
if (inputs?.exposeError === true || inputs?.outcomePorts === true)
|
|
22
|
+
return true;
|
|
23
|
+
if (inputs?.outcomePorts === false)
|
|
24
|
+
return false;
|
|
25
|
+
if (inputs?.variant !== undefined)
|
|
26
|
+
return false;
|
|
27
|
+
if (pinnedVersion !== undefined)
|
|
28
|
+
return false;
|
|
29
|
+
const outcomes = inputs?.outcomes ?? [];
|
|
30
|
+
if (outcomes.length <= 1)
|
|
31
|
+
return false;
|
|
32
|
+
return !outcomes.every((o) => typeof o !== 'string' && o?.action === 'End');
|
|
33
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/** One author-named exit: the value an arm matches on, and the port it wires. */
|
|
2
|
+
export interface DeclaredExit {
|
|
3
|
+
/** What `.stepSwitch()`'s arm `value` matches — the outcome or branch NAME. */
|
|
4
|
+
value: string;
|
|
5
|
+
/** The emitted source handle. */
|
|
6
|
+
port: string;
|
|
7
|
+
/**
|
|
8
|
+
* This exit ends the RUN, so it needs no arm and no auto-End: a human-task
|
|
9
|
+
* outcome with `action: 'End'`.
|
|
10
|
+
*/
|
|
11
|
+
endsProcess?: boolean;
|
|
12
|
+
/** The fall-through exit — HTTP's `default`. Last, and optional in an arm list. */
|
|
13
|
+
isDefault?: boolean;
|
|
14
|
+
}
|
|
15
|
+
/** Why a spec has no routable fan-out, phrased for a diagnostic. */
|
|
16
|
+
export interface NoExits {
|
|
17
|
+
reason: string;
|
|
18
|
+
/** What to do instead. */
|
|
19
|
+
suggestion?: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Structural, not the `FlowActionSpec` union: this module is imported by
|
|
23
|
+
* `flow-sdk.ts`'s dependents AND describes their specs, so naming the union
|
|
24
|
+
* here would close a cycle. Every field it reads is optional anyway.
|
|
25
|
+
*/
|
|
26
|
+
type SpecLike = {
|
|
27
|
+
kind?: string;
|
|
28
|
+
inputs?: unknown;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* The author-declared exits of `spec`, or a `NoExits` explaining why there are
|
|
32
|
+
* none to route.
|
|
33
|
+
*/
|
|
34
|
+
export declare function declaredExits(spec: SpecLike | undefined, pinnedVersion?: string): DeclaredExit[] | NoExits;
|
|
35
|
+
/** Narrowing helper — `declaredExits` returns one or the other. */
|
|
36
|
+
export declare function hasExits(r: DeclaredExit[] | NoExits): r is DeclaredExit[];
|
|
37
|
+
export {};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The exits an action declares that the AUTHOR named — a human task's outcomes,
|
|
3
|
+
* an HTTP call's response branches — and the port each one leaves on.
|
|
4
|
+
*
|
|
5
|
+
* This is what `.stepSwitch()` routes over, and the reason it exists as a table
|
|
6
|
+
* rather than as two more `if (spec.kind === …)` arms: `serialize` has to turn
|
|
7
|
+
* an arm's `value` into a port, `check` has to say which values are legal and
|
|
8
|
+
* which were left out, and `decompile` has to turn a port back into a value.
|
|
9
|
+
* Those three read the same list here instead of each carrying their own copy
|
|
10
|
+
* of "how a human task names its ports" and "how an HTTP branch names its
|
|
11
|
+
* ports".
|
|
12
|
+
*
|
|
13
|
+
* `undefined` means the family has no author-declared fan-out at all, which is
|
|
14
|
+
* what makes `.stepSwitch()` on it an error rather than a no-op.
|
|
15
|
+
*
|
|
16
|
+
* The `error` port is deliberately NOT here. It is declared by the definition
|
|
17
|
+
* rather than by the author, it exists on families that have no other fan-out,
|
|
18
|
+
* and it is already routed by `.onError()` / `.stepToList('error', …)`. Folding
|
|
19
|
+
* it in would make every action a `.stepSwitch` candidate and would put failure
|
|
20
|
+
* handling and outcome routing in one list, where an omitted arm would mean two
|
|
21
|
+
* different things.
|
|
22
|
+
*/
|
|
23
|
+
import { hitlRoutesPerOutcome, outcomeSlug } from './hitl-routing.js';
|
|
24
|
+
/**
|
|
25
|
+
* The author-declared exits of `spec`, or a `NoExits` explaining why there are
|
|
26
|
+
* none to route.
|
|
27
|
+
*/
|
|
28
|
+
export function declaredExits(spec, pinnedVersion) {
|
|
29
|
+
const kind = spec && 'kind' in spec ? spec.kind : undefined;
|
|
30
|
+
if (kind === 'hitl') {
|
|
31
|
+
const inputs = spec?.inputs;
|
|
32
|
+
const outcomes = (inputs?.outcomes ?? []);
|
|
33
|
+
if (outcomes.length === 0) {
|
|
34
|
+
return { reason: 'it declares no outcomes', suggestion: `outcomes: ['Approve', 'Reject']` };
|
|
35
|
+
}
|
|
36
|
+
if (!hitlRoutesPerOutcome(inputs, pinnedVersion)) {
|
|
37
|
+
// WHY it kept the single exit decides what the author can do about it, so
|
|
38
|
+
// the caller gets the specific one rather than "ports are off".
|
|
39
|
+
if (inputs?.outcomePorts === false) {
|
|
40
|
+
return {
|
|
41
|
+
reason: 'outcomePorts: false keeps its single `completed` exit',
|
|
42
|
+
suggestion: 'Drop `outcomePorts: false`.',
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
if (inputs?.variant !== undefined) {
|
|
46
|
+
return {
|
|
47
|
+
reason: `the '${inputs.variant}' variant has no per-outcome definition version to select`,
|
|
48
|
+
suggestion: `Drop the variant, or use .step() and switch on out('<step>', 'Action').`,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
if (pinnedVersion !== undefined) {
|
|
52
|
+
return {
|
|
53
|
+
reason: `{ version: '${pinnedVersion}' } pins the definition whose only exit is 'completed'`,
|
|
54
|
+
suggestion: 'Drop the pin.',
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
if (outcomes.length === 1) {
|
|
58
|
+
return {
|
|
59
|
+
reason: 'a single outcome has a single exit, so there is nothing to route',
|
|
60
|
+
suggestion: 'Use .step() — the next step follows it unconditionally.',
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
reason: 'every outcome ends the process, so no exit distinguishes them',
|
|
65
|
+
suggestion: 'Use .step().',
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
return outcomes.map((o, i) => {
|
|
69
|
+
const name = (typeof o === 'string' ? o : o?.name) ?? '';
|
|
70
|
+
return {
|
|
71
|
+
value: String(name),
|
|
72
|
+
port: `outcome-${outcomeSlug(name, i)}`,
|
|
73
|
+
...(typeof o !== 'string' && o?.action === 'End' ? { endsProcess: true } : {}),
|
|
74
|
+
};
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
if (kind === 'http') {
|
|
78
|
+
const branches = (spec?.inputs?.branches
|
|
79
|
+
?? []);
|
|
80
|
+
if (branches.length === 0) {
|
|
81
|
+
return {
|
|
82
|
+
reason: 'it declares no response branches',
|
|
83
|
+
suggestion: `branches: [{ name: 'rateLimited', condition: … }]`,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
// The fall-through is an exit like any other here — it is what `.switch()`
|
|
87
|
+
// spells as its default arm, and leaving it out is the same omission as
|
|
88
|
+
// leaving out a branch.
|
|
89
|
+
return [
|
|
90
|
+
...branches.map((b) => ({ value: String(b?.name ?? ''), port: `branch-${b?.name ?? ''}` })),
|
|
91
|
+
{ value: 'default', port: 'default', isDefault: true },
|
|
92
|
+
];
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
reason: `a ${kind ?? 'custom'} step declares no author-named exits`,
|
|
96
|
+
suggestion: 'Use .step(); route failures with .onError().',
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
/** Narrowing helper — `declaredExits` returns one or the other. */
|
|
100
|
+
export function hasExits(r) {
|
|
101
|
+
return Array.isArray(r);
|
|
102
|
+
}
|
package/dist/decompile.js
CHANGED
|
@@ -408,16 +408,39 @@ class Graph {
|
|
|
408
408
|
const first = outcomes[0]?.id;
|
|
409
409
|
return typeof first === 'string' && first !== '' ? `outcome-${first}` : undefined;
|
|
410
410
|
}
|
|
411
|
+
/**
|
|
412
|
+
* Every declared exit of `id`, when ALL of them are wired — the `.stepSwitch`
|
|
413
|
+
* shape. `undefined` otherwise, which leaves the node with a tacit main path
|
|
414
|
+
* and its siblings as side arms.
|
|
415
|
+
*/
|
|
416
|
+
fullyWiredExits(id) {
|
|
417
|
+
const node = this.byId.get(id);
|
|
418
|
+
const declared = node ? declaredExitsOfNode(node) : undefined;
|
|
419
|
+
if (declared === undefined || declared.length < 2)
|
|
420
|
+
return undefined;
|
|
421
|
+
const wired = new Set(this.outEdges(id).map((e) => e.sourcePort));
|
|
422
|
+
if (!declared.every((d) => wired.has(d.port)))
|
|
423
|
+
return undefined;
|
|
424
|
+
return new Set(declared.map((d) => d.port));
|
|
425
|
+
}
|
|
411
426
|
/**
|
|
412
427
|
* Out-edges excluding the side paths recovered as port lists: `error`,
|
|
413
428
|
* `branch-*`, and every `outcome-*` except the primary one.
|
|
429
|
+
*
|
|
430
|
+
* UNLESS every declared exit is wired. Then the node genuinely FORKS — there
|
|
431
|
+
* is no main path, each arm is a successor — and the post-dominator has to see
|
|
432
|
+
* that or it computes the join as the primary arm's first node. Which it did:
|
|
433
|
+
* a converged two-outcome task decompiled with one arm empty and the other
|
|
434
|
+
* swallowing the tail, because `ipdom(review)` came back as `publish`.
|
|
414
435
|
*/
|
|
415
436
|
successorEdges(id) {
|
|
416
437
|
const primary = this.primaryOutcomePort(id);
|
|
438
|
+
const forked = this.fullyWiredExits(id);
|
|
417
439
|
return this.outEdges(id).filter((e) => e.sourcePort !== 'error'
|
|
418
440
|
&& !ARTIFACT_PORTS.has(e.sourcePort)
|
|
419
|
-
&&
|
|
420
|
-
|
|
441
|
+
&& (forked?.has(e.sourcePort)
|
|
442
|
+
|| (!e.sourcePort.startsWith('branch-')
|
|
443
|
+
&& (!e.sourcePort.startsWith('outcome-') || e.sourcePort === primary))));
|
|
421
444
|
}
|
|
422
445
|
}
|
|
423
446
|
// ─── Node → step-spec source ──────────────────────────────────────────────────
|
|
@@ -1952,6 +1975,25 @@ function region(cursor, stop, scope, ipdom, ctx, refInto) {
|
|
|
1952
1975
|
}
|
|
1953
1976
|
// A regular action step (mock / script / http / transform / connector / …).
|
|
1954
1977
|
const spec = emitStepSpec(node, ctx.imp, ctx.inputNames, ctx.o, ctx.graph);
|
|
1978
|
+
// `.stepSwitch` when the node's author-declared exits are ALL wired and the
|
|
1979
|
+
// arms rejoin: that graph has no tacit main path to recover, so the
|
|
1980
|
+
// `.step` + `.stepToList` form below cannot express it. It used to come back
|
|
1981
|
+
// as `.stepToRef("end")` — a reference to the synthetic End node, which
|
|
1982
|
+
// `compile` then refused (REF_UNKNOWN_TARGET), so a converged port graph did
|
|
1983
|
+
// not round-trip at all. Reachable from the designer long before
|
|
1984
|
+
// `.stepSwitch` existed; the arms just had nothing to decompile INTO.
|
|
1985
|
+
const exitPorts = portArmExits(node, ctx);
|
|
1986
|
+
if (exitPorts && exitPorts.length > 1) {
|
|
1987
|
+
const r = ipdom.get(node.id) ?? stop;
|
|
1988
|
+
const armSrcs = exitPorts.map(({ value, port }) => {
|
|
1989
|
+
const armSegs = region(portTarget(ctx.graph, node.id, port), r, scope, ipdom, ctx, refInto);
|
|
1990
|
+
return `{ value: ${str(value)}, body: ${armCb('b', armSegs)} }`;
|
|
1991
|
+
});
|
|
1992
|
+
const armList = `[\n${armSrcs.map((a) => reindent(a, ' ')).join(',\n')},\n]`;
|
|
1993
|
+
segs.push(`.stepSwitch(${str(node.id)}, ${spec}, ${armList}${optsArg(node, ctx)})`);
|
|
1994
|
+
cursor = r === EXIT ? undefined : r;
|
|
1995
|
+
continue;
|
|
1996
|
+
}
|
|
1955
1997
|
segs.push(`.step(${str(node.id)}, ${spec}${optsArg(node, ctx)})`);
|
|
1956
1998
|
// An error handler on this step (source port `error` + `errorHandlingEnabled`).
|
|
1957
1999
|
const succ = scopeSucc(ctx.graph, node.id, scope);
|
|
@@ -2037,6 +2079,53 @@ function forwardReach(entries, scope, graph) {
|
|
|
2037
2079
|
}
|
|
2038
2080
|
return seen;
|
|
2039
2081
|
}
|
|
2082
|
+
/**
|
|
2083
|
+
* The node's author-declared exits when EVERY one of them is wired — the shape
|
|
2084
|
+
* `.stepSwitch` authors. `undefined` when the node has no such exits, or when
|
|
2085
|
+
* one is unwired, in which case the `.step` + `.stepToList` recovery still
|
|
2086
|
+
* applies and the unwired port stays unwired.
|
|
2087
|
+
*
|
|
2088
|
+
* Deliberately strict about "every one": a partially wired node still has a
|
|
2089
|
+
* tacit main path, and recovering THAT as `.stepSwitch` would invent arms the
|
|
2090
|
+
* flow does not have.
|
|
2091
|
+
*/
|
|
2092
|
+
function portArmExits(node, ctx) {
|
|
2093
|
+
// One predicate, on the Graph, because `successorEdges` has to agree with this
|
|
2094
|
+
// exactly: if the CFG forks here the arms must be emitted as arms, and if it
|
|
2095
|
+
// does not they must not be.
|
|
2096
|
+
if (ctx.graph.fullyWiredExits(node.id) === undefined)
|
|
2097
|
+
return undefined;
|
|
2098
|
+
return declaredExitsOfNode(node);
|
|
2099
|
+
}
|
|
2100
|
+
/**
|
|
2101
|
+
* The exits a node declares, read back off the EMITTED artifact rather than off
|
|
2102
|
+
* an authored spec — `inputs.schema.outcomes` for a human task at 1.1/1.2,
|
|
2103
|
+
* `inputs.branches` plus `default` for an HTTP call. The authoring-side twin is
|
|
2104
|
+
* `declaredExits` in `core/step-ports.ts`; they answer about different inputs,
|
|
2105
|
+
* which is why this is not a call into it.
|
|
2106
|
+
*/
|
|
2107
|
+
function declaredExitsOfNode(node) {
|
|
2108
|
+
const inputs = (node.inputs ?? {});
|
|
2109
|
+
if (String(node.type).startsWith('uipath.human-in-the-loop')) {
|
|
2110
|
+
const version = String(node.typeVersion ?? '');
|
|
2111
|
+
if (version !== '1.1' && version !== '1.2')
|
|
2112
|
+
return undefined;
|
|
2113
|
+
const outcomes = (inputs.schema?.outcomes ?? []);
|
|
2114
|
+
if (outcomes.length === 0)
|
|
2115
|
+
return undefined;
|
|
2116
|
+
return outcomes.map((o) => ({ value: String(o?.name ?? ''), port: `outcome-${o?.id ?? ''}` }));
|
|
2117
|
+
}
|
|
2118
|
+
if (node.type === T.http || node.type === T.httpV2) {
|
|
2119
|
+
const branches = (inputs.branches ?? []);
|
|
2120
|
+
if (branches.length === 0)
|
|
2121
|
+
return undefined;
|
|
2122
|
+
return [
|
|
2123
|
+
...branches.map((b) => ({ value: String(b?.name ?? ''), port: `branch-${b?.name ?? ''}` })),
|
|
2124
|
+
{ value: 'default', port: 'default' },
|
|
2125
|
+
];
|
|
2126
|
+
}
|
|
2127
|
+
return undefined;
|
|
2128
|
+
}
|
|
2040
2129
|
/**
|
|
2041
2130
|
* Emit a `.switch(...)` from a `core.logic.switch` node. Each case's serialized
|
|
2042
2131
|
* expression is `<discriminant> === <JSON-literal>`; the discriminant (shared by
|
package/dist/flow-expr-check.js
CHANGED
|
@@ -85,6 +85,10 @@ function addStepNames(steps, roots) {
|
|
|
85
85
|
if (s.default)
|
|
86
86
|
addStepNames(s.default, roots);
|
|
87
87
|
break;
|
|
88
|
+
case 'stepSwitch':
|
|
89
|
+
for (const c of s.cases)
|
|
90
|
+
addStepNames(c.body, roots);
|
|
91
|
+
break;
|
|
88
92
|
case 'loop':
|
|
89
93
|
addStepNames(s.body, roots);
|
|
90
94
|
break;
|
|
@@ -145,6 +149,13 @@ function walk(steps, roots, out) {
|
|
|
145
149
|
walk(s.default, caseRoots, out);
|
|
146
150
|
break;
|
|
147
151
|
}
|
|
152
|
+
case 'stepSwitch':
|
|
153
|
+
// The action's own inputs, then each arm — no discriminant to bind, the
|
|
154
|
+
// exits are named by the step rather than compared against a value.
|
|
155
|
+
deepExprs(s.spec.inputs, s.name, roots, out);
|
|
156
|
+
for (const c of s.cases)
|
|
157
|
+
walk(c.body, roots, out);
|
|
158
|
+
break;
|
|
148
159
|
case 'loop':
|
|
149
160
|
pushExpr(s.collection.js, s.collection.literal, s.name, roots, out);
|
|
150
161
|
if (s.options?.completionCondition) {
|
package/dist/flow-sdk.d.ts
CHANGED
|
@@ -292,6 +292,19 @@ export interface SwitchArm {
|
|
|
292
292
|
label?: string;
|
|
293
293
|
body: Step[];
|
|
294
294
|
}
|
|
295
|
+
/**
|
|
296
|
+
* One arm of a built `.stepSwitch` — the same shape as {@link SwitchArm}, but
|
|
297
|
+
* `value` names an exit the ACTION declares (a human-task outcome, an HTTP
|
|
298
|
+
* response branch) rather than a value to compare against. No `label`: a port
|
|
299
|
+
* edge carries none, and the exit's own name is already the label the canvas
|
|
300
|
+
* draws.
|
|
301
|
+
*/
|
|
302
|
+
export interface PortArm {
|
|
303
|
+
/** The exit this arm routes — a human-task outcome name, an HTTP branch name, or `'default'`. */
|
|
304
|
+
value: string;
|
|
305
|
+
/** The steps that run on that exit. Ends in `.return()` to be terminal; otherwise it converges. */
|
|
306
|
+
body: Step[];
|
|
307
|
+
}
|
|
295
308
|
/**
|
|
296
309
|
* Options shared by every builder method that creates a definition-backed node:
|
|
297
310
|
* `version` selects the exact node definition to compile against, and `updates`
|
|
@@ -425,6 +438,12 @@ export type Step = {
|
|
|
425
438
|
cases: SwitchArm[];
|
|
426
439
|
default?: Step[];
|
|
427
440
|
options?: NodeOptions;
|
|
441
|
+
} | {
|
|
442
|
+
kind: 'stepSwitch';
|
|
443
|
+
name: string;
|
|
444
|
+
spec: FlowActionSpec;
|
|
445
|
+
cases: PortArm[];
|
|
446
|
+
options?: NodeOptions;
|
|
428
447
|
} | {
|
|
429
448
|
kind: 'loop';
|
|
430
449
|
name: string;
|
|
@@ -693,6 +712,48 @@ declare class StepList {
|
|
|
693
712
|
* @returns This builder, so calls chain.
|
|
694
713
|
*/
|
|
695
714
|
step(name: string, spec: FlowActionSpec | FlowAction, options?: NodeOptions): this;
|
|
715
|
+
/**
|
|
716
|
+
* Add an action node and route EVERY exit it declares, one arm per exit.
|
|
717
|
+
*
|
|
718
|
+
* The symmetric form of `.step()` + `.stepToList()`. Where those make the
|
|
719
|
+
* first exit the tacit next step and the rest side branches, this makes all
|
|
720
|
+
* of them arms of one construct — so nothing about a human task's routing
|
|
721
|
+
* depends on which outcome happens to be listed first.
|
|
722
|
+
*
|
|
723
|
+
* Arms behave exactly like `.switch()`'s: one that ends in `.return()` is
|
|
724
|
+
* terminal, and one that does not CONVERGES, so the step after the
|
|
725
|
+
* `.stepSwitch` fans in from every arm that reaches it. (That is the
|
|
726
|
+
* difference from `.stepToList`, whose arms get an End of their own.)
|
|
727
|
+
*
|
|
728
|
+
* `value` names an exit the ACTION declares — a human task's outcome name, an
|
|
729
|
+
* HTTP response branch's name, or `'default'` for HTTP's fall-through — not a
|
|
730
|
+
* value to compare against. `check` refuses a value the step does not declare
|
|
731
|
+
* and warns about a declared exit left out, which compiles to an End node.
|
|
732
|
+
*
|
|
733
|
+
* @param name - The step's id, as `.step()`.
|
|
734
|
+
* @param spec - What the node does, from an action factory.
|
|
735
|
+
* @param cases - One arm per declared exit.
|
|
736
|
+
* @param options - Node options, as `.step()`.
|
|
737
|
+
* @returns This builder, so calls chain.
|
|
738
|
+
*
|
|
739
|
+
* @example
|
|
740
|
+
* **Route a human task's outcomes, and converge on one return**
|
|
741
|
+
* ```ts
|
|
742
|
+
* export default flow('draft-review')
|
|
743
|
+
* .output({ summary: types.string })
|
|
744
|
+
* .var('summary', types.string)
|
|
745
|
+
* .stepSwitch('review', hitl({ fields: [], outcomes: ['Publish', 'Revise'] }), [
|
|
746
|
+
* { value: 'Publish', body: (b) => b.step('publish', script({ code: 'return "published";', returns: 'string' }), { updates: { summary: out('publish') } }) },
|
|
747
|
+
* { value: 'Revise', body: (b) => b.step('sendBack', script({ code: 'return "revise";', returns: 'string' }), { updates: { summary: out('sendBack') } }) },
|
|
748
|
+
* ])
|
|
749
|
+
* .return({ summary: v('summary') })
|
|
750
|
+
* .build();
|
|
751
|
+
* ```
|
|
752
|
+
*/
|
|
753
|
+
stepSwitch(name: string, spec: FlowActionSpec | FlowAction, cases: {
|
|
754
|
+
value: string;
|
|
755
|
+
body: (b: ArmBuilder) => void;
|
|
756
|
+
}[], options?: NodeOptions): this;
|
|
696
757
|
/**
|
|
697
758
|
* Handle the PREVIOUS step's failure: if it fails, the flow runs `bodyFn`'s
|
|
698
759
|
* steps instead of continuing.
|
package/dist/flow-sdk.js
CHANGED
|
@@ -303,6 +303,67 @@ class StepList {
|
|
|
303
303
|
this.steps.push({ kind: 'action', name, spec: spec, ...(options ? { options } : {}) });
|
|
304
304
|
return this;
|
|
305
305
|
}
|
|
306
|
+
/**
|
|
307
|
+
* Add an action node and route EVERY exit it declares, one arm per exit.
|
|
308
|
+
*
|
|
309
|
+
* The symmetric form of `.step()` + `.stepToList()`. Where those make the
|
|
310
|
+
* first exit the tacit next step and the rest side branches, this makes all
|
|
311
|
+
* of them arms of one construct — so nothing about a human task's routing
|
|
312
|
+
* depends on which outcome happens to be listed first.
|
|
313
|
+
*
|
|
314
|
+
* Arms behave exactly like `.switch()`'s: one that ends in `.return()` is
|
|
315
|
+
* terminal, and one that does not CONVERGES, so the step after the
|
|
316
|
+
* `.stepSwitch` fans in from every arm that reaches it. (That is the
|
|
317
|
+
* difference from `.stepToList`, whose arms get an End of their own.)
|
|
318
|
+
*
|
|
319
|
+
* `value` names an exit the ACTION declares — a human task's outcome name, an
|
|
320
|
+
* HTTP response branch's name, or `'default'` for HTTP's fall-through — not a
|
|
321
|
+
* value to compare against. `check` refuses a value the step does not declare
|
|
322
|
+
* and warns about a declared exit left out, which compiles to an End node.
|
|
323
|
+
*
|
|
324
|
+
* @param name - The step's id, as `.step()`.
|
|
325
|
+
* @param spec - What the node does, from an action factory.
|
|
326
|
+
* @param cases - One arm per declared exit.
|
|
327
|
+
* @param options - Node options, as `.step()`.
|
|
328
|
+
* @returns This builder, so calls chain.
|
|
329
|
+
*
|
|
330
|
+
* @example
|
|
331
|
+
* **Route a human task's outcomes, and converge on one return**
|
|
332
|
+
* ```ts
|
|
333
|
+
* export default flow('draft-review')
|
|
334
|
+
* .output({ summary: types.string })
|
|
335
|
+
* .var('summary', types.string)
|
|
336
|
+
* .stepSwitch('review', hitl({ fields: [], outcomes: ['Publish', 'Revise'] }), [
|
|
337
|
+
* { value: 'Publish', body: (b) => b.step('publish', script({ code: 'return "published";', returns: 'string' }), { updates: { summary: out('publish') } }) },
|
|
338
|
+
* { value: 'Revise', body: (b) => b.step('sendBack', script({ code: 'return "revise";', returns: 'string' }), { updates: { summary: out('sendBack') } }) },
|
|
339
|
+
* ])
|
|
340
|
+
* .return({ summary: v('summary') })
|
|
341
|
+
* .build();
|
|
342
|
+
* ```
|
|
343
|
+
*/
|
|
344
|
+
stepSwitch(name, spec, cases, options) {
|
|
345
|
+
if (!Array.isArray(cases) || cases.length === 0) {
|
|
346
|
+
throw new TypeError(`.stepSwitch('${name}'): needs at least one arm. With nothing to route, use .step().`);
|
|
347
|
+
}
|
|
348
|
+
const arms = cases.map((c) => {
|
|
349
|
+
const arm = new ArmBuilder();
|
|
350
|
+
c.body?.(arm);
|
|
351
|
+
// A `.switch()` arm's label becomes `inputs.cases[].label` on the decision
|
|
352
|
+
// node. There is no such slot here: an edge carries no label, and the port
|
|
353
|
+
// is already named by the outcome or branch it leaves. Refusing beats
|
|
354
|
+
// accepting it and dropping it on the floor.
|
|
355
|
+
if (arm.armLabel !== undefined) {
|
|
356
|
+
throw new TypeError(`.stepSwitch('${name}') arm '${c.value}': .label() has nowhere to go — a port edge carries no `
|
|
357
|
+
+ `label, and the exit is named by "${c.value}" already. Use options.label to rename the NODE.`);
|
|
358
|
+
}
|
|
359
|
+
return { value: String(c.value), body: arm.steps };
|
|
360
|
+
});
|
|
361
|
+
this.steps.push({
|
|
362
|
+
kind: 'stepSwitch', name, spec: spec, cases: arms,
|
|
363
|
+
...(options ? { options } : {}),
|
|
364
|
+
});
|
|
365
|
+
return this;
|
|
366
|
+
}
|
|
306
367
|
/**
|
|
307
368
|
* Handle the PREVIOUS step's failure: if it fails, the flow runs `bodyFn`'s
|
|
308
369
|
* steps instead of continuing.
|
package/dist/serialize.js
CHANGED
|
@@ -14,6 +14,8 @@ import { Expr, toExpr, SCHEDULE_PRESETS, DELAY_PRESETS, parseIxpProjectType, fla
|
|
|
14
14
|
import { FlowAction, FlowTrigger } from './core/node-classes.js';
|
|
15
15
|
import { connectorRawNodeRefusal } from './core/connector-raw-node.js';
|
|
16
16
|
import { bindingSelfNameMessage } from './core/binding-messages.js';
|
|
17
|
+
import { hitlRoutesPerOutcome } from './core/hitl-routing.js';
|
|
18
|
+
import { declaredExits, hasExits } from './core/step-ports.js';
|
|
17
19
|
import { buildConfiguration, transportHttpMethod } from './config.js';
|
|
18
20
|
import { stableId } from './core/stable-id.js';
|
|
19
21
|
import { readEventFilter, eventFilterProblem, eventFilterJmes, eventFilterTreeLeaf } from './event-filters.js';
|
|
@@ -38,6 +40,7 @@ function bodyUsesBreak(steps) {
|
|
|
38
40
|
return steps.some((s) => s.kind === 'break'
|
|
39
41
|
|| (s.kind === 'branch' && (bodyUsesBreak(s.then) || bodyUsesBreak(s.otherwise)))
|
|
40
42
|
|| (s.kind === 'switch' && (s.cases.some((c) => bodyUsesBreak(c.body)) || (s.default ? bodyUsesBreak(s.default) : false)))
|
|
43
|
+
|| (s.kind === 'stepSwitch' && s.cases.some((c) => bodyUsesBreak(c.body)))
|
|
41
44
|
|| (s.kind === 'parallel' && s.arms.some(bodyUsesBreak))
|
|
42
45
|
|| (s.kind === 'stepToList' && bodyUsesBreak(s.body)));
|
|
43
46
|
}
|
|
@@ -3676,6 +3679,8 @@ function stepNamesOf(steps, out = []) {
|
|
|
3676
3679
|
s.cases.forEach((c) => stepNamesOf(c.body, out));
|
|
3677
3680
|
stepNamesOf(s.default ?? [], out);
|
|
3678
3681
|
}
|
|
3682
|
+
else if (s.kind === 'stepSwitch')
|
|
3683
|
+
s.cases.forEach((c) => stepNamesOf(c.body, out));
|
|
3679
3684
|
else if (s.kind === 'parallel')
|
|
3680
3685
|
s.arms.forEach((a) => stepNamesOf(a, out));
|
|
3681
3686
|
else if (s.kind === 'loop')
|
|
@@ -4982,18 +4987,24 @@ export function serialize(built, opts = {}) {
|
|
|
4982
4987
|
const id = stepUid(scope, step.name);
|
|
4983
4988
|
const hitlType = hitlNodeType(spec.inputs);
|
|
4984
4989
|
// Rich options select a newer definition, same rule as the scheduled
|
|
4985
|
-
// trigger's cron:
|
|
4986
|
-
// exits) and `exposeError` needs 1.2 (adds the `error`
|
|
4987
|
-
// are the same per-outcome handles).
|
|
4988
|
-
// default, byte-identically.
|
|
4989
|
-
|
|
4990
|
+
// trigger's cron: per-outcome routing needs 1.1 (per-outcome
|
|
4991
|
+
// `outcome-<id>` exits) and `exposeError` needs 1.2 (adds the `error`
|
|
4992
|
+
// output; its exits are the same per-outcome handles). A zero- or
|
|
4993
|
+
// one-outcome task stays on the pinned default, byte-identically.
|
|
4994
|
+
//
|
|
4995
|
+
// More than one outcome INFERS the routing — see `hitlRoutesPerOutcome`
|
|
4996
|
+
// for the four things that turn that off, one of which is an explicit
|
|
4997
|
+
// `{ version }`, which is what keeps decompile→compile exact.
|
|
4998
|
+
const pinned = step.options?.version;
|
|
4999
|
+
const outcomeRouted = hitlRoutesPerOutcome(spec.inputs, pinned);
|
|
4990
5000
|
const neededVersion = spec.inputs?.exposeError ? '1.2' : outcomeRouted ? '1.1' : undefined;
|
|
4991
5001
|
if (neededVersion !== undefined && hitlType !== NODE_TYPE.hitl) {
|
|
4992
5002
|
throw new Error(`"${step.name}": outcomePorts/exposeError need the base human task's ${neededVersion} `
|
|
4993
5003
|
+ `definition — the '${spec.inputs?.variant}' variant has no per-outcome version. `
|
|
4994
5004
|
+ `Drop the variant, or route on out('${step.name}', 'Action') downstream.`);
|
|
4995
5005
|
}
|
|
4996
|
-
|
|
5006
|
+
// Both refusals below can only be reached by an EXPLICIT flag: the
|
|
5007
|
+
// inference already declines on a variant and on a pinned version.
|
|
4997
5008
|
if (neededVersion !== undefined && pinned !== undefined && pinned !== neededVersion) {
|
|
4998
5009
|
throw new Error(`"${step.name}": ${spec.inputs?.exposeError ? 'exposeError' : 'outcomePorts'} needs the task's ${neededVersion} `
|
|
4999
5010
|
+ `definition, but { version: '${pinned}' } pins it. Drop the pin or match it.`);
|
|
@@ -5307,6 +5318,72 @@ export function serialize(built, opts = {}) {
|
|
|
5307
5318
|
else if (step.kind === 'stepToList') {
|
|
5308
5319
|
tails = emitPortList(step, lastAction ?? '?', tails, scope, parentId, breakTarget);
|
|
5309
5320
|
}
|
|
5321
|
+
else if (step.kind === 'stepSwitch') {
|
|
5322
|
+
// The action, then EVERY exit it declares as an arm — no tacit tail.
|
|
5323
|
+
//
|
|
5324
|
+
// The exits are resolved BEFORE the node is emitted, because a spec with
|
|
5325
|
+
// no routable fan-out (a human task pinned to 1.0, a variant, an http
|
|
5326
|
+
// with no branches) must refuse rather than emit a node and then
|
|
5327
|
+
// discover it has one port. `declaredExits` carries the reason.
|
|
5328
|
+
const exits = declaredExits(step.spec, step.options?.version);
|
|
5329
|
+
if (!hasExits(exits)) {
|
|
5330
|
+
throw new Error(`.stepSwitch("${step.name}"): ${exits.reason}, so there are no exits to route.`
|
|
5331
|
+
+ (exits.suggestion ? ` ${exits.suggestion}` : ''));
|
|
5332
|
+
}
|
|
5333
|
+
const action = {
|
|
5334
|
+
kind: 'action', name: step.name, spec: step.spec,
|
|
5335
|
+
...(step.options ? { options: step.options } : {}),
|
|
5336
|
+
};
|
|
5337
|
+
// The tail `emitAction` hands back is the family's tacit exit — the
|
|
5338
|
+
// primary outcome, http's `default`. Discarded on purpose: every exit is
|
|
5339
|
+
// an arm here, so that port is wired by the arm that names it (and
|
|
5340
|
+
// wiring both is flow-builder-sdk#741).
|
|
5341
|
+
emitAction(action, tails, scope, parentId);
|
|
5342
|
+
applyUpdates(action);
|
|
5343
|
+
applyLabel(action);
|
|
5344
|
+
const nodeId = scope.stepNodeId.get(step.name);
|
|
5345
|
+
if (!nodeId)
|
|
5346
|
+
throw new Error(`.stepSwitch("${step.name}"): emitted node not found.`);
|
|
5347
|
+
const armTails = [];
|
|
5348
|
+
const routed = new Set();
|
|
5349
|
+
for (const c of step.cases) {
|
|
5350
|
+
const exit = exits.find((e) => e.value === c.value);
|
|
5351
|
+
if (!exit) {
|
|
5352
|
+
throw new Error(`.stepSwitch("${step.name}"): no exit named ${JSON.stringify(c.value)}. `
|
|
5353
|
+
+ `It declares ${exits.map((e) => JSON.stringify(e.value)).join(', ')}.`);
|
|
5354
|
+
}
|
|
5355
|
+
routed.add(exit.port);
|
|
5356
|
+
// Arms CONVERGE, exactly as `.switch()`'s do: every arm that does not
|
|
5357
|
+
// end in `.return()` contributes its tail, so the next step fans in
|
|
5358
|
+
// from all of them. (`.stepToList` instead gives each arm a private
|
|
5359
|
+
// End, which silently drops the flow's outputs on that path —
|
|
5360
|
+
// flow-builder-sdk#742.)
|
|
5361
|
+
armTails.push(...emit(c.body, [{ node: nodeId, port: exit.port }], scope, parentId, breakTarget));
|
|
5362
|
+
}
|
|
5363
|
+
// A declared exit with no arm gets an End, so the run FINISHES there
|
|
5364
|
+
// rather than reaching a port with no edge and stalling. `check` warns
|
|
5365
|
+
// (STEP_SWITCH_EXIT_UNROUTED) — this is the compile-side half of it.
|
|
5366
|
+
//
|
|
5367
|
+
// `action: 'End'` outcomes get one TOO, and that is deliberate: the
|
|
5368
|
+
// designer draws a handle for every entry in `inputs.schema.outcomes`
|
|
5369
|
+
// whatever its action, so skipping them left a drawn handle wired to
|
|
5370
|
+
// nothing. UiPath/skills' own gate says the same and is stricter than
|
|
5371
|
+
// this was — `assert_outcome_wiring` requires an edge per outcome with
|
|
5372
|
+
// no exemption, and its convention makes every non-primary outcome an
|
|
5373
|
+
// `End`. What the action changes is the WARNING, not the edge: nobody
|
|
5374
|
+
// needs to be told to write an arm for an outcome that ends the run.
|
|
5375
|
+
for (const e of exits) {
|
|
5376
|
+
if (routed.has(e.port))
|
|
5377
|
+
continue;
|
|
5378
|
+
const endId = uid(scope, `${step.name}${e.port.charAt(0).toUpperCase()}${e.port.slice(1)}End`);
|
|
5379
|
+
const end = makeNode(NODE_TYPE.end, endId, 'End', parentId ? { parentId } : {});
|
|
5380
|
+
end.inputs = {};
|
|
5381
|
+
scope.nodes.push(end);
|
|
5382
|
+
addEdge(scope, { node: nodeId, port: e.port }, endId);
|
|
5383
|
+
}
|
|
5384
|
+
tails = armTails;
|
|
5385
|
+
lastAction = step.name;
|
|
5386
|
+
}
|
|
5310
5387
|
else if (step.kind === 'branch') {
|
|
5311
5388
|
const id = stepUid(scope, step.name);
|
|
5312
5389
|
const node = makeNode(NODE_TYPE.decision, id, step.options?.label ?? step.label ?? step.name, { parentId, requestedVersion: step.options?.version });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uipath/maestro-builder-sdk",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "6.0.0",
|
|
4
4
|
"description": "Build UiPath Flow, Case, and BPMN artifacts by writing TypeScript.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://docs.uipath.com/maestro",
|
|
@@ -87,5 +87,5 @@
|
|
|
87
87
|
"@types/node": "^22.7.0",
|
|
88
88
|
"esbuild": "^0.28.1"
|
|
89
89
|
},
|
|
90
|
-
"gitref": "
|
|
90
|
+
"gitref": "8c327b39940d34432128dd713c731027bb00fbde"
|
|
91
91
|
}
|