@uipath/maestro-builder-sdk 6.15.6 → 6.16.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
@@ -16,7 +16,7 @@ import { hitlDerivesOutcomeHandles, hitlExitModel, hitlRoutesPerOutcome } from '
16
16
  import { childStepLists, declaredExits, hasExits, stopsAtEnd } from './core/step-ports.js';
17
17
  import { FLOW_SCHEMA_VERSIONS } from './format-profile.js';
18
18
  import { compareSchemaVersions, schemaVersionRefusal } from './schema-version.js';
19
- import { planFlowGlobals } from './flow-variables.js';
19
+ import { planFlowGlobals, planSharedInputs, sharedInputRoots } from './flow-variables.js';
20
20
  export function check(built, opts = {}) {
21
21
  let diags = [];
22
22
  const uses = collectConnectionUses(() => { diags = checkFlow(built, [], opts); });
@@ -81,11 +81,17 @@ function checkFlow(built, ancestors, opts = {}) {
81
81
  declared.add(built.triggerId);
82
82
  for (const ep of built.entryPoints ?? [])
83
83
  declared.add(ep.id);
84
+ // A variable an input feeds with `{ shared }` is declared by the SDK when the
85
+ // author did not declare it (#924), so it is a readable `$vars.<name>` too.
86
+ const sharedAutoVars = planSharedInputs({ roots: sharedInputRoots(built), outputs: built.outputs, vars: built.vars }).autoVars;
87
+ for (const a of sharedAutoVars)
88
+ declared.add(a.name);
84
89
  // Declared type per name, for the output/return category check below.
85
90
  const typeOf = new Map([
86
91
  ...built.inputs.map((x) => [x.name, String(x.type)]),
87
92
  ...(built.entryPoints ?? []).flatMap((ep) => ep.inputs.map((x) => [x.name, String(x.type)])),
88
93
  ...built.vars.map((x) => [x.name, String(x.type)]),
94
+ ...sharedAutoVars.map((x) => [x.name, x.type]),
89
95
  ]);
90
96
  const outputType = new Map(built.outputs.map((x) => [x.name, String(x.type)]));
91
97
  const stepNames = new Set();
@@ -1411,6 +1417,14 @@ function checkSubflow(step, child, inputs, parent, ancestors, diags) {
1411
1417
  at('SUBFLOW_CHILD_TRIGGER', `Subflow "${step}" runs a flow with its own \`.trigger(…)\`. A subflow is started by its caller, never by a `
1412
1418
  + `trigger of its own — the schedule would be dropped. Remove the .trigger() from "${child.id}".`);
1413
1419
  }
1420
+ // `{ shared }` merges one input across a flow's ROOTS (#924). A subflow has
1421
+ // one root, its caller, so there is nothing to merge; serialize refuses it.
1422
+ for (const inp of child.inputs) {
1423
+ if (inp.shared === undefined)
1424
+ continue;
1425
+ at('SHARED_INPUT_IN_SUBFLOW', `Subflow "${step}" runs "${child.id}", whose input "${inp.name}" has { shared: "${inp.shared}" }. A subflow has `
1426
+ + `one root — its caller — so there is nothing to merge. Read the input with input('${inp.name}') inside the child.`);
1427
+ }
1414
1428
  const declared = new Map(child.inputs.map((i) => [i.name, String(i.type)]));
1415
1429
  const given = inputs ?? {};
1416
1430
  for (const key of Object.keys(given)) {
@@ -1653,7 +1667,14 @@ const INTERVAL_WORDS = [
1653
1667
  * said `Errors: 0`, validate refused the file).
1654
1668
  */
1655
1669
  function checkGlobalNames(built, diags) {
1656
- for (const p of planFlowGlobals({ inputs: built.inputs ?? [], outputs: built.outputs ?? [], vars: built.vars ?? [] }).problems) {
1670
+ // `{ shared }` inputs (#924) may add `.var()`s; the plan is the serializer's
1671
+ // own (`emittableGlobals`), so check and the emitted file agree on them.
1672
+ const shared = planSharedInputs({ roots: sharedInputRoots(built), outputs: built.outputs ?? [], vars: built.vars ?? [] });
1673
+ for (const p of shared.problems) {
1674
+ diags.push({ level: p.level, code: p.code, message: p.message, ...(p.suggestion ? { suggestion: p.suggestion } : {}) });
1675
+ }
1676
+ const vars = [...(built.vars ?? []), ...shared.autoVars];
1677
+ for (const p of planFlowGlobals({ inputs: built.inputs ?? [], outputs: built.outputs ?? [], vars }).problems) {
1657
1678
  diags.push({ level: 'error', code: p.code, message: p.message, ...(p.suggestion ? { suggestion: p.suggestion } : {}) });
1658
1679
  }
1659
1680
  }
@@ -1665,7 +1686,12 @@ function checkGlobalNames(built, diags) {
1665
1686
  */
1666
1687
  function checkNodeUpdates(built, diags) {
1667
1688
  const inputs = new Set(built.inputs.map((v) => v.name));
1668
- const writable = new Set([...built.vars.map((v) => v.name), ...built.outputs.map((v) => v.name)]);
1689
+ const writable = new Set([
1690
+ ...built.vars.map((v) => v.name),
1691
+ ...built.outputs.map((v) => v.name),
1692
+ // A variable declared by `{ shared }` (#924) is a `.var()` like any other.
1693
+ ...planSharedInputs({ roots: sharedInputRoots(built), outputs: built.outputs, vars: built.vars }).autoVars.map((v) => v.name),
1694
+ ]);
1669
1695
  const visit = (steps) => {
1670
1696
  for (const s of steps) {
1671
1697
  const updates = s.options?.updates;
package/dist/decompile.js CHANGED
@@ -1739,7 +1739,7 @@ function typeExpr(type, imp) {
1739
1739
  * (`{ type: types.x, description, subType, schema }`); a bare one keeps the
1740
1740
  * compact `name: types.x` form.
1741
1741
  */
1742
- function declExpr(g, withDefault, imp) {
1742
+ function declExpr(g, withDefault, imp, shared) {
1743
1743
  const meta = [];
1744
1744
  if (withDefault && g.defaultValue !== undefined)
1745
1745
  meta.push(`default: ${literal(g.defaultValue)}`);
@@ -1749,16 +1749,58 @@ function declExpr(g, withDefault, imp) {
1749
1749
  meta.push(`subType: ${str(g.subType)}`);
1750
1750
  if (g.schema !== undefined)
1751
1751
  meta.push(`schema: ${literal(g.schema)}`);
1752
+ if (shared !== undefined)
1753
+ meta.push(`shared: ${str(shared)}`);
1752
1754
  if (meta.length === 0)
1753
1755
  return typeExpr(g.type, imp);
1754
1756
  return `{ type: ${typeExpr(g.type, imp)}, ${meta.join(', ')} }`;
1755
1757
  }
1756
- function emitVariables(e, depth, flow, imp) {
1758
+ function sharedCopies(flow, triggers) {
1757
1759
  const globals = readFlowGlobals(flow.variables, runStrict);
1758
- const inputs = globals.filter((g) => g.direction === 'in');
1760
+ const byInput = new Map();
1761
+ const unrecognized = [];
1762
+ const fed = new Map();
1763
+ for (const t of triggers) {
1764
+ const rows = flow.variables?.variableUpdates?.[t.id] ?? [];
1765
+ const seenOnRoot = new Set();
1766
+ for (const row of rows) {
1767
+ const src = unwrapSource(row.expression);
1768
+ const m = src?.startsWith('=js:') ? /^\$vars\.([A-Za-z0-9_]+)\.output\.([A-Za-z0-9_]+)$/.exec(src.slice(4).trim()) : null;
1769
+ const inp = m && m[1] === t.id
1770
+ ? globals.find((g) => g.direction === 'in' && g.id === m[2] && g.triggerNodeId === t.id)
1771
+ : undefined;
1772
+ const target = globals.find((g) => g.id === row.variableId && g.direction !== 'in');
1773
+ const ok = inp !== undefined && target !== undefined && target.type === inp.type
1774
+ && !byInput.has(`${t.id}\0${inp.id}`) && !seenOnRoot.has(row.variableId);
1775
+ if (!ok) {
1776
+ unrecognized.push({ trigger: t.id, variableId: row.variableId });
1777
+ continue;
1778
+ }
1779
+ seenOnRoot.add(row.variableId);
1780
+ byInput.set(`${t.id}\0${inp.id}`, row.variableId);
1781
+ fed.set(row.variableId, (fed.get(row.variableId) ?? 0) + 1);
1782
+ }
1783
+ }
1784
+ const auto = new Set();
1785
+ for (const name of fed.keys()) {
1786
+ const g = globals.find((x) => x.id === name);
1787
+ if (g.direction === 'inout' && g.defaultValue === undefined && g.description === undefined
1788
+ && g.subType === undefined && g.schema === undefined)
1789
+ auto.add(name);
1790
+ }
1791
+ return { byInput, auto, unrecognized };
1792
+ }
1793
+ function emitVariables(e, depth, flow, imp, epTriggers = new Set(), shared) {
1794
+ const globals = readFlowGlobals(flow.variables, runStrict);
1795
+ // `.input()` declares the DEFAULT root's inputs. An input bound to another
1796
+ // root comes back on that root's `.entryPoint(…, { inputs })` instead —
1797
+ // listing it here too declared it twice, which recompiles to
1798
+ // ENTRY_POINT_INPUT_COLLISION.
1799
+ const inputs = globals.filter((g) => g.direction === 'in' && !(g.triggerNodeId !== undefined && epTriggers.has(g.triggerNodeId)));
1759
1800
  const outputs = globals.filter((g) => g.direction === 'out');
1760
- const inouts = globals.filter((g) => g.direction === 'inout');
1761
- const shape = (list, withDefault) => `{ ${list.map((g) => `${key(g.name ?? g.id)}: ${declExpr(g, withDefault, imp)}`).join(', ')} }`;
1801
+ const inouts = globals.filter((g) => g.direction === 'inout' && !shared?.auto.has(g.id));
1802
+ const sharedOf = (g) => g.triggerNodeId !== undefined ? shared?.byInput.get(`${g.triggerNodeId}\0${g.id}`) : undefined;
1803
+ const shape = (list, withDefault) => `{ ${list.map((g) => `${key(g.name ?? g.id)}: ${declExpr(g, withDefault, imp, sharedOf(g))}`).join(', ')} }`;
1762
1804
  if (inputs.length)
1763
1805
  e.line(depth, `.input(${shape(inputs, true)})`);
1764
1806
  if (outputs.length)
@@ -2652,9 +2694,22 @@ export function decompile(flow, options = {}) {
2652
2694
  e.line(d, `// TODO: node ${str(n.id)} (${n.type}) is not reachable from any trigger and is NOT in this source; recompiling drops it. Connect it in the .flow or edit its JSON directly.`);
2653
2695
  }
2654
2696
  }
2655
- emitVariables(e, d, flow, imp);
2697
+ // `{ shared }` input copies on the roots (#924). Any other trigger-level
2698
+ // update has no builder spelling: say so instead of dropping it.
2699
+ const shared = sharedCopies(flow, triggerNodes);
2700
+ if (shared.unrecognized.length) {
2701
+ const listing = shared.unrecognized.map((u) => `${str(u.variableId)} on ${str(u.trigger)}`).join(', ');
2702
+ const message = `${shared.unrecognized.length} variable update(s) on a trigger node are not a { shared } input copy: ${listing}. `
2703
+ + 'The builder writes a trigger-level update only for { type, shared: \'<var>\' } (the row `<var> ← $vars.<trigger>.output.<input>`), '
2704
+ + 'so this source omits them and recompiling drops them.';
2705
+ if (runStrict)
2706
+ throw new Error(`decompile: ${message}`);
2707
+ warnOnce(message);
2708
+ e.line(d, `// TODO: ${message}`);
2709
+ }
2710
+ emitVariables(e, d, flow, imp, new Set(triggerNodes.filter((t) => t !== trigger).map((t) => t.id)), shared);
2656
2711
  // Reconstruct the body: structured control flow recovered from the graph.
2657
- const ctx = { graph, flow, imp, inputNames, o: options, emitted: new Set() };
2712
+ const ctx = { graph, flow, imp, inputNames, o: options, emitted: new Set(), shared };
2658
2713
  const scope = scopeIds(flow.nodes ?? [], undefined);
2659
2714
  const startId = trigger?.id ?? 'start';
2660
2715
  const first = scopeSucc(graph, startId, scope)[0];
@@ -2746,7 +2801,8 @@ function entryPointSeg(t, scope, ipdom, ctx) {
2746
2801
  .filter((g) => g.direction === 'in' && g.triggerNodeId === t.id);
2747
2802
  const optParts = [];
2748
2803
  if (scoped.length) {
2749
- optParts.push(`inputs: { ${scoped.map((g) => `${key(g.name ?? g.id)}: ${declExpr(g, true, ctx.imp)}`).join(', ')} }`);
2804
+ const sharedOf = (g) => ctx.shared?.byInput.get(`${t.id}\0${g.id}`);
2805
+ optParts.push(`inputs: { ${scoped.map((g) => `${key(g.name ?? g.id)}: ${declExpr(g, true, ctx.imp, sharedOf(g))}`).join(', ')} }`);
2750
2806
  }
2751
2807
  const ver = verClause(t);
2752
2808
  if (ver)
@@ -58,6 +58,12 @@ function globalRoots(built) {
58
58
  roots.add('start');
59
59
  if (built.triggerId)
60
60
  roots.add(built.triggerId);
61
+ // A variable an input feeds with `{ shared }` is declared by the SDK when the
62
+ // author did not declare it — readable as `v('<name>')` all the same (#924).
63
+ for (const inp of [...built.inputs, ...(built.entryPoints ?? []).flatMap((ep) => ep.inputs)]) {
64
+ if (inp.shared !== undefined)
65
+ roots.add(inp.shared);
66
+ }
61
67
  for (const ep of built.entryPoints ?? []) {
62
68
  roots.add(ep.id);
63
69
  addStepNames(ep.steps, roots);
@@ -54,6 +54,8 @@ export interface VarDecl {
54
54
  subType?: string;
55
55
  /** JSON schema for complex type validation (object/array shapes). */
56
56
  schema?: Record<string, unknown>;
57
+ /** Inputs only: the flow variable this root's input is copied into when the root fires. See {@link VarSpec.shared}. */
58
+ shared?: string;
57
59
  }
58
60
  /**
59
61
  * The rich spelling of a variable declaration: pass it instead of a bare
@@ -77,6 +79,39 @@ export interface VarSpec {
77
79
  * `.output({ product: { type: types.number, default: 1 } })`.
78
80
  */
79
81
  default?: unknown;
82
+ /**
83
+ * Inputs only: copy this input into the flow variable named here when its
84
+ * root fires, so a body shared by several roots reads one variable,
85
+ * `v('<shared>')`, whichever root started the run.
86
+ *
87
+ * @remarks
88
+ * For a flow with more than one root: the default `.input()` root plus
89
+ * `.entryPoint(...)` roots. Each root keeps its own input name. The format
90
+ * keys every global, and every entry point's caller-facing input, by one
91
+ * unique id, so two roots cannot both take `x`. `shared` names the variable
92
+ * they all feed.
93
+ *
94
+ * - The variable is declared for you unless a `.var()` or `.output()` of that
95
+ * name exists. Declare the `.var()` yourself to give it a default.
96
+ * - The copy is a `variableUpdates` row on the root's TRIGGER node,
97
+ * `<shared> ← $vars.<triggerId>.output.<input>`. So it is done before the
98
+ * root's own prefix steps run.
99
+ * - A root that feeds nothing leaves the variable at its default, or null
100
+ * if it has none.
101
+ * - Every input feeding one variable must have that variable's type, or
102
+ * `check` reports `SHARED_INPUT_TYPE_MISMATCH`.
103
+ * - Not accepted on a subflow child's input. A subflow has one root, its
104
+ * caller, so there is nothing to merge.
105
+ *
106
+ * @example
107
+ * ```ts
108
+ * flow('order')
109
+ * .input({ orderAmount: { type: types.number, shared: 'amount' } })
110
+ * .entryPoint('retry', manual(), { inputs: { retryAmount: { type: types.number, shared: 'amount' } } })
111
+ * .step('double', script({ code: 'return $vars.amount * 2;', returns: 'number' })) // v('amount') in an Expr slot
112
+ * ```
113
+ */
114
+ shared?: string;
80
115
  }
81
116
  /**
82
117
  * The eight intervals the designer offers in its Frequency dropdown. Any other
@@ -1229,6 +1264,18 @@ declare class FlowBuilder extends StepList {
1229
1264
  * .step('normalize', script({ code: 'return 1;' })) // shared body
1230
1265
  * ```
1231
1266
  *
1267
+ * **Sharing an input across roots.** Mark each root's input with the variable
1268
+ * it feeds, `{ type, shared: '<var>' }`. The SDK copies it on that root's
1269
+ * trigger node, and the shared body reads `v('<var>')` whichever root fired.
1270
+ * See {@link VarSpec.shared}.
1271
+ *
1272
+ * ```ts
1273
+ * flow('order')
1274
+ * .input({ orderAmount: { type: types.number, shared: 'amount' } })
1275
+ * .entryPoint('retry', manual(), { inputs: { retryAmount: { type: types.number, shared: 'amount' } } })
1276
+ * .step('double', script({ code: 'return $vars.amount * 2;', returns: 'number' })) // v('amount') in an Expr slot
1277
+ * ```
1278
+ *
1232
1279
  * @param id - The root's trigger-node id — unique among entry points, steps,
1233
1280
  * and the default trigger id. Expressions read it as `$vars.<id>.output.*`.
1234
1281
  * @param trigger - What fires this root: `manual()`, `scheduled(...)`, or
@@ -1319,6 +1366,11 @@ declare class FlowBuilder extends StepList {
1319
1366
  /**
1320
1367
  * Declare the flow's inputs. Read them with `input('<name>')`.
1321
1368
  *
1369
+ * @remarks
1370
+ * In a flow with several roots, give an input `{ type, shared: '<var>' }` to
1371
+ * copy it into a flow variable the shared body reads with `v('<var>')`. See
1372
+ * {@link VarSpec.shared}.
1373
+ *
1322
1374
  * @param shape - Input names to type descriptors, e.g. `{ amount: types.number }`.
1323
1375
  * @returns This builder, so calls chain.
1324
1376
  * @see input
package/dist/flow-sdk.js CHANGED
@@ -71,12 +71,24 @@ function varDeclOf(method, name, value) {
71
71
  if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
72
72
  const spec = value;
73
73
  assertType(method, name, spec.type);
74
+ if (spec.shared !== undefined) {
75
+ if (method !== 'input') {
76
+ throw new TypeError(`.${method}() declared ${JSON.stringify(name)} with { shared }, which only an input takes: it names `
77
+ + `the flow variable a root's input is copied into. Declare the variable with .var() and feed it from `
78
+ + `.input({ <name>: { type, shared: ${JSON.stringify(spec.shared)} } }).`);
79
+ }
80
+ if (typeof spec.shared !== 'string' || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(spec.shared)) {
81
+ throw new TypeError(`Input ${JSON.stringify(name)}: { shared } must be a variable name (letters, digits, _), got `
82
+ + `${JSON.stringify(spec.shared)}.`);
83
+ }
84
+ }
74
85
  return {
75
86
  type: spec.type,
76
87
  ...(spec.default !== undefined ? { default: spec.default } : {}),
77
88
  ...(spec.description !== undefined ? { description: spec.description } : {}),
78
89
  ...(spec.subType !== undefined ? { subType: spec.subType } : {}),
79
90
  ...(spec.schema !== undefined ? { schema: spec.schema } : {}),
91
+ ...(spec.shared !== undefined ? { shared: spec.shared } : {}),
80
92
  };
81
93
  }
82
94
  assertType(method, name, value);
@@ -967,6 +979,18 @@ class FlowBuilder extends StepList {
967
979
  * .step('normalize', script({ code: 'return 1;' })) // shared body
968
980
  * ```
969
981
  *
982
+ * **Sharing an input across roots.** Mark each root's input with the variable
983
+ * it feeds, `{ type, shared: '<var>' }`. The SDK copies it on that root's
984
+ * trigger node, and the shared body reads `v('<var>')` whichever root fired.
985
+ * See {@link VarSpec.shared}.
986
+ *
987
+ * ```ts
988
+ * flow('order')
989
+ * .input({ orderAmount: { type: types.number, shared: 'amount' } })
990
+ * .entryPoint('retry', manual(), { inputs: { retryAmount: { type: types.number, shared: 'amount' } } })
991
+ * .step('double', script({ code: 'return $vars.amount * 2;', returns: 'number' })) // v('amount') in an Expr slot
992
+ * ```
993
+ *
970
994
  * @param id - The root's trigger-node id — unique among entry points, steps,
971
995
  * and the default trigger id. Expressions read it as `$vars.<id>.output.*`.
972
996
  * @param trigger - What fires this root: `manual()`, `scheduled(...)`, or
@@ -1081,6 +1105,11 @@ class FlowBuilder extends StepList {
1081
1105
  /**
1082
1106
  * Declare the flow's inputs. Read them with `input('<name>')`.
1083
1107
  *
1108
+ * @remarks
1109
+ * In a flow with several roots, give an input `{ type, shared: '<var>' }` to
1110
+ * copy it into a flow variable the shared body reads with `v('<var>')`. See
1111
+ * {@link VarSpec.shared}.
1112
+ *
1084
1113
  * @param shape - Input names to type descriptors, e.g. `{ amount: types.number }`.
1085
1114
  * @returns This builder, so calls chain.
1086
1115
  * @see input
@@ -77,3 +77,77 @@ export declare function planFlowGlobals<D extends DeclaredVariable>(decl: {
77
77
  outputs: readonly D[];
78
78
  vars: readonly D[];
79
79
  }): FlowGlobalsPlan<D>;
80
+ /** One root's input that feeds a shared variable. */
81
+ export interface SharedInputFeed {
82
+ /** The root's trigger-node id at AUTHORING time: `start` for the default root, else the entry-point id. */
83
+ root: string;
84
+ /** The input's declared name on that root (its global id). */
85
+ input: string;
86
+ type: string;
87
+ }
88
+ /** One variable that inputs feed with `{ shared: '<name>' }`. */
89
+ export interface SharedInputTarget {
90
+ name: string;
91
+ type: string;
92
+ /** What declares it: an explicit `.var()` / `.output()`, or the SDK (`auto`). */
93
+ declaredAs: 'var' | 'output' | 'auto';
94
+ hasDefault: boolean;
95
+ feeds: SharedInputFeed[];
96
+ }
97
+ export interface SharedInputsProblem {
98
+ level: 'error' | 'warning';
99
+ code: 'SHARED_INPUT_TARGET_IS_INPUT' | 'SHARED_INPUT_TYPE_MISMATCH' | 'SHARED_INPUT_SAME_ROOT' | 'SHARED_INPUT_ROOT_MISSING';
100
+ name: string;
101
+ message: string;
102
+ suggestion?: string;
103
+ }
104
+ export interface SharedInputsPlan {
105
+ /** Every shared variable, in order of its first feeding input. */
106
+ targets: SharedInputTarget[];
107
+ /** The `.var()` declarations the SDK adds — one per `auto` target, emitted after the explicit vars. */
108
+ autoVars: {
109
+ name: string;
110
+ type: string;
111
+ }[];
112
+ problems: SharedInputsProblem[];
113
+ }
114
+ /** The `{ shared }` slice of an input declaration. */
115
+ interface SharedCapable extends DeclaredVariable {
116
+ shared?: string;
117
+ }
118
+ /**
119
+ * Plan the variables that root inputs feed with `{ shared: '<var>' }`.
120
+ *
121
+ * Shared by `check` and the serializer, so the two cannot disagree about which
122
+ * variable exists, what type it is, or which problems refuse the flow (the
123
+ * #899/#904 lesson: `check` said `Errors: 0`, validate refused the file).
124
+ *
125
+ * Why the inputs keep their own names: `flow validate` refuses a repeated id in
126
+ * `variables.globals` whatever its `triggerNodeId`, and each entry point's
127
+ * caller-facing input schema is keyed by that id. So every root's input is a
128
+ * distinct global, and the variable they feed is a distinct one too.
129
+ */
130
+ export declare function planSharedInputs(decl: {
131
+ roots: readonly {
132
+ id: string;
133
+ inputs: readonly SharedCapable[];
134
+ }[];
135
+ outputs: readonly DeclaredVariable[];
136
+ vars: readonly DeclaredVariable[];
137
+ }): SharedInputsPlan;
138
+ /**
139
+ * A flow's roots for {@link planSharedInputs}: the default root first (under
140
+ * its EMITTED trigger id, `.triggerId()` applied), then each `.entryPoint()`.
141
+ */
142
+ export declare function sharedInputRoots(built: {
143
+ inputs: readonly SharedCapable[];
144
+ entryPoints?: readonly {
145
+ id: string;
146
+ inputs: readonly SharedCapable[];
147
+ }[];
148
+ triggerId?: string;
149
+ }): {
150
+ id: string;
151
+ inputs: readonly SharedCapable[];
152
+ }[];
153
+ export {};
@@ -124,3 +124,104 @@ export function planFlowGlobals(decl) {
124
124
  const vars = decl.vars.filter((vd) => !mergedSet.has(vd.name));
125
125
  return { outputs, vars, merged, problems };
126
126
  }
127
+ /**
128
+ * Plan the variables that root inputs feed with `{ shared: '<var>' }`.
129
+ *
130
+ * Shared by `check` and the serializer, so the two cannot disagree about which
131
+ * variable exists, what type it is, or which problems refuse the flow (the
132
+ * #899/#904 lesson: `check` said `Errors: 0`, validate refused the file).
133
+ *
134
+ * Why the inputs keep their own names: `flow validate` refuses a repeated id in
135
+ * `variables.globals` whatever its `triggerNodeId`, and each entry point's
136
+ * caller-facing input schema is keyed by that id. So every root's input is a
137
+ * distinct global, and the variable they feed is a distinct one too.
138
+ */
139
+ export function planSharedInputs(decl) {
140
+ const problems = [];
141
+ const inputNames = new Set(decl.roots.flatMap((r) => r.inputs.map((i) => i.name)));
142
+ const byName = new Map();
143
+ for (const root of decl.roots) {
144
+ for (const inp of root.inputs) {
145
+ if (inp.shared === undefined)
146
+ continue;
147
+ const name = inp.shared;
148
+ let target = byName.get(name);
149
+ if (!target) {
150
+ const vd = decl.vars.find((x) => x.name === name);
151
+ const od = decl.outputs.find((x) => x.name === name);
152
+ const declared = od ?? vd;
153
+ target = {
154
+ name,
155
+ type: declared ? String(declared.type) : String(inp.type),
156
+ declaredAs: od ? 'output' : vd ? 'var' : 'auto',
157
+ hasDefault: (od?.default ?? vd?.default) !== undefined,
158
+ feeds: [],
159
+ };
160
+ byName.set(name, target);
161
+ }
162
+ target.feeds.push({ root: root.id, input: inp.name, type: String(inp.type) });
163
+ }
164
+ }
165
+ for (const t of byName.values()) {
166
+ if (inputNames.has(t.name)) {
167
+ problems.push({
168
+ level: 'error', code: 'SHARED_INPUT_TARGET_IS_INPUT', name: t.name,
169
+ message: `{ shared: ${JSON.stringify(t.name)} } names a flow INPUT. The shared variable is its own global, and `
170
+ + `\`flow validate\` refuses two globals with one id, so it needs a name no input uses.`,
171
+ suggestion: `Rename the variable, e.g. { shared: '${t.name}Value' }, and read it with v('${t.name}Value').`,
172
+ });
173
+ continue;
174
+ }
175
+ const wrong = t.feeds.filter((f) => f.type !== t.type);
176
+ if (wrong.length) {
177
+ const source = t.declaredAs === 'auto'
178
+ ? `the first input feeding it ("${t.feeds[0].input}" on root "${t.feeds[0].root}") is ${t.type}`
179
+ : `it is declared by .${t.declaredAs}() as ${t.type}`;
180
+ problems.push({
181
+ level: 'error', code: 'SHARED_INPUT_TYPE_MISMATCH', name: t.name,
182
+ message: `Shared variable "${t.name}": ${source}, but `
183
+ + wrong.map((f) => `"${f.input}" on root "${f.root}" is ${f.type}`).join(', ')
184
+ + `. Every input copied into one variable must have that variable's type.`,
185
+ });
186
+ }
187
+ const perRoot = new Map();
188
+ for (const f of t.feeds)
189
+ (perRoot.get(f.root) ?? perRoot.set(f.root, []).get(f.root)).push(f.input);
190
+ for (const [root, inputs] of perRoot) {
191
+ if (inputs.length > 1) {
192
+ problems.push({
193
+ level: 'error', code: 'SHARED_INPUT_SAME_ROOT', name: t.name,
194
+ message: `Root "${root}" feeds shared variable "${t.name}" from ${inputs.length} inputs `
195
+ + `(${inputs.map((i) => `"${i}"`).join(', ')}). A root's copy writes the variable once, so only one of `
196
+ + `its inputs can feed it.`,
197
+ });
198
+ }
199
+ }
200
+ if (decl.roots.length > 1 && !t.hasDefault) {
201
+ const missing = decl.roots.filter((r) => !perRoot.has(r.id)).map((r) => r.id);
202
+ if (missing.length) {
203
+ problems.push({
204
+ level: 'warning', code: 'SHARED_INPUT_ROOT_MISSING', name: t.name,
205
+ message: `Shared variable "${t.name}" is fed by ${[...perRoot.keys()].map((r) => `"${r}"`).join(', ')} but not by `
206
+ + `${missing.map((r) => `"${r}"`).join(', ')}. A run started there reads it as null.`,
207
+ suggestion: `Feed it from that root too, or declare it with a default: .var('${t.name}', types.${t.type}, <default>).`,
208
+ });
209
+ }
210
+ }
211
+ }
212
+ const targets = [...byName.values()];
213
+ // A target that names an input is already refused above; leaving it out here
214
+ // keeps the global plan from reporting the same name a second time.
215
+ const autoVars = targets.filter((t) => t.declaredAs === 'auto' && !inputNames.has(t.name)).map((t) => ({ name: t.name, type: t.type }));
216
+ return { targets, autoVars, problems };
217
+ }
218
+ /**
219
+ * A flow's roots for {@link planSharedInputs}: the default root first (under
220
+ * its EMITTED trigger id, `.triggerId()` applied), then each `.entryPoint()`.
221
+ */
222
+ export function sharedInputRoots(built) {
223
+ return [
224
+ { id: built.triggerId ?? 'start', inputs: built.inputs },
225
+ ...(built.entryPoints ?? []).map((ep) => ({ id: ep.id, inputs: ep.inputs })),
226
+ ];
227
+ }
package/dist/serialize.js CHANGED
@@ -45,7 +45,7 @@ import { declaredScriptShape, inferScriptShape } from './core/script-return.js';
45
45
  import { migrateToSchemaVersion } from './migrate.js';
46
46
  import { FLOW_SCHEMA_VERSIONS } from './format-profile.js';
47
47
  import { resolveSchemaVersion } from './schema-version.js';
48
- import { planFlowGlobals } from './flow-variables.js';
48
+ import { planFlowGlobals, planSharedInputs, sharedInputRoots } from './flow-variables.js';
49
49
  import { prepareCommand } from './core/cli-spelling.js';
50
50
  import { QUEUE_ITEM_RECORD_SCHEMA } from './core/queue-record.js';
51
51
  const HERE = dirname(fileURLToPath(import.meta.url));
@@ -3959,16 +3959,38 @@ function misplacedLookupMessage(token) {
3959
3959
  * (`compile --no-check`, `emitFlow`, a direct `serialize()`) cannot write a
3960
3960
  * file `flow validate` rejects for a duplicate variable id.
3961
3961
  */
3962
- function emittableGlobals(built, where) {
3962
+ function emittableGlobals(built, where, kind = 'root') {
3963
3963
  // Entry-point inputs are rows of the same globals[], so they share the
3964
3964
  // namespace; `check` reports those as ENTRY_POINT_INPUT_COLLISION.
3965
3965
  const inputs = [...built.inputs, ...(built.entryPoints ?? []).flatMap((ep) => ep.inputs)];
3966
- const plan = planFlowGlobals({ inputs, outputs: built.outputs, vars: built.vars });
3966
+ // A variable an input feeds with `{ shared }` (#924) is one more `.var()`
3967
+ // unless the author declared it, so it goes through the same collision plan.
3968
+ const shared = sharedInputsOf(built, where, kind);
3969
+ const plan = planFlowGlobals({ inputs, outputs: built.outputs, vars: [...built.vars, ...shared.autoVars] });
3967
3970
  if (plan.problems.length) {
3968
3971
  throw new Error(`${where}: ${plan.problems.map((p) => `[${p.code}] ${p.message}`).join(' ')}`);
3969
3972
  }
3970
3973
  return plan;
3971
3974
  }
3975
+ /**
3976
+ * The flow's `{ shared }` input plan (#924), refused here as `check` refuses it
3977
+ * — so `compile --no-check` cannot write what `check` would have stopped.
3978
+ * Warnings (a root that feeds nothing) are `check`'s alone.
3979
+ */
3980
+ function sharedInputsOf(built, where, kind) {
3981
+ if (kind === 'subflow') {
3982
+ const marked = built.inputs.filter((i) => i.shared !== undefined);
3983
+ if (marked.length) {
3984
+ throw new Error(`${where}: [SHARED_INPUT_IN_SUBFLOW] input "${marked[0].name}" has { shared: "${marked[0].shared}" }, but a subflow `
3985
+ + `has one root — its caller — so there is nothing to merge. Read the input with input('${marked[0].name}').`);
3986
+ }
3987
+ }
3988
+ const plan = planSharedInputs({ roots: sharedInputRoots(built), outputs: built.outputs, vars: built.vars });
3989
+ const errors = plan.problems.filter((p) => p.level === 'error');
3990
+ if (errors.length)
3991
+ throw new Error(`${where}: ${errors.map((p) => `[${p.code}] ${p.message}`).join(' ')}`);
3992
+ return plan;
3993
+ }
3972
3994
  function newScope(outputs, vars = []) {
3973
3995
  return {
3974
3996
  nodes: [],
@@ -4417,7 +4439,7 @@ export function serialize(built, opts = {}) {
4417
4439
  * a null. It is emitted here, always, so it cannot be forgotten.
4418
4440
  */
4419
4441
  function emitSubflowBody(child, nodeId) {
4420
- const childGlobals = emittableGlobals(child, `subflow "${nodeId}"`);
4442
+ const childGlobals = emittableGlobals(child, `subflow "${nodeId}"`, 'subflow');
4421
4443
  const scope = newScope(childGlobals.outputs, childGlobals.vars);
4422
4444
  // Claim this copy's node ids UP FRONT, so the rename map is complete before
4423
4445
  // the first expression renders. A child's step names are free the first
@@ -6544,6 +6566,30 @@ export function serialize(built, opts = {}) {
6544
6566
  }
6545
6567
  }
6546
6568
  }
6569
+ // ── Shared root inputs (#924) ────────────────────────────────────────────
6570
+ // An input marked `{ shared: '<var>' }` is copied into that variable on its
6571
+ // root's TRIGGER node: a `variableUpdates[<triggerId>]` row, the same
6572
+ // construct `{ updates }` writes for a step. On the trigger, the copy is done
6573
+ // before the root's own prefix steps run. The platform engine applies a start
6574
+ // event's output rows in order, so the row sees the trigger's input mapping
6575
+ // (measured live, #924). The default root's row is written against `start`
6576
+ // and renamed with every other `input()` read, so `.triggerId()` follows.
6577
+ {
6578
+ const roots = [
6579
+ { node: built.triggerId ?? 'start', read: 'start', inputs: built.inputs },
6580
+ ...entryPoints.map((ep) => ({ node: ep.id, read: ep.id, inputs: ep.inputs })),
6581
+ ];
6582
+ for (const r of roots) {
6583
+ const copies = {};
6584
+ for (const inp of r.inputs) {
6585
+ if (inp.shared !== undefined)
6586
+ copies[inp.shared] = new Expr(`$vars.${r.read}.output.${inp.name}`);
6587
+ }
6588
+ if (Object.keys(copies).length === 0)
6589
+ continue;
6590
+ root.variableUpdates[r.node] = [...(root.variableUpdates[r.node] ?? []), ...renderUpdates(copies, root.rename)];
6591
+ }
6592
+ }
6547
6593
  const globals = [];
6548
6594
  const triggerNodeId = built.triggerId ?? 'start';
6549
6595
  for (const inp of built.inputs)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uipath/maestro-builder-sdk",
3
- "version": "6.15.6",
3
+ "version": "6.16.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",
@@ -92,5 +92,5 @@
92
92
  "@types/node": "^22.7.0",
93
93
  "esbuild": "^0.28.1"
94
94
  },
95
- "gitref": "d8d72580682f02833d28d720ee382ffe9d346f6e"
95
+ "gitref": "ac943f282d466660e0366f2703dffdea618a7711"
96
96
  }