@uipath/maestro-builder-sdk 5.4.1 → 6.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/generators/_connections.py +44 -0
- package/dist/generators/_resolve.py +52 -6
- package/dist/generators/prepare_connector.py +172 -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 {};
|