@uipath/maestro-builder-sdk 6.8.3 → 6.8.4
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 +357 -114
- package/dist/core/actions.d.ts +20 -20
- package/dist/core/hitl-routing.d.ts +129 -34
- package/dist/core/hitl-routing.js +97 -13
- package/dist/core/step-ports.d.ts +8 -0
- package/dist/core/step-ports.js +37 -30
- package/dist/decompile.js +85 -14
- package/dist/serialize.js +192 -58
- package/package.json +2 -2
package/dist/decompile.js
CHANGED
|
@@ -37,6 +37,7 @@ import { inferScriptShape } from './core/script-return.js';
|
|
|
37
37
|
import { DEFAULT_TRIGGER_ID } from './core/expr.js';
|
|
38
38
|
import { parseIxpProjectType } from './core/actions.js';
|
|
39
39
|
import { readFlowGlobals } from './flow-variables.js';
|
|
40
|
+
import { OUTCOME_HANDLE_NODE_TYPES } from './core/hitl-routing.js';
|
|
40
41
|
import { eventFilterFromTreeLeaf } from './event-filters.js';
|
|
41
42
|
/**
|
|
42
43
|
* The trigger id of the flow currently being decompiled, so a reference through a
|
|
@@ -410,13 +411,43 @@ class Graph {
|
|
|
410
411
|
const first = outcomes[0]?.id;
|
|
411
412
|
return typeof first === 'string' && first !== '' ? `outcome-${first}` : undefined;
|
|
412
413
|
}
|
|
414
|
+
/**
|
|
415
|
+
* A base or quick-form human task whose EVERY outcome is wired, all to ONE
|
|
416
|
+
* step, with no other exit edge — what `serialize` emits for `.step()` after a
|
|
417
|
+
* task that does not route per outcome (quick-form's default, a pinned 1.0,
|
|
418
|
+
* `outcomePorts: false`), and what the designer saves when every outcome
|
|
419
|
+
* handle is dragged to the same node. It decompiles to a `.step()` with
|
|
420
|
+
* `outcomePorts: false` (2+ outcomes), which recompiles to the same edges.
|
|
421
|
+
*
|
|
422
|
+
* A base task on its 1.1/1.2 definition is left out on purpose: that version
|
|
423
|
+
* IS its routing (`outcomePorts: true` / `exposeError: true` select it), and
|
|
424
|
+
* it already round-trips exactly as a `.stepSwitch` with empty arms.
|
|
425
|
+
*/
|
|
426
|
+
convergedFanOut(id) {
|
|
427
|
+
const n = this.byId.get(id);
|
|
428
|
+
if (!n || !OUTCOME_HANDLE_NODE_TYPES.has(String(n.type)) || versionRoutesHitl(n))
|
|
429
|
+
return false;
|
|
430
|
+
const ids = outcomeIdsOfNode(n);
|
|
431
|
+
if (ids.length === 0)
|
|
432
|
+
return false;
|
|
433
|
+
const exits = this.outEdges(id).filter((e) => e.sourcePort !== 'error' && !ARTIFACT_PORTS.has(e.sourcePort));
|
|
434
|
+
if (exits.length !== ids.length)
|
|
435
|
+
return false;
|
|
436
|
+
const ports = new Set(exits.map((e) => e.sourcePort));
|
|
437
|
+
if (!ids.every((i) => ports.has(`outcome-${i}`)))
|
|
438
|
+
return false;
|
|
439
|
+
return new Set(exits.map((e) => e.targetNodeId)).size === 1;
|
|
440
|
+
}
|
|
413
441
|
/**
|
|
414
442
|
* Every declared exit of `id`, when ALL of them are wired — the `.stepSwitch`
|
|
415
443
|
* shape. `undefined` otherwise, which leaves the node with a tacit main path
|
|
416
|
-
* and its siblings as side arms
|
|
444
|
+
* and its siblings as side arms — and for a converged fan-out, whose one
|
|
445
|
+
* target is the main path.
|
|
417
446
|
*/
|
|
418
447
|
fullyWiredExits(id) {
|
|
419
448
|
const node = this.byId.get(id);
|
|
449
|
+
if (this.convergedFanOut(id))
|
|
450
|
+
return undefined;
|
|
420
451
|
const declared = node ? declaredExitsOfNode(node) : undefined;
|
|
421
452
|
if (declared === undefined || declared.length < 2)
|
|
422
453
|
return undefined;
|
|
@@ -609,7 +640,7 @@ function emitStepSpec(node, imp, inputNames, o, graph) {
|
|
|
609
640
|
case T.hitlQuickForm:
|
|
610
641
|
case T.hitlActionApp:
|
|
611
642
|
case T.hitlDocValidation:
|
|
612
|
-
return emitHitl(node, imp, inputNames);
|
|
643
|
+
return emitHitl(node, imp, inputNames, graph);
|
|
613
644
|
case T.queueCreate:
|
|
614
645
|
case T.queueCreateAndWait: {
|
|
615
646
|
// `itemData` is the node's name for the surface's `item` (the reference
|
|
@@ -1320,7 +1351,7 @@ function emitAgentCluster(node, imp, inputNames, graph) {
|
|
|
1320
1351
|
* definition's own `inputDefaults.recipient` for delivery. Recovering those
|
|
1321
1352
|
* would add arguments the author never wrote to every task.
|
|
1322
1353
|
*/
|
|
1323
|
-
function emitHitl(node, imp, inputNames) {
|
|
1354
|
+
function emitHitl(node, imp, inputNames, graph) {
|
|
1324
1355
|
const i = (node.inputs ?? {});
|
|
1325
1356
|
const schema = (i.schema ?? {});
|
|
1326
1357
|
const plain = (v) => {
|
|
@@ -1417,11 +1448,39 @@ function emitHitl(node, imp, inputNames) {
|
|
|
1417
1448
|
['render', render !== undefined && render !== 'standard' ? str(render) : undefined],
|
|
1418
1449
|
['taxonomy', stationExpr(i.taxonomy)],
|
|
1419
1450
|
]);
|
|
1420
|
-
//
|
|
1421
|
-
//
|
|
1451
|
+
// On the base node the VERSION says it: 1.1 routes per outcome, 1.2 adds the
|
|
1452
|
+
// error output (and implies the routing). Everywhere else — quick-form, a
|
|
1453
|
+
// base task on 1.0 — the EDGES say it: outcomes that lead to different places
|
|
1454
|
+
// are routed per outcome (`outcomePorts: true`, which a pinned 1.0 keeps at
|
|
1455
|
+
// 1.0), and outcomes that all lead to one step are a fan-out, which the
|
|
1456
|
+
// task's default recompiles to.
|
|
1422
1457
|
const version = String(node.typeVersion ?? '');
|
|
1423
1458
|
const exposeError = version === '1.2';
|
|
1424
|
-
const
|
|
1459
|
+
const outEdges = graph?.outEdges(node.id) ?? [];
|
|
1460
|
+
const derivesHandles = OUTCOME_HANDLE_NODE_TYPES.has(String(node.type));
|
|
1461
|
+
const outcomePorts = (node.type === T.hitl && version === '1.1')
|
|
1462
|
+
|| (derivesHandles && !versionRoutesHitl(node) && graph !== undefined && !graph.convergedFanOut(node.id)
|
|
1463
|
+
&& outEdges.some((e) => e.sourcePort.startsWith('outcome-')));
|
|
1464
|
+
// A lone `completed` edge on a task with several outcomes is the shape the
|
|
1465
|
+
// designer stopped drawing: before flow-workbench #3512 (09-08) it was
|
|
1466
|
+
// unconditioned, so every outcome continued down it; since then Studio Web and
|
|
1467
|
+
// the converter remap it to the primary outcome only. Recompiling writes one
|
|
1468
|
+
// `outcome-<id>` edge per outcome to the same step — the first meaning — so
|
|
1469
|
+
// this is the one place decompile→compile is deliberately not exact. Say so.
|
|
1470
|
+
const legacyCompleted = derivesHandles && outcomeIdsOfNode(node).length > 1
|
|
1471
|
+
&& outEdges.some((e) => e.sourcePort === 'completed');
|
|
1472
|
+
// Both a converged fan-out and a legacy lone `completed` mean "every outcome
|
|
1473
|
+
// continues to the next step". Say so in the source with `outcomePorts: false`
|
|
1474
|
+
// rather than leaving it to the `{ version }` pin, so the intent survives an
|
|
1475
|
+
// author dropping the pin.
|
|
1476
|
+
const fanOut = !outcomePorts && derivesHandles && !versionRoutesHitl(node) && outcomeIdsOfNode(node).length > 1
|
|
1477
|
+
&& (legacyCompleted || graph?.convergedFanOut(node.id) === true);
|
|
1478
|
+
if (legacyCompleted) {
|
|
1479
|
+
warnOnce(`human task ${str(node.id)} leaves on the legacy 'completed' handle, which Studio Web no longer draws `
|
|
1480
|
+
+ `once a task has outcomes (it reads the edge as the primary outcome only). Recompiling emits one `
|
|
1481
|
+
+ `outcome-<id> edge per outcome to the same step, so every outcome continues — what the edge meant `
|
|
1482
|
+
+ `before flow-workbench #3512.`);
|
|
1483
|
+
}
|
|
1425
1484
|
const title = plain(i.title);
|
|
1426
1485
|
const priority = plain(i.priority);
|
|
1427
1486
|
return `${imp.use('hitl')}(${opts([
|
|
@@ -1438,7 +1497,7 @@ function emitHitl(node, imp, inputNames) {
|
|
|
1438
1497
|
['fields', fields.length ? `[${fields.join(', ')}]`
|
|
1439
1498
|
: (variant === 'action-app' || variant === 'document-validation') ? undefined : '[]'],
|
|
1440
1499
|
['outcomes', `[${outcomes.join(', ')}]`],
|
|
1441
|
-
['outcomePorts', outcomePorts ? 'true' : undefined],
|
|
1500
|
+
['outcomePorts', outcomePorts ? 'true' : fanOut ? 'false' : undefined],
|
|
1442
1501
|
['exposeError', exposeError ? 'true' : undefined],
|
|
1443
1502
|
])})`;
|
|
1444
1503
|
}
|
|
@@ -2081,7 +2140,9 @@ function region(cursor, stop, scope, ipdom, ctx, refInto) {
|
|
|
2081
2140
|
// A per-outcome human task routes each NON-primary outcome from its own
|
|
2082
2141
|
// `outcome-<slug>` exit; the primary one continues the main path, so it is
|
|
2083
2142
|
// already a successor (see Graph.primaryOutcomePort).
|
|
2084
|
-
|
|
2143
|
+
// A converged fan-out has none: every outcome continues to the one
|
|
2144
|
+
// successor, which is what `.step()` recompiles to.
|
|
2145
|
+
if (!ctx.graph.convergedFanOut(node.id)) {
|
|
2085
2146
|
const primary = ctx.graph.primaryOutcomePort(node.id);
|
|
2086
2147
|
for (const oe of ctx.graph.outEdges(node.id)
|
|
2087
2148
|
.filter((e) => e.sourcePort.startsWith('outcome-') && e.sourcePort !== primary)) {
|
|
@@ -2165,18 +2226,28 @@ function portArmExits(node, ctx) {
|
|
|
2165
2226
|
return undefined;
|
|
2166
2227
|
return declaredExitsOfNode(node);
|
|
2167
2228
|
}
|
|
2229
|
+
/** A base human task on its 1.1/1.2 definition, where the VERSION selects per-outcome routing. */
|
|
2230
|
+
function versionRoutesHitl(node) {
|
|
2231
|
+
const version = String(node.typeVersion ?? '');
|
|
2232
|
+
return node.type === T.hitl && (version === '1.1' || version === '1.2');
|
|
2233
|
+
}
|
|
2234
|
+
/** The outcome ids in a human task's `inputs.schema.outcomes`. */
|
|
2235
|
+
function outcomeIdsOfNode(node) {
|
|
2236
|
+
const outcomes = (node.inputs?.schema?.outcomes ?? []);
|
|
2237
|
+
return outcomes.map((o) => o?.id).filter((id) => typeof id === 'string' && id !== '');
|
|
2238
|
+
}
|
|
2168
2239
|
/**
|
|
2169
2240
|
* The exits a node declares, read back off the EMITTED artifact rather than off
|
|
2170
|
-
* an authored spec — `inputs.schema.outcomes` for a
|
|
2171
|
-
*
|
|
2172
|
-
* `
|
|
2173
|
-
*
|
|
2241
|
+
* an authored spec — `inputs.schema.outcomes` for a base or quick-form human
|
|
2242
|
+
* task (the product draws one `outcome-<id>` handle per outcome on both, at
|
|
2243
|
+
* every version), `inputs.branches` plus `default` for an HTTP call. The
|
|
2244
|
+
* authoring-side twin is `declaredExits` in `core/step-ports.ts`; they answer
|
|
2245
|
+
* about different inputs, which is why this is not a call into it.
|
|
2174
2246
|
*/
|
|
2175
2247
|
function declaredExitsOfNode(node) {
|
|
2176
2248
|
const inputs = (node.inputs ?? {});
|
|
2177
2249
|
if (String(node.type).startsWith('uipath.human-in-the-loop')) {
|
|
2178
|
-
|
|
2179
|
-
if (version !== '1.1' && version !== '1.2')
|
|
2250
|
+
if (!OUTCOME_HANDLE_NODE_TYPES.has(String(node.type)))
|
|
2180
2251
|
return undefined;
|
|
2181
2252
|
const outcomes = (inputs.schema?.outcomes ?? []);
|
|
2182
2253
|
if (outcomes.length === 0)
|
package/dist/serialize.js
CHANGED
|
@@ -14,8 +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 {
|
|
18
|
-
import { declaredExits, hasExits } from './core/step-ports.js';
|
|
17
|
+
import { OUTCOME_HANDLE_NODE_TYPES, assertHitlExitInvariant, hitlExitModel, hitlExitViolation, outcomeSlug } from './core/hitl-routing.js';
|
|
18
|
+
import { declaredExits, hasExits, stopsAtEnd } from './core/step-ports.js';
|
|
19
19
|
import { buildConfiguration, transportHttpMethod } from './config.js';
|
|
20
20
|
import { stableId } from './core/stable-id.js';
|
|
21
21
|
import { readEventFilter, eventFilterProblem, eventFilterJmes, eventFilterTreeLeaf } from './event-filters.js';
|
|
@@ -2793,14 +2793,13 @@ function bindingPath(value, rename) {
|
|
|
2793
2793
|
// `__hitlInputBindings` the designer reads back stores the binding verbatim.
|
|
2794
2794
|
return path.startsWith('vars.') ? path : `vars.${path}`;
|
|
2795
2795
|
}
|
|
2796
|
-
/**
|
|
2796
|
+
/**
|
|
2797
|
+
* `Approve` → `approve`; a name that slugifies to nothing falls back to its index.
|
|
2798
|
+
* The same rule that names the `outcome-<id>` ports (`outcomeSlug`), so an id
|
|
2799
|
+
* this writes into `schema.outcomes` is always one a port can name.
|
|
2800
|
+
*/
|
|
2797
2801
|
function outcomeId(name, index) {
|
|
2798
|
-
|
|
2799
|
-
.trim()
|
|
2800
|
-
.toLowerCase()
|
|
2801
|
-
.replace(/[^a-z0-9]+/g, '-')
|
|
2802
|
-
.replace(/^-+|-+$/g, '');
|
|
2803
|
-
return slug || `outcome-${index + 1}`;
|
|
2802
|
+
return outcomeSlug(name, index);
|
|
2804
2803
|
}
|
|
2805
2804
|
/**
|
|
2806
2805
|
* A deterministic UUID for the form schema, derived from the flow and node ids.
|
|
@@ -3859,7 +3858,28 @@ export function serialize(built, opts = {}) {
|
|
|
3859
3858
|
scope.stepNodeId.set(stepName, id);
|
|
3860
3859
|
return id;
|
|
3861
3860
|
}
|
|
3861
|
+
/**
|
|
3862
|
+
* Base and quick-form human tasks emitted so far, by node id. `addEdge` holds
|
|
3863
|
+
* every edge out of one to the product's handle rule (`hitlExitViolation`), so
|
|
3864
|
+
* `completed` on a task with outcomes fails at the edge that would write it;
|
|
3865
|
+
* `assertHitlExitInvariant` repeats the check over each finished scope for the
|
|
3866
|
+
* edges that do not come through here (parked `.stepToRef` rejoins).
|
|
3867
|
+
*/
|
|
3868
|
+
const hitlNodes = new Map();
|
|
3869
|
+
/**
|
|
3870
|
+
* Human tasks with an outcome whose path stops at an End while another
|
|
3871
|
+
* outcome continues — read by `.parallel`, whose join would wait forever for
|
|
3872
|
+
* the continuing path (check's HITL_END_OUTCOME_IN_PARALLEL). Two ways in: a
|
|
3873
|
+
* fan-out task's `action: 'End'` outcome, and a `.stepSwitch` outcome with no
|
|
3874
|
+
* arm (it gets an End) or whose arm ends in `.return()`. `why` finishes the
|
|
3875
|
+
* sentence "human task X …" in the refusal.
|
|
3876
|
+
*/
|
|
3877
|
+
const hitlEndExits = [];
|
|
3862
3878
|
function addEdge(scope, from, toNode, toPort = 'input') {
|
|
3879
|
+
const hitlNode = hitlNodes.get(from.node);
|
|
3880
|
+
const refused = hitlNode ? hitlExitViolation(hitlNode, from.port) : undefined;
|
|
3881
|
+
if (refused)
|
|
3882
|
+
throw new Error(`serialize: ${refused}`);
|
|
3863
3883
|
scope.edges.push({ id: `edge_${edgeSeq++}`, sourceNodeId: from.node, sourcePort: from.port, targetNodeId: toNode, targetPort: toPort });
|
|
3864
3884
|
}
|
|
3865
3885
|
let col = 0;
|
|
@@ -3959,6 +3979,7 @@ export function serialize(built, opts = {}) {
|
|
|
3959
3979
|
for (const vd of child.vars)
|
|
3960
3980
|
globals.push({ id: vd.name, name: vd.name, direction: 'inout', type: vd.type, ...varMeta(vd) });
|
|
3961
3981
|
resolveRejoins(scope);
|
|
3982
|
+
assertHitlExitInvariant(scope.nodes, scope.edges);
|
|
3962
3983
|
typeAgentInputSchemas(scope, globals);
|
|
3963
3984
|
emitStickyNotes(child, scope);
|
|
3964
3985
|
const layoutEdges = applyAuthorLayout(child.layout, scope, `subflow "${nodeId}"`);
|
|
@@ -4077,13 +4098,11 @@ export function serialize(built, opts = {}) {
|
|
|
4077
4098
|
// the shipped `sample-flows/queue-operations.flow` wires its edges to it.
|
|
4078
4099
|
if (type === NODE_TYPE.queueCreate || type === NODE_TYPE.queueCreateAndWait)
|
|
4079
4100
|
return 'success';
|
|
4080
|
-
//
|
|
4081
|
-
//
|
|
4082
|
-
//
|
|
4083
|
-
//
|
|
4084
|
-
//
|
|
4085
|
-
// — and all three human-task node types declare the same one, so the port is
|
|
4086
|
-
// the same whichever `variant` was named.
|
|
4101
|
+
// Every human-task definition declares one static source handle, `completed`.
|
|
4102
|
+
// It is the real exit only where the product draws it: action-app,
|
|
4103
|
+
// document-validation, and a task with no outcomes. A base or quick-form task
|
|
4104
|
+
// with outcomes leaves on `outcome-<id>` instead (`hitlExitModel`), and never
|
|
4105
|
+
// reaches this function for its tail.
|
|
4087
4106
|
if (type === NODE_TYPE.hitl || type === NODE_TYPE.hitlQuickForm || type === NODE_TYPE.hitlActionApp
|
|
4088
4107
|
|| type === NODE_TYPE.hitlDocValidation)
|
|
4089
4108
|
return 'completed';
|
|
@@ -5094,28 +5113,31 @@ export function serialize(built, opts = {}) {
|
|
|
5094
5113
|
if (spec.kind === 'hitl') {
|
|
5095
5114
|
const id = stepUid(scope, step.name);
|
|
5096
5115
|
const hitlType = hitlNodeType(spec.inputs);
|
|
5097
|
-
//
|
|
5098
|
-
//
|
|
5099
|
-
// `outcome-<id>` exits) and `exposeError` needs 1.2 (adds the `error`
|
|
5100
|
-
// output; its exits are the same per-outcome handles). A zero- or
|
|
5101
|
-
// one-outcome task stays on the pinned default, byte-identically.
|
|
5116
|
+
// How the task leaves its node — `hitlExitModel` (core/hitl-routing.ts)
|
|
5117
|
+
// has the product rule and what `.step()` means after each shape.
|
|
5102
5118
|
//
|
|
5103
|
-
//
|
|
5104
|
-
//
|
|
5105
|
-
//
|
|
5119
|
+
// Only the BASE node has definition versions to select: routing per
|
|
5120
|
+
// outcome picks 1.1 and `exposeError` picks 1.2 (which adds the `error`
|
|
5121
|
+
// output). quick-form draws the same per-outcome handles on the one
|
|
5122
|
+
// definition it has, so it routes per outcome without moving. An explicit
|
|
5123
|
+
// pin wins over the inferred 1.1 — `flow validate` accepts `outcome-<id>`
|
|
5124
|
+
// edges on base@1.0 — but not over `exposeError`, whose output only 1.2
|
|
5125
|
+
// declares. A zero- or one-outcome task stays on the pinned default.
|
|
5106
5126
|
const pinned = step.options?.version;
|
|
5107
|
-
const
|
|
5108
|
-
|
|
5109
|
-
|
|
5110
|
-
|
|
5111
|
-
+ `definition — the '${spec.inputs?.variant}' variant has no per-outcome version. `
|
|
5112
|
-
+ `Drop the variant, or route on out('${step.name}', 'Action') downstream.`);
|
|
5127
|
+
const exitModel = hitlExitModel(spec.inputs, pinned);
|
|
5128
|
+
if (hitlType !== NODE_TYPE.hitl && spec.inputs?.exposeError === true) {
|
|
5129
|
+
throw new Error(`"${step.name}": exposeError needs the base human task's 1.2 definition, which adds the `
|
|
5130
|
+
+ `error output — the '${spec.inputs?.variant}' variant has no such version. Drop the variant or exposeError.`);
|
|
5113
5131
|
}
|
|
5114
|
-
|
|
5115
|
-
|
|
5116
|
-
|
|
5117
|
-
|
|
5118
|
-
|
|
5132
|
+
if (spec.inputs?.outcomePorts === true && !OUTCOME_HANDLE_NODE_TYPES.has(hitlType)) {
|
|
5133
|
+
throw new Error(`"${step.name}": outcomePorts: true, but the '${spec.inputs?.variant}' variant reads its outcomes `
|
|
5134
|
+
+ `from its app and leaves on its single 'completed' exit. Route on out('${step.name}', 'Action') downstream.`);
|
|
5135
|
+
}
|
|
5136
|
+
const neededVersion = hitlType !== NODE_TYPE.hitl ? undefined
|
|
5137
|
+
: spec.inputs?.exposeError ? '1.2' : exitModel.shape === 'per-outcome' ? '1.1' : undefined;
|
|
5138
|
+
if (spec.inputs?.exposeError === true && pinned !== undefined && pinned !== '1.2') {
|
|
5139
|
+
throw new Error(`"${step.name}": exposeError needs the task's 1.2 definition, but { version: '${pinned}' } pins it. `
|
|
5140
|
+
+ `Drop the pin or match it.`);
|
|
5119
5141
|
}
|
|
5120
5142
|
const node = makeNode(hitlType, id, step.name, { parentId, requestedVersion: pinned ?? neededVersion });
|
|
5121
5143
|
const { inputs, outputs } = hitlInputs(spec.inputs, id, built.id, step.name, scope.rename);
|
|
@@ -5124,15 +5146,19 @@ export function serialize(built, opts = {}) {
|
|
|
5124
5146
|
// gives their shape (see hitlInputs), so the instance's version wins.
|
|
5125
5147
|
node.outputs = outputs;
|
|
5126
5148
|
scope.nodes.push(node);
|
|
5149
|
+
if (OUTCOME_HANDLE_NODE_TYPES.has(hitlType))
|
|
5150
|
+
hitlNodes.set(id, node);
|
|
5127
5151
|
for (const t of tails)
|
|
5128
5152
|
addEdge(scope, t, id);
|
|
5129
|
-
if (
|
|
5130
|
-
//
|
|
5131
|
-
//
|
|
5132
|
-
|
|
5133
|
-
|
|
5134
|
-
|
|
5135
|
-
|
|
5153
|
+
if (exitModel.shape === 'per-outcome') {
|
|
5154
|
+
// The PRIMARY (first) outcome continues the main path; the author routes
|
|
5155
|
+
// the rest with .stepToList('outcome-<id>', …) or as .stepSwitch arms.
|
|
5156
|
+
return [{ node: id, port: exitModel.exits[0]?.port ?? `outcome-${outcomeSlug(undefined, 0)}` }];
|
|
5157
|
+
}
|
|
5158
|
+
if (exitModel.shape === 'every-outcome') {
|
|
5159
|
+
// Fan-out: every outcome continues to the next step, each on its own
|
|
5160
|
+
// handle. End outcomes are marked, and `emit` ends them.
|
|
5161
|
+
return exitModel.exits.map((e) => ({ node: id, port: e.port, ...(e.endsProcess ? { endsRun: true } : {}) }));
|
|
5136
5162
|
}
|
|
5137
5163
|
return [{ node: id, port: successPort(hitlType) }];
|
|
5138
5164
|
}
|
|
@@ -5370,6 +5396,34 @@ export function serialize(built, opts = {}) {
|
|
|
5370
5396
|
* are backward — every deployed rejoin is forward convergence, which is why
|
|
5371
5397
|
* `check` refuses a backward one outright.
|
|
5372
5398
|
*/
|
|
5399
|
+
/**
|
|
5400
|
+
* An End node on one exit of `nodeId`, so the run FINISHES there — a
|
|
5401
|
+
* `.stepSwitch` exit with no arm, and a fan-out human task's `action: 'End'`
|
|
5402
|
+
* outcome.
|
|
5403
|
+
*/
|
|
5404
|
+
function endExit(scope, nodeId, port, stepName, parentId) {
|
|
5405
|
+
const endId = uid(scope, `${stepName}${port.charAt(0).toUpperCase()}${port.slice(1)}End`);
|
|
5406
|
+
const end = makeNode(NODE_TYPE.end, endId, 'End', parentId ? { parentId } : {});
|
|
5407
|
+
end.inputs = {};
|
|
5408
|
+
scope.nodes.push(end);
|
|
5409
|
+
addEdge(scope, { node: nodeId, port }, endId);
|
|
5410
|
+
}
|
|
5411
|
+
/**
|
|
5412
|
+
* Refuse a side exit on a human task's port that the step's tails already
|
|
5413
|
+
* continue from: that would put two edges on one port (flow-builder-sdk#741).
|
|
5414
|
+
* After a fan-out task that is EVERY outcome port. `check` reports the same
|
|
5415
|
+
* as HITL_OUTCOME_PORTS_OFF; this is the compile-side half.
|
|
5416
|
+
*/
|
|
5417
|
+
function refuseRewire(method, port, source, tails) {
|
|
5418
|
+
if (!hitlNodes.has(tails[0]?.node ?? '') || !tails.some((t) => t.port === port))
|
|
5419
|
+
return;
|
|
5420
|
+
throw new Error(tails.length > 1
|
|
5421
|
+
? `${method}('${port}', …) after human task "${source}": every outcome already continues to the next `
|
|
5422
|
+
+ `step, '${port}' included, because this task does not route per outcome. Add outcomePorts: true `
|
|
5423
|
+
+ `(the first outcome then continues and the others are routed), or route every outcome with .stepSwitch.`
|
|
5424
|
+
: `${method}('${port}', …) after human task "${source}": '${port}' already continues the main path, so `
|
|
5425
|
+
+ `routing it here too would put two edges on one port (flow-builder-sdk#741).`);
|
|
5426
|
+
}
|
|
5373
5427
|
/**
|
|
5374
5428
|
* A step list hanging off a named PORT of the step just emitted.
|
|
5375
5429
|
*
|
|
@@ -5379,12 +5433,14 @@ export function serialize(built, opts = {}) {
|
|
|
5379
5433
|
*/
|
|
5380
5434
|
function emitPortList(step, source, tails, scope, parentId, breakTarget) {
|
|
5381
5435
|
const handler = step.body;
|
|
5382
|
-
// `emitAction` returns
|
|
5383
|
-
//
|
|
5384
|
-
//
|
|
5385
|
-
|
|
5386
|
-
|
|
5436
|
+
// `emitAction` returns one tail for every action kind — the node's own
|
|
5437
|
+
// success port — except a fan-out human task, which returns one per outcome
|
|
5438
|
+
// from the SAME node. Tails from two nodes mean a new action shape reached
|
|
5439
|
+
// here without being taught about side exits; say so rather than guess.
|
|
5440
|
+
if (tails.length === 0 || tails.some((t) => t.node !== tails[0].node)) {
|
|
5441
|
+
throw new Error(`.stepToList('${step.port}', …) after step "${source}": expected the step to leave from exactly one node, got ${tails.length} tail(s).`);
|
|
5387
5442
|
}
|
|
5443
|
+
refuseRewire('.stepToList', step.port, source, tails);
|
|
5388
5444
|
const nodeId = tails[0].node;
|
|
5389
5445
|
const node = scope.nodes.find((n) => n.id === nodeId);
|
|
5390
5446
|
if (!node)
|
|
@@ -5451,12 +5507,33 @@ export function serialize(built, opts = {}) {
|
|
|
5451
5507
|
throw new Error(`label on "${step.name}": emitted node not found.`);
|
|
5452
5508
|
node.display = { ...(node.display ?? {}), label };
|
|
5453
5509
|
};
|
|
5454
|
-
for (const step of steps) {
|
|
5510
|
+
for (const [stepIndex, step] of steps.entries()) {
|
|
5455
5511
|
if (step.kind === 'action') {
|
|
5456
5512
|
tails = emitAction(step, tails, scope, parentId);
|
|
5457
5513
|
applyUpdates(step);
|
|
5458
5514
|
applyLabel(step);
|
|
5459
5515
|
lastAction = step.name;
|
|
5516
|
+
// A fan-out human task's `action: 'End'` outcomes end the run here: the
|
|
5517
|
+
// flow runtime never reads `action`, so only an End node can. When the
|
|
5518
|
+
// very next step is a `.return()` they join it instead — it is an End
|
|
5519
|
+
// already, and it writes the flow's return values on that path too.
|
|
5520
|
+
if (tails.some((t) => t.endsRun)) {
|
|
5521
|
+
// Side exits attach to the task; the step that CONTINUES it is the
|
|
5522
|
+
// first one after them (check's HITL_UNREACHABLE_AFTER reads the same).
|
|
5523
|
+
const joinsReturn = steps.slice(stepIndex + 1)
|
|
5524
|
+
.find((x) => x.kind !== 'stepToList' && x.kind !== 'stepToRef')?.kind === 'return';
|
|
5525
|
+
const continuing = [];
|
|
5526
|
+
for (const t of tails) {
|
|
5527
|
+
if (t.endsRun && !joinsReturn)
|
|
5528
|
+
endExit(scope, t.node, t.port, step.name, parentId);
|
|
5529
|
+
else
|
|
5530
|
+
continuing.push({ node: t.node, port: t.port });
|
|
5531
|
+
}
|
|
5532
|
+
if (!joinsReturn && continuing.length > 0) {
|
|
5533
|
+
hitlEndExits.push({ scope, parentId, step: step.name, why: "ends the run on an action: 'End' outcome" });
|
|
5534
|
+
}
|
|
5535
|
+
tails = continuing;
|
|
5536
|
+
}
|
|
5460
5537
|
}
|
|
5461
5538
|
else if (step.kind === 'stepToList') {
|
|
5462
5539
|
tails = emitPortList(step, lastAction ?? '?', tails, scope, parentId, breakTarget);
|
|
@@ -5489,6 +5566,9 @@ export function serialize(built, opts = {}) {
|
|
|
5489
5566
|
throw new Error(`.stepSwitch("${step.name}"): emitted node not found.`);
|
|
5490
5567
|
const armTails = [];
|
|
5491
5568
|
const routed = new Set();
|
|
5569
|
+
// Outcomes of a human task whose path stops at an End: an arm that ends
|
|
5570
|
+
// in `.return()` here, an outcome with no arm below.
|
|
5571
|
+
const stopped = [];
|
|
5492
5572
|
for (const c of step.cases) {
|
|
5493
5573
|
const exit = exits.find((e) => e.value === c.value);
|
|
5494
5574
|
if (!exit) {
|
|
@@ -5501,7 +5581,12 @@ export function serialize(built, opts = {}) {
|
|
|
5501
5581
|
// from all of them. (`.stepToList` instead gives each arm a private
|
|
5502
5582
|
// End, which silently drops the flow's outputs on that path —
|
|
5503
5583
|
// flow-builder-sdk#742.)
|
|
5504
|
-
|
|
5584
|
+
const caseTails = emit(c.body, [{ node: nodeId, port: exit.port }], scope, parentId, breakTarget);
|
|
5585
|
+
if (caseTails.length === 0 && stopsAtEnd(c.body))
|
|
5586
|
+
stopped.push(String(c.value));
|
|
5587
|
+
armTails.push(...(hitlNodes.has(nodeId)
|
|
5588
|
+
? caseTails.map((t) => (t.humanSwitch ? t : { ...t, humanSwitch: step.name }))
|
|
5589
|
+
: caseTails));
|
|
5505
5590
|
}
|
|
5506
5591
|
// A declared exit with no arm gets an End, so the run FINISHES there
|
|
5507
5592
|
// rather than reaching a port with no edge and stalling. `check` warns
|
|
@@ -5516,13 +5601,20 @@ export function serialize(built, opts = {}) {
|
|
|
5516
5601
|
// `End`. What the action changes is the WARNING, not the edge: nobody
|
|
5517
5602
|
// needs to be told to write an arm for an outcome that ends the run.
|
|
5518
5603
|
for (const e of exits) {
|
|
5519
|
-
if (routed.has(e.port))
|
|
5520
|
-
|
|
5521
|
-
|
|
5522
|
-
|
|
5523
|
-
|
|
5524
|
-
|
|
5525
|
-
|
|
5604
|
+
if (!routed.has(e.port)) {
|
|
5605
|
+
endExit(scope, nodeId, e.port, step.name, parentId);
|
|
5606
|
+
stopped.push(String(e.value));
|
|
5607
|
+
}
|
|
5608
|
+
}
|
|
5609
|
+
// On a human task those Ends sit beside the arms that continue, and only
|
|
5610
|
+
// one outcome fires. Inside a `.parallel` arm the join then waits
|
|
5611
|
+
// forever for the continuing path, as for an `action: 'End'` outcome.
|
|
5612
|
+
if (hitlNodes.has(nodeId) && stopped.length > 0 && armTails.length > 0) {
|
|
5613
|
+
hitlEndExits.push({
|
|
5614
|
+
scope, parentId, step: step.name,
|
|
5615
|
+
why: `stops at an End on outcome${stopped.length === 1 ? '' : 's'} ${stopped.join(', ')} of its `
|
|
5616
|
+
+ `.stepSwitch (no arm, or an arm that ends in .return())`,
|
|
5617
|
+
});
|
|
5526
5618
|
}
|
|
5527
5619
|
tails = armTails;
|
|
5528
5620
|
lastAction = step.name;
|
|
@@ -5590,9 +5682,49 @@ export function serialize(built, opts = {}) {
|
|
|
5590
5682
|
// tail and so contributes no incoming edge; that is rejected at rung 1
|
|
5591
5683
|
// too (PARALLEL_ARM_RETURNS), since a BPMN join waits for every branch —
|
|
5592
5684
|
// UNLESS no arm reaches the join at all, which is the next paragraph.
|
|
5685
|
+
// The join waits for a token on EVERY incoming edge, and a fan-out human
|
|
5686
|
+
// task's outcome edges are mutually exclusive — only one fires. So two
|
|
5687
|
+
// of them straight into the join, or an End outcome beside a continuing
|
|
5688
|
+
// one, hang the run with no incident. check reports both
|
|
5689
|
+
// (HITL_FAN_OUT_INTO_JOIN, HITL_END_OUTCOME_IN_PARALLEL); this is the
|
|
5690
|
+
// compile-side half, and it fails closed.
|
|
5593
5691
|
const armTails = [];
|
|
5594
|
-
|
|
5595
|
-
|
|
5692
|
+
const endsBefore = hitlEndExits.length;
|
|
5693
|
+
for (const arm of step.arms) {
|
|
5694
|
+
const t = emit(arm, tails, scope, parentId, breakTarget);
|
|
5695
|
+
// An arm hands the join ONE edge, or several that are alternatives —
|
|
5696
|
+
// `.branch` / `.switch` / `.stepSwitch` arms, a fan-out task's
|
|
5697
|
+
// outcomes — of which only one fires. Refused when any of them is a
|
|
5698
|
+
// human task's outcome edge: however the task got there (at the arm's
|
|
5699
|
+
// end, one empty `.stepSwitch` arm beside a body, one side of a
|
|
5700
|
+
// `.branch`), the join waits for all of them. Rejoining arms with no
|
|
5701
|
+
// human task in them are the older, general case (still emitted).
|
|
5702
|
+
const outcomeTail = t.find((x) => hitlNodes.has(x.node) && x.port.startsWith('outcome-'));
|
|
5703
|
+
// The same holds when each outcome's arm ended on a step of its own:
|
|
5704
|
+
// a complete `.stepSwitch` with nothing after it hands the join one
|
|
5705
|
+
// edge per arm, and still only one arm runs.
|
|
5706
|
+
const switchTail = outcomeTail ? undefined : t.find((x) => x.humanSwitch !== undefined);
|
|
5707
|
+
if (t.length >= 2 && switchTail) {
|
|
5708
|
+
throw new Error(`.parallel("${step.name}"): an arm sends ${t.length} alternative edges into join "${step.name}" — `
|
|
5709
|
+
+ `among them the end of an arm of human task "${switchTail.humanSwitch}"'s .stepSwitch — and only one `
|
|
5710
|
+
+ `of them fires, while the join waits for all of them. Add a step at the end of the arm, after the `
|
|
5711
|
+
+ `.stepSwitch, so its arms meet there first, or move the task out of the .parallel.`);
|
|
5712
|
+
}
|
|
5713
|
+
if (t.length >= 2 && outcomeTail) {
|
|
5714
|
+
throw new Error(`.parallel("${step.name}"): an arm sends ${t.length} alternative edges into join "${step.name}" — `
|
|
5715
|
+
+ `among them outcome edge "${outcomeTail.port}" of human task "${outcomeTail.node}" — and only one `
|
|
5716
|
+
+ `of them fires, while the join waits for all of them. Add a step at the end of the arm so its paths `
|
|
5717
|
+
+ `meet there first, or move the task out of the .parallel.`);
|
|
5718
|
+
}
|
|
5719
|
+
armTails.push(...t);
|
|
5720
|
+
}
|
|
5721
|
+
const starved = hitlEndExits.slice(endsBefore).find((x) => x.scope === scope && x.parentId === parentId);
|
|
5722
|
+
if (starved && armTails.length > 0) {
|
|
5723
|
+
throw new Error(`.parallel("${step.name}"): human task "${starved.step}" ${starved.why} inside an arm, and join `
|
|
5724
|
+
+ `"${step.name}" would wait forever for the path that continues. Give every outcome a path that `
|
|
5725
|
+
+ `continues to the end of the arm (drop action: 'End', add the missing .stepSwitch arm, move the `
|
|
5726
|
+
+ `.return() after the .parallel), or move the task out of the .parallel.`);
|
|
5727
|
+
}
|
|
5596
5728
|
// When EVERY arm is terminal — a terminate, or an End — there is nothing
|
|
5597
5729
|
// left to join, and emitting a Merge would add a node with no incoming
|
|
5598
5730
|
// edge, which is unreachable. The platform's own flows agree:
|
|
@@ -5714,6 +5846,7 @@ export function serialize(built, opts = {}) {
|
|
|
5714
5846
|
const from = tails[0];
|
|
5715
5847
|
if (!from)
|
|
5716
5848
|
throw new Error(`.stepToRef('${step.port}', '${step.target}') has no step to leave from.`);
|
|
5849
|
+
refuseRewire('.stepToRef', step.port, lastAction ?? '?', tails);
|
|
5717
5850
|
if (step.port === 'error') {
|
|
5718
5851
|
const node = scope.nodes.find((n) => n.id === from.node);
|
|
5719
5852
|
if (node)
|
|
@@ -5866,6 +5999,7 @@ export function serialize(built, opts = {}) {
|
|
|
5866
5999
|
}
|
|
5867
6000
|
}
|
|
5868
6001
|
resolveRejoins(root);
|
|
6002
|
+
assertHitlExitInvariant(root.nodes, root.edges);
|
|
5869
6003
|
typeAgentInputSchemas(root, globals);
|
|
5870
6004
|
emitStickyNotes(built, root);
|
|
5871
6005
|
const rootLayoutEdges = applyAuthorLayout(built.layout, root, `flow "${built.id}"`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uipath/maestro-builder-sdk",
|
|
3
|
-
"version": "6.8.
|
|
3
|
+
"version": "6.8.4",
|
|
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": "7620c5d1340ead9f1ef6a5eaff9da5317e0756f1"
|
|
91
91
|
}
|