@uipath/maestro-builder-sdk 5.3.1 → 5.4.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 CHANGED
@@ -104,6 +104,13 @@ function checkFlow(built, ancestors, opts = {}) {
104
104
  // (and their declared outcome ids), and which ids actually got a route.
105
105
  const hitlOutcomeSteps = new Map();
106
106
  const hitlOutcomeRouted = new Map();
107
+ // The other half of that bookkeeping: hitl steps that declare a CHOICE (more
108
+ // than one outcome) and did NOT opt into per-outcome exits, so the graph gives
109
+ // every outcome the same single exit. Whether that is a mistake depends on
110
+ // something no single step can see — whether anything downstream reads the
111
+ // chosen outcome — so the decision waits until the expressions are collected
112
+ // (see HITL_OUTCOMES_UNREACHABLE below).
113
+ const hitlChoiceNoPorts = new Map();
107
114
  const dataFabricUpdates = [];
108
115
  // Placeholder (`mock()`) step names. The platform's `core.logic.mock` declares
109
116
  // one output whose `source` is the literal `"null"` — it publishes nothing —
@@ -322,6 +329,19 @@ function checkFlow(built, ancestors, opts = {}) {
322
329
  if (s.spec.kind === 'hitl') {
323
330
  checkHitl(s.name, s.spec.inputs, diags);
324
331
  hitlForms.set(s.name, s.spec.inputs);
332
+ const outcomes = s.spec.inputs?.outcomes ?? [];
333
+ // `action: 'End'` ends the PROCESS at the node, so a task whose every
334
+ // outcome ends it has nothing downstream to distinguish — one exit is
335
+ // the whole truth there, and the rule below would be a false positive.
336
+ const allEnd = outcomes.length > 0
337
+ && outcomes.every((o) => typeof o !== 'string' && o?.action === 'End');
338
+ if (outcomes.length > 1 && !allEnd
339
+ && s.spec.inputs?.outcomePorts !== true && s.spec.inputs?.exposeError !== true) {
340
+ hitlChoiceNoPorts.set(s.name, {
341
+ names: outcomes.map((o) => String((typeof o === 'string' ? o : o?.name) ?? '').trim()),
342
+ variant: s.spec.inputs?.variant,
343
+ });
344
+ }
325
345
  }
326
346
  if (s.spec.kind === 'rpaWorkflow')
327
347
  checkRpaWorkflow(s.name, s.spec.inputs, diags);
@@ -577,6 +597,7 @@ function checkFlow(built, ancestors, opts = {}) {
577
597
  });
578
598
  }
579
599
  }
600
+ reportUnreachableHitlOutcomes(hitlChoiceNoPorts, exprs, diags);
580
601
  for (const { step, inputs } of dataFabricUpdates) {
581
602
  const fromRead = inputs.record?.fromRead;
582
603
  if (typeof fromRead === 'string' && !dataFabricReads.has(fromRead)) {
@@ -3821,6 +3842,78 @@ function outcomeSlug(name, index) {
3821
3842
  .replace(/^-+|-+$/g, '');
3822
3843
  return slug || `outcome-${index + 1}`;
3823
3844
  }
3845
+ /**
3846
+ * HITL_OUTCOMES_UNREACHABLE — a human task offers the reviewer a CHOICE that
3847
+ * nothing downstream can act on.
3848
+ *
3849
+ * `outcomes` and `outcomePorts` are two halves of one decision, and only the
3850
+ * first half reads as load-bearing. Declaring outcomes without the flag emits
3851
+ * the node's 1.0 definition, whose ONE source handle is `completed`: the
3852
+ * outcomes still reach the reviewer and still appear in
3853
+ * `inputs.schema.outcomes`, and then every button the reviewer can press leaves
3854
+ * the node the same way. Two outcomes, one path — an approval whose approve and
3855
+ * reject arms are indistinguishable is a modelling error, not a style choice,
3856
+ * yet it compiles and `flow validate` answers `Valid` (flow-builder-sdk#735).
3857
+ *
3858
+ * The one thing that makes the single-exit shape correct is a read of the
3859
+ * chosen outcome: `out('<step>', 'Action')` feeding a `.switch()` is the
3860
+ * DEFAULT documented shape, and it is the only shape the quick-form,
3861
+ * action-app and document-validation variants have — they carry no per-outcome
3862
+ * definition for `outcomePorts` to select (HITL_OUTCOME_PORTS_VARIANT). So the
3863
+ * rule is not "outcomes without ports", which would refuse that shape; it is
3864
+ * "outcomes that no exit and no expression distinguishes". That is why it runs
3865
+ * here, after `walk`, rather than inside `checkHitl`: the deciding evidence is
3866
+ * the rest of the flow.
3867
+ *
3868
+ * A read counts when it can SEE the outcome — `.output.Action`, the whole
3869
+ * answer object (`out('<step>')` / `$vars.<step>.output`, which carries
3870
+ * `Action` into a script), `.status` (the platform's own projection of it,
3871
+ * redirected by HITL_READ_STATUS but still an author reaching for the outcome),
3872
+ * a bare `$vars.<step>`, or any bracket read, whose path this scan cannot
3873
+ * resolve. One named answer field (`$vars.<step>.output.note`) does not: it
3874
+ * says nothing about which button was pressed.
3875
+ *
3876
+ * A warning, not an error, for the same reason HITL_OUTCOME_UNROUTED is: the
3877
+ * artifact deploys and the task works — what is lost is the branch.
3878
+ */
3879
+ function reportUnreachableHitlOutcomes(choices, exprs, diags) {
3880
+ if (choices.size === 0)
3881
+ return;
3882
+ const readsOutcome = new Set();
3883
+ for (const { js } of exprs) {
3884
+ for (const m of js.matchAll(/\$vars\.([A-Za-z0-9_]+)(?:\.([A-Za-z0-9_]+))?(?:\.([A-Za-z0-9_]+))?/g)) {
3885
+ const [, name, field, tail] = m;
3886
+ if (field === undefined || field === 'status')
3887
+ readsOutcome.add(name);
3888
+ else if (field === 'output' && (tail === undefined || tail === 'Action'))
3889
+ readsOutcome.add(name);
3890
+ }
3891
+ // `$vars.<step>['output']` — VARS_BRACKET_READ's shape. The path is not
3892
+ // statically resolvable here, so it counts as a read: this rule fails open.
3893
+ for (const m of js.matchAll(/\$vars\.([A-Za-z0-9_]+)\s*\[/g))
3894
+ readsOutcome.add(m[1]);
3895
+ }
3896
+ for (const [step, { names, variant }] of choices) {
3897
+ if (readsOutcome.has(step))
3898
+ continue;
3899
+ const ids = names.map((n, i) => outcomeSlug(n, i));
3900
+ diags.push({
3901
+ level: 'warning', code: 'HITL_OUTCOMES_UNREACHABLE', step,
3902
+ message: `Human task "${step}" declares ${names.length} outcomes (${names.join(', ')}), but nothing `
3903
+ + `downstream tells them apart: the node emits its single 'completed' exit and every outcome leaves `
3904
+ + `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
+ + (variant === undefined
3907
+ ? `Set outcomePorts: true to give each outcome its own exit (${ids.map((i) => `'outcome-${i}'`).join(', ')}), `
3908
+ + `or switch on out('${step}', 'Action') to route it as data.`
3909
+ : `The "${variant}" variant has no per-outcome definition for outcomePorts to select, so switch on `
3910
+ + `out('${step}', 'Action') to route it as data.`),
3911
+ suggestion: variant === undefined
3912
+ ? 'outcomePorts: true'
3913
+ : `.switch('route', out('${step}', 'Action'), …)`,
3914
+ });
3915
+ }
3916
+ }
3824
3917
  /**
3825
3918
  * A human task's form.
3826
3919
  *
@@ -564,7 +564,11 @@ 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.
567
+ * first is the primary (default) one. Declaring them creates no EXITS: with
568
+ * more than one, set `outcomePorts` to fork in the graph, or read
569
+ * `out('<step>', 'Action')` to route as data. With neither, every outcome
570
+ * leaves on the same `completed` exit and nothing downstream can tell them
571
+ * apart — which `check` warns about as `HITL_OUTCOMES_UNREACHABLE`.
568
572
  *
569
573
  * Which one they pressed is `out('<step>', 'Action')` — that is what the
570
574
  * platform's own routing conditions compare against, so branch on it.
@@ -578,7 +582,9 @@ export interface HitlInputs {
578
582
  * Route each outcome from its OWN port instead of the single `completed`
579
583
  * exit. Selects the node's **1.1** definition, whose exits are
580
584
  * `outcome-<id>` handles (one per outcome; ids are the outcome names
581
- * slugified, e.g. `'Approve'` → `outcome-approve`).
585
+ * slugified, e.g. `'Approve'` → `outcome-approve`). It REPLACES `completed`
586
+ * rather than adding to it, and it is not an optional refinement — see
587
+ * `outcomes` for the choice it is one half of.
582
588
  *
583
589
  * The FIRST (primary) outcome continues the main path; route the others with
584
590
  * `.stepToList('outcome-<id>', …)`. Base variant only — the sub-typed nodes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uipath/maestro-builder-sdk",
3
- "version": "5.3.1",
3
+ "version": "5.4.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": "dbd94aff19eeaff9d162b8d4e3ee923554fb5c2a"
90
+ "gitref": "b5c108408c7198fa36dcaf2c8d84bf46b3c1ee72"
91
91
  }