@uipath/maestro-builder-sdk 6.20.5 → 6.20.6

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,148 @@
1
+ /**
2
+ * The parts of a JavaScript expression or script body that are code.
3
+ *
4
+ * Short version: `$vars.summary` inside a quoted string is text, not a read.
5
+ * A rewrite that renames `$vars.<name>` (a step's node id, an output's backing
6
+ * Variable, #1092) must leave string literals, comments and regex literals
7
+ * alone, or it changes what the flow does:
8
+ *
9
+ * ```js
10
+ * return "$vars.summary" + $vars.summary;
11
+ * // ^^^^^^^^^^^^^^^ text: kept ^^^^^^^^^^^^^ code: renamed
12
+ * ```
13
+ *
14
+ * A template literal's text is kept too, and its `${…}` holes are code.
15
+ *
16
+ * This is a scanner, not a parser. It tells a regex literal from a division by
17
+ * the character before the `/` (the usual heuristic), so `return /"/` is read
18
+ * as a division. Nothing in a flow's expressions depends on that case.
19
+ */
20
+ /** Characters after which a `/` starts a regex literal, not a division. */
21
+ const REGEX_AFTER = new Set('(,=:[!&|?{};+-*%<>~^'.split(''));
22
+ /**
23
+ * `text` with `fn` applied to each run of code, and every string, template
24
+ * text, comment and regex literal copied as written.
25
+ */
26
+ export function mapJsCode(text, fn) {
27
+ let out = '';
28
+ const n = text.length;
29
+ const flushCode = (from, to) => { if (to > from)
30
+ out += fn(text.slice(from, to)); };
31
+ /** Index just past the quoted string that opens at `i`. */
32
+ const endQuoted = (i, q) => {
33
+ let j = i + 1;
34
+ while (j < n && text[j] !== q && text[j] !== '\n')
35
+ j += text[j] === '\\' ? 2 : 1;
36
+ return Math.min(j + 1, n);
37
+ };
38
+ /** Index just past the regex literal that opens at `i`. */
39
+ const endRegex = (i) => {
40
+ let j = i + 1;
41
+ let inClass = false;
42
+ while (j < n && text[j] !== '\n') {
43
+ const c = text[j];
44
+ if (c === '\\') {
45
+ j += 2;
46
+ continue;
47
+ }
48
+ if (c === '[')
49
+ inClass = true;
50
+ else if (c === ']')
51
+ inClass = false;
52
+ else if (c === '/' && !inClass) {
53
+ j++;
54
+ break;
55
+ }
56
+ j++;
57
+ }
58
+ while (j < n && /[A-Za-z]/.test(text[j]))
59
+ j++;
60
+ return Math.min(j, n);
61
+ };
62
+ /**
63
+ * Scan code from `i`. Inside a template hole, stop at its closing `}` and
64
+ * return that index; otherwise run to the end.
65
+ */
66
+ const scanCode = (i, inHole) => {
67
+ let start = i;
68
+ let depth = 0;
69
+ let prev = '';
70
+ while (i < n) {
71
+ const c = text[i];
72
+ if (c === '"' || c === "'") {
73
+ flushCode(start, i);
74
+ const j = endQuoted(i, c);
75
+ out += text.slice(i, j);
76
+ i = start = j;
77
+ prev = c;
78
+ continue;
79
+ }
80
+ if (c === '`') {
81
+ flushCode(start, i);
82
+ i = start = scanTemplate(i);
83
+ prev = c;
84
+ continue;
85
+ }
86
+ if (c === '/' && (text[i + 1] === '/' || text[i + 1] === '*')) {
87
+ flushCode(start, i);
88
+ const close = text[i + 1] === '/' ? text.indexOf('\n', i) : text.indexOf('*/', i + 2);
89
+ const j = close < 0 ? n : text[i + 1] === '/' ? close : close + 2;
90
+ out += text.slice(i, j);
91
+ i = start = j;
92
+ continue;
93
+ }
94
+ if (c === '/' && (prev === '' || REGEX_AFTER.has(prev))) {
95
+ flushCode(start, i);
96
+ const j = endRegex(i);
97
+ out += text.slice(i, j);
98
+ i = start = j;
99
+ prev = '/';
100
+ continue;
101
+ }
102
+ if (inHole) {
103
+ if (c === '{')
104
+ depth++;
105
+ else if (c === '}') {
106
+ if (depth === 0) {
107
+ flushCode(start, i);
108
+ return i;
109
+ }
110
+ depth--;
111
+ }
112
+ }
113
+ if (!/\s/.test(c))
114
+ prev = c;
115
+ i++;
116
+ }
117
+ flushCode(start, i);
118
+ return i;
119
+ };
120
+ /** Copy the template literal that opens at `i`, mapping its holes; return the index past it. */
121
+ const scanTemplate = (i) => {
122
+ let start = i;
123
+ let j = i + 1;
124
+ while (j < n) {
125
+ const c = text[j];
126
+ if (c === '\\') {
127
+ j += 2;
128
+ continue;
129
+ }
130
+ if (c === '`') {
131
+ out += text.slice(start, j + 1);
132
+ return j + 1;
133
+ }
134
+ if (c === '$' && text[j + 1] === '{') {
135
+ out += text.slice(start, j + 2);
136
+ j = scanCode(j + 2, true);
137
+ start = j; // the hole's `}` opens the next verbatim run
138
+ j++;
139
+ continue;
140
+ }
141
+ j++;
142
+ }
143
+ out += text.slice(start, n);
144
+ return n;
145
+ };
146
+ scanCode(0, false);
147
+ return out;
148
+ }
package/dist/decompile.js CHANGED
@@ -40,6 +40,8 @@ import { contextIndexSlug, parseContextIndexType } from './core/context-index.js
40
40
  import { wireExprBody } from './core/wire-expr.js';
41
41
  import { parseIxpProjectType } from './core/actions.js';
42
42
  import { readFlowGlobals } from './flow-variables.js';
43
+ import { outputBackingGlobal, outputBackingName } from './flow-output-backing.js';
44
+ import { mapJsCode } from './core/js-code-spans.js';
43
45
  import { missingBindingLabels } from './core/binding-messages.js';
44
46
  import { prepareCommand } from './core/cli-spelling.js';
45
47
  import { OUTCOME_HANDLE_NODE_TYPES, outcomeSlug } from './core/hitl-routing.js';
@@ -2342,6 +2344,7 @@ function armCb(param, segs) {
2342
2344
  */
2343
2345
  function returnSeg(node, ctx) {
2344
2346
  const updates = ctx.flow.variables?.variableUpdates?.[node.id] ?? [];
2347
+ const outputIds = new Set(readFlowGlobals(ctx.flow.variables).filter((g) => g.direction === 'out').map((g) => g.id));
2345
2348
  const opts = returnOptsArg(node);
2346
2349
  const entries = updates.map((u) => `${key(u.variableId)}: ${emitVal(u.expression, ctx.imp, ctx.inputNames)}`);
2347
2350
  const written = new Set(updates.map((u) => u.variableId));
@@ -2350,7 +2353,7 @@ function returnSeg(node, ctx) {
2350
2353
  continue;
2351
2354
  const source = out?.source;
2352
2355
  const raw = unwrapSource(source);
2353
- if (raw === undefined || isIdentityEndSource(raw, name))
2356
+ if (raw === undefined || isIdentityEndSource(raw, name, outputIds.has(name)))
2354
2357
  continue;
2355
2358
  entries.push(`${key(name)}: ${emitVal(source, ctx.imp, ctx.inputNames)}`);
2356
2359
  }
@@ -2358,10 +2361,15 @@ function returnSeg(node, ctx) {
2358
2361
  return `.return({}${opts})`;
2359
2362
  return `.return({ ${entries.join(', ')} }${opts})`;
2360
2363
  }
2361
- /** The fill-in mapping `endOutputs` writes for an output no `.return` named: `=js:$vars.<name>`. */
2362
- function isIdentityEndSource(raw, name) {
2363
- const body = raw.startsWith('=js:') ? raw.slice(4) : raw;
2364
- return body.trim() === `$vars.${name}`;
2364
+ /**
2365
+ * The fill-in mapping `endOutputs` writes for a variable no `.return` named:
2366
+ * `=js:$vars.<name>`, read through the backing Variable `foldOutputBacking`
2367
+ * has already folded back. An output with no backing Variable fills in as
2368
+ * `null` (#1092), since nothing but `.return()` sets it.
2369
+ */
2370
+ function isIdentityEndSource(raw, name, isOutput = false) {
2371
+ const body = (raw.startsWith('=js:') ? raw.slice(4) : raw).trim();
2372
+ return body === `$vars.${name}` || (isOutput && body === 'null');
2365
2373
  }
2366
2374
  /**
2367
2375
  * The options argument for a `.return(...)` — `version`, plus the End's `name`
@@ -3131,7 +3139,101 @@ function slugId(flow) {
3131
3139
  const slug = base.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
3132
3140
  return slug || 'flow';
3133
3141
  }
3142
+ // ─── Output backing Variables (#1092) ─────────────────────────────────────────
3143
+ /** Sorted-key JSON, so two globals compare equal whatever their key order. */
3144
+ function canonicalJson(v) {
3145
+ if (Array.isArray(v))
3146
+ return `[${v.map(canonicalJson).join(',')}]`;
3147
+ if (v && typeof v === 'object') {
3148
+ const o = v;
3149
+ return `{${Object.keys(o).sort().map((k) => `${JSON.stringify(k)}:${canonicalJson(o[k])}`).join(',')}}`;
3150
+ }
3151
+ return JSON.stringify(v);
3152
+ }
3153
+ /**
3154
+ * The wire fields whose plain-string value is JavaScript (no `=js:` prefix):
3155
+ * a script body, a decision or switch `expression`, a loop's `condition` and
3156
+ * `completionCondition`, an HTTP branch's `conditionExpression`, a transform's
3157
+ * `collection`. `serialize` renames `$vars` reads in exactly these and in
3158
+ * `=js:` values.
3159
+ */
3160
+ const RAW_JS_FIELDS = new Set(['script', 'expression', 'condition', 'completionCondition', 'conditionExpression', 'collection']);
3161
+ /**
3162
+ * Every `$vars.<from>` read (the whole root, not a prefix of a longer one) in
3163
+ * `v`'s code, read as `$vars.<to>`. Code is a `=js:` wire value, a plain
3164
+ * `$vars.` path, or a `RAW_JS_FIELDS` value; in each, only the code spans are
3165
+ * rewritten. A label, a literal input, or a quoted `"$vars.<from>"` inside a
3166
+ * script stays as written, the way `serialize` left it (#1092 review).
3167
+ */
3168
+ function renameVarRoot(v, from, to) {
3169
+ const re = new RegExp(`\\$vars\\.${from.replace(/[$]/g, '\\$&')}(?![A-Za-z0-9_$])`, 'g');
3170
+ const inCode = (js) => mapJsCode(js, (code) => code.replace(re, `$vars.${to}`));
3171
+ const walk = (x, key) => {
3172
+ if (typeof x === 'string') {
3173
+ if (x.startsWith('=js:'))
3174
+ return '=js:' + inCode(x.slice(4));
3175
+ if (x.startsWith('$vars.') || (key !== undefined && RAW_JS_FIELDS.has(key)))
3176
+ return inCode(x);
3177
+ return x;
3178
+ }
3179
+ if (Array.isArray(x))
3180
+ return x.map((y) => walk(y, key));
3181
+ if (x && typeof x === 'object')
3182
+ return Object.fromEntries(Object.entries(x).map(([k, y]) => [k, walk(y, k)]));
3183
+ return x;
3184
+ };
3185
+ return walk(v);
3186
+ }
3187
+ /**
3188
+ * Undo the backing-Variable lowering `serialize` applies to an output the flow
3189
+ * sets from inside (`flow-output-backing.ts`): fold the `inout` `<output>Value`
3190
+ * global back into its output, so the source says `{ updates: { summary } }`
3191
+ * and `v('summary')` again and recompiling emits the same bytes.
3192
+ *
3193
+ * A pair is folded only when the Variable is exactly what `serialize` would
3194
+ * emit for that output (`outputBackingGlobal`): same id, type, default,
3195
+ * subType and schema, no description, no trigger. Any other `inout` global,
3196
+ * one a person named `summaryValue` with a description say, stays a `.var()`.
3197
+ * Applied to each subflow body too, against that body's own globals.
3198
+ */
3199
+ function foldOutputBacking(flow) {
3200
+ const subflows = flow.subflows
3201
+ ? Object.fromEntries(Object.entries(flow.subflows).map(([k, c]) => [k, foldOutputBacking(c)]))
3202
+ : undefined;
3203
+ let out = subflows ? { ...flow, subflows } : flow;
3204
+ const globals = Array.isArray(flow.variables?.globals) ? flow.variables.globals : [];
3205
+ for (const o of globals.filter((g) => g.direction === 'out')) {
3206
+ const id = outputBackingName(o.id);
3207
+ const b = globals.find((g) => g.id === id && g.direction === 'inout');
3208
+ if (!b)
3209
+ continue;
3210
+ const expected = outputBackingGlobal({ name: o.id, type: o.type, default: o.defaultValue, subType: o.subType, schema: o.schema }, id);
3211
+ if (canonicalJson(b) !== canonicalJson(expected))
3212
+ continue;
3213
+ const vars = out.variables;
3214
+ const updates = Object.fromEntries(Object.entries(vars.variableUpdates ?? {}).map(([node, rows]) => [
3215
+ node, rows.map((r) => (r.variableId === id ? { ...r, variableId: o.id } : r)),
3216
+ ]));
3217
+ const nodes = (out.nodes ?? []).map((n) => {
3218
+ if (n.type !== 'core.control.end' || !n.outputs || !(id in n.outputs))
3219
+ return n;
3220
+ const { [id]: _dropped, ...rest } = n.outputs;
3221
+ return { ...n, outputs: rest };
3222
+ });
3223
+ out = {
3224
+ ...out,
3225
+ nodes: renameVarRoot(nodes, id, o.id),
3226
+ variables: {
3227
+ ...vars,
3228
+ globals: (vars.globals ?? []).filter((g) => g !== b && g.id !== id),
3229
+ variableUpdates: renameVarRoot(updates, id, o.id),
3230
+ },
3231
+ };
3232
+ }
3233
+ return out;
3234
+ }
3134
3235
  export function decompile(flow, options = {}) {
3236
+ flow = foldOutputBacking(flow);
3135
3237
  const importSpecifier = options.importSpecifier ?? './flow-sdk.js';
3136
3238
  runStrict = options.bestEffort === false;
3137
3239
  runWarn = options.warn;
@@ -0,0 +1,53 @@
1
+ import type { BuiltFlow, Step, VarDecl } from './flow-sdk.js';
2
+ /** The suffix of a backing Variable's id: `summary` → `summaryValue`. */
3
+ export declare const OUTPUT_BACKING_SUFFIX = "Value";
4
+ /** The id of the Variable that backs output `output`. */
5
+ export declare function outputBackingName(output: string): string;
6
+ /** One reason a backing Variable cannot be emitted. */
7
+ export interface OutputBackingProblem {
8
+ code: 'OUTPUT_BACKING_COLLISION';
9
+ name: string;
10
+ message: string;
11
+ suggestion?: string;
12
+ }
13
+ /** Which outputs get a backing Variable, in output declaration order. */
14
+ export interface OutputBackingPlan {
15
+ /** Output name → backing Variable id. */
16
+ backing: Map<string, string>;
17
+ /**
18
+ * The same pairs, minus outputs that share their name with a step. Those
19
+ * `$vars.<name>` reads are the step's output (`out('slug')` with an output
20
+ * also called `slug`), so only these are redirected to the Variable.
21
+ */
22
+ reads: Map<string, string>;
23
+ problems: OutputBackingProblem[];
24
+ }
25
+ /**
26
+ * The variable names one scope's steps write with `{ updates }`, the `$vars`
27
+ * roots its expressions and script bodies read, and its step names. A
28
+ * subflow's child flow is skipped: its globals are its own.
29
+ */
30
+ export declare function scopeVariableUse(steps: readonly Step[]): {
31
+ written: Set<string>;
32
+ read: Set<string>;
33
+ stepNames: Set<string>;
34
+ };
35
+ /**
36
+ * Plan the backing Variables for one scope (the flow, or one subflow body).
37
+ *
38
+ * @param built - The scope's flow.
39
+ * @param outputs - Its outputs after the `.output()` + `.var()` merge (`planFlowGlobals`).
40
+ * @param vars - The `.var()`s it emits, the SDK-declared `{ shared }` ones included.
41
+ * @param sharedOutputs - The outputs a `{ shared }` input feeds.
42
+ */
43
+ export declare function planOutputBacking(built: Pick<BuiltFlow, 'inputs' | 'steps' | 'entryPoints' | 'triggerId'>, outputs: readonly VarDecl[], vars: readonly {
44
+ name: string;
45
+ }[], sharedOutputs?: ReadonlySet<string>): OutputBackingPlan;
46
+ /** The `inout` global that backs output `o`: its type and value metadata, no description. */
47
+ export declare function outputBackingGlobal(o: {
48
+ name: string;
49
+ type: string;
50
+ default?: unknown;
51
+ subType?: string;
52
+ schema?: Record<string, unknown>;
53
+ }, id: string): Record<string, unknown>;
@@ -0,0 +1,200 @@
1
+ /**
2
+ * Backing variables for flow outputs that a step sets (flow-builder-sdk#1092).
3
+ *
4
+ * Short version: in Studio Web an Output (`direction: 'out'`) is set ONLY by an
5
+ * End node's output mapping. "Update Variable" offers only Variables
6
+ * (`direction: 'inout'`), and expressions can't read an Output. So when an
7
+ * author's source sets an output from inside the flow, the SDK emits the shape a
8
+ * Studio Web user builds by hand: a Variable that holds the running value, and
9
+ * an End mapping that copies it into the Output.
10
+ *
11
+ * ```ts
12
+ * flow('probe').output({ summary: types.string })
13
+ * .step('work', script({ … }), { updates: { summary: out('work') } })
14
+ * ```
15
+ *
16
+ * emits
17
+ *
18
+ * ```
19
+ * globals: summary (out), summaryValue (inout)
20
+ * variableUpdates: work → summaryValue = $vars.work.output
21
+ * end.outputs: summary ← $vars.summaryValue, summaryValue ← $vars.summaryValue
22
+ * ```
23
+ *
24
+ * The oracle (flow-workbench `develop`):
25
+ * - `VariablesContext.tsx` `getWritableVariables` / `useWritableVariables`:
26
+ * the Update Variable editor lists `direction === 'inout'` only, and creates
27
+ * new rows as `inout` (`VariableUpdateEditor.tsx`).
28
+ * - `VariablesContext.tsx` `getReadableVariables` / `filterReadableVariables`:
29
+ * expression editors drop `direction === 'out'`; the validator's `$vars`
30
+ * declarations (`generate-variable-declarations.ts`) hold `in` and `inout`.
31
+ * Reading `$vars.<output>` is `EXPRESSION_DIAGNOSTIC: Property '<output>'
32
+ * does not exist`.
33
+ * - `end-node-sets-output.flow` / `end-node-different-branches.flow`: an End
34
+ * sets an Output from an expression, `outputs.<id>.source`, and writes no
35
+ * variable update.
36
+ * - The product has no naming rule for the Variable (the dialog checks only
37
+ * identifier syntax and uniqueness against `useReservedIds`). The SDK names
38
+ * it `<output>Value`, the same suffix its `{ shared }` suggestion uses, and
39
+ * refuses a source that already uses that name for something else.
40
+ *
41
+ * An output gets a backing Variable when the flow's own source sets or reads it
42
+ * outside a `.return()` value:
43
+ * - a step's `{ updates }` (any step kind, `.return(…, { updates })` included);
44
+ * - a `{ shared }` input that feeds it;
45
+ * - a declared `default` (including the one a same-named `.var()` gives it,
46
+ * flow-builder-sdk#899), because the default is the value an End returns when
47
+ * no step set it;
48
+ * - an expression or script body that reads `$vars.<output>` in code (unless a
49
+ * step has the output's name: then `$vars.<name>` is that step's output). A
50
+ * label, a literal input, or a quoted `"$vars.<output>"` inside a script is
51
+ * text and reads nothing.
52
+ *
53
+ * An output that only `.return({ … })` sets needs none: each End maps it from
54
+ * the returned expression, and an End that does not name it maps `null`, which
55
+ * is what the old `$vars.<output>` read evaluated to on that path.
56
+ */
57
+ import { Expr } from './core/expr.js';
58
+ import { mapJsCode } from './core/js-code-spans.js';
59
+ /** The suffix of a backing Variable's id: `summary` → `summaryValue`. */
60
+ export const OUTPUT_BACKING_SUFFIX = 'Value';
61
+ /** The id of the Variable that backs output `output`. */
62
+ export function outputBackingName(output) {
63
+ return `${output}${OUTPUT_BACKING_SUFFIX}`;
64
+ }
65
+ /** A `$vars.<root>` read: the root identifier, ended by anything that cannot continue it. */
66
+ const VARS_ROOT_RE = /\$vars\.([A-Za-z_$][A-Za-z0-9_$]*)/g;
67
+ /** The `$vars` roots that `js`'s code reads; a quoted `"$vars.x"` is text, not a read. */
68
+ function readsOf(js, into) {
69
+ mapJsCode(js, (code) => {
70
+ for (const m of code.matchAll(VARS_ROOT_RE))
71
+ into.add(m[1]);
72
+ return code;
73
+ });
74
+ }
75
+ /** A wire expression (`=js:…`), as a raw node carries one. */
76
+ const WIRE_JS_PREFIX = '=js:';
77
+ /** A child flow (a subflow's body) is its own scope with its own globals. */
78
+ function isBuiltFlow(v) {
79
+ const o = v;
80
+ return Array.isArray(o.steps) && Array.isArray(o.outputs) && Array.isArray(o.inputs) && Array.isArray(o.vars);
81
+ }
82
+ /**
83
+ * The variable names one scope's steps write with `{ updates }`, the `$vars`
84
+ * roots its expressions and script bodies read, and its step names. A
85
+ * subflow's child flow is skipped: its globals are its own.
86
+ */
87
+ export function scopeVariableUse(steps) {
88
+ const written = new Set();
89
+ const read = new Set();
90
+ const stepNames = new Set();
91
+ const seen = new WeakSet();
92
+ const visit = (val) => {
93
+ // A plain string is a label, a prompt or a literal input, never a read;
94
+ // only a raw node's `=js:` wire expression is code (#1092 review).
95
+ if (typeof val === 'string') {
96
+ if (val.startsWith(WIRE_JS_PREFIX))
97
+ readsOf(val.slice(WIRE_JS_PREFIX.length), read);
98
+ return;
99
+ }
100
+ if (val === null || typeof val !== 'object')
101
+ return;
102
+ if (seen.has(val))
103
+ return;
104
+ seen.add(val);
105
+ if (val instanceof Expr) {
106
+ if (!val.literal)
107
+ readsOf(val.js, read);
108
+ return;
109
+ }
110
+ if (isBuiltFlow(val))
111
+ return;
112
+ if (Array.isArray(val)) {
113
+ for (const v of val)
114
+ visit(v);
115
+ return;
116
+ }
117
+ const rec = val;
118
+ // A script body is a plain string that is code (`script({ code })`).
119
+ if (rec.kind === 'script' && typeof rec.inputs?.code === 'string') {
120
+ readsOf(rec.inputs.code, read);
121
+ }
122
+ if (typeof rec.kind === 'string') {
123
+ // A step. Its name is a node id (a `.return()`'s synthetic one is not).
124
+ if (typeof rec.name === 'string' && rec.kind !== 'return')
125
+ stepNames.add(rec.name);
126
+ const updates = rec.options?.updates;
127
+ if (updates && typeof updates === 'object' && !(updates instanceof Expr)) {
128
+ for (const k of Object.keys(updates))
129
+ written.add(k);
130
+ }
131
+ }
132
+ for (const v of Object.values(rec))
133
+ visit(v);
134
+ };
135
+ for (const s of steps)
136
+ visit(s);
137
+ return { written, read, stepNames };
138
+ }
139
+ /**
140
+ * Plan the backing Variables for one scope (the flow, or one subflow body).
141
+ *
142
+ * @param built - The scope's flow.
143
+ * @param outputs - Its outputs after the `.output()` + `.var()` merge (`planFlowGlobals`).
144
+ * @param vars - The `.var()`s it emits, the SDK-declared `{ shared }` ones included.
145
+ * @param sharedOutputs - The outputs a `{ shared }` input feeds.
146
+ */
147
+ export function planOutputBacking(built, outputs, vars, sharedOutputs = new Set()) {
148
+ const steps = [...built.steps, ...(built.entryPoints ?? []).flatMap((ep) => ep.steps)];
149
+ const use = scopeVariableUse(steps);
150
+ const taken = new Map();
151
+ const claim = (name, what) => { if (!taken.has(name))
152
+ taken.set(name, what); };
153
+ for (const i of built.inputs)
154
+ claim(i.name, `input "${i.name}"`);
155
+ for (const ep of built.entryPoints ?? []) {
156
+ claim(ep.id, `entry point "${ep.id}"`);
157
+ for (const i of ep.inputs)
158
+ claim(i.name, `input "${i.name}"`);
159
+ }
160
+ for (const o of outputs)
161
+ claim(o.name, `output "${o.name}"`);
162
+ for (const v of vars)
163
+ claim(v.name, `variable "${v.name}"`);
164
+ for (const s of use.stepNames)
165
+ claim(s, `step "${s}"`);
166
+ claim(built.triggerId ?? 'start', `trigger "${built.triggerId ?? 'start'}"`);
167
+ const backing = new Map();
168
+ const problems = [];
169
+ for (const o of outputs) {
170
+ const readsIt = use.read.has(o.name) && !use.stepNames.has(o.name);
171
+ const needs = use.written.has(o.name) || readsIt || sharedOutputs.has(o.name) || o.default !== undefined;
172
+ if (!needs)
173
+ continue;
174
+ const id = outputBackingName(o.name);
175
+ const holder = taken.get(id);
176
+ if (holder) {
177
+ problems.push({
178
+ code: 'OUTPUT_BACKING_COLLISION', name: o.name,
179
+ message: `Output "${o.name}" is set inside the flow, so compile backs it with a Variable named "${id}": `
180
+ + `Studio Web sets an Output only on an End node, and Update Variable writes only Variables. `
181
+ + `But "${id}" is already the ${holder}, and the .flow keeps one id per variable or step.`,
182
+ suggestion: `Rename the ${holder}.`,
183
+ });
184
+ continue;
185
+ }
186
+ taken.set(id, `backing variable of output "${o.name}"`);
187
+ backing.set(o.name, id);
188
+ }
189
+ const reads = new Map([...backing].filter(([o]) => !use.stepNames.has(o)));
190
+ return { backing, reads, problems };
191
+ }
192
+ /** The `inout` global that backs output `o`: its type and value metadata, no description. */
193
+ export function outputBackingGlobal(o, id) {
194
+ return {
195
+ id, name: id, direction: 'inout', type: o.type,
196
+ ...(o.default !== undefined ? { defaultValue: o.default } : {}),
197
+ ...(o.subType !== undefined ? { subType: o.subType } : {}),
198
+ ...(o.schema !== undefined ? { schema: o.schema } : {}),
199
+ };
200
+ }
@@ -379,6 +379,11 @@ export interface NodeOptions {
379
379
  * Flow JSON stores it. Keys name declared `.var()` variables (or flow
380
380
  * outputs); values are `Expr`s or raw literals.
381
381
  *
382
+ * A key that names a flow output writes the output's backing Variable,
383
+ * `<output>Value` (`direction: 'inout'`), because Studio Web's Update
384
+ * Variable cannot target an Output; every End then maps the output from that
385
+ * Variable.
386
+ *
382
387
  * @remarks
383
388
  * Not accepted on `.branch()`, `.switch()`, `.parallel()`, or `.trigger()` —
384
389
  * the platform strips updates from decision/switch/merge nodes on save, and a
@@ -1439,6 +1444,16 @@ declare class FlowBuilder extends StepList {
1439
1444
  * second variable (its default becomes the output's); a different type is a
1440
1445
  * `check` error, `OUTPUT_VAR_MISMATCH`.
1441
1446
  *
1447
+ * In the `.flow`, Studio Web sets an Output only on an End node, and neither
1448
+ * Update Variable nor an expression can touch one. So an output that the flow
1449
+ * sets or reads from inside (`{ updates }`, a `{ shared }` input, a
1450
+ * `default`, or a `v('<name>')` read) is emitted with a backing Variable,
1451
+ * `<name>Value` (`direction: 'inout'`): updates and reads go to the Variable,
1452
+ * and every End maps the output from it. That id must be free; a step,
1453
+ * input or `.var()` already called `<name>Value` is a `check` error,
1454
+ * `OUTPUT_BACKING_COLLISION`. An output only `.return({ … })` sets needs no
1455
+ * Variable: an End that does not name it maps `null`.
1456
+ *
1442
1457
  * @example
1443
1458
  * **A loop accumulator**
1444
1459
  * ```ts
package/dist/flow-sdk.js CHANGED
@@ -1176,6 +1176,16 @@ class FlowBuilder extends StepList {
1176
1176
  * second variable (its default becomes the output's); a different type is a
1177
1177
  * `check` error, `OUTPUT_VAR_MISMATCH`.
1178
1178
  *
1179
+ * In the `.flow`, Studio Web sets an Output only on an End node, and neither
1180
+ * Update Variable nor an expression can touch one. So an output that the flow
1181
+ * sets or reads from inside (`{ updates }`, a `{ shared }` input, a
1182
+ * `default`, or a `v('<name>')` read) is emitted with a backing Variable,
1183
+ * `<name>Value` (`direction: 'inout'`): updates and reads go to the Variable,
1184
+ * and every End maps the output from it. That id must be free; a step,
1185
+ * input or `.var()` already called `<name>Value` is a `check` error,
1186
+ * `OUTPUT_BACKING_COLLISION`. An output only `.return({ … })` sets needs no
1187
+ * Variable: an End that does not name it maps `null`.
1188
+ *
1179
1189
  * @example
1180
1190
  * **A loop accumulator**
1181
1191
  * ```ts
@@ -54,10 +54,12 @@ export interface FlowGlobalsPlan<D extends DeclaredVariable = DeclaredVariable>
54
54
  * `@uipath/maestro-sdk` (the code that turns a `.flow` into what runs):
55
55
  * - only an `out` global is a flow OUTPUT at the root — `buildNodeOutputMappings`
56
56
  * drops an End node's mapping for any id that is not `direction: 'out'`;
57
- * - `inout` is published as an INPUT too — solution discovery puts it on the
58
- * entry point's input schema — so `inout` would widen the caller's contract;
59
- * - `{ updates }` on an `out` global is the same output mapping (`var: <id>`) as
60
- * on an `inout` one, so a loop body can read and rewrite it mid-run;
57
+ * - `inout` is not part of the caller's contract either way: `@uipath/flow-schema`
58
+ * packaging builds the entry-point input schema from `in` globals bound to the
59
+ * trigger, and the output schema from `out` globals only;
60
+ * - a loop body that reads and rewrites the value mid-run does so through the
61
+ * output's backing `inout` Variable (`flow-output-backing.ts`, #1092), because
62
+ * Studio Web neither updates nor reads an Output;
61
63
  * - the `default` on `<uipath:output>` is serialized (`collectVariablesFromNodes`)
62
64
  * and a subflow seeds its `out` globals from `defaultValue ?? null`
63
65
  * (`buildSeedTask`). The v1 CLI's `variable add --direction out
@@ -58,10 +58,12 @@ const sameJson = (a, b) => JSON.stringify(a) === JSON.stringify(b);
58
58
  * `@uipath/maestro-sdk` (the code that turns a `.flow` into what runs):
59
59
  * - only an `out` global is a flow OUTPUT at the root — `buildNodeOutputMappings`
60
60
  * drops an End node's mapping for any id that is not `direction: 'out'`;
61
- * - `inout` is published as an INPUT too — solution discovery puts it on the
62
- * entry point's input schema — so `inout` would widen the caller's contract;
63
- * - `{ updates }` on an `out` global is the same output mapping (`var: <id>`) as
64
- * on an `inout` one, so a loop body can read and rewrite it mid-run;
61
+ * - `inout` is not part of the caller's contract either way: `@uipath/flow-schema`
62
+ * packaging builds the entry-point input schema from `in` globals bound to the
63
+ * trigger, and the output schema from `out` globals only;
64
+ * - a loop body that reads and rewrites the value mid-run does so through the
65
+ * output's backing `inout` Variable (`flow-output-backing.ts`, #1092), because
66
+ * Studio Web neither updates nor reads an Output;
65
67
  * - the `default` on `<uipath:output>` is serialized (`collectVariablesFromNodes`)
66
68
  * and a subflow seeds its `out` globals from `defaultValue ?? null`
67
69
  * (`buildSeedTask`). The v1 CLI's `variable add --direction out