@uipath/maestro-builder-sdk 6.20.5 → 6.21.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.
Files changed (36) hide show
  1. package/dist/api-index.md +72 -72
  2. package/dist/api-members.md +143 -143
  3. package/dist/check.js +34 -160
  4. package/dist/core/actions.d.ts +0 -126
  5. package/dist/core/actions.js +0 -48
  6. package/dist/core/edit-distance.d.ts +8 -0
  7. package/dist/core/edit-distance.js +26 -0
  8. package/dist/core/js-code-spans.d.ts +24 -0
  9. package/dist/core/js-code-spans.js +148 -0
  10. package/dist/core/nodes/node-type.d.ts +17 -2
  11. package/dist/core/typed-raw-node.js +0 -2
  12. package/dist/decompile.js +107 -36
  13. package/dist/flow-node-registry.js +2 -1
  14. package/dist/flow-node-type.d.ts +6 -0
  15. package/dist/flow-output-backing.d.ts +53 -0
  16. package/dist/flow-output-backing.js +200 -0
  17. package/dist/flow-sdk.d.ts +16 -0
  18. package/dist/flow-sdk.js +11 -0
  19. package/dist/flow-variables.d.ts +6 -4
  20. package/dist/flow-variables.js +6 -4
  21. package/dist/nodes/queue/flow/check.d.ts +35 -0
  22. package/dist/nodes/queue/flow/check.js +126 -0
  23. package/dist/nodes/queue/flow/factories.d.ts +49 -0
  24. package/dist/nodes/queue/flow/factories.js +50 -0
  25. package/dist/nodes/queue/flow/family.d.ts +3 -0
  26. package/dist/nodes/queue/flow/family.js +8 -0
  27. package/dist/nodes/queue/flow/index.d.ts +3 -0
  28. package/dist/nodes/queue/flow/index.js +3 -0
  29. package/dist/nodes/queue/flow/node-type.d.ts +8 -0
  30. package/dist/nodes/queue/flow/node-type.js +265 -0
  31. package/dist/nodes/queue/flow/spec.d.ts +92 -0
  32. package/dist/nodes/queue/flow/spec.js +1 -0
  33. package/dist/serialize.js +70 -183
  34. package/package.json +2 -2
  35. /package/dist/{core/queue-record.d.ts → nodes/queue/flow/record.d.ts} +0 -0
  36. /package/dist/{core/queue-record.js → nodes/queue/flow/record.js} +0 -0
@@ -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
+ }
@@ -45,12 +45,26 @@ export interface OutputRead {
45
45
  /** The step whose expression contains the read. */
46
46
  reader: string;
47
47
  }
48
+ /**
49
+ * How the artifact words a read of a `fields` output: the code prefix its read
50
+ * diagnostics share (`<PREFIX>_READ_NOT_OUTPUT`, `_READ_WITHOUT_RETURNS`,
51
+ * `_READ_UNKNOWN_FIELD`, each in the family's catalog), how a message names the
52
+ * node (`'Queue item'`), and what to tell an author who declared no fields.
53
+ */
54
+ export interface FieldsRead {
55
+ readonly codePrefix: string;
56
+ readonly label: string;
57
+ readonly whenUndeclared: string;
58
+ }
48
59
  /**
49
60
  * What reading a node's output yields at run time.
50
61
  *
51
62
  * `none` keeps the diagnostic code of the rule it replaces
52
63
  * (`DATAFABRIC_DELETE_HAS_NO_OUTPUT`, `MOCK_HAS_NO_OUTPUT`, …) and builds the
53
- * message itself, so a port changes no diagnostic an author sees.
64
+ * message itself, so a port changes no diagnostic an author sees. `fields`
65
+ * states the fields a read may name — `undefined` when the author declared none,
66
+ * which makes any field read its own mistake — and `read` keeps the wording of
67
+ * the read rule it replaces, for the same reason. `open` admits any field.
54
68
  */
55
69
  export type OutputContract = {
56
70
  readonly kind: 'none';
@@ -58,8 +72,9 @@ export type OutputContract = {
58
72
  message(read: OutputRead): string;
59
73
  } | {
60
74
  readonly kind: 'fields';
61
- readonly fields: Readonly<Record<string, TypeDesc>>;
75
+ readonly fields: Readonly<Record<string, TypeDesc>> | undefined;
62
76
  readonly open?: boolean;
77
+ readonly read: FieldsRead;
63
78
  } | {
64
79
  readonly kind: 'opaque';
65
80
  };
@@ -23,8 +23,6 @@ import { ownEntry } from './own-entry.js';
23
23
  export const TYPED_FACTORIES = {
24
24
  'core.action.http': { factory: 'http({ managed: false })' },
25
25
  'core.action.http.v2': { factory: 'http({ managed: true })' },
26
- 'core.action.queue.create': { factory: 'queueItem()' },
27
- 'core.action.queue.create-and-wait': { factory: 'queueItem({ wait: true })' },
28
26
  'core.control.end': { factory: '.return()' },
29
27
  'core.logic.decision': { factory: '.branch()' },
30
28
  'core.logic.dowhile': { factory: '.doWhile(name, condition, body, { limit, breakEnabled })' },
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';
@@ -226,8 +228,6 @@ const T = {
226
228
  hitlQuickForm: 'uipath.human-in-the-loop.quick-form',
227
229
  hitlActionApp: 'uipath.human-in-the-loop.coded-action-app',
228
230
  hitlDocValidation: 'uipath.human-in-the-loop.document-validation',
229
- queueCreate: 'core.action.queue.create',
230
- queueCreateAndWait: 'core.action.queue.create-and-wait',
231
231
  summarize: 'uipath.pattern.deep-rag',
232
232
  batchTransform: 'uipath.pattern.batch-transform',
233
233
  sendMessage: 'uipath.conversational.send-message',
@@ -642,35 +642,6 @@ function emitStepSpec(node, imp, inputNames, o, graph) {
642
642
  case T.hitlActionApp:
643
643
  case T.hitlDocValidation:
644
644
  return emitHitl(node, imp, inputNames, graph);
645
- case T.queueCreate:
646
- case T.queueCreateAndWait: {
647
- // `itemData` is the node's name for the surface's `item` (the reference
648
- // documents the mapping); `queue` carries the three identity fields.
649
- const q = (inputs.queue ?? {});
650
- const rows = Object.entries((inputs.itemData ?? {}))
651
- .map(([k, v]) => [k, emitVal(v, imp, inputNames)]);
652
- const optStr = (key) => {
653
- const raw = unwrapSource(inputs[key]);
654
- // serialize writes '' for an omitted optional — that is not an authored
655
- // value, so it must not come back as one.
656
- if (raw === undefined || raw === '')
657
- return undefined;
658
- return raw.startsWith('=js:') ? emitBareExpr(raw.slice(4), imp, inputNames) : tsLiteral(raw);
659
- };
660
- const priority = unwrapPlain(inputs.priority);
661
- return `${imp.use('queueItem')}(${opts([
662
- ['queue', tsText(q.name)],
663
- ['folderPath', tsText(q.folderPath)],
664
- ['key', tsText(q.key)],
665
- ['item', rows.length ? objLit(rows) : '{}'],
666
- // `Normal` is the definition's default — only a departure was authored.
667
- ['priority', typeof priority === 'string' && priority !== 'Normal' ? tsLiteral(priority) : undefined],
668
- ['reference', optStr('reference')],
669
- ['deferDate', optStr('deferDate')],
670
- ['dueDate', optStr('dueDate')],
671
- ['wait', node.type === T.queueCreateAndWait ? 'true' : undefined],
672
- ])})`;
673
- }
674
645
  case T.summarize:
675
646
  case T.batchTransform: {
676
647
  const attachment = emitVal(inputs.attachment, imp, inputNames);
@@ -2342,6 +2313,7 @@ function armCb(param, segs) {
2342
2313
  */
2343
2314
  function returnSeg(node, ctx) {
2344
2315
  const updates = ctx.flow.variables?.variableUpdates?.[node.id] ?? [];
2316
+ const outputIds = new Set(readFlowGlobals(ctx.flow.variables).filter((g) => g.direction === 'out').map((g) => g.id));
2345
2317
  const opts = returnOptsArg(node);
2346
2318
  const entries = updates.map((u) => `${key(u.variableId)}: ${emitVal(u.expression, ctx.imp, ctx.inputNames)}`);
2347
2319
  const written = new Set(updates.map((u) => u.variableId));
@@ -2350,7 +2322,7 @@ function returnSeg(node, ctx) {
2350
2322
  continue;
2351
2323
  const source = out?.source;
2352
2324
  const raw = unwrapSource(source);
2353
- if (raw === undefined || isIdentityEndSource(raw, name))
2325
+ if (raw === undefined || isIdentityEndSource(raw, name, outputIds.has(name)))
2354
2326
  continue;
2355
2327
  entries.push(`${key(name)}: ${emitVal(source, ctx.imp, ctx.inputNames)}`);
2356
2328
  }
@@ -2358,10 +2330,15 @@ function returnSeg(node, ctx) {
2358
2330
  return `.return({}${opts})`;
2359
2331
  return `.return({ ${entries.join(', ')} }${opts})`;
2360
2332
  }
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}`;
2333
+ /**
2334
+ * The fill-in mapping `endOutputs` writes for a variable no `.return` named:
2335
+ * `=js:$vars.<name>`, read through the backing Variable `foldOutputBacking`
2336
+ * has already folded back. An output with no backing Variable fills in as
2337
+ * `null` (#1092), since nothing but `.return()` sets it.
2338
+ */
2339
+ function isIdentityEndSource(raw, name, isOutput = false) {
2340
+ const body = (raw.startsWith('=js:') ? raw.slice(4) : raw).trim();
2341
+ return body === `$vars.${name}` || (isOutput && body === 'null');
2365
2342
  }
2366
2343
  /**
2367
2344
  * The options argument for a `.return(...)` — `version`, plus the End's `name`
@@ -3131,7 +3108,101 @@ function slugId(flow) {
3131
3108
  const slug = base.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
3132
3109
  return slug || 'flow';
3133
3110
  }
3111
+ // ─── Output backing Variables (#1092) ─────────────────────────────────────────
3112
+ /** Sorted-key JSON, so two globals compare equal whatever their key order. */
3113
+ function canonicalJson(v) {
3114
+ if (Array.isArray(v))
3115
+ return `[${v.map(canonicalJson).join(',')}]`;
3116
+ if (v && typeof v === 'object') {
3117
+ const o = v;
3118
+ return `{${Object.keys(o).sort().map((k) => `${JSON.stringify(k)}:${canonicalJson(o[k])}`).join(',')}}`;
3119
+ }
3120
+ return JSON.stringify(v);
3121
+ }
3122
+ /**
3123
+ * The wire fields whose plain-string value is JavaScript (no `=js:` prefix):
3124
+ * a script body, a decision or switch `expression`, a loop's `condition` and
3125
+ * `completionCondition`, an HTTP branch's `conditionExpression`, a transform's
3126
+ * `collection`. `serialize` renames `$vars` reads in exactly these and in
3127
+ * `=js:` values.
3128
+ */
3129
+ const RAW_JS_FIELDS = new Set(['script', 'expression', 'condition', 'completionCondition', 'conditionExpression', 'collection']);
3130
+ /**
3131
+ * Every `$vars.<from>` read (the whole root, not a prefix of a longer one) in
3132
+ * `v`'s code, read as `$vars.<to>`. Code is a `=js:` wire value, a plain
3133
+ * `$vars.` path, or a `RAW_JS_FIELDS` value; in each, only the code spans are
3134
+ * rewritten. A label, a literal input, or a quoted `"$vars.<from>"` inside a
3135
+ * script stays as written, the way `serialize` left it (#1092 review).
3136
+ */
3137
+ function renameVarRoot(v, from, to) {
3138
+ const re = new RegExp(`\\$vars\\.${from.replace(/[$]/g, '\\$&')}(?![A-Za-z0-9_$])`, 'g');
3139
+ const inCode = (js) => mapJsCode(js, (code) => code.replace(re, `$vars.${to}`));
3140
+ const walk = (x, key) => {
3141
+ if (typeof x === 'string') {
3142
+ if (x.startsWith('=js:'))
3143
+ return '=js:' + inCode(x.slice(4));
3144
+ if (x.startsWith('$vars.') || (key !== undefined && RAW_JS_FIELDS.has(key)))
3145
+ return inCode(x);
3146
+ return x;
3147
+ }
3148
+ if (Array.isArray(x))
3149
+ return x.map((y) => walk(y, key));
3150
+ if (x && typeof x === 'object')
3151
+ return Object.fromEntries(Object.entries(x).map(([k, y]) => [k, walk(y, k)]));
3152
+ return x;
3153
+ };
3154
+ return walk(v);
3155
+ }
3156
+ /**
3157
+ * Undo the backing-Variable lowering `serialize` applies to an output the flow
3158
+ * sets from inside (`flow-output-backing.ts`): fold the `inout` `<output>Value`
3159
+ * global back into its output, so the source says `{ updates: { summary } }`
3160
+ * and `v('summary')` again and recompiling emits the same bytes.
3161
+ *
3162
+ * A pair is folded only when the Variable is exactly what `serialize` would
3163
+ * emit for that output (`outputBackingGlobal`): same id, type, default,
3164
+ * subType and schema, no description, no trigger. Any other `inout` global,
3165
+ * one a person named `summaryValue` with a description say, stays a `.var()`.
3166
+ * Applied to each subflow body too, against that body's own globals.
3167
+ */
3168
+ function foldOutputBacking(flow) {
3169
+ const subflows = flow.subflows
3170
+ ? Object.fromEntries(Object.entries(flow.subflows).map(([k, c]) => [k, foldOutputBacking(c)]))
3171
+ : undefined;
3172
+ let out = subflows ? { ...flow, subflows } : flow;
3173
+ const globals = Array.isArray(flow.variables?.globals) ? flow.variables.globals : [];
3174
+ for (const o of globals.filter((g) => g.direction === 'out')) {
3175
+ const id = outputBackingName(o.id);
3176
+ const b = globals.find((g) => g.id === id && g.direction === 'inout');
3177
+ if (!b)
3178
+ continue;
3179
+ const expected = outputBackingGlobal({ name: o.id, type: o.type, default: o.defaultValue, subType: o.subType, schema: o.schema }, id);
3180
+ if (canonicalJson(b) !== canonicalJson(expected))
3181
+ continue;
3182
+ const vars = out.variables;
3183
+ const updates = Object.fromEntries(Object.entries(vars.variableUpdates ?? {}).map(([node, rows]) => [
3184
+ node, rows.map((r) => (r.variableId === id ? { ...r, variableId: o.id } : r)),
3185
+ ]));
3186
+ const nodes = (out.nodes ?? []).map((n) => {
3187
+ if (n.type !== 'core.control.end' || !n.outputs || !(id in n.outputs))
3188
+ return n;
3189
+ const { [id]: _dropped, ...rest } = n.outputs;
3190
+ return { ...n, outputs: rest };
3191
+ });
3192
+ out = {
3193
+ ...out,
3194
+ nodes: renameVarRoot(nodes, id, o.id),
3195
+ variables: {
3196
+ ...vars,
3197
+ globals: (vars.globals ?? []).filter((g) => g !== b && g.id !== id),
3198
+ variableUpdates: renameVarRoot(updates, id, o.id),
3199
+ },
3200
+ };
3201
+ }
3202
+ return out;
3203
+ }
3134
3204
  export function decompile(flow, options = {}) {
3205
+ flow = foldOutputBacking(flow);
3135
3206
  const importSpecifier = options.importSpecifier ?? './flow-sdk.js';
3136
3207
  runStrict = options.bestEffort === false;
3137
3208
  runWarn = options.warn;
@@ -12,8 +12,9 @@ import { dataFabricFlowFamily } from './nodes/data-fabric/flow/family.js';
12
12
  import { delayFlowFamily } from './nodes/delay/flow/family.js';
13
13
  import { documentFlowFamily } from './nodes/document/flow/family.js';
14
14
  import { mockFlowFamily } from './nodes/mock/flow/family.js';
15
+ import { queueFlowFamily } from './nodes/queue/flow/family.js';
15
16
  import { scriptFlowFamily } from './nodes/script/flow/family.js';
16
17
  import { transformFlowFamily } from './nodes/transform/flow/family.js';
17
18
  /** The families, in no significant order. Adding one is the whole of registering it. */
18
- export const FLOW_NODE_FAMILIES = [dataFabricFlowFamily, delayFlowFamily, documentFlowFamily, mockFlowFamily, scriptFlowFamily, transformFlowFamily];
19
+ export const FLOW_NODE_FAMILIES = [dataFabricFlowFamily, delayFlowFamily, documentFlowFamily, mockFlowFamily, queueFlowFamily, scriptFlowFamily, transformFlowFamily];
19
20
  export const FLOW_NODE_TYPES = new NodeTypeRegistry('flow', FLOW_NODE_FAMILIES, { hasBundledDefinition });
@@ -75,6 +75,12 @@ export interface FlowSerializeContext {
75
75
  } & Readonly<Record<string, unknown>>): void;
76
76
  /** The bundled definition `<typeName>@<version>`, or undefined when the SDK does not ship it. */
77
77
  definition(typeName: string, version: string): unknown;
78
+ /**
79
+ * The flow's bindings so far, by id — read-only. For a type that SHARES a
80
+ * binding with earlier steps on the same resource (a queue's name + folder
81
+ * pair) or must pick an id that is not taken; add entries through `bind`.
82
+ */
83
+ readonly bindings: ReadonlyMap<string, Readonly<Record<string, unknown>>>;
78
84
  }
79
85
  /** What `deserialize` may ask of the decompiler, for one serialized node. */
80
86
  export interface FlowDeserializeContext {
@@ -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
+ }