@uipath/maestro-builder-sdk 6.15.1 → 6.15.3

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
@@ -9,9 +9,10 @@ import { Expr, SCHEDULE_PRESETS, DELAY_PRESETS, VARIANT_OPERATION, parseIxpProje
9
9
  import { FlowNode } from './core/node-classes.js';
10
10
  import { QUEUE_ITEM_RECORD_SCHEMA } from './core/queue-record.js';
11
11
  import { checkEventResolution, checkEventSubscription, eventParamsFor, } from './core/event-checks.js';
12
- import { FLOW_EXPR_DIALECT, checkBindingLabels, checkConnectorContract as coreCheckConnectorContract, checkConnectorLookups as coreCheckConnectorLookups, checkConnectorSchema as coreCheckConnectorSchema, collectConnectionUses, connectionConnectorConflicts, } from './core/connector-checks.js';
12
+ import { FLOW_EXPR_DIALECT, checkBindingLabels, checkBindingPresence, checkConnectorContract as coreCheckConnectorContract, checkConnectorLookups as coreCheckConnectorLookups, checkConnectorSchema as coreCheckConnectorSchema, collectConnectionUses, connectionConnectorConflicts, } from './core/connector-checks.js';
13
13
  import { prepareCommand } from './core/cli-spelling.js';
14
14
  import { isAnswerField } from './core/hitl-answers.js';
15
+ import { globalNameCollisionMessage, globalNameCollisions } from './core/global-names.js';
15
16
  import { hitlDerivesOutcomeHandles, hitlExitModel, hitlRoutesPerOutcome } from './core/hitl-routing.js';
16
17
  import { childStepLists, declaredExits, hasExits, stopsAtEnd } from './core/step-ports.js';
17
18
  import { FLOW_SCHEMA_VERSIONS } from './format-profile.js';
@@ -36,6 +37,11 @@ function checkFlow(built, ancestors, opts = {}) {
36
37
  diags.push({ level: 'error', code: 'NO_ID', message: 'flow() requires an id.' });
37
38
  checkSchemaVersion(built, ancestors, diags);
38
39
  checkNodeUpdates(built, diags);
40
+ // Inputs, outputs and vars are one namespace in the .flow (variables.globals[]);
41
+ // `serialize` throws the same sentence (flow-builder-sdk#890).
42
+ for (const collision of globalNameCollisions(built.inputs ?? [], built.outputs ?? [], built.vars ?? [])) {
43
+ diags.push({ level: 'error', code: 'VARIABLE_NAME_COLLISION', message: globalNameCollisionMessage(collision) });
44
+ }
39
45
  const maybeCustomTrigger = built.trigger;
40
46
  if (maybeCustomTrigger !== undefined && !('kind' in maybeCustomTrigger) && maybeCustomTrigger instanceof FlowNode) {
41
47
  // A custom FlowTrigger subclass reports through its own diagnostics() hook;
@@ -543,6 +549,7 @@ function checkFlow(built, ancestors, opts = {}) {
543
549
  checkEventSubscription(s.spec.subscription, `waitForEvent "${stepName}"`, push('error'), push('warning'), eventParamsFor(s.spec.subscription, opts));
544
550
  checkEventResolution(s.spec.subscription, `waitForEvent "${stepName}"`, 'event', opts, push('error'), push('warning'), eventParamsFor(s.spec.subscription, opts));
545
551
  checkBindingLabels(`waitForEvent "${stepName}"`, s.spec.subscription, opts, push('error'), eventTarget(s.spec.subscription), { id: stepName, connector: eventTarget(s.spec.subscription)?.key });
552
+ checkBindingPresence(`waitForEvent "${stepName}"`, s.spec.subscription, push('error'), eventTarget(s.spec.subscription));
546
553
  }
547
554
  if (s.spec.kind === 'subflow') {
548
555
  // Only a real child goes in the map: a missing one is reported by
@@ -848,7 +855,7 @@ function checkFlow(built, ancestors, opts = {}) {
848
855
  }
849
856
  }
850
857
  else {
851
- checkTrigger(ep.trigger, diags, opts);
858
+ checkTrigger(ep.trigger, diags, opts, `onEvent() entry point "${ep.id}"`);
852
859
  }
853
860
  for (const inp of ep.inputs) {
854
861
  const collides = built.inputs.some((g) => g.name === inp.name)
@@ -894,20 +901,17 @@ function checkFlow(built, ancestors, opts = {}) {
894
901
  // Both forms resolve at run time — measured, `$vars.call["output"].x` and
895
902
  // `$vars["call"].output.x` both returned the value — so this is not a broken
896
903
  // read. It is an unrewritable one. Every reference the SDK emits goes through
897
- // `renameRefs`, whose regex matches the DOTTED form only, and three separate
904
+ // `renameRefs`, whose regex matches the DOTTED form only, and two separate
898
905
  // rewrites ride on it:
899
906
  //
900
907
  // * a subflow copy renames node ids (`rename.nodeIds`), so a bracketed read
901
908
  // keeps pointing at the uncopied name;
902
- // * the queue family publishes its result as `<step>.response`, not
903
- // `<step>.output` (`rename.runtimeOutputs`), so the two spellings read
904
- // DIFFERENT variables;
905
909
  // * a failure envelope resolves per node family (`rename.errorEnvelopes`).
906
910
  //
907
- // So the bracketed spelling is correct only for as long as none of those
911
+ // So the bracketed spelling is correct only for as long as neither of those
908
912
  // applies to the step — and it fails silently when one starts to. This is the
909
913
  // shape an eval agent actually wrote (`$vars.enqueue["output"].Key` on a queue
910
- // step, where the emitted read had to be `.response.Key`).
914
+ // step, at a time when the SDK still aliased queue reads to `.response`, #891).
911
915
  //
912
916
  // Deeper brackets are untouched: `$vars.rows.output["odd key"]` is ordinary
913
917
  // data access, it is the only way to spell a key that is not an identifier,
@@ -916,16 +920,15 @@ function checkFlow(built, ancestors, opts = {}) {
916
920
  const flag = (name, read, key) => {
917
921
  const suggestion = key === 'error'
918
922
  ? `err('${name}', 'message')`
919
- : key && key !== 'output' && key !== 'response'
923
+ : key && key !== 'output'
920
924
  ? `out('${name}', '${key}')`
921
925
  : `out('${name}')`;
922
926
  diags.push({
923
927
  level: 'error',
924
928
  code: 'VARS_BRACKET_READ',
925
929
  message: `"${read}" in "${step}" reaches step "${name}" through a bracket. It resolves today, but only the `
926
- + `DOTTED form is rewritten on the way out: a subflow copy renames node ids, the queue family publishes `
927
- + `its result as "${name}.response" rather than "${name}.output", and a failure envelope resolves per node `
928
- + `family. A bracketed read opts out of all three and goes silently stale when one applies.`,
930
+ + `DOTTED form is rewritten on the way out: a subflow copy renames node ids, and a failure envelope `
931
+ + `resolves per node family. A bracketed read opts out of both and goes silently stale when one applies.`,
929
932
  step,
930
933
  suggestion,
931
934
  });
@@ -1261,9 +1264,15 @@ function checkConnectorSchema(step, spec, opts, diags) {
1261
1264
  const { where, report } = connectorSite(step, diags);
1262
1265
  coreCheckConnectorSchema(where, spec, opts, FLOW_EXPR_DIALECT, report);
1263
1266
  }
1267
+ /**
1268
+ * The core contract (fields with a library, names with a bindings.json), plus
1269
+ * BINDING_MISSING, which needs neither: an absent label is wrong whatever the
1270
+ * file says.
1271
+ */
1264
1272
  function checkConnectorContract(step, spec, opts, diags) {
1265
1273
  const { where, report } = connectorSite(step, diags);
1266
1274
  coreCheckConnectorContract(where, spec, opts, report, step);
1275
+ checkBindingPresence(`connector "${step}"`, spec, (code, message) => report({ level: 'error', code, message }), { key: spec.key, action: spec.action });
1267
1276
  }
1268
1277
  /** Reject a wrapper hop when the runtime shape of a list has been measured. */
1269
1278
  function checkLoopListEnvelope(loop, connectors, opts, diags) {
@@ -1651,7 +1660,7 @@ function checkNodeUpdates(built, diags) {
1651
1660
  };
1652
1661
  visit(built.steps);
1653
1662
  }
1654
- function checkTrigger(spec, diags, opts = {}) {
1663
+ function checkTrigger(spec, diags, opts = {}, where) {
1655
1664
  const push = (level) => (code, message, suggestion) => {
1656
1665
  diags.push({ level, code, message, step: 'trigger', ...(suggestion ? { suggestion } : {}) });
1657
1666
  };
@@ -1663,6 +1672,7 @@ function checkTrigger(spec, diags, opts = {}) {
1663
1672
  checkEventSubscription(sub, 'trigger', at, push('warning'), eventParamsFor(sub, opts));
1664
1673
  checkEventResolution(sub, 'trigger', 'trigger', opts, at, push('warning'), eventParamsFor(sub, opts));
1665
1674
  checkBindingLabels('event trigger', sub, opts, at, eventTarget(sub), { id: 'trigger', connector: eventTarget(sub)?.key });
1675
+ checkBindingPresence(where ?? 'event trigger', sub, at, eventTarget(sub));
1666
1676
  return;
1667
1677
  }
1668
1678
  // manual() is the explicit spelling of the default — nothing to validate.
@@ -1679,7 +1689,7 @@ function checkTrigger(spec, diags, opts = {}) {
1679
1689
  if (spec?.kind !== 'scheduled') {
1680
1690
  at('TRIGGER_UNKNOWN_KIND', `.trigger() was given ${JSON.stringify(spec?.kind)}, which is not a trigger. `
1681
1691
  + `Wrap it in a factory: scheduled({ every: 'R/PT1H' }) for a schedule, `
1682
- + `onEvent({ connector, event, where }) to start on a connector event, formTrigger() to start from a `
1692
+ + `onEvent({ connector, event, where, connection, folder }) to start on a connector event, formTrigger() to start from a `
1683
1693
  + `submitted form, or conversationTrigger() to start on a new conversation. Omit .trigger() entirely `
1684
1694
  + `for the manual trigger (the default).`, `scheduled({ every: 'R/PT1H' })`);
1685
1695
  return;
@@ -187,9 +187,12 @@ export interface ScriptInputs {
187
187
  export interface ConnectorOpts {
188
188
  /** Connector action version (defaults to the library's newest match). */
189
189
  version?: string;
190
- /** Symbolic connection name declared in bindings.json (→ ConnectionId). */
190
+ /**
191
+ * Symbolic connection name declared in bindings.json (→ ConnectionId). Required with
192
+ * `folder`: `check` refuses a connector step without both (BINDING_MISSING).
193
+ */
191
194
  connection?: string;
192
- /** Symbolic folder name declared in bindings.json (→ connectionFolderKey). */
195
+ /** Symbolic folder name declared in bindings.json (→ connectionFolderKey). Required with `connection`. */
193
196
  folder?: string;
194
197
  /**
195
198
  * Which OBJECT a **generic** operation addresses — e.g. ServiceNow's "List All
@@ -1593,7 +1596,8 @@ export interface InlineAgentInputs {
1593
1596
  * ```ts
1594
1597
  * tools: [
1595
1598
  * { kind: 'builtin', tool: 'summarize' },
1596
- * { kind: 'connector', connector: 'uipath-atlassian-jira', operation: 'create-issue' },
1599
+ * { kind: 'connector', connector: 'uipath-atlassian-jira', operation: 'create-issue',
1600
+ * connection: 'jira', folder: 'shared' }, // bindings.json labels, both required
1597
1601
  * { kind: 'process', key: '6235d64e-b190-4b7a-8918-723df964c068',
1598
1602
  * name: 'Create TestCase', folderPath: 'Shared',
1599
1603
  * inputs: { title: 'string' }, returns: { id: 'string' } },
@@ -3420,9 +3424,12 @@ export interface TriggerOptions<W = Record<string, string>> {
3420
3424
  object?: string;
3421
3425
  /** Optional filters on the payload. Omit to take every event in scope. */
3422
3426
  filters?: EventFilter[];
3423
- /** bindings.json id for the connection (defaults to the flow's single binding). */
3427
+ /**
3428
+ * bindings.json id for the connection. Required with `folder`: `check` refuses an event
3429
+ * node without both (BINDING_MISSING) — it would compile to `connectionId: null`.
3430
+ */
3424
3431
  connection?: string;
3425
- /** bindings.json id for the folder key. */
3432
+ /** bindings.json id for the folder key. Required with `connection` (BINDING_MISSING). */
3426
3433
  folder?: string;
3427
3434
  /** Override the descriptor's version. */
3428
3435
  version?: string;
@@ -3460,9 +3467,12 @@ export interface EventSubscription {
3460
3467
  object?: string;
3461
3468
  /** Optional filters on the payload. Omit to take every event in scope. */
3462
3469
  filters?: EventFilter[];
3463
- /** bindings.json id for the connection (defaults to the flow's single binding). */
3470
+ /**
3471
+ * bindings.json id for the connection. Required with `folder`: `check` refuses an event
3472
+ * node without both (BINDING_MISSING) — it would compile to `connectionId: null`.
3473
+ */
3464
3474
  connection?: string;
3465
- /** bindings.json id for the folder key. */
3475
+ /** bindings.json id for the folder key. Required with `connection` (BINDING_MISSING). */
3466
3476
  folder?: string;
3467
3477
  version?: string;
3468
3478
  }
@@ -3493,6 +3503,7 @@ export declare function subscriptionFrom<W>(a: EventSubscription | TriggerDescri
3493
3503
  * event: 'email-received',
3494
3504
  * where: { parentFolderId: inboxId },
3495
3505
  * filters: [{ field: 'subject', contains: 'TestWaitFor' }],
3506
+ * connection: 'outlook', folder: 'shared', // bindings.json labels, both required
3496
3507
  * }))
3497
3508
  * ```
3498
3509
  *
@@ -1090,8 +1090,9 @@ export function connector(a, b, c, d) {
1090
1090
  export function subscriptionFrom(a, opts) {
1091
1091
  if (!isTriggerDescriptor(a))
1092
1092
  return a;
1093
- // No options at all is legal: an operation with no event parameters (HTTP
1094
- // Webhook) has nothing to put in them, and the descriptor carries the version.
1093
+ // No `where` is legal: an operation with no event parameters (HTTP Webhook)
1094
+ // has nothing to put in it, and the descriptor carries the version. The
1095
+ // `connection` / `folder` labels are still required (BINDING_MISSING).
1095
1096
  return {
1096
1097
  connector: a.connector,
1097
1098
  event: a.event,
@@ -5,7 +5,9 @@ export type BindingRefusalCode = 'BINDING_SELF_NAME' | 'BINDING_UNDECLARED'
5
5
  /** An emitted definition's `<bindings.NAME>` placeholder has no `bindings[]` entry named NAME (#865). */
6
6
  | 'BINDING_PLACEHOLDER_UNRESOLVED'
7
7
  /** Two steps on one connection disagree on which connector it belongs to (#866). */
8
- | 'CONNECTION_CONNECTOR_CONFLICT';
8
+ | 'CONNECTION_CONNECTOR_CONFLICT'
9
+ /** A connector-bound node names no `connection` or no `folder` label (#890). */
10
+ | 'BINDING_MISSING';
9
11
  /**
10
12
  * What `serialize` throws for a label it refuses. It is an ordinary `Error` whose
11
13
  * message is the sentence `check` prints; the `code` lets `compile --no-check`
@@ -73,3 +75,22 @@ export declare function bindingUndeclaredMessage(where: string, name: string, at
73
75
  * (flow-builder-sdk#866).
74
76
  */
75
77
  export declare function connectionConnectorConflictMessage(connection: string, users: ReadonlyMap<string, readonly string[]>): string;
78
+ /**
79
+ * Which of a connector-bound node's two labels the author left out.
80
+ *
81
+ * A connector step, an event trigger, an `.entryPoint` onEvent and a
82
+ * `waitForEvent` all call Integration Service on a tenant connection, so the
83
+ * node needs BOTH halves: `connection` (→ `detail.connectionId`) and `folder`
84
+ * (→ `detail.folderKey`). Omitting a label used to compile the node with
85
+ * `connectionId: null` and no binding at all — `check` and `compile` were
86
+ * silent, and only `uip maestro flow validate` refused it ("A connection is
87
+ * required to run this activity." / "FolderKey is required for the connection
88
+ * binding.", flow-builder-sdk#890). Managed HTTP is NOT one of these sites: its
89
+ * pair is optional by design (absent = an unmanaged request).
90
+ */
91
+ export declare function missingBindingLabels(labels: {
92
+ connection?: unknown;
93
+ folder?: unknown;
94
+ } | undefined): ('connection' | 'folder')[];
95
+ /** Why compile refuses a connector-bound node with no `connection` / `folder` label, and how to bind it. */
96
+ export declare function bindingMissingMessage(where: string, missing: readonly ('connection' | 'folder')[], target?: PrepareTarget): string;
@@ -110,3 +110,30 @@ export function connectionConnectorConflictMessage(connection, users) {
110
110
  + `\`http({ managed: true, … })\` step on another connector's connection must name that connector in `
111
111
  + `\`targetConnector\`.`;
112
112
  }
113
+ /**
114
+ * Which of a connector-bound node's two labels the author left out.
115
+ *
116
+ * A connector step, an event trigger, an `.entryPoint` onEvent and a
117
+ * `waitForEvent` all call Integration Service on a tenant connection, so the
118
+ * node needs BOTH halves: `connection` (→ `detail.connectionId`) and `folder`
119
+ * (→ `detail.folderKey`). Omitting a label used to compile the node with
120
+ * `connectionId: null` and no binding at all — `check` and `compile` were
121
+ * silent, and only `uip maestro flow validate` refused it ("A connection is
122
+ * required to run this activity." / "FolderKey is required for the connection
123
+ * binding.", flow-builder-sdk#890). Managed HTTP is NOT one of these sites: its
124
+ * pair is optional by design (absent = an unmanaged request).
125
+ */
126
+ export function missingBindingLabels(labels) {
127
+ const blank = (v) => typeof v !== 'string' || v.trim() === '';
128
+ return ['connection', 'folder'].filter((k) => blank(labels?.[k]));
129
+ }
130
+ /** Why compile refuses a connector-bound node with no `connection` / `folder` label, and how to bind it. */
131
+ export function bindingMissingMessage(where, missing, target) {
132
+ return `${where} has no ${missing.map((m) => `\`${m}\``).join(' or ')}. It calls Integration Service on a tenant `
133
+ + `connection, so the node needs both labels — \`connection\` (the connection id) and \`folder\` (its folder key), `
134
+ + `each naming a bindings.json entry. Without them the .flow carries \`connectionId: null\` and `
135
+ + `\`uip maestro flow validate\` refuses it ("A connection is required to run this activity." / "FolderKey is `
136
+ + `required for the connection binding."). \`${prepareFor(target)}\` finds the connection and writes both entries; `
137
+ + `with nothing real to bind yet, declare both labels in bindings.json with a placeholder resourceKey (a `
138
+ + `CONNECTION_STUB warning, never an invented GUID).`;
139
+ }
@@ -185,6 +185,17 @@ use?: {
185
185
  id: string;
186
186
  connector: string | undefined;
187
187
  }): void;
188
+ /**
189
+ * BINDING_MISSING: a connector-bound node with no `connection` or no `folder`
190
+ * label — the sentence compile throws (see `requireBindingPair` in serialize).
191
+ * Unlike {@link checkBindingLabels} this needs no bindings.json: an ABSENT label
192
+ * is wrong whatever the file says, because the node then compiles with
193
+ * `connectionId: null` and `flow validate` refuses it (flow-builder-sdk#890).
194
+ */
195
+ export declare function checkBindingPresence(where: string, labels: {
196
+ connection?: unknown;
197
+ folder?: unknown;
198
+ } | undefined, report: (code: 'BINDING_MISSING', message: string) => void, target?: PrepareTarget): void;
188
199
  /**
189
200
  * The two phrases every lookup diagnostic needs: what the field sends, and how
190
201
  * to resolve it.
@@ -26,7 +26,7 @@ import { isLookupToken, resolvedValue, } from './lookups.js';
26
26
  import { Expr } from './expr.js';
27
27
  import { extractRefs } from './expr-check.js';
28
28
  import { prepareCommand, resolveFlagArg } from './cli-spelling.js';
29
- import { bindingSelfNameMessage, bindingUndeclaredMessage, connectionConnectorConflictMessage, undeclaredBinding, } from './binding-messages.js';
29
+ import { bindingMissingMessage, bindingSelfNameMessage, bindingUndeclaredMessage, connectionConnectorConflictMessage, missingBindingLabels, undeclaredBinding, } from './binding-messages.js';
30
30
  /**
31
31
  * Flow: an input is an {@link Expr} object. A non-literal one is runtime; a
32
32
  * literal one (`lit(…)`) carries the constant it was built from.
@@ -372,6 +372,18 @@ use) {
372
372
  }
373
373
  }
374
374
  }
375
+ /**
376
+ * BINDING_MISSING: a connector-bound node with no `connection` or no `folder`
377
+ * label — the sentence compile throws (see `requireBindingPair` in serialize).
378
+ * Unlike {@link checkBindingLabels} this needs no bindings.json: an ABSENT label
379
+ * is wrong whatever the file says, because the node then compiles with
380
+ * `connectionId: null` and `flow validate` refuses it (flow-builder-sdk#890).
381
+ */
382
+ export function checkBindingPresence(where, labels, report, target) {
383
+ const missing = missingBindingLabels(labels);
384
+ if (missing.length > 0)
385
+ report('BINDING_MISSING', bindingMissingMessage(where, missing, target));
386
+ }
375
387
  /**
376
388
  * The two phrases every lookup diagnostic needs: what the field sends, and how
377
389
  * to resolve it.
@@ -72,10 +72,9 @@ export declare function entryInput(entryPointId: string, name: string): Expr;
72
72
  * Write the reference this way, or dotted by hand — never through a bracket
73
73
  * (`$vars.fetchRate["output"]`, `$vars["fetchRate"].output`). Both of those
74
74
  * resolve at run time, so the mistake is invisible, but only the DOTTED form is
75
- * rewritten on the way out: a subflow copy renames node ids, the queue family
76
- * publishes its result as `<step>.response`, and a failure envelope resolves per
77
- * node family. A bracketed read opts out of all three and goes stale the moment
78
- * one applies. `path` may still bracket INTO the value — `out('rows', 'items')`
75
+ * rewritten on the way out: a subflow copy renames node ids, and a failure
76
+ * envelope resolves per node family. A bracketed read opts out of both and goes
77
+ * stale the moment one applies. `path` may still bracket INTO the value — `out('rows', 'items')`
79
78
  * then `["odd key"]` — because nothing rewrites past the runtime variable.
80
79
  *
81
80
  * @param step - The step whose result to read, named as it was in `.step(...)`.
package/dist/core/expr.js CHANGED
@@ -92,10 +92,9 @@ export function entryInput(entryPointId, name) {
92
92
  * Write the reference this way, or dotted by hand — never through a bracket
93
93
  * (`$vars.fetchRate["output"]`, `$vars["fetchRate"].output`). Both of those
94
94
  * resolve at run time, so the mistake is invisible, but only the DOTTED form is
95
- * rewritten on the way out: a subflow copy renames node ids, the queue family
96
- * publishes its result as `<step>.response`, and a failure envelope resolves per
97
- * node family. A bracketed read opts out of all three and goes stale the moment
98
- * one applies. `path` may still bracket INTO the value — `out('rows', 'items')`
95
+ * rewritten on the way out: a subflow copy renames node ids, and a failure
96
+ * envelope resolves per node family. A bracketed read opts out of both and goes
97
+ * stale the moment one applies. `path` may still bracket INTO the value — `out('rows', 'items')`
99
98
  * then `["odd key"]` — because nothing rewrites past the runtime variable.
100
99
  *
101
100
  * @param step - The step whose result to read, named as it was in `.step(...)`.
@@ -0,0 +1,39 @@
1
+ /**
2
+ * One namespace for a flow's declared variables.
3
+ *
4
+ * `.input()`, `.output()` and `.var()` each look like their own namespace in the
5
+ * builder, but the `.flow` stores all three as rows of ONE list,
6
+ * `variables.globals[]`, keyed by `id`:
7
+ *
8
+ * .input({ x }) → { id: 'x', direction: 'in' }
9
+ * .output({ x }) → { id: 'x', direction: 'out' }
10
+ * .var('x') → { id: 'x', direction: 'inout' }
11
+ *
12
+ * The product refuses two rows with one id (`uip maestro flow validate`:
13
+ * `Duplicate variable ID: "x" (appears 2 times)`). Before flow-builder-sdk#890,
14
+ * `check` and `compile` were silent and wrote both rows.
15
+ *
16
+ * The common shape is `.output({ summary }) + .var('summary')` — "the output
17
+ * the arms assign". The remedy keeps the output and renames the var:
18
+ * `.var('draftSummary')` + `.return({ summary: v('draftSummary') })`. The output
19
+ * then stays a declared `out` global — the direction the Flow docs and the eval
20
+ * checkers (`direction == "out"`) treat as the flow's output contract. Dropping the name from `.output()` also
21
+ * validates — a `.var()` is an `inout` global, which the product counts as an
22
+ * output too — but the flow then has no declared `out` for it.
23
+ */
24
+ export type GlobalKind = 'input' | 'output' | 'var';
25
+ export interface GlobalNameCollision {
26
+ name: string;
27
+ /** Every declaration of the name, in declaration order (`input`, then `output`, then `var`). */
28
+ kinds: GlobalKind[];
29
+ }
30
+ /** The names declared more than once across `.input()`, `.output()` and `.var()`. */
31
+ export declare function globalNameCollisions(inputs: readonly {
32
+ name: string;
33
+ }[], outputs: readonly {
34
+ name: string;
35
+ }[], vars: readonly {
36
+ name: string;
37
+ }[]): GlobalNameCollision[];
38
+ /** The sentence `check` (VARIABLE_NAME_COLLISION) prints and `serialize` throws for one collision. */
39
+ export declare function globalNameCollisionMessage({ name, kinds }: GlobalNameCollision): string;
@@ -0,0 +1,60 @@
1
+ /**
2
+ * One namespace for a flow's declared variables.
3
+ *
4
+ * `.input()`, `.output()` and `.var()` each look like their own namespace in the
5
+ * builder, but the `.flow` stores all three as rows of ONE list,
6
+ * `variables.globals[]`, keyed by `id`:
7
+ *
8
+ * .input({ x }) → { id: 'x', direction: 'in' }
9
+ * .output({ x }) → { id: 'x', direction: 'out' }
10
+ * .var('x') → { id: 'x', direction: 'inout' }
11
+ *
12
+ * The product refuses two rows with one id (`uip maestro flow validate`:
13
+ * `Duplicate variable ID: "x" (appears 2 times)`). Before flow-builder-sdk#890,
14
+ * `check` and `compile` were silent and wrote both rows.
15
+ *
16
+ * The common shape is `.output({ summary }) + .var('summary')` — "the output
17
+ * the arms assign". The remedy keeps the output and renames the var:
18
+ * `.var('draftSummary')` + `.return({ summary: v('draftSummary') })`. The output
19
+ * then stays a declared `out` global — the direction the Flow docs and the eval
20
+ * checkers (`direction == "out"`) treat as the flow's output contract. Dropping the name from `.output()` also
21
+ * validates — a `.var()` is an `inout` global, which the product counts as an
22
+ * output too — but the flow then has no declared `out` for it.
23
+ */
24
+ /** The names declared more than once across `.input()`, `.output()` and `.var()`. */
25
+ export function globalNameCollisions(inputs, outputs, vars) {
26
+ const seen = new Map();
27
+ const add = (kind, list) => {
28
+ for (const { name } of list) {
29
+ const kinds = seen.get(name);
30
+ if (kinds)
31
+ kinds.push(kind);
32
+ else
33
+ seen.set(name, [kind]);
34
+ }
35
+ };
36
+ add('input', inputs);
37
+ add('output', outputs);
38
+ add('var', vars);
39
+ return [...seen].filter(([, kinds]) => kinds.length > 1).map(([name, kinds]) => ({ name, kinds }));
40
+ }
41
+ const SPELLING = {
42
+ input: (name) => `.input({ ${name} })`,
43
+ output: (name) => `.output({ ${name} })`,
44
+ var: (name) => `.var('${name}')`,
45
+ };
46
+ /** The sentence `check` (VARIABLE_NAME_COLLISION) prints and `serialize` throws for one collision. */
47
+ export function globalNameCollisionMessage({ name, kinds }) {
48
+ const declared = kinds.map((k) => SPELLING[k](name)).join(' and ');
49
+ const head = `"${name}" is declared ${kinds.length} times (${declared}). The .flow keeps inputs, outputs and `
50
+ + `variables in one list, variables.globals[], so this compiles to ${kinds.length} globals with the id "${name}" `
51
+ + `and \`uip maestro flow validate\` refuses it: Duplicate variable ID: "${name}".`;
52
+ const distinct = new Set(kinds);
53
+ if (distinct.size === 2 && distinct.has('output') && distinct.has('var')) {
54
+ return `${head} Keep the output and give the variable its own name: `
55
+ + `.var('${name}Value', …) with { updates: { ${name}Value: … } }, then .return({ ${name}: v('${name}Value') }). `
56
+ + `(Dropping "${name}" from .output() also validates — a .var() compiles to an \`inout\` global, which the `
57
+ + `product counts as an output — but then the flow declares no \`out\` variable for it.)`;
58
+ }
59
+ return `${head} Rename one of them.`;
60
+ }
package/dist/decompile.js CHANGED
@@ -37,6 +37,8 @@ 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 { missingBindingLabels } from './core/binding-messages.js';
41
+ import { prepareCommand } from './core/cli-spelling.js';
40
42
  import { OUTCOME_HANDLE_NODE_TYPES } from './core/hitl-routing.js';
41
43
  import { eventFilterFromTreeLeaf } from './core/event-filters.js';
42
44
  /**
@@ -1517,6 +1519,25 @@ function emitHitl(node, imp, inputNames, graph) {
1517
1519
  * `filterExpression` string, but reading the structured copies avoids parsing a
1518
1520
  * string this file generated.
1519
1521
  */
1522
+ /**
1523
+ * A connector-bound node the file carries UNBOUND: Studio Web saves a draft whose
1524
+ * connection was never picked as `connectionId: null`. The source cannot invent
1525
+ * labels for it, and `compile` refuses a node without both (BINDING_MISSING, the
1526
+ * rule `flow validate` applies too: flow-builder-sdk#890). So the source says so
1527
+ * at the node and the run warns once, instead of handing back a file whose first
1528
+ * compile fails with no context. Returns the inline marker, or '' when bound.
1529
+ */
1530
+ function unboundMarker(node, labels, key, action) {
1531
+ const missing = missingBindingLabels(labels);
1532
+ if (missing.length === 0)
1533
+ return '';
1534
+ const names = missing.join(' and ');
1535
+ warnOnce(`node ${str(node.id)} (${node.type}) has no ${missing.join(' or ')} in the .flow (an unbound draft). `
1536
+ + `The source leaves ${missing.length > 1 ? 'them' : 'it'} out, and compile refuses the node (BINDING_MISSING) `
1537
+ + `until it names both labels: run \`${prepareCommand(key, action ?? '<action>')}\`, then pass `
1538
+ + `\`connection\` and \`folder\`.`);
1539
+ return ` /* TODO: bind ${names}; compile refuses this node until it has both labels (BINDING_MISSING) */`;
1540
+ }
1520
1541
  function emitEventSubscription(node, imp, inputNames) {
1521
1542
  const prefix = node.type.startsWith('uipath.connector.event.') ? 'uipath.connector.event.'
1522
1543
  : node.type.startsWith('uipath.connector.trigger.') ? 'uipath.connector.trigger.'
@@ -1574,7 +1595,7 @@ function emitEventSubscription(node, imp, inputNames) {
1574
1595
  ['filters', filterRows.length ? `[${filterRows.join(', ')}]` : undefined],
1575
1596
  ['connection', conn !== undefined ? str(conn) : undefined],
1576
1597
  ['folder', folder !== undefined ? str(folder) : undefined],
1577
- ]);
1598
+ ]) + unboundMarker(node, { connection: conn, folder }, connector, event);
1578
1599
  }
1579
1600
  function emitPublishedResource(node, imp, inputNames) {
1580
1601
  const FAMILIES = [
@@ -1658,7 +1679,8 @@ function emitConnector(node, imp, inputNames, o) {
1658
1679
  ]);
1659
1680
  const inputsSrc = objLit(flat) || '{}';
1660
1681
  const args = [str(split.key), str(split.action), inputsSrc, ...(optsSrc ? [optsSrc] : [])];
1661
- return `${imp.use('connector')}(${args.join(', ')})`;
1682
+ return `${imp.use('connector')}(${args.join(', ')})`
1683
+ + unboundMarker(node, { connection, folder }, split.key, split.action);
1662
1684
  }
1663
1685
  /** Best-effort placeholder for a node type the reverse-map does not yet cover. */
1664
1686
  function unsupportedSpec(node, imp, o) {
@@ -148,6 +148,7 @@ export declare function manual(): TriggerSpec;
148
148
  * connector: 'uipath-microsoft-outlook365',
149
149
  * event: 'email-received',
150
150
  * where: { parentFolderId: inboxId },
151
+ * connection: 'outlook', folder: 'shared', // bindings.json labels, both required
151
152
  * }))
152
153
  * .step('extract', script({ code: 'return { subject: $vars.start.output.subject };' }))
153
154
  * .build();
@@ -740,12 +741,13 @@ declare class StepList {
740
741
  * ```ts
741
742
  * export default flow('draft-review')
742
743
  * .output({ summary: types.string })
743
- * .var('summary', types.string)
744
+ * // Not `.var('summary')`: an output and a var share one namespace (VARIABLE_NAME_COLLISION).
745
+ * .var('result', types.string)
744
746
  * .stepSwitch('review', hitl({ fields: [], outcomes: ['Publish', 'Revise'] }), [
745
- * { value: 'Publish', body: (b) => b.step('publish', script({ code: 'return "published";', returns: 'string' }), { updates: { summary: out('publish') } }) },
746
- * { value: 'Revise', body: (b) => b.step('sendBack', script({ code: 'return "revise";', returns: 'string' }), { updates: { summary: out('sendBack') } }) },
747
+ * { value: 'Publish', body: (b) => b.step('publish', script({ code: 'return "published";', returns: 'string' }), { updates: { result: out('publish') } }) },
748
+ * { value: 'Revise', body: (b) => b.step('sendBack', script({ code: 'return "revise";', returns: 'string' }), { updates: { result: out('sendBack') } }) },
747
749
  * ])
748
- * .return({ summary: v('summary') })
750
+ * .return({ summary: v('result') })
749
751
  * .build();
750
752
  * ```
751
753
  */
package/dist/flow-sdk.js CHANGED
@@ -332,12 +332,13 @@ class StepList {
332
332
  * ```ts
333
333
  * export default flow('draft-review')
334
334
  * .output({ summary: types.string })
335
- * .var('summary', types.string)
335
+ * // Not `.var('summary')`: an output and a var share one namespace (VARIABLE_NAME_COLLISION).
336
+ * .var('result', types.string)
336
337
  * .stepSwitch('review', hitl({ fields: [], outcomes: ['Publish', 'Revise'] }), [
337
- * { value: 'Publish', body: (b) => b.step('publish', script({ code: 'return "published";', returns: 'string' }), { updates: { summary: out('publish') } }) },
338
- * { value: 'Revise', body: (b) => b.step('sendBack', script({ code: 'return "revise";', returns: 'string' }), { updates: { summary: out('sendBack') } }) },
338
+ * { value: 'Publish', body: (b) => b.step('publish', script({ code: 'return "published";', returns: 'string' }), { updates: { result: out('publish') } }) },
339
+ * { value: 'Revise', body: (b) => b.step('sendBack', script({ code: 'return "revise";', returns: 'string' }), { updates: { result: out('sendBack') } }) },
339
340
  * ])
340
- * .return({ summary: v('summary') })
341
+ * .return({ summary: v('result') })
341
342
  * .build();
342
343
  * ```
343
344
  */