@uipath/maestro-builder-sdk 6.29.0 → 6.29.2

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.
@@ -0,0 +1,51 @@
1
+ /**
2
+ * The source handles a node's definition declares, read against the node's own
3
+ * inputs — one reader for every layer that has to know which ports a node can
4
+ * leave on (`merge-roundtrip.ts`'s handle checks, `decompile`'s arm recovery for
5
+ * a node with no typed factory, and `.stepSwitch` over a `rawNode`).
6
+ *
7
+ * A definition lists its handles in `handleConfiguration[].handles[]`. A static
8
+ * handle is its `id`; a REPEATED one is a template expanded once per item of an
9
+ * input array:
10
+ *
11
+ * ```
12
+ * { id: 'case-{item.id}', type: 'source', repeat: 'inputs.criteria' }
13
+ * inputs.criteria = [{ id: 'returns' }, { id: 'billing' }]
14
+ * → case-returns, case-billing
15
+ * ```
16
+ *
17
+ * This is how `core.logic.agentic-switch` (one exit per choice, plus `default`)
18
+ * and `core.logic.switch` (one per case) declare their exits.
19
+ */
20
+ type Def = {
21
+ readonly [k: string]: unknown;
22
+ } | undefined;
23
+ /**
24
+ * Every source handle `def` declares for a node with `inputs`, in declaration
25
+ * order, templates expanded. `undefined` when `def` has no
26
+ * `handleConfiguration`, or has a template this cannot expand (a `repeat` that
27
+ * is not an `inputs.` path), so a caller can tell "no handles" from "cannot
28
+ * know".
29
+ */
30
+ export declare function expandSourceHandles(def: Def, inputs: unknown): string[] | undefined;
31
+ /**
32
+ * The FORWARD exits a node's definition declares when it has more than one —
33
+ * every source handle except `error` and an agent's artifact handles, templates
34
+ * expanded. `undefined` for a definition with one forward exit or fewer, or one
35
+ * whose handles cannot be known: such a node has no fan-out to route.
36
+ */
37
+ export declare function forwardFanOut(def: Def, inputs: unknown): string[] | undefined;
38
+ /**
39
+ * The port a node's main path leaves on — the edge a plain `.step()` writes
40
+ * and a following step continues from: the first concrete forward source
41
+ * handle (`isDefaultForType` wins), else `output`. `undefined` when every
42
+ * forward source handle is templated (`case-{item.id}`), so no main path leaves
43
+ * the node.
44
+ *
45
+ * `serialize` writes the main-path edge on this port, and `decompile` reads the
46
+ * main path back off it, so the two must agree: a `core.logic.agentic-switch`
47
+ * (`case-{item.id}` + `default`) continues on `default`, and each other wired
48
+ * exit is a `.stepToList('case-<id>', …)` (#1214).
49
+ */
50
+ export declare function contributionPort(def: Def): string | undefined;
51
+ export {};
@@ -0,0 +1,95 @@
1
+ /**
2
+ * The source handles a node's definition declares, read against the node's own
3
+ * inputs — one reader for every layer that has to know which ports a node can
4
+ * leave on (`merge-roundtrip.ts`'s handle checks, `decompile`'s arm recovery for
5
+ * a node with no typed factory, and `.stepSwitch` over a `rawNode`).
6
+ *
7
+ * A definition lists its handles in `handleConfiguration[].handles[]`. A static
8
+ * handle is its `id`; a REPEATED one is a template expanded once per item of an
9
+ * input array:
10
+ *
11
+ * ```
12
+ * { id: 'case-{item.id}', type: 'source', repeat: 'inputs.criteria' }
13
+ * inputs.criteria = [{ id: 'returns' }, { id: 'billing' }]
14
+ * → case-returns, case-billing
15
+ * ```
16
+ *
17
+ * This is how `core.logic.agentic-switch` (one exit per choice, plus `default`)
18
+ * and `core.logic.switch` (one per case) declare their exits.
19
+ */
20
+ /**
21
+ * Every source handle `def` declares for a node with `inputs`, in declaration
22
+ * order, templates expanded. `undefined` when `def` has no
23
+ * `handleConfiguration`, or has a template this cannot expand (a `repeat` that
24
+ * is not an `inputs.` path), so a caller can tell "no handles" from "cannot
25
+ * know".
26
+ */
27
+ export function expandSourceHandles(def, inputs) {
28
+ const groups = def?.handleConfiguration;
29
+ if (!Array.isArray(groups))
30
+ return undefined;
31
+ const out = [];
32
+ for (const g of groups) {
33
+ for (const h of (g?.handles ?? [])) {
34
+ if (h?.type !== 'source' || typeof h.id !== 'string')
35
+ continue;
36
+ const template = /\{item\.([A-Za-z0-9_]+)\}/.exec(h.id);
37
+ if (!template) {
38
+ if (!out.includes(h.id))
39
+ out.push(h.id);
40
+ continue;
41
+ }
42
+ if (typeof h.repeat !== 'string' || !h.repeat.startsWith('inputs.'))
43
+ return undefined;
44
+ let items = inputs;
45
+ for (const seg of h.repeat.slice('inputs.'.length).split('.'))
46
+ items = items?.[seg];
47
+ for (const item of Array.isArray(items) ? items : []) {
48
+ const v = item?.[template[1]];
49
+ if (typeof v !== 'string' && typeof v !== 'number')
50
+ continue;
51
+ const id = h.id.replace(template[0], String(v));
52
+ if (!out.includes(id))
53
+ out.push(id);
54
+ }
55
+ }
56
+ }
57
+ return out;
58
+ }
59
+ /**
60
+ * Source handles that are not exits: `error` (routed by `.onError()`) and an
61
+ * agent's artifact handles, which attach its tools, context, escalations and
62
+ * memory rather than continue the flow.
63
+ */
64
+ const NOT_FORWARD = new Set(['error', 'tool', 'context', 'escalation', 'memory']);
65
+ /**
66
+ * The FORWARD exits a node's definition declares when it has more than one —
67
+ * every source handle except `error` and an agent's artifact handles, templates
68
+ * expanded. `undefined` for a definition with one forward exit or fewer, or one
69
+ * whose handles cannot be known: such a node has no fan-out to route.
70
+ */
71
+ export function forwardFanOut(def, inputs) {
72
+ const handles = expandSourceHandles(def, inputs)?.filter((h) => !NOT_FORWARD.has(h));
73
+ return handles !== undefined && handles.length > 1 ? handles : undefined;
74
+ }
75
+ /**
76
+ * The port a node's main path leaves on — the edge a plain `.step()` writes
77
+ * and a following step continues from: the first concrete forward source
78
+ * handle (`isDefaultForType` wins), else `output`. `undefined` when every
79
+ * forward source handle is templated (`case-{item.id}`), so no main path leaves
80
+ * the node.
81
+ *
82
+ * `serialize` writes the main-path edge on this port, and `decompile` reads the
83
+ * main path back off it, so the two must agree: a `core.logic.agentic-switch`
84
+ * (`case-{item.id}` + `default`) continues on `default`, and each other wired
85
+ * exit is a `.stepToList('case-<id>', …)` (#1214).
86
+ */
87
+ export function contributionPort(def) {
88
+ const groups = Array.isArray(def?.handleConfiguration) ? def.handleConfiguration : [];
89
+ const handles = groups.flatMap((g) => (g?.handles ?? []));
90
+ const sources = handles.filter((h) => h?.type === 'source' && h?.id !== 'error');
91
+ const concrete = sources.filter((h) => typeof h?.id === 'string' && !h.id.includes('{'));
92
+ if (concrete.length === 0 && sources.length > 0)
93
+ return undefined;
94
+ return concrete.find((h) => h?.isDefaultForType)?.id ?? concrete[0]?.id ?? 'output';
95
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * The exits `.stepSwitch` routes over when its step is a `rawNode`: the
3
+ * manifest's forward source handles, `repeat` ones expanded over the node's
4
+ * inputs, each arm's `value` being its port. A `core.logic.agentic-switch`
5
+ * with choices `returns` and `billing` has `case-returns`, `case-billing` and
6
+ * `default`:
7
+ *
8
+ * ```ts
9
+ * .stepSwitch('route', rawNode({ nodeType: 'core.logic.agentic-switch', … }), [
10
+ * { value: 'case-returns', body: (b) => b.step('returns', …) },
11
+ * { value: 'case-billing', body: (b) => b.step('billing', …) },
12
+ * { value: 'default', body: (b) => b.step('triage', …) },
13
+ * ])
14
+ * ```
15
+ *
16
+ * Internal, not package API: `raw-node.ts` (whose module the package
17
+ * re-exports) puts the answer on its node under this symbol, and
18
+ * `flow-step-exits.ts` reads it. `decompile` reads the same handles off the
19
+ * file's definition, so what it emits recompiles (#1141).
20
+ */
21
+ import type { DeclaredExit } from './step-ports.js';
22
+ /** The method a raw node answers its exits under. */
23
+ export declare const RAW_NODE_EXITS: unique symbol;
24
+ /**
25
+ * `action`'s exits when it is a raw node whose manifest declares more than one
26
+ * forward exit; `undefined` otherwise (then it has nothing to route).
27
+ */
28
+ export declare function rawNodeExits(action: unknown): DeclaredExit[] | undefined;
@@ -0,0 +1,10 @@
1
+ /** The method a raw node answers its exits under. */
2
+ export const RAW_NODE_EXITS = Symbol('rawNodeExits');
3
+ /**
4
+ * `action`'s exits when it is a raw node whose manifest declares more than one
5
+ * forward exit; `undefined` otherwise (then it has nothing to route).
6
+ */
7
+ export function rawNodeExits(action) {
8
+ const own = action?.[RAW_NODE_EXITS];
9
+ return typeof own === 'function' ? own.call(action) : undefined;
10
+ }
package/dist/decompile.js CHANGED
@@ -46,6 +46,7 @@ import { mapJsCode } from './core/js-code-spans.js';
46
46
  import { missingBindingLabels } from './core/binding-messages.js';
47
47
  import { prepareCommand } from './core/cli-spelling.js';
48
48
  import { OUTCOME_HANDLE_NODE_TYPES } from './core/hitl-routing.js';
49
+ import { contributionPort, forwardFanOut } from './core/definition-handles.js';
49
50
  import { STICKY_NOTE_TYPE } from './core/definitionless-node-types.js';
50
51
  import { eventFilterFromTreeLeaf } from './core/event-filters.js';
51
52
  import { sdkFormFieldType } from './core/form-field-type.js';
@@ -531,11 +532,34 @@ class Graph {
531
532
  successorEdges(id) {
532
533
  const primary = this.primaryOutcomePort(id);
533
534
  const forked = this.fullyWiredExits(id);
535
+ const side = forked ? undefined : this.rawSidePorts(id);
534
536
  return this.outEdges(id).filter((e) => e.sourcePort !== 'error'
535
537
  && !ARTIFACT_PORTS.has(e.sourcePort)
536
538
  && (forked?.has(e.sourcePort)
537
539
  || (!e.sourcePort.startsWith('branch-')
538
- && (!e.sourcePort.startsWith('outcome-') || e.sourcePort === primary))));
540
+ && (!e.sourcePort.startsWith('outcome-') || e.sourcePort === primary)
541
+ && !side?.has(e.sourcePort))));
542
+ }
543
+ /**
544
+ * A multi-exit `rawNode`'s SIDE exits when not every exit is wired: each
545
+ * declared exit except the one its main path leaves on (`contributionPort`,
546
+ * the port serialize writes a plain `.step()`'s edge on — `default` for a
547
+ * `core.logic.agentic-switch`). Each is recovered as a
548
+ * `.stepToList('<port>', …)` after the `.step`, as `branch-*` and non-primary
549
+ * `outcome-*` exits are. `undefined` for every other node, and for a raw node
550
+ * whose exits are all wired (that one is a `.stepSwitch`).
551
+ *
552
+ * Without this, a partially wired agentic switch had two successors and no
553
+ * merge join, so the walk emitted an unstructured-fan-out TODO and dropped
554
+ * every arm (#1214).
555
+ */
556
+ rawSidePorts(id) {
557
+ const node = this.byId.get(id);
558
+ const exits = node ? rawNodeExits(node) : undefined;
559
+ if (!node || exits === undefined || this.fullyWiredExits(id) !== undefined)
560
+ return undefined;
561
+ const main = contributionPort(rawNodeDefinition(node));
562
+ return new Set(exits.map((e) => e.port).filter((p) => p !== main));
539
563
  }
540
564
  }
541
565
  // ─── Node → step-spec source ──────────────────────────────────────────────────
@@ -2396,6 +2420,17 @@ function region(cursor, stop, scope, ipdom, ctx, refInto) {
2396
2420
  segs.push(`.stepToList(${tsLiteral(oe.sourcePort)}, ${armCb('p', outSegs)})`);
2397
2421
  }
2398
2422
  }
2423
+ // A partially wired multi-exit raw node (an agentic switch with one choice
2424
+ // unwired): every wired exit but the main path's is its own port list, and
2425
+ // an unwired one stays unwired (#1214).
2426
+ const rawSide = ctx.graph.rawSidePorts(node.id);
2427
+ for (const re of rawSide ? ctx.graph.outEdges(node.id).filter((e) => rawSide.has(e.sourcePort)) : []) {
2428
+ const mainReach = forwardReach(succ, scope, ctx.graph);
2429
+ const sideSegs = edgeRegion(re, EXIT, scope, ipdom, ctx, mainReach);
2430
+ if (sideSegs.length === 0)
2431
+ sideSegs.push('.return({})');
2432
+ segs.push(`.stepToList(${tsLiteral(re.sourcePort)}, ${armCb('p', sideSegs)})`);
2433
+ }
2399
2434
  // A body edge into the enclosing container's break handle: the path exits
2400
2435
  // the loop here. Emit `.break()` and end the region — recompiling recreates
2401
2436
  // exactly this edge and adds no continue back-edge for the path.
@@ -2534,7 +2569,9 @@ function declaredExitsOfNode(node) {
2534
2569
  // A registered type that declares exits answers from its own spec, lifted off
2535
2570
  // this node; one that declares none is not lifted at all (this runs per edge).
2536
2571
  const impl = registeredImpl(node);
2537
- if (!impl?.exits)
2572
+ if (!impl)
2573
+ return rawNodeExits(node);
2574
+ if (!impl.exits)
2538
2575
  return undefined;
2539
2576
  const lifted = liftRegistered(node, impl);
2540
2577
  const exits = lifted && impl.exits(lifted.spec);
@@ -2542,6 +2579,41 @@ function declaredExitsOfNode(node) {
2542
2579
  return undefined;
2543
2580
  return exits.map((e) => ({ value: e.value, port: e.port }));
2544
2581
  }
2582
+ /**
2583
+ * The exits of a node that decompiles to a `rawNode` (a type this SDK has no
2584
+ * factory for, carried with the file's own definition): that definition's
2585
+ * forward source handles, `repeat` ones expanded over the node's inputs, when it
2586
+ * has more than one. `core.logic.agentic-switch` declares `case-{item.id}` over
2587
+ * `inputs.criteria` plus `default`, so a node with choices `returns` and
2588
+ * `billing` has the exits `case-returns`, `case-billing`, `default`. Each exit's
2589
+ * value is its port, which is what `.stepSwitch` over a `rawNode` matches
2590
+ * (`rawNodeExits` in raw-node.ts reads the same handles off the manifest).
2591
+ *
2592
+ * Without this, a fully wired multi-exit raw node had ONE exit to the walker:
2593
+ * it emitted `.step(...)` with a TODO and dropped every arm (#1141).
2594
+ *
2595
+ * `undefined` for every node the decompiler names some other way (the walker's
2596
+ * structural types, triggers, connectors, agents, published resources, IxP):
2597
+ * their exits are not a raw node's, and an agent's artifact handles (`tool`,
2598
+ * `context`, …) are not exits at all.
2599
+ */
2600
+ function rawNodeExits(node) {
2601
+ const type = String(node.type);
2602
+ // A registered type (an HTTP call, say) answers from its own `exits`.
2603
+ if (registeredImpl(node) !== undefined)
2604
+ return undefined;
2605
+ if (Object.values(T).includes(type) || isTrigger(type))
2606
+ return undefined;
2607
+ if (['uipath.connector.', 'uipath.core.', 'uipath.ixp.', 'uipath.agent.', 'uipath.conversational.']
2608
+ .some((p) => type.startsWith(p)))
2609
+ return undefined;
2610
+ return forwardFanOut(rawNodeDefinition(node), node.inputs)?.map((port) => ({ value: port, port }));
2611
+ }
2612
+ /** The definition the file carries for `node`'s type (its version's, else the type's). */
2613
+ function rawNodeDefinition(node) {
2614
+ const type = String(node.type);
2615
+ return runDefs.get(`${type}@${node.typeVersion}`) ?? runDefs.get(type);
2616
+ }
2545
2617
  /**
2546
2618
  * Split a switch case expression into `<discriminant> === <literal>`, the only
2547
2619
  * form `.switch(on, [{ value }])` models. Returns `undefined` for any other
@@ -8,6 +8,7 @@
8
8
  import { resolveVersion } from './core/nodes/versioned.js';
9
9
  import { declaredExits } from './core/step-ports.js';
10
10
  import { FLOW_NODE_TYPES } from './flow-node-registry.js';
11
+ import { rawNodeExits } from './core/raw-node-exits.js';
11
12
  /**
12
13
  * The author-declared exits of a step's spec: a registered type's own `exits`,
13
14
  * at the version the step resolves to (its pin, else the type's choice; an
@@ -16,6 +17,10 @@ import { FLOW_NODE_TYPES } from './flow-node-registry.js';
16
17
  * here, so they agree on what a port means.
17
18
  */
18
19
  export function stepExits(spec, pinnedVersion) {
20
+ // A `rawNode` declares its exits through the manifest it carries (#1141).
21
+ const raw = rawNodeExits(spec);
22
+ if (raw !== undefined)
23
+ return raw;
19
24
  const entry = typeof spec?.kind === 'string' ? FLOW_NODE_TYPES.byKind(spec.kind, spec) : undefined;
20
25
  if (!entry)
21
26
  return declaredExits(spec, pinnedVersion);
@@ -50,6 +50,7 @@
50
50
  * merged file still has it.
51
51
  */
52
52
  import { bindingIdentity } from './core/binding-identity.js';
53
+ import { expandSourceHandles } from './core/definition-handles.js';
53
54
  import { OUTCOME_HANDLE_NODE_TYPES } from './core/hitl-routing.js';
54
55
  /** A file's definitions, keyed `nodeType@version`. */
55
56
  function defsOf(f) {
@@ -279,32 +280,8 @@ export function roundTripFindings(original, baseline, merged) {
279
280
  }
280
281
  /** The source handles a node's definition declares, or `undefined` when they cannot be known. */
281
282
  function declaredSourceHandles(node, def) {
282
- const groups = def?.handleConfiguration;
283
- if (!Array.isArray(groups))
284
- return undefined;
285
- const out = new Set();
286
- for (const g of groups) {
287
- for (const h of (g?.handles ?? [])) {
288
- if (h?.type !== 'source' || typeof h.id !== 'string')
289
- continue;
290
- const template = /\{item\.([A-Za-z0-9_]+)\}/.exec(h.id);
291
- if (!template) {
292
- out.add(h.id);
293
- continue;
294
- }
295
- if (typeof h.repeat !== 'string' || !h.repeat.startsWith('inputs.'))
296
- return undefined; // a template this check cannot expand
297
- let items = node.inputs;
298
- for (const seg of h.repeat.slice('inputs.'.length).split('.'))
299
- items = items?.[seg];
300
- for (const item of Array.isArray(items) ? items : []) {
301
- const v = item?.[template[1]];
302
- if (typeof v === 'string' || typeof v === 'number')
303
- out.add(h.id.replace(template[0], String(v)));
304
- }
305
- }
306
- }
307
- return out;
283
+ const handles = expandSourceHandles(def, node.inputs);
284
+ return handles === undefined ? undefined : new Set(handles);
308
285
  }
309
286
  /**
310
287
  * The handles a node derives from its own inputs, in order: a definition's
package/dist/raw-node.js CHANGED
@@ -8,6 +8,8 @@
8
8
  import { FlowAction, } from './core/node-classes.js';
9
9
  import { connectorRawNodeRefusal } from './core/connector-raw-node.js';
10
10
  import { rawNodeInputProblem, rawNodeTypedFamilyWarning } from './raw-node-advice.js';
11
+ import { forwardFanOut } from './core/definition-handles.js';
12
+ import { RAW_NODE_EXITS } from './core/raw-node-exits.js';
11
13
  /**
12
14
  * Place a node this SDK has no factory for, carrying its definition verbatim
13
15
  * (`uipath.exotic.thing@2.1` and the manifest the platform served for it).
@@ -88,6 +90,11 @@ class RawNode extends FlowAction {
88
90
  out.push({ code: 'RAW_NODE_TYPED_FAMILY', severity: 'warning', message: typed });
89
91
  return out;
90
92
  }
93
+ /** Its exits, for `.stepSwitch` (core/raw-node-exits.ts). */
94
+ [RAW_NODE_EXITS]() {
95
+ const manifest = this.spec.manifest;
96
+ return forwardFanOut(manifest, this.spec.inputs ?? {})?.map((port) => ({ value: port, port }));
97
+ }
91
98
  contribute(context) {
92
99
  // compile does not run diagnostics(); refuse here too so the escape hatch
93
100
  // cannot emit an unconfigured connector node. (An input a registered node
package/dist/serialize.js CHANGED
@@ -57,6 +57,7 @@ import { resolveSchemaVersion } from './schema-version.js';
57
57
  import { planFlowGlobals, planSharedInputs, sharedInputRoots } from './flow-variables.js';
58
58
  import { outputBackingGlobal, planOutputBacking } from './flow-output-backing.js';
59
59
  import { mapJsCode } from './core/js-code-spans.js';
60
+ import { contributionPort } from './core/definition-handles.js';
60
61
  import { prepareCommand } from './core/cli-spelling.js';
61
62
  import { AGENTIC_PROCESS_FORMS } from './core/agentic-process-forms.js';
62
63
  import { resourceNodeSortOrder } from './core/process-resource-sort-order.js';
@@ -3531,18 +3532,6 @@ function declaresForwardPort(def, port) {
3531
3532
  return port.length > head.length + tail.length && port.startsWith(head) && port.endsWith(tail);
3532
3533
  });
3533
3534
  }
3534
- /**
3535
- * Forward port: the first concrete source handle (`isDefaultForType` wins), else `output`;
3536
- * `undefined` when every forward source handle is templated (`case-{item.id}`).
3537
- */
3538
- function contributionPort(def) {
3539
- const handles = (def?.handleConfiguration ?? []).flatMap((g) => g?.handles ?? []);
3540
- const sources = handles.filter((h) => h?.type === 'source' && h?.id !== 'error');
3541
- const concrete = sources.filter((h) => typeof h?.id === 'string' && !h.id.includes('{'));
3542
- if (concrete.length === 0 && sources.length > 0)
3543
- return undefined;
3544
- return concrete.find((h) => h?.isDefaultForType)?.id ?? concrete[0]?.id ?? 'output';
3545
- }
3546
3535
  /**
3547
3536
  * Render a switch case value as a JS literal for the case expression.
3548
3537
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uipath/maestro-builder-sdk",
3
- "version": "6.29.0",
3
+ "version": "6.29.2",
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",
@@ -92,5 +92,5 @@
92
92
  "@types/node": "^22.7.0",
93
93
  "esbuild": "^0.28.1"
94
94
  },
95
- "gitref": "af71b0c21f732f056d19e802af1c987fd8528336"
95
+ "gitref": "b0d822e147a21ef1051b36c474b303839e980b13"
96
96
  }