@uipath/maestro-builder-sdk 6.18.10 → 6.20.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/api-index.md +185 -184
- package/dist/api-members.md +506 -473
- package/dist/check.js +358 -695
- package/dist/cli/commands/flow.js +3 -0
- package/dist/compile-cli.js +51 -0
- package/dist/core/actions.d.ts +132 -765
- package/dist/core/actions.js +1 -307
- package/dist/core/binding-identity.d.ts +22 -0
- package/dist/core/binding-identity.js +28 -0
- package/dist/core/context-index.d.ts +34 -0
- package/dist/core/context-index.js +38 -0
- package/dist/core/definitionless-node-types.d.ts +16 -0
- package/dist/core/definitionless-node-types.js +16 -0
- package/dist/core/error-port.d.ts +6 -0
- package/dist/core/error-port.js +9 -0
- package/dist/core/form-field-type.d.ts +33 -0
- package/dist/core/form-field-type.js +30 -0
- package/dist/core/hitl-routing.d.ts +8 -0
- package/dist/core/hitl-routing.js +17 -1
- package/dist/core/node-classes.d.ts +4 -63
- package/dist/core/node-classes.js +0 -112
- package/dist/core/nodes/family.d.ts +36 -0
- package/dist/core/nodes/family.js +1 -0
- package/dist/core/nodes/node-type.d.ts +136 -0
- package/dist/core/nodes/node-type.js +1 -0
- package/dist/core/nodes/registry.d.ts +57 -0
- package/dist/core/nodes/registry.js +122 -0
- package/dist/core/nodes/versioned.d.ts +50 -0
- package/dist/core/nodes/versioned.js +46 -0
- package/dist/core/process-resource-sort-order.d.ts +40 -0
- package/dist/core/process-resource-sort-order.js +44 -0
- package/dist/core/typed-raw-node.d.ts +13 -62
- package/dist/core/typed-raw-node.js +40 -154
- package/dist/core/wire-expr.d.ts +27 -0
- package/dist/core/wire-expr.js +34 -0
- package/dist/decompile-cli.js +11 -1
- package/dist/decompile.d.ts +8 -3
- package/dist/decompile.js +665 -354
- package/dist/flow-definitions.d.ts +10 -0
- package/dist/flow-definitions.js +35 -0
- package/dist/flow-expr-check.js +1 -1
- package/dist/flow-node-registry.d.ts +13 -0
- package/dist/flow-node-registry.js +19 -0
- package/dist/flow-node-type.d.ts +192 -0
- package/dist/flow-node-type.js +1 -0
- package/dist/flow-sdk.d.ts +40 -1
- package/dist/flow-sdk.js +29 -2
- package/dist/flow-step-tree.js +2 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -0
- package/dist/merge-cli.js +25 -8
- package/dist/merge-roundtrip.d.ts +81 -0
- package/dist/merge-roundtrip.js +404 -0
- package/dist/merge.d.ts +35 -6
- package/dist/merge.js +124 -24
- package/dist/node-placement.d.ts +93 -0
- package/dist/node-placement.js +163 -0
- package/dist/nodes/data-fabric/flow/check.d.ts +13 -0
- package/dist/nodes/data-fabric/flow/check.js +164 -0
- package/dist/nodes/data-fabric/flow/factories.d.ts +113 -0
- package/dist/nodes/data-fabric/flow/factories.js +134 -0
- package/dist/nodes/data-fabric/flow/family.d.ts +7 -0
- package/dist/nodes/data-fabric/flow/family.js +9 -0
- package/dist/nodes/data-fabric/flow/index.d.ts +8 -0
- package/dist/nodes/data-fabric/flow/index.js +8 -0
- package/dist/nodes/data-fabric/flow/raw.d.ts +32 -0
- package/dist/nodes/data-fabric/flow/raw.js +100 -0
- package/dist/nodes/data-fabric/flow/shared.d.ts +77 -0
- package/dist/nodes/data-fabric/flow/shared.js +174 -0
- package/dist/nodes/data-fabric/flow/spec.d.ts +238 -0
- package/dist/nodes/data-fabric/flow/spec.js +1 -0
- package/dist/nodes/data-fabric/flow/verbs.d.ts +29 -0
- package/dist/nodes/data-fabric/flow/verbs.js +239 -0
- package/dist/nodes/delay/flow/check.d.ts +16 -0
- package/dist/nodes/delay/flow/check.js +86 -0
- package/dist/nodes/delay/flow/factories.d.ts +28 -0
- package/dist/nodes/delay/flow/factories.js +38 -0
- package/dist/nodes/delay/flow/family.d.ts +3 -0
- package/dist/nodes/delay/flow/family.js +8 -0
- package/dist/nodes/delay/flow/index.d.ts +3 -0
- package/dist/nodes/delay/flow/index.js +3 -0
- package/dist/nodes/delay/flow/node-type.d.ts +13 -0
- package/dist/nodes/delay/flow/node-type.js +71 -0
- package/dist/nodes/delay/flow/spec.d.ts +43 -0
- package/dist/nodes/delay/flow/spec.js +16 -0
- package/dist/nodes/document/flow/factories.d.ts +27 -0
- package/dist/nodes/document/flow/factories.js +30 -0
- package/dist/nodes/document/flow/family.d.ts +3 -0
- package/dist/nodes/document/flow/family.js +7 -0
- package/dist/nodes/document/flow/index.d.ts +3 -0
- package/dist/nodes/document/flow/index.js +3 -0
- package/dist/nodes/document/flow/node-type.d.ts +14 -0
- package/dist/nodes/document/flow/node-type.js +150 -0
- package/dist/nodes/document/flow/spec.d.ts +65 -0
- package/dist/nodes/document/flow/spec.js +1 -0
- package/dist/nodes/mock/flow/factories.d.ts +51 -0
- package/dist/nodes/mock/flow/factories.js +47 -0
- package/dist/nodes/mock/flow/family.d.ts +3 -0
- package/dist/nodes/mock/flow/family.js +7 -0
- package/dist/nodes/mock/flow/index.d.ts +2 -0
- package/dist/nodes/mock/flow/index.js +2 -0
- package/dist/nodes/mock/flow/node-type.d.ts +11 -0
- package/dist/nodes/mock/flow/node-type.js +51 -0
- package/dist/nodes/script/flow/check.d.ts +19 -0
- package/dist/nodes/script/flow/check.js +49 -0
- package/dist/nodes/script/flow/factories.d.ts +14 -0
- package/dist/nodes/script/flow/factories.js +15 -0
- package/dist/nodes/script/flow/family.d.ts +3 -0
- package/dist/nodes/script/flow/family.js +8 -0
- package/dist/nodes/script/flow/index.d.ts +3 -0
- package/dist/nodes/script/flow/index.js +3 -0
- package/dist/nodes/script/flow/node-type.d.ts +7 -0
- package/dist/nodes/script/flow/node-type.js +125 -0
- package/dist/nodes/script/flow/spec.d.ts +52 -0
- package/dist/nodes/script/flow/spec.js +1 -0
- package/dist/nodes/transform/flow/check.d.ts +6 -0
- package/dist/nodes/transform/flow/check.js +229 -0
- package/dist/nodes/transform/flow/factories.d.ts +52 -0
- package/dist/nodes/transform/flow/factories.js +53 -0
- package/dist/nodes/transform/flow/family.d.ts +3 -0
- package/dist/nodes/transform/flow/family.js +8 -0
- package/dist/nodes/transform/flow/index.d.ts +3 -0
- package/dist/nodes/transform/flow/index.js +3 -0
- package/dist/nodes/transform/flow/node-type.d.ts +7 -0
- package/dist/nodes/transform/flow/node-type.js +245 -0
- package/dist/nodes/transform/flow/spec.d.ts +116 -0
- package/dist/nodes/transform/flow/spec.js +10 -0
- package/dist/raw-node-advice.d.ts +16 -0
- package/dist/raw-node-advice.js +36 -0
- package/dist/raw-node.d.ts +71 -0
- package/dist/raw-node.js +116 -0
- package/dist/registry/uip/pipeline-front-door.js +5 -3
- package/dist/serialize.js +246 -549
- package/package.json +2 -2
package/dist/decompile.js
CHANGED
|
@@ -28,21 +28,23 @@
|
|
|
28
28
|
* display labels on plain steps, and formatting are not preserved (layout is
|
|
29
29
|
* re-attached by `flow-merge`; see `docs/FLOW_PARITY.md`).
|
|
30
30
|
*/
|
|
31
|
-
import { readFileSync } from 'node:fs';
|
|
32
31
|
import { FLOW_SCHEMA_VERSIONS } from './format-profile.js';
|
|
33
32
|
import { preserveSchemaVersion } from './schema-version.js';
|
|
34
|
-
import {
|
|
35
|
-
import {
|
|
36
|
-
import {
|
|
37
|
-
import { DEFAULT_TRIGGER_ID } from './core/expr.js';
|
|
33
|
+
import { BUNDLED_DEFINITIONS, bundledVersions } from './flow-definitions.js';
|
|
34
|
+
import { DEFAULT_TRIGGER_ID, Expr } from './core/expr.js';
|
|
35
|
+
import { FLOW_NODE_TYPES } from './flow-node-registry.js';
|
|
38
36
|
import { commentText } from './core/comment-text.js';
|
|
39
37
|
import { tsLiteral, tsNumber, tsText } from './core/ts-literal.js';
|
|
38
|
+
import { contextIndexSlug, parseContextIndexType } from './core/context-index.js';
|
|
39
|
+
import { wireExprBody } from './core/wire-expr.js';
|
|
40
40
|
import { parseIxpProjectType } from './core/actions.js';
|
|
41
41
|
import { readFlowGlobals } from './flow-variables.js';
|
|
42
42
|
import { missingBindingLabels } from './core/binding-messages.js';
|
|
43
43
|
import { prepareCommand } from './core/cli-spelling.js';
|
|
44
|
-
import { OUTCOME_HANDLE_NODE_TYPES } from './core/hitl-routing.js';
|
|
44
|
+
import { OUTCOME_HANDLE_NODE_TYPES, outcomeSlug } from './core/hitl-routing.js';
|
|
45
|
+
import { STICKY_NOTE_TYPE } from './core/definitionless-node-types.js';
|
|
45
46
|
import { eventFilterFromTreeLeaf } from './core/event-filters.js';
|
|
47
|
+
import { sdkFormFieldType } from './core/form-field-type.js';
|
|
46
48
|
/**
|
|
47
49
|
* The trigger id of the flow currently being decompiled, so a reference through a
|
|
48
50
|
* RENAMED trigger is still recognized as `input('x')` rather than falling back to
|
|
@@ -81,6 +83,17 @@ let runStrict = false;
|
|
|
81
83
|
/** `DecompileOptions.warn` for the current run, and the messages already sent to it (one line per finding). */
|
|
82
84
|
let runWarn;
|
|
83
85
|
let runWarned = new Set();
|
|
86
|
+
/**
|
|
87
|
+
* The agent RESOURCE nodes the agent-cluster inverter folded into an agent's
|
|
88
|
+
* spec (`inlineAgent({ tools, context, escalation, memory })`), mapped to the
|
|
89
|
+
* node id compile will mint for each (`<agent>Context`, `<agent>Tool2`, ...), or
|
|
90
|
+
* to `undefined` when compile will not re-emit the node at all.
|
|
91
|
+
*
|
|
92
|
+
* A folded resource is not a step, so its id is not the author's to choose:
|
|
93
|
+
* compile names it. Anything decompile writes that names the node, such as a
|
|
94
|
+
* `.layout()` key, must use the id compile will produce, not the file's.
|
|
95
|
+
*/
|
|
96
|
+
let runAbsorbed = new Map();
|
|
84
97
|
/** `DecompileOptions.readSidecar` for the current run. */
|
|
85
98
|
let runReadSidecar;
|
|
86
99
|
function warnOnce(message) {
|
|
@@ -89,16 +102,6 @@ function warnOnce(message) {
|
|
|
89
102
|
runWarned.add(message);
|
|
90
103
|
runWarn?.(message);
|
|
91
104
|
}
|
|
92
|
-
/**
|
|
93
|
-
* The `<nodeType>@<version>` pairs this SDK bundles for core nodes — the same
|
|
94
|
-
* file `serialize` resolves an explicit `{ version }` against (design §2.6:
|
|
95
|
-
* exact, no floating). Decompile preserves the file's pair (design §5.3), so
|
|
96
|
-
* when the file pins a version the bundle lacks, `compile` WILL refuse the
|
|
97
|
-
* emitted source; saying so here is the difference between a loop that fails
|
|
98
|
-
* at step 2 with a hint and one that fails at step 2 with a puzzle.
|
|
99
|
-
*/
|
|
100
|
-
const CORE_DEFINITIONS = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), 'core-definitions.json'), 'utf8'));
|
|
101
|
-
const CORE_DEFINITION_KEYS = new Set(Object.keys(CORE_DEFINITIONS));
|
|
102
105
|
/**
|
|
103
106
|
* The label the canvas gives a freshly dropped `node`: its definition's
|
|
104
107
|
* `display.label`, from the file's own `definitions[]`, else the bundled core
|
|
@@ -106,7 +109,7 @@ const CORE_DEFINITION_KEYS = new Set(Object.keys(CORE_DEFINITIONS));
|
|
|
106
109
|
*/
|
|
107
110
|
function definitionLabel(node) {
|
|
108
111
|
const key = `${node.type}@${node.typeVersion}`;
|
|
109
|
-
const def = runDefs.get(key) ??
|
|
112
|
+
const def = runDefs.get(key) ?? BUNDLED_DEFINITIONS[key] ?? runDefs.get(node.type);
|
|
110
113
|
const label = def?.display?.label;
|
|
111
114
|
return typeof label === 'string' ? label : undefined;
|
|
112
115
|
}
|
|
@@ -124,13 +127,33 @@ function verClause(node) {
|
|
|
124
127
|
+ `the file's definitions[] disagrees with the instance.`);
|
|
125
128
|
}
|
|
126
129
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
130
|
+
// A pin the SDK cannot resolve: `serialize` resolves an explicit { version }
|
|
131
|
+
// exactly (design §2.6) — a registered type against its registered versions,
|
|
132
|
+
// any other type against the bundle. Keeping that pin would emit source
|
|
133
|
+
// `compile` refuses (#1128: quick-form@1.6 from `node add`, @1.4 from
|
|
134
|
+
// `migrate`, script@1.0 from a pre-migrate file), so the source omits it: the
|
|
135
|
+
// node compiles at the SDK's default, and `merge --baseline` keeps the
|
|
136
|
+
// UNTOUCHED original node, with its own version and definition, verbatim
|
|
137
|
+
// (`nodeUnchanged` ignores typeVersion). Interim: a node the author EDITS is
|
|
138
|
+
// re-emitted at the default — the root fix is for compile to accept the
|
|
139
|
+
// definition the file carries.
|
|
140
|
+
//
|
|
141
|
+
// A type with no SDK-known version at all (a connector, a published-resource
|
|
142
|
+
// `uipath.core.*` node, a rawNode) resolves its version elsewhere, so its pin
|
|
143
|
+
// is kept.
|
|
144
|
+
const registered = FLOW_NODE_TYPES.byTypeName(node.type);
|
|
145
|
+
const shipped = registered ? Object.keys(registered.type.versions) : bundledVersions(node.type);
|
|
146
|
+
if (shipped.length > 0 && !shipped.includes(tv)) {
|
|
147
|
+
const known = `${node.type}@${shipped.join(', ')}`;
|
|
148
|
+
if (runStrict) {
|
|
149
|
+
// Strict never changes a node's version behind the author's back.
|
|
150
|
+
throw new Error(`decompile: node "${node.id}" pins ${node.type}@${tv}, which this SDK cannot resolve (it ${registered ? 'registers' : 'bundles'} `
|
|
151
|
+
+ `${known}); compile would refuse the pin, and omitting it changes the node's version.`);
|
|
152
|
+
}
|
|
153
|
+
warnOnce(`node ${tsLiteral(node.id)} pins ${node.type}@${tv}, but this SDK ${registered ? 'registers' : 'bundles'} ${known} only, `
|
|
154
|
+
+ `so the source omits that { version } and \`compile\` emits the SDK's default (versions resolve exactly, flow-builder-sdk#71). `
|
|
155
|
+
+ `\`merge --baseline\` keeps the node at ${tv} if you leave it untouched; if you edit it, the merged node moves to the SDK's version.`);
|
|
156
|
+
return '';
|
|
134
157
|
}
|
|
135
158
|
return `version: ${tsLiteral(tv)}`;
|
|
136
159
|
}
|
|
@@ -210,21 +233,11 @@ const T = {
|
|
|
210
233
|
endCall: 'uipath.conversational.voice.end-call',
|
|
211
234
|
http: 'core.action.http',
|
|
212
235
|
httpV2: 'core.action.http.v2',
|
|
213
|
-
script: 'core.action.script',
|
|
214
|
-
transform: 'core.action.transform',
|
|
215
|
-
delay: 'core.logic.delay',
|
|
216
|
-
mock: 'core.logic.mock',
|
|
217
236
|
decision: 'core.logic.decision',
|
|
218
237
|
switch: 'core.logic.switch',
|
|
219
238
|
merge: 'core.logic.merge',
|
|
220
239
|
loop: 'core.logic.loop',
|
|
221
240
|
doWhile: 'core.logic.dowhile',
|
|
222
|
-
dataFabricRead: 'core.datafabric.read',
|
|
223
|
-
dataFabricCreate: 'core.datafabric.create',
|
|
224
|
-
dataFabricDelete: 'core.datafabric.delete',
|
|
225
|
-
documentClassify: 'uipath.document.classify',
|
|
226
|
-
dynamicExtract: 'uipath.ixp.extract-document-builder',
|
|
227
|
-
dataFabricUpdate: 'core.datafabric.update',
|
|
228
241
|
subflow: 'core.subflow',
|
|
229
242
|
terminate: 'core.logic.terminate',
|
|
230
243
|
end: 'core.control.end',
|
|
@@ -409,7 +422,19 @@ class Graph {
|
|
|
409
422
|
byId = new Map();
|
|
410
423
|
out = new Map();
|
|
411
424
|
inc = new Map();
|
|
425
|
+
/**
|
|
426
|
+
* Edge accounting (#1125): every edge the source carries — walked into by a
|
|
427
|
+
* construct, or one serialize recreates on its own — is added here as the walk
|
|
428
|
+
* emits it. What is left at the end has no spelling in the emitted source, so
|
|
429
|
+
* recompiling would drop it; `droppedEdgesSeg` (at the site) and
|
|
430
|
+
* `sweepDroppedEdges` (edges no walk reached) say so instead of letting the
|
|
431
|
+
* round trip lose it silently. The edge counterpart of the node check
|
|
432
|
+
* (`detachedTopLevelNodes`).
|
|
433
|
+
*/
|
|
434
|
+
accounted = new Set();
|
|
435
|
+
edges;
|
|
412
436
|
constructor(nodes, edges) {
|
|
437
|
+
this.edges = edges;
|
|
413
438
|
for (const n of nodes)
|
|
414
439
|
this.byId.set(n.id, n);
|
|
415
440
|
for (const e of edges) {
|
|
@@ -503,147 +528,90 @@ class Graph {
|
|
|
503
528
|
}
|
|
504
529
|
}
|
|
505
530
|
// ─── Node → step-spec source ──────────────────────────────────────────────────
|
|
531
|
+
/**
|
|
532
|
+
* A serialized value back as a spec holds it: `=js:` text (bare, or in a
|
|
533
|
+
* `{ type: 'jsExpression', expression }` object) becomes an `Expr`; anything else
|
|
534
|
+
* is a literal and stays itself.
|
|
535
|
+
*/
|
|
536
|
+
function specValue(raw) {
|
|
537
|
+
if (typeof raw === 'string')
|
|
538
|
+
return raw.startsWith('=js:') ? new Expr(raw.slice(4)) : raw;
|
|
539
|
+
const o = raw;
|
|
540
|
+
if (o && typeof o === 'object' && o.type === 'jsExpression' && typeof o.expression === 'string')
|
|
541
|
+
return new Expr(o.expression);
|
|
542
|
+
return raw;
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* Print a node whose type is registered (flow-node-registry.ts): the node type
|
|
546
|
+
* deserializes it to its spec and prints the factory call. A node at a version
|
|
547
|
+
* the type does not register is lifted by its default version's implementation —
|
|
548
|
+
* as every built-in family lifts any version — and `verClause` omits a
|
|
549
|
+
* `{ version }` the type does not register, with a warning. `undefined` when the type is not registered, or the node type declines
|
|
550
|
+
* the node; the caller's branches take it from there.
|
|
551
|
+
*/
|
|
552
|
+
function emitRegisteredSpec(node, imp, inputNames) {
|
|
553
|
+
const entry = typeof node.type === 'string' ? FLOW_NODE_TYPES.byTypeName(node.type) : undefined;
|
|
554
|
+
if (!entry)
|
|
555
|
+
return undefined;
|
|
556
|
+
const version = String(node.typeVersion ?? '');
|
|
557
|
+
const impl = (Object.hasOwn(entry.type.versions, version)
|
|
558
|
+
? entry.type.versions[version]
|
|
559
|
+
: entry.type.versions[entry.type.defaultVersion]);
|
|
560
|
+
const ctx = {
|
|
561
|
+
nodeId: node.id,
|
|
562
|
+
expr: specValue,
|
|
563
|
+
sourceText: unwrapSource,
|
|
564
|
+
plainValue: unwrapPlain,
|
|
565
|
+
stepName: (nodeId) => nodeId,
|
|
566
|
+
definition: () => runDefs.get(`${node.type}@${version}`) ?? runDefs.get(node.type),
|
|
567
|
+
warn: warnOnce,
|
|
568
|
+
};
|
|
569
|
+
const spec = impl.deserialize(node, ctx);
|
|
570
|
+
if (spec === undefined)
|
|
571
|
+
return undefined;
|
|
572
|
+
// A spec value as source, recursing into arrays and plain objects so an Expr
|
|
573
|
+
// nested in one (a filter row's value) prints as the helper an author writes —
|
|
574
|
+
// in the `{ k: v }` / `[a, b]` spelling the hand-written printers use.
|
|
575
|
+
const printValue = (v) => {
|
|
576
|
+
if (v instanceof Expr)
|
|
577
|
+
return v.literal ? tsLiteral(v.value) : emitRef(v.js, imp, inputNames);
|
|
578
|
+
if (Array.isArray(v))
|
|
579
|
+
return `[${v.map(printValue).join(', ')}]`;
|
|
580
|
+
if (v && typeof v === 'object') {
|
|
581
|
+
const parts = Object.entries(v).filter(([, x]) => x !== undefined).map(([k, x]) => `${key(k)}: ${printValue(x)}`);
|
|
582
|
+
return parts.length ? `{ ${parts.join(', ')} }` : '{}';
|
|
583
|
+
}
|
|
584
|
+
return tsLiteral(v);
|
|
585
|
+
};
|
|
586
|
+
const print = {
|
|
587
|
+
value: printValue,
|
|
588
|
+
string: (t) => tsLiteral(t),
|
|
589
|
+
json: (v) => tsLiteral(v),
|
|
590
|
+
key: (k) => key(k),
|
|
591
|
+
helper: (name) => imp.use(name),
|
|
592
|
+
};
|
|
593
|
+
const call = impl.print?.(spec, print) ?? {
|
|
594
|
+
factory: impl.factory,
|
|
595
|
+
options: Object.entries(spec.inputs ?? {})
|
|
596
|
+
.map(([k, v]) => [k, v === undefined ? undefined : print.value(v)]),
|
|
597
|
+
};
|
|
598
|
+
const options = opts((call.options ?? []).map(([k, v]) => [k, v]));
|
|
599
|
+
const args = [...(call.positional ?? []), ...(options ? [options] : [])];
|
|
600
|
+
return `${imp.use(call.factory)}(${args.join(', ')})`;
|
|
601
|
+
}
|
|
506
602
|
/**
|
|
507
603
|
* Emit the action-spec argument for a `.step(id, <spec>)` call — e.g.
|
|
508
604
|
* `script({ code: "…" })`. Returns `undefined` for a node that is not a plain
|
|
509
605
|
* action (control-flow / trigger / end), which the traversal handles specially.
|
|
510
606
|
*/
|
|
511
607
|
function emitStepSpec(node, imp, inputNames, o, graph) {
|
|
608
|
+
// A registered node type lifts itself; every other type falls through to the
|
|
609
|
+
// branches below until its family is ported (FB014's pins are the list).
|
|
610
|
+
const lifted = emitRegisteredSpec(node, imp, inputNames);
|
|
611
|
+
if (lifted !== undefined)
|
|
612
|
+
return lifted;
|
|
512
613
|
const inputs = node.inputs ?? {};
|
|
513
614
|
switch (node.type) {
|
|
514
|
-
case T.mock:
|
|
515
|
-
return `${imp.use('mock')}()`;
|
|
516
|
-
case T.script: {
|
|
517
|
-
const code = typeof inputs.script === 'string' ? inputs.script : '';
|
|
518
|
-
// `returns` is emitted only when the node's declared output type is
|
|
519
|
-
// something re-compiling the body would NOT produce on its own. Serialize
|
|
520
|
-
// infers from the body first (see `scriptOutputs`), so a script whose type
|
|
521
|
-
// is already plain in its code round-trips without it — emitting one there
|
|
522
|
-
// would add an argument the author never wrote to every such step.
|
|
523
|
-
return `${imp.use('script')}(${opts([['code', tsLiteral(code)], ...scriptReturnsOpt(node, code)])})`;
|
|
524
|
-
}
|
|
525
|
-
case T.delay: {
|
|
526
|
-
// serialize's `delayInputs` writes `{timerType, timerPreset, timerValue?}`
|
|
527
|
-
// for a duration and `{timerType:'timeDate', timerDate}` for an absolute
|
|
528
|
-
// wait. Invert all three spellings.
|
|
529
|
-
if (inputs.timerType === 'timeDate') {
|
|
530
|
-
const until = typeof inputs.timerDate === 'string' ? inputs.timerDate : '';
|
|
531
|
-
return `${imp.use('delay')}(${opts([['until', tsLiteral(until)]])})`;
|
|
532
|
-
}
|
|
533
|
-
const duration = inputs.timerPreset === 'custom'
|
|
534
|
-
? (typeof inputs.timerValue === 'string' ? inputs.timerValue : '')
|
|
535
|
-
: (typeof inputs.timerPreset === 'string' ? inputs.timerPreset : '');
|
|
536
|
-
return `${imp.use('delay')}(${opts([['duration', tsLiteral(duration)]])})`;
|
|
537
|
-
}
|
|
538
|
-
case T.documentClassify:
|
|
539
|
-
case T.dynamicExtract: {
|
|
540
|
-
const fr = unwrapSource(inputs.fileRef) ?? '';
|
|
541
|
-
const fileSrc = fr.startsWith('=js:') ? emitBareExpr(fr.slice(4), imp, inputNames) : tsLiteral(fr);
|
|
542
|
-
const pageRange = typeof inputs.pageRange === 'string' && inputs.pageRange !== '' ? tsLiteral(inputs.pageRange) : undefined;
|
|
543
|
-
if (node.type === T.documentClassify) {
|
|
544
|
-
const split = unwrapPlain(inputs.splitPages);
|
|
545
|
-
return `${imp.use('documentClassify')}(${opts([
|
|
546
|
-
['fileRef', fileSrc],
|
|
547
|
-
['pageRange', pageRange],
|
|
548
|
-
['splitPages', split === true ? 'true' : undefined],
|
|
549
|
-
])})`;
|
|
550
|
-
}
|
|
551
|
-
const schema = inputs.schemaDocument !== undefined ? tsLiteral(inputs.schemaDocument) : undefined;
|
|
552
|
-
return `${imp.use('dynamicExtract')}(${opts([
|
|
553
|
-
['fileRef', fileSrc],
|
|
554
|
-
['schema', schema],
|
|
555
|
-
['pageRange', pageRange],
|
|
556
|
-
])})`;
|
|
557
|
-
}
|
|
558
|
-
case T.dataFabricRead:
|
|
559
|
-
case T.dataFabricCreate:
|
|
560
|
-
case T.dataFabricUpdate:
|
|
561
|
-
case T.dataFabricDelete: {
|
|
562
|
-
const cfg = (inputs.entityConfig ?? {});
|
|
563
|
-
const entity = tsText(cfg.entityName);
|
|
564
|
-
const folder = typeof cfg._folderKey === 'string' ? tsLiteral(cfg._folderKey) : undefined;
|
|
565
|
-
const resourceKey = typeof cfg._resourceKey === 'string' ? tsLiteral(cfg._resourceKey) : undefined;
|
|
566
|
-
const val = (v) => {
|
|
567
|
-
// 1.3 let `recordId` and a create's field value be a `{ type:
|
|
568
|
-
// 'jsExpression', expression }` object as well as a `=js:` string. A
|
|
569
|
-
// product-authored node can carry either; both recover as an expression.
|
|
570
|
-
const obj = v;
|
|
571
|
-
if (obj && typeof obj === 'object' && obj.type === 'jsExpression') {
|
|
572
|
-
return emitBareExpr(String(obj.expression ?? ''), imp, inputNames);
|
|
573
|
-
}
|
|
574
|
-
const raw = typeof v === 'string' ? v : String(v ?? '');
|
|
575
|
-
return raw.startsWith('=js:') ? emitBareExpr(raw.slice(4), imp, inputNames) : tsLiteral(raw);
|
|
576
|
-
};
|
|
577
|
-
const fieldObject = (rows) => {
|
|
578
|
-
const list = Array.isArray(rows) ? rows : [];
|
|
579
|
-
return `{ ${list.map((r) => `${key(String(r.field ?? ''))}: ${val(r.value)}`).join(', ')} }`;
|
|
580
|
-
};
|
|
581
|
-
const record = cfg.recordSource === 'fromRead'
|
|
582
|
-
? `{ fromRead: ${tsText(cfg.readEntityNodeId)} }`
|
|
583
|
-
: `{ byId: ${val(cfg.recordId)} }`;
|
|
584
|
-
if (node.type === T.dataFabricRead) {
|
|
585
|
-
// The array is what the platform serializer reads and what compile
|
|
586
|
-
// writes; the grouped object is the newer authoring shape, tolerated
|
|
587
|
-
// here so a canvas-authored file still decompiles. Both flatten to the
|
|
588
|
-
// builder's row list, where `or: true` marks each new OR alternative.
|
|
589
|
-
const filterRow = (r, or = false) => opts([
|
|
590
|
-
['field', tsText(r.field)],
|
|
591
|
-
['operator', r.operator !== undefined && r.operator !== '=' ? tsLiteral(String(r.operator)) : undefined],
|
|
592
|
-
['value', val(r.value)],
|
|
593
|
-
['or', or ? 'true' : undefined],
|
|
594
|
-
]);
|
|
595
|
-
const q = cfg._filters;
|
|
596
|
-
let rows = [];
|
|
597
|
-
if (Array.isArray(q)) {
|
|
598
|
-
rows = q.map((r, i) => filterRow(r, i > 0 && r['join'] === 'OR'));
|
|
599
|
-
}
|
|
600
|
-
else if (q && typeof q === 'object') {
|
|
601
|
-
// Grouped: each root row and each group is one alternative, so every
|
|
602
|
-
// one but the first opens with `or: true`.
|
|
603
|
-
const alts = [
|
|
604
|
-
...(Array.isArray(q.rows) ? q.rows.map((r) => [r]) : []),
|
|
605
|
-
...(Array.isArray(q.groups) ? q.groups : [])
|
|
606
|
-
.map((g) => (Array.isArray(g.rows) ? g.rows : [])),
|
|
607
|
-
].filter((a) => a.length);
|
|
608
|
-
rows = alts.flatMap((alt, ai) => alt.map((r, ri) => filterRow(r, ai > 0 && ri === 0)));
|
|
609
|
-
}
|
|
610
|
-
const filters = rows.length ? `[${rows.join(', ')}]` : undefined;
|
|
611
|
-
// `resultMode` only exists from 1.4, and recovering it re-selects 1.4 on
|
|
612
|
-
// the way back out — so a 1.0 node stays a 1.0 node through the round trip.
|
|
613
|
-
const mode = cfg.resultMode === 'single' || cfg.resultMode === 'multiple'
|
|
614
|
-
? tsLiteral(cfg.resultMode) : undefined;
|
|
615
|
-
const sort = cfg._sort && typeof cfg._sort === 'object'
|
|
616
|
-
? opts([
|
|
617
|
-
['field', tsText(cfg._sort.field)],
|
|
618
|
-
['direction', cfg._sort.direction === 'desc' ? tsLiteral('desc') : undefined],
|
|
619
|
-
])
|
|
620
|
-
: undefined;
|
|
621
|
-
const fields = Array.isArray(cfg._selectedFieldNames) && cfg._selectedFieldNames.length
|
|
622
|
-
? `[${cfg._selectedFieldNames.map((f) => tsLiteral(String(f))).join(', ')}]`
|
|
623
|
-
: undefined;
|
|
624
|
-
return `${imp.use('dataFabricRead')}(${opts([
|
|
625
|
-
['entity', entity], ['filters', filters], ['resultMode', mode],
|
|
626
|
-
['limit', typeof cfg._recordLimit === 'number' ? String(cfg._recordLimit) : undefined],
|
|
627
|
-
['skip', typeof cfg._skip === 'number' ? String(cfg._skip) : undefined],
|
|
628
|
-
['sort', sort], ['fields', fields],
|
|
629
|
-
['folderKey', folder], ['resourceKey', resourceKey],
|
|
630
|
-
])})`;
|
|
631
|
-
}
|
|
632
|
-
if (node.type === T.dataFabricCreate) {
|
|
633
|
-
return `${imp.use('dataFabricCreate')}(${opts([
|
|
634
|
-
['entity', entity], ['values', fieldObject(cfg.fieldValues)], ['folderKey', folder], ['resourceKey', resourceKey],
|
|
635
|
-
])})`;
|
|
636
|
-
}
|
|
637
|
-
if (node.type === T.dataFabricDelete) {
|
|
638
|
-
return `${imp.use('dataFabricDelete')}(${opts([
|
|
639
|
-
['entity', entity], ['record', record], ['folderKey', folder], ['resourceKey', resourceKey],
|
|
640
|
-
])})`;
|
|
641
|
-
}
|
|
642
|
-
return `${imp.use('dataFabricUpdate')}(${opts([
|
|
643
|
-
['entity', entity], ['record', record], ['set', fieldObject(cfg.fieldUpdates)],
|
|
644
|
-
['folderKey', folder], ['resourceKey', resourceKey],
|
|
645
|
-
])})`;
|
|
646
|
-
}
|
|
647
615
|
case T.createOutgoingCall:
|
|
648
616
|
case T.endCall: {
|
|
649
617
|
const i = (node.inputs ?? {});
|
|
@@ -766,8 +734,6 @@ function emitStepSpec(node, imp, inputNames, o, graph) {
|
|
|
766
734
|
case T.httpV2:
|
|
767
735
|
return emitHttp(node, true, imp, inputNames);
|
|
768
736
|
default:
|
|
769
|
-
if (node.type.startsWith('core.action.transform'))
|
|
770
|
-
return emitTransform(node, imp, inputNames);
|
|
771
737
|
// A connector action node: `uipath.connector.<key>.<action>` (but NOT the
|
|
772
738
|
// event nodes `uipath.connector.event.*` / `.trigger.*`, which are triggers).
|
|
773
739
|
if (node.type.startsWith('uipath.connector.') && !node.type.startsWith('uipath.connector.event.') && !node.type.startsWith('uipath.connector.trigger.')) {
|
|
@@ -823,49 +789,6 @@ function emitStepSpec(node, imp, inputNames, o, graph) {
|
|
|
823
789
|
return unsupportedSpec(node, imp, o);
|
|
824
790
|
}
|
|
825
791
|
}
|
|
826
|
-
/**
|
|
827
|
-
* The `returns` argument for a script step, or nothing.
|
|
828
|
-
*
|
|
829
|
-
* Recovers what the author declared by DIFFERENCE: the node says what its output
|
|
830
|
-
* is, `inferScriptShape` says what re-compiling the body would declare on its
|
|
831
|
-
* own, and only a gap between the two needs writing down. Fields are recovered
|
|
832
|
-
* the same way — a property map the body does not produce is one the author gave
|
|
833
|
-
* as `returns: {…}`.
|
|
834
|
-
*/
|
|
835
|
-
function scriptReturnsOpt(node, code) {
|
|
836
|
-
const out = node.outputs?.output;
|
|
837
|
-
const declared = typeof out?.type === 'string' ? out.type : undefined;
|
|
838
|
-
if (!declared)
|
|
839
|
-
return [];
|
|
840
|
-
const props = out?.properties;
|
|
841
|
-
// What re-compiling this body with NO `returns` would declare. Mirror
|
|
842
|
-
// `scriptOutputs` exactly: an inference of a bare `object` (or none at all)
|
|
843
|
-
// leaves the definition's own `{type:'object'}` standing, untouched.
|
|
844
|
-
const inferred = inferScriptShape(code);
|
|
845
|
-
const effective = inferred && !(inferred.type === 'object' && !inferred.properties)
|
|
846
|
-
? inferred
|
|
847
|
-
: { type: 'object', properties: undefined };
|
|
848
|
-
const same = effective.type === declared
|
|
849
|
-
&& JSON.stringify(sortKeys(effective.properties ?? {})) === JSON.stringify(sortKeys(props ?? {}));
|
|
850
|
-
if (same)
|
|
851
|
-
return [];
|
|
852
|
-
// Not what the body alone would say — so the author declared it, and it has to
|
|
853
|
-
// be written back or the round-trip loses it. That includes declaring the OPEN
|
|
854
|
-
// object over a body that looks typed, which is a real thing to want.
|
|
855
|
-
if (props && Object.keys(props).length > 0) {
|
|
856
|
-
// A field map is the richer statement, so it wins when there is one.
|
|
857
|
-
const entries = Object.entries(props)
|
|
858
|
-
.filter(([, v]) => typeof v?.type === 'string')
|
|
859
|
-
.map(([k, v]) => [k, tsLiteral(v.type)]);
|
|
860
|
-
if (entries.length > 0)
|
|
861
|
-
return [['returns', opts(entries)]];
|
|
862
|
-
}
|
|
863
|
-
return [['returns', tsLiteral(declared)]];
|
|
864
|
-
}
|
|
865
|
-
/** Key-sorted copy, so a property map compares by content and not by order. */
|
|
866
|
-
function sortKeys(o) {
|
|
867
|
-
return Object.fromEntries(Object.entries(o).sort(([a], [b]) => a.localeCompare(b)));
|
|
868
|
-
}
|
|
869
792
|
/**
|
|
870
793
|
* Invert an HTTP node. The plain node (`core.action.http`) keeps its request at
|
|
871
794
|
* the top level (`url` / `method` / `headers` / `queryParams` / `body` /
|
|
@@ -897,7 +820,7 @@ function emitHttp(node, managed, imp, inputNames) {
|
|
|
897
820
|
const branchesSrc = branchRows.length
|
|
898
821
|
? `[${branchRows.map((b) => {
|
|
899
822
|
const cond = unwrapSource(b.conditionExpression) ?? '';
|
|
900
|
-
const expr = emitBareExpr(
|
|
823
|
+
const expr = emitBareExpr(wireExprBody(cond), imp, inputNames);
|
|
901
824
|
return `{ name: ${tsText(b.name ?? b.id)}, condition: ${expr} }`;
|
|
902
825
|
}).join(', ')}]`
|
|
903
826
|
: undefined;
|
|
@@ -926,68 +849,6 @@ function emitHttp(node, managed, imp, inputNames) {
|
|
|
926
849
|
]);
|
|
927
850
|
return `${imp.use('http')}(${spec})`;
|
|
928
851
|
}
|
|
929
|
-
/**
|
|
930
|
-
* Invert a Transform node (generic `core.action.transform` or a `.map` / `.filter`
|
|
931
|
-
* / `.group-by` variant). The `collection` is a bare `$vars.…` path; each
|
|
932
|
-
* operation's serialized `{id,type,config}` becomes a builder operation object.
|
|
933
|
-
* (The variant node types carry a `groupBy` operation spelled with a hyphen in
|
|
934
|
-
* the type — `group-by` — but `groupBy` in the operation, the platform's own
|
|
935
|
-
* mismatch, mirrored here.)
|
|
936
|
-
*/
|
|
937
|
-
function emitTransform(node, imp, inputNames) {
|
|
938
|
-
const inputs = node.inputs ?? {};
|
|
939
|
-
const suffix = node.type.slice('core.action.transform'.length).replace(/^\./, '');
|
|
940
|
-
const variant = suffix || undefined; // 'map' | 'filter' | 'group-by' | undefined
|
|
941
|
-
const collRaw = unwrapSource(inputs.collection);
|
|
942
|
-
const collection = collRaw !== undefined
|
|
943
|
-
? emitBareExpr(collRaw.startsWith('=js:') ? collRaw.slice(4) : collRaw, imp, inputNames)
|
|
944
|
-
: `${imp.use('lit')}([])`;
|
|
945
|
-
const ops = Array.isArray(inputs.operations) ? inputs.operations : [];
|
|
946
|
-
const opSrcs = ops.map((op) => {
|
|
947
|
-
const cfg = op?.config ?? {};
|
|
948
|
-
if (op?.type === 'filter') {
|
|
949
|
-
const filters = (cfg.filters ?? []).map((f) => objLit([
|
|
950
|
-
['field', tsLiteral(f.field)],
|
|
951
|
-
['condition', tsLiteral(f.condition)],
|
|
952
|
-
...(f.value !== undefined ? [['value', tsLiteral(f.value)]] : []),
|
|
953
|
-
]));
|
|
954
|
-
return objLit([
|
|
955
|
-
['type', tsLiteral('filter')],
|
|
956
|
-
...(cfg.operation !== undefined ? [['operation', tsLiteral(cfg.operation)]] : []),
|
|
957
|
-
['filters', `[${filters.join(', ')}]`],
|
|
958
|
-
]);
|
|
959
|
-
}
|
|
960
|
-
if (op?.type === 'map') {
|
|
961
|
-
const mappings = (cfg.mappings ?? []).map((m) => objLit([
|
|
962
|
-
['field', tsLiteral(m.field)],
|
|
963
|
-
...(m.transformation && m.transformation !== 'copy' ? [['transformation', tsLiteral(m.transformation)]] : []),
|
|
964
|
-
...(m.renameTo ? [['renameTo', tsLiteral(m.renameTo)]] : []),
|
|
965
|
-
]));
|
|
966
|
-
return objLit([
|
|
967
|
-
['type', tsLiteral('map')],
|
|
968
|
-
...(cfg.keepOriginalFields ? [['keepOriginalFields', boolLit(cfg.keepOriginalFields)]] : []),
|
|
969
|
-
['mappings', `[${mappings.join(', ')}]`],
|
|
970
|
-
]);
|
|
971
|
-
}
|
|
972
|
-
// groupBy
|
|
973
|
-
const aggs = (cfg.aggregations ?? []).map((a) => objLit([
|
|
974
|
-
['operation', tsLiteral(a.operation)],
|
|
975
|
-
['alias', tsLiteral(a.alias)],
|
|
976
|
-
...(a.field ? [['field', tsLiteral(a.field)]] : []),
|
|
977
|
-
]));
|
|
978
|
-
return objLit([
|
|
979
|
-
['type', tsLiteral('groupBy')],
|
|
980
|
-
['groupByField', tsLiteral(cfg.groupByField)],
|
|
981
|
-
['aggregations', `[${aggs.join(', ')}]`],
|
|
982
|
-
]);
|
|
983
|
-
});
|
|
984
|
-
const spec = opts([
|
|
985
|
-
['collection', collection],
|
|
986
|
-
['variant', variant ? tsLiteral(variant) : undefined],
|
|
987
|
-
['operations', `[${opSrcs.join(', ')}]`],
|
|
988
|
-
]);
|
|
989
|
-
return `${imp.use('transform')}(${spec})`;
|
|
990
|
-
}
|
|
991
852
|
/**
|
|
992
853
|
* Split a connector `nodeType` (`uipath.connector.<key>.<action>`) into key +
|
|
993
854
|
* action. Mirrors `core/connectors.ts`'s `splitConnectorNodeType`.
|
|
@@ -1117,6 +978,72 @@ function sidecarAgentInputs(node) {
|
|
|
1117
978
|
...(isVoice && settings.voice && typeof settings.voice === 'object' ? { voice: settings.voice } : {}),
|
|
1118
979
|
};
|
|
1119
980
|
}
|
|
981
|
+
/**
|
|
982
|
+
* Where a context-index resource node keeps its identity, read in the order
|
|
983
|
+
* that holds for every writer.
|
|
984
|
+
*
|
|
985
|
+
* SDK compile writes `indexId` / `indexName` / `folderPath` onto the node. The
|
|
986
|
+
* canvas and the CLI do not: their node carries only `inputs.source`, and the
|
|
987
|
+
* identity lives in two other places.
|
|
988
|
+
*
|
|
989
|
+
* - The node TYPE, `…context.index.<slug>.<uuid>`: the uuid is the index id
|
|
990
|
+
* (the `ContextIndexRef.id` JSDoc says to read it there).
|
|
991
|
+
* - The resource sidecar `<agent source>/resources/<inputs.source>/resource.json`,
|
|
992
|
+
* whose `name` is the index's real name (the one the slug is made from) and
|
|
993
|
+
* whose `folderPath` is its folder. `display.label` is only what the canvas
|
|
994
|
+
* shows, and a canvas rename changes it.
|
|
995
|
+
*
|
|
996
|
+
* So the id comes from the type (then `inputs.indexId`). The name is the first
|
|
997
|
+
* of `inputs.indexName`, sidecar `name`, sidecar `indexName`, label that slugs to
|
|
998
|
+
* the type's slug, else the first one present in that order. Compile derives
|
|
999
|
+
* the type from `slug(name)` + id, so when the chosen name does not slug back to
|
|
1000
|
+
* the type's slug the recompiled node gets a different type, and that is warned.
|
|
1001
|
+
*/
|
|
1002
|
+
function contextIndexIdentity(agent, c, rPlain) {
|
|
1003
|
+
const fromType = parseContextIndexType(c.type);
|
|
1004
|
+
const sidecar = contextResourceSidecar(agent, c);
|
|
1005
|
+
const sidecarText = (k) => typeof sidecar?.[k] === 'string' && sidecar[k] !== '' ? sidecar[k] : undefined;
|
|
1006
|
+
const label = typeof c.display?.label === 'string' && c.display.label !== '' ? c.display.label : undefined;
|
|
1007
|
+
// The canvas can set both sidecar `name` and `indexName`, and they can differ.
|
|
1008
|
+
// The one that slugs to the type's slug is the one the type was built from, so
|
|
1009
|
+
// it wins; the fixed order is the fallback when none matches.
|
|
1010
|
+
const candidates = [rPlain(c, 'indexName'), sidecarText('name'), sidecarText('indexName'), label]
|
|
1011
|
+
.filter((n) => n !== undefined);
|
|
1012
|
+
const name = (fromType?.slug ? candidates.find((n) => contextIndexSlug(n) === fromType.slug) : undefined)
|
|
1013
|
+
?? candidates[0] ?? '';
|
|
1014
|
+
const id = (fromType?.id || undefined) ?? rPlain(c, 'indexId') ?? '';
|
|
1015
|
+
if (fromType !== undefined && fromType.slug !== '' && contextIndexSlug(name) !== fromType.slug) {
|
|
1016
|
+
warnOnce(`context index "${c.id}" on agent "${agent.id}": its node type names the index "${fromType.slug}", `
|
|
1017
|
+
+ `but the name decompile recovered, ${JSON.stringify(name)}, slugs to "${contextIndexSlug(name)}", so compile `
|
|
1018
|
+
+ `will write a different node type. Set \`name\` to the index's real name`
|
|
1019
|
+
+ (sidecar === undefined ? ', or keep the resources/ folder beside the .flow so decompile can read it.' : '.'));
|
|
1020
|
+
}
|
|
1021
|
+
return { name, id, folderPath: rPlain(c, 'folderPath') ?? sidecarText('folderPath') };
|
|
1022
|
+
}
|
|
1023
|
+
/**
|
|
1024
|
+
* A context resource's `resource.json` sidecar, parsed; `undefined` when it is
|
|
1025
|
+
* not beside the `.flow`, or (with a warning) when it is not a JSON object.
|
|
1026
|
+
*/
|
|
1027
|
+
function contextResourceSidecar(agent, c) {
|
|
1028
|
+
const agentSource = unwrapSource(agent.inputs?.source);
|
|
1029
|
+
const resourceSource = unwrapSource(c.inputs?.source);
|
|
1030
|
+
if (!agentSource || !resourceSource)
|
|
1031
|
+
return undefined;
|
|
1032
|
+
const path = `${agentSource}/resources/${resourceSource}/resource.json`;
|
|
1033
|
+
const raw = runReadSidecar?.(path);
|
|
1034
|
+
if (raw === undefined)
|
|
1035
|
+
return undefined;
|
|
1036
|
+
try {
|
|
1037
|
+
const json = JSON.parse(raw);
|
|
1038
|
+
if (json && typeof json === 'object' && !Array.isArray(json))
|
|
1039
|
+
return json;
|
|
1040
|
+
}
|
|
1041
|
+
catch {
|
|
1042
|
+
// Reported below with the non-object case.
|
|
1043
|
+
}
|
|
1044
|
+
warnOnce(`context index "${c.id}": ${path} is not valid JSON, so its name and folder decompile from the node.`);
|
|
1045
|
+
return undefined;
|
|
1046
|
+
}
|
|
1120
1047
|
/**
|
|
1121
1048
|
* An AGENT and its attached resources — the inline autonomous agent, the
|
|
1122
1049
|
* conversational agent, and the voice agent.
|
|
@@ -1170,18 +1097,47 @@ function emitAgentCluster(node, imp, inputNames, graph) {
|
|
|
1170
1097
|
.slice()
|
|
1171
1098
|
.sort((a, b) => vars.findIndex((v) => v.id === a.flatName) - vars.findIndex((v) => v.id === b.flatName))
|
|
1172
1099
|
.map((r) => {
|
|
1173
|
-
|
|
1174
|
-
return [r.flatName, emitBareExpr(js, imp, inputNames)];
|
|
1100
|
+
return [r.flatName, emitBareExpr(wireExprBody(r.binding), imp, inputNames)];
|
|
1175
1101
|
});
|
|
1176
1102
|
const outVars = (Array.isArray(i.agentOutputVariables) ? i.agentOutputVariables : []);
|
|
1177
1103
|
const returns = outVars.length
|
|
1178
1104
|
? objLit(outVars.map((v) => [String(v.id ?? ''), tsLiteral(String(v.type ?? 'string'))]))
|
|
1179
1105
|
: undefined;
|
|
1180
|
-
// The resources, from the agent's own artifact edges.
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1106
|
+
// The resources, from the agent's own artifact edges. Each edge read here is
|
|
1107
|
+
// carried by the agent spec (edge accounting, #1125); `limit` is for a port
|
|
1108
|
+
// the spec carries only one resource on.
|
|
1109
|
+
const resource = (port, limit = Infinity) => {
|
|
1110
|
+
const found = [];
|
|
1111
|
+
for (const e of graph?.outEdges(node.id) ?? []) {
|
|
1112
|
+
if (e.sourcePort !== port || found.length >= limit)
|
|
1113
|
+
continue;
|
|
1114
|
+
const n = graph.byId.get(e.targetNodeId);
|
|
1115
|
+
if (n === undefined)
|
|
1116
|
+
continue;
|
|
1117
|
+
graph.accounted.add(e);
|
|
1118
|
+
found.push(n);
|
|
1119
|
+
}
|
|
1120
|
+
return found;
|
|
1121
|
+
};
|
|
1122
|
+
// Record what compile will call each folded resource node. Serialize mints
|
|
1123
|
+
// `uid(scope, \`${step.name}<Kind>\`)` per resource, in spec order, and the
|
|
1124
|
+
// spec order is this edge order: the first is `<agent><Kind>`, the n-th
|
|
1125
|
+
// `<agent><Kind><n>` (claimId's dedupe). Only the first memory is recovered,
|
|
1126
|
+
// so every further memory maps to `undefined`.
|
|
1127
|
+
// This reads the edges directly, not through `resource()`: it must see EVERY
|
|
1128
|
+
// artifact edge, including ones the spec does not carry, and it must not
|
|
1129
|
+
// account for any edge.
|
|
1130
|
+
for (const [port, kind, emittedCount] of [
|
|
1131
|
+
['context', 'Context', Infinity], ['tool', 'Tool', Infinity],
|
|
1132
|
+
['escalation', 'Escalation', Infinity], ['memory', 'Memory', 1],
|
|
1133
|
+
]) {
|
|
1134
|
+
const base = `${node.id}${kind}`.replace(/[^A-Za-z0-9_]/g, '_');
|
|
1135
|
+
(graph?.outEdges(node.id) ?? [])
|
|
1136
|
+
.filter((e) => e.sourcePort === port && graph.byId.has(e.targetNodeId))
|
|
1137
|
+
.forEach((e, k) => {
|
|
1138
|
+
runAbsorbed.set(e.targetNodeId, k >= emittedCount ? undefined : k === 0 ? base : `${base}${k + 1}`);
|
|
1139
|
+
});
|
|
1140
|
+
}
|
|
1185
1141
|
const rInputs = (n) => (n.inputs ?? {});
|
|
1186
1142
|
const rPlain = (n, k) => {
|
|
1187
1143
|
const raw = unwrapSource(rInputs(n)[k]);
|
|
@@ -1292,10 +1248,11 @@ function emitAgentCluster(node, imp, inputNames, graph) {
|
|
|
1292
1248
|
: typeof q?.textValue === 'string' && q.textValue !== '' ? q.textValue
|
|
1293
1249
|
: typeof q === 'string' && q !== '' ? q
|
|
1294
1250
|
: undefined;
|
|
1251
|
+
const { name, id, folderPath } = contextIndexIdentity(node, c, rPlain);
|
|
1295
1252
|
return opts([
|
|
1296
|
-
['name', tsLiteral(
|
|
1297
|
-
['id', tsLiteral(
|
|
1298
|
-
['folderPath',
|
|
1253
|
+
['name', tsLiteral(name)],
|
|
1254
|
+
['id', tsLiteral(id)],
|
|
1255
|
+
['folderPath', folderPath !== undefined ? tsLiteral(folderPath) : undefined],
|
|
1299
1256
|
['folderKey', rPlain(c, 'folderKey') !== undefined ? tsLiteral(rPlain(c, 'folderKey')) : undefined],
|
|
1300
1257
|
['query', queryText !== undefined ? tsLiteral(queryText) : undefined],
|
|
1301
1258
|
]);
|
|
@@ -1318,7 +1275,7 @@ function emitAgentCluster(node, imp, inputNames, graph) {
|
|
|
1318
1275
|
['description', rPlain(e, 'description') !== undefined ? tsLiteral(rPlain(e, 'description')) : undefined],
|
|
1319
1276
|
['fields', rows.length ? `[${rows.map((f) => opts([
|
|
1320
1277
|
['id', tsText(f.id)],
|
|
1321
|
-
['type', tsLiteral(
|
|
1278
|
+
['type', tsLiteral(sdkFormFieldType(f.type ?? 'text') ?? String(f.type))],
|
|
1322
1279
|
['direction', tsLiteral(String(f.direction ?? 'output'))],
|
|
1323
1280
|
])).join(', ')}]` : undefined],
|
|
1324
1281
|
['app', app === undefined ? undefined : opts([
|
|
@@ -1333,7 +1290,7 @@ function emitAgentCluster(node, imp, inputNames, graph) {
|
|
|
1333
1290
|
['priority', typeof extra.priority === 'string' && extra.priority !== 'medium' ? tsLiteral(extra.priority) : undefined],
|
|
1334
1291
|
]);
|
|
1335
1292
|
});
|
|
1336
|
-
const mem = resource('memory')[0];
|
|
1293
|
+
const mem = resource('memory', 1)[0];
|
|
1337
1294
|
const memorySrc = mem === undefined ? undefined : (() => {
|
|
1338
1295
|
// The memory's id is the node type's tail; the def carries it too.
|
|
1339
1296
|
const tail = mem.type.slice('uipath.agent.resource.memory.'.length);
|
|
@@ -1368,8 +1325,9 @@ function emitAgentCluster(node, imp, inputNames, graph) {
|
|
|
1368
1325
|
['inputs', inputRows.length ? objLit(inputRows) : undefined],
|
|
1369
1326
|
['callContext', (() => {
|
|
1370
1327
|
const raw = plain(i.callContext) ?? '';
|
|
1371
|
-
const js =
|
|
1372
|
-
|
|
1328
|
+
const js = wireExprBody(raw);
|
|
1329
|
+
// Anything but a `$vars.` reference stays the wire text verbatim.
|
|
1330
|
+
return js.startsWith('$vars.') ? emitBareExpr(js, imp, inputNames) : tsLiteral(raw);
|
|
1373
1331
|
})()],
|
|
1374
1332
|
['voice', voiceSrc],
|
|
1375
1333
|
['maxIterations', num(i.maxIterations)],
|
|
@@ -1471,7 +1429,7 @@ function emitHitl(node, imp, inputNames, graph) {
|
|
|
1471
1429
|
const raw = plain(v);
|
|
1472
1430
|
if (raw === undefined)
|
|
1473
1431
|
return undefined;
|
|
1474
|
-
const js =
|
|
1432
|
+
const js = wireExprBody(raw);
|
|
1475
1433
|
return emitBareExpr(js.startsWith('vars.') ? `$${js}` : js, imp, inputNames);
|
|
1476
1434
|
};
|
|
1477
1435
|
const rows = (Array.isArray(schema.fields) ? schema.fields : []);
|
|
@@ -1482,17 +1440,29 @@ function emitHitl(node, imp, inputNames, graph) {
|
|
|
1482
1440
|
['id', tsLiteral(id)],
|
|
1483
1441
|
// serialize defaults a row's label to its id.
|
|
1484
1442
|
['label', typeof f.label === 'string' && f.label !== id ? tsLiteral(f.label) : undefined],
|
|
1485
|
-
|
|
1443
|
+
// The product's own spelling (`"string"`, `"datetime"`, …) → the SDK's (#1143).
|
|
1444
|
+
['type', tsLiteral(sdkFormFieldType(f.type ?? 'text') ?? String(f.type))],
|
|
1486
1445
|
['direction', tsLiteral(direction)],
|
|
1487
1446
|
['value', direction === 'input' || direction === 'inOut' ? bindingExpr(f.binding) : undefined],
|
|
1488
1447
|
['required', f.required === true ? 'true' : undefined],
|
|
1489
1448
|
]);
|
|
1490
1449
|
});
|
|
1491
1450
|
const outcomeRows = (Array.isArray(schema.outcomes) ? schema.outcomes : []);
|
|
1492
|
-
const outcomes = outcomeRows.map((o) => {
|
|
1451
|
+
const outcomes = outcomeRows.map((o, idx) => {
|
|
1493
1452
|
const name = String(o.name ?? '');
|
|
1494
|
-
//
|
|
1495
|
-
|
|
1453
|
+
// The file's id comes back only when serialize would not derive it from the
|
|
1454
|
+
// name: a designer-drawn task's ids (`outcome-1778015958049`) are not slugs,
|
|
1455
|
+
// and merge keeps the ORIGINAL node, so recompiled edges must use its ids
|
|
1456
|
+
// (#1130). An SDK-written task's ids are slugs and stay implicit.
|
|
1457
|
+
const id = typeof o.id === 'string' && o.id !== '' && o.id !== outcomeSlug(name, idx) ? o.id : undefined;
|
|
1458
|
+
// A bare string is the shorthand; `action: 'End'` or a kept id needs the object.
|
|
1459
|
+
return o.action === 'End' || id !== undefined
|
|
1460
|
+
? opts([
|
|
1461
|
+
['name', tsLiteral(name)],
|
|
1462
|
+
['action', o.action === 'End' ? tsLiteral('End') : undefined],
|
|
1463
|
+
['id', id !== undefined ? tsLiteral(id) : undefined],
|
|
1464
|
+
])
|
|
1465
|
+
: tsLiteral(name);
|
|
1496
1466
|
});
|
|
1497
1467
|
// Delivery: only an override comes back. The definition's own default is what
|
|
1498
1468
|
// serialize emits when the author wrote nothing.
|
|
@@ -1523,7 +1493,7 @@ function emitHitl(node, imp, inputNames, graph) {
|
|
|
1523
1493
|
const bindings = (i.appInputBindings ?? {});
|
|
1524
1494
|
const appInputs = Object.keys(bindings).length === 0 ? undefined : objLit(Object.entries(bindings).map(([k, v]) => {
|
|
1525
1495
|
const raw = String(v ?? '');
|
|
1526
|
-
return [k, raw.startsWith('=') ? emitBareExpr(
|
|
1496
|
+
return [k, raw.startsWith('=') ? emitBareExpr(wireExprBody(raw), imp, inputNames) : tsLiteral(raw)];
|
|
1527
1497
|
}));
|
|
1528
1498
|
const appSrc = app === undefined ? undefined : opts([
|
|
1529
1499
|
// `displayName` is serialize's echo of `name`; only `name` is authored.
|
|
@@ -1534,13 +1504,14 @@ function emitHitl(node, imp, inputNames, graph) {
|
|
|
1534
1504
|
: typeof app.appSystemName === 'string' ? tsLiteral(app.appSystemName) : undefined],
|
|
1535
1505
|
['inputs', appInputs],
|
|
1536
1506
|
]);
|
|
1537
|
-
// The validation station's own inputs ride as PLAIN `$vars.…` paths.
|
|
1507
|
+
// The validation station's own inputs ride as PLAIN `$vars.…` paths. Anything
|
|
1508
|
+
// but a `$vars.` reference stays the wire text verbatim.
|
|
1538
1509
|
const stationExpr = (v) => {
|
|
1539
1510
|
const raw = plain(v);
|
|
1540
1511
|
if (raw === undefined)
|
|
1541
1512
|
return undefined;
|
|
1542
|
-
const js =
|
|
1543
|
-
return js.startsWith('$vars.') ? emitBareExpr(js, imp, inputNames) : tsLiteral(
|
|
1513
|
+
const js = wireExprBody(raw);
|
|
1514
|
+
return js.startsWith('$vars.') ? emitBareExpr(js, imp, inputNames) : tsLiteral(raw);
|
|
1544
1515
|
};
|
|
1545
1516
|
const render = plain(i.validationRenderType);
|
|
1546
1517
|
const documentSrc = node.type !== T.hitlDocValidation ? undefined : opts([
|
|
@@ -2018,7 +1989,7 @@ function scopeSucc(graph, id, scope) {
|
|
|
2018
1989
|
* a fork. Returns a map from node id → its ipdom (`EXIT` when its only join is
|
|
2019
1990
|
* the scope exit).
|
|
2020
1991
|
*/
|
|
2021
|
-
function postDominators(entry, scope, graph) {
|
|
1992
|
+
function postDominators(entry, scope, graph, container) {
|
|
2022
1993
|
// Forward successors within scope (EXIT for terminals / scope-exits), over the
|
|
2023
1994
|
// subgraph reachable from `entry`.
|
|
2024
1995
|
const succ = new Map();
|
|
@@ -2032,7 +2003,13 @@ function postDominators(entry, scope, graph) {
|
|
|
2032
2003
|
continue;
|
|
2033
2004
|
seen.add(n);
|
|
2034
2005
|
const s = scopeSucc(graph, n, scope);
|
|
2035
|
-
|
|
2006
|
+
// An edge back into the enclosing container (`continue` / `break` /
|
|
2007
|
+
// `loopBack`) leaves the body: it is a path to EXIT like a terminal is, so
|
|
2008
|
+
// a branch with one empty arm does not "reconverge" at the other arm's
|
|
2009
|
+
// first step.
|
|
2010
|
+
const leaves = container !== undefined
|
|
2011
|
+
&& graph.successorEdges(n).some((e) => e.targetNodeId === container);
|
|
2012
|
+
const ss = s.length === 0 ? [EXIT] : leaves ? [...s, EXIT] : s;
|
|
2036
2013
|
succ.set(n, ss);
|
|
2037
2014
|
for (const t of ss) {
|
|
2038
2015
|
addP(n, t);
|
|
@@ -2091,9 +2068,164 @@ function postDominators(entry, scope, graph) {
|
|
|
2091
2068
|
}
|
|
2092
2069
|
return ipdom;
|
|
2093
2070
|
}
|
|
2094
|
-
/**
|
|
2071
|
+
/**
|
|
2072
|
+
* The target of `id`'s first edge on `port`. Every caller walks into what it
|
|
2073
|
+
* returns, so the edge counts as emitted (edge accounting, #1125). A second edge
|
|
2074
|
+
* on the same port is not carried and stays unaccounted.
|
|
2075
|
+
*/
|
|
2095
2076
|
function portTarget(graph, id, port) {
|
|
2096
|
-
|
|
2077
|
+
const edge = graph.outEdges(id).find((e) => e.sourcePort === port);
|
|
2078
|
+
if (edge)
|
|
2079
|
+
graph.accounted.add(edge);
|
|
2080
|
+
return edge?.targetNodeId;
|
|
2081
|
+
}
|
|
2082
|
+
/** `id`'s first edge on `port`, for a caller that walks it with `edgeRegion`. */
|
|
2083
|
+
function portEdge(graph, id, port) {
|
|
2084
|
+
return graph.outEdges(id).find((e) => e.sourcePort === port);
|
|
2085
|
+
}
|
|
2086
|
+
/**
|
|
2087
|
+
* The region an arm, handler or port list starts with `edge` — accounting for
|
|
2088
|
+
* the edge (#1125). An edge straight back into the enclosing container (an empty
|
|
2089
|
+
* arm inside a loop body, wired to the container's `continue`) is no region at all,
|
|
2090
|
+
* not `.stepToRef(<container>)`, which compiles to the container's `input` port
|
|
2091
|
+
* and re-enters the loop. On the body's main path that edge is one serialize
|
|
2092
|
+
* recreates for an empty arm; on a side path (`refInto` set: a handler, a port
|
|
2093
|
+
* list, an arm inside one) it is `.continue()`. An edge straight to the
|
|
2094
|
+
* container's `break` handle is `.break()`.
|
|
2095
|
+
*/
|
|
2096
|
+
function edgeRegion(edge, stop, scope, ipdom, ctx, refInto) {
|
|
2097
|
+
if (edge === undefined)
|
|
2098
|
+
return [];
|
|
2099
|
+
if (ctx.breakInto !== undefined && edge.targetNodeId === ctx.breakInto) {
|
|
2100
|
+
// An arm that breaks straight away: `.break()` on its own recreates the edge.
|
|
2101
|
+
if (edge.targetPort === 'break') {
|
|
2102
|
+
ctx.graph.accounted.add(edge);
|
|
2103
|
+
return ['.break()'];
|
|
2104
|
+
}
|
|
2105
|
+
if (edge.targetPort === 'continue' || edge.targetPort === 'loopBack') {
|
|
2106
|
+
ctx.graph.accounted.add(edge);
|
|
2107
|
+
return refInto === undefined ? [] : ['.continue()'];
|
|
2108
|
+
}
|
|
2109
|
+
return [];
|
|
2110
|
+
}
|
|
2111
|
+
ctx.graph.accounted.add(edge);
|
|
2112
|
+
return region(edge.targetNodeId, stop, scope, ipdom, ctx, refInto);
|
|
2113
|
+
}
|
|
2114
|
+
/** Account for the successor edges of `from` that a walk followed to `to` (#1125). */
|
|
2115
|
+
function follow(graph, from, to) {
|
|
2116
|
+
if (to === undefined)
|
|
2117
|
+
return undefined;
|
|
2118
|
+
for (const e of graph.successorEdges(from)) {
|
|
2119
|
+
if (e.targetNodeId === to && e.targetPort !== 'loopBack')
|
|
2120
|
+
graph.accounted.add(e);
|
|
2121
|
+
}
|
|
2122
|
+
return to;
|
|
2123
|
+
}
|
|
2124
|
+
/**
|
|
2125
|
+
* Account for the edges serialize recreates on its own when `node` ends a path:
|
|
2126
|
+
* a loop or do-while body tail on the MAIN path of the body goes back to the
|
|
2127
|
+
* container's `continue` handle (`loopBack` on a 1.0 loop) without any source
|
|
2128
|
+
* spelling it. A tail on a side path — an `.onError` handler, an HTTP branch,
|
|
2129
|
+
* a non-primary outcome (`refInto` set) — does NOT: serialize caps it with an
|
|
2130
|
+
* End of its own, so its edge into the container is left unaccounted.
|
|
2131
|
+
*/
|
|
2132
|
+
function accountImplicitTail(node, ctx, refInto) {
|
|
2133
|
+
if (ctx.breakInto === undefined || refInto !== undefined)
|
|
2134
|
+
return;
|
|
2135
|
+
for (const e of ctx.graph.outEdges(node.id)) {
|
|
2136
|
+
if (e.targetNodeId === ctx.breakInto && (e.targetPort === 'continue' || e.targetPort === 'loopBack')) {
|
|
2137
|
+
ctx.graph.accounted.add(e);
|
|
2138
|
+
}
|
|
2139
|
+
}
|
|
2140
|
+
}
|
|
2141
|
+
/**
|
|
2142
|
+
* Where the walk goes after `from` leaves on `port` — a container's `success`,
|
|
2143
|
+
* a join's `output`. That edge can lead straight back into the ENCLOSING
|
|
2144
|
+
* container (the body's tail: `inner@success -> outer@continue`). Walking into
|
|
2145
|
+
* the container would emit `.stepToRef(<container>)`, which compiles to its
|
|
2146
|
+
* `input` port and re-enters the loop, so it ends the path instead:
|
|
2147
|
+
*
|
|
2148
|
+
* - into `<container>@break`: `{ brk: true }`, the caller emits `.break()`;
|
|
2149
|
+
* - into `<container>@continue` / `@loopBack`: on the main path no cursor,
|
|
2150
|
+
* and the edge is one serialize recreates (`accountImplicitTail`); on a
|
|
2151
|
+
* side path `{ cont: true }`, the caller emits `.continue()`;
|
|
2152
|
+
* - anything else: the target, with the edge accounted (as `portTarget`).
|
|
2153
|
+
*/
|
|
2154
|
+
function continueOn(from, port, ctx, refInto) {
|
|
2155
|
+
const edge = portEdge(ctx.graph, from.id, port);
|
|
2156
|
+
if (edge !== undefined && ctx.breakInto !== undefined && edge.targetNodeId === ctx.breakInto) {
|
|
2157
|
+
if (edge.targetPort === 'break') {
|
|
2158
|
+
ctx.graph.accounted.add(edge);
|
|
2159
|
+
return { brk: true };
|
|
2160
|
+
}
|
|
2161
|
+
if (edge.targetPort === 'continue' || edge.targetPort === 'loopBack') {
|
|
2162
|
+
if (refInto !== undefined) {
|
|
2163
|
+
// A side path has no implicit tail: it goes on with `.continue()`.
|
|
2164
|
+
ctx.graph.accounted.add(edge);
|
|
2165
|
+
return { cont: true };
|
|
2166
|
+
}
|
|
2167
|
+
accountImplicitTail(from, ctx, refInto);
|
|
2168
|
+
return {};
|
|
2169
|
+
}
|
|
2170
|
+
}
|
|
2171
|
+
if (edge === undefined) {
|
|
2172
|
+
accountImplicitTail(from, ctx, refInto);
|
|
2173
|
+
return {};
|
|
2174
|
+
}
|
|
2175
|
+
ctx.graph.accounted.add(edge);
|
|
2176
|
+
return { cursor: edge.targetNodeId };
|
|
2177
|
+
}
|
|
2178
|
+
/** `<source>@<port> -> <target>@<port>` — how an edge is named in a message. */
|
|
2179
|
+
function edgeName(e) {
|
|
2180
|
+
return `${e.sourceNodeId ?? '?'}@${e.sourcePort ?? '?'} -> ${e.targetNodeId ?? '?'}@${e.targetPort ?? '?'}`;
|
|
2181
|
+
}
|
|
2182
|
+
/** The edges this run could not carry into the source, for the strict-mode refusal. */
|
|
2183
|
+
let runDropped = [];
|
|
2184
|
+
/** The warning for an edge with no spelling in the emitted source. */
|
|
2185
|
+
function droppedEdgeMessage(e) {
|
|
2186
|
+
return `edge ${edgeName(e)} has no spelling in the decompiled source, so recompiling drops it `
|
|
2187
|
+
+ '(and merge writes that loss into the original). If this edge matters to your edit, '
|
|
2188
|
+
+ 'change the .flow JSON directly instead of round-tripping it through the source.';
|
|
2189
|
+
}
|
|
2190
|
+
/**
|
|
2191
|
+
* The out-edges of `node` that nothing emitted for it accounts for — called
|
|
2192
|
+
* once the walk has finished with the node, so every construct it heads has had
|
|
2193
|
+
* its chance to claim them. Each is warned once, remembered for strict mode,
|
|
2194
|
+
* and returned as a `/* TODO *\/` segment for the site.
|
|
2195
|
+
*/
|
|
2196
|
+
function droppedEdgesSeg(node, ctx) {
|
|
2197
|
+
const dropped = ctx.graph.outEdges(node.id).filter((e) => !ctx.graph.accounted.has(e));
|
|
2198
|
+
if (dropped.length === 0)
|
|
2199
|
+
return undefined;
|
|
2200
|
+
for (const e of dropped) {
|
|
2201
|
+
ctx.graph.accounted.add(e);
|
|
2202
|
+
runDropped.push(e);
|
|
2203
|
+
warnOnce(droppedEdgeMessage(e));
|
|
2204
|
+
}
|
|
2205
|
+
return `/* TODO: ${commentText(`edge(s) ${dropped.map(edgeName).join(', ')} are NOT in this source; recompiling drops them.`)} */`;
|
|
2206
|
+
}
|
|
2207
|
+
/** Push `node`'s dropped-edge marker (if any) onto `segs`. */
|
|
2208
|
+
function flagDropped(node, ctx, segs) {
|
|
2209
|
+
const seg = droppedEdgesSeg(node, ctx);
|
|
2210
|
+
if (seg !== undefined)
|
|
2211
|
+
segs.push(seg);
|
|
2212
|
+
}
|
|
2213
|
+
/**
|
|
2214
|
+
* The edges no walk reached at all (their source was never emitted, typically
|
|
2215
|
+
* because the only path to it was itself dropped), as `// TODO` lines. Edges on
|
|
2216
|
+
* a detached node are left out: node accounting already reported that node.
|
|
2217
|
+
*/
|
|
2218
|
+
function sweepDroppedEdges(graph, skip) {
|
|
2219
|
+
const lines = [];
|
|
2220
|
+
for (const e of graph.edges) {
|
|
2221
|
+
if (graph.accounted.has(e) || skip.has(e.sourceNodeId) || skip.has(e.targetNodeId))
|
|
2222
|
+
continue;
|
|
2223
|
+
graph.accounted.add(e);
|
|
2224
|
+
runDropped.push(e);
|
|
2225
|
+
warnOnce(droppedEdgeMessage(e));
|
|
2226
|
+
lines.push(`// TODO: ${commentText(`edge ${edgeName(e)} is NOT in this source; recompiling drops it.`)}`);
|
|
2227
|
+
}
|
|
2228
|
+
return lines;
|
|
2097
2229
|
}
|
|
2098
2230
|
/** Re-indent every non-empty line of a (possibly multi-line) segment by `pad`. */
|
|
2099
2231
|
function reindent(seg, pad) {
|
|
@@ -2194,7 +2326,7 @@ function errorHandlerSeg(nodeId, succ, scope, ipdom, ctx) {
|
|
|
2194
2326
|
if (!errEdge)
|
|
2195
2327
|
return undefined;
|
|
2196
2328
|
const mainReach = forwardReach(succ, scope, ctx.graph);
|
|
2197
|
-
const handlerSegs =
|
|
2329
|
+
const handlerSegs = edgeRegion(errEdge, EXIT, scope, ipdom, ctx, mainReach);
|
|
2198
2330
|
if (handlerSegs.length === 0)
|
|
2199
2331
|
handlerSegs.push('.return({})');
|
|
2200
2332
|
return `.onError(${armCb('h', handlerSegs)})`;
|
|
@@ -2229,34 +2361,45 @@ function region(cursor, stop, scope, ipdom, ctx, refInto) {
|
|
|
2229
2361
|
ctx.emitted.add(cursor);
|
|
2230
2362
|
if (node.type === T.end) {
|
|
2231
2363
|
segs.push(returnSeg(node, ctx));
|
|
2364
|
+
flagDropped(node, ctx, segs);
|
|
2232
2365
|
return segs;
|
|
2233
2366
|
}
|
|
2234
2367
|
if (node.type === T.terminate) {
|
|
2235
2368
|
segs.push(`.terminate(${tsLiteral(node.id)}, undefined${optsArg(node, ctx)})`);
|
|
2369
|
+
flagDropped(node, ctx, segs);
|
|
2236
2370
|
return segs;
|
|
2237
2371
|
}
|
|
2238
2372
|
if (node.type === T.merge) {
|
|
2239
2373
|
// A bare merge reached on the main path (its fork was not detected as a
|
|
2240
2374
|
// parallel). Skip it and continue from its output — best effort.
|
|
2241
|
-
|
|
2375
|
+
const next = continueOn(node, 'output', ctx, refInto);
|
|
2376
|
+
if (next.brk || next.cont) {
|
|
2377
|
+
segs.push(next.brk ? '.break()' : '.continue()');
|
|
2378
|
+
flagDropped(node, ctx, segs);
|
|
2379
|
+
return segs;
|
|
2380
|
+
}
|
|
2381
|
+
cursor = next.cursor;
|
|
2382
|
+
flagDropped(node, ctx, segs);
|
|
2242
2383
|
continue;
|
|
2243
2384
|
}
|
|
2244
2385
|
if (node.type === T.decision) {
|
|
2245
2386
|
const r = ipdom.get(node.id) ?? stop;
|
|
2246
2387
|
const cond = emitBareExpr(String((node.inputs ?? {}).expression ?? ''), ctx.imp, ctx.inputNames);
|
|
2247
|
-
const thenSegs =
|
|
2248
|
-
const elseSegs =
|
|
2388
|
+
const thenSegs = edgeRegion(portEdge(ctx.graph, node.id, 'true'), r, scope, ipdom, ctx, refInto);
|
|
2389
|
+
const elseSegs = edgeRegion(portEdge(ctx.graph, node.id, 'false'), r, scope, ipdom, ctx, refInto);
|
|
2249
2390
|
const labels = armLabels(node);
|
|
2250
2391
|
if (labels.trueLabel)
|
|
2251
2392
|
thenSegs.unshift(`.label(${tsLiteral(labels.trueLabel)})`);
|
|
2252
2393
|
if (labels.falseLabel)
|
|
2253
2394
|
elseSegs.unshift(`.label(${tsLiteral(labels.falseLabel)})`);
|
|
2254
2395
|
segs.push(`.branch(${tsLiteral(node.id)}, ${cond},\n${reindent(armCb('t', thenSegs), ' ')},\n${reindent(armCb('e', elseSegs), ' ')}${verArg(node)})`);
|
|
2396
|
+
flagDropped(node, ctx, segs);
|
|
2255
2397
|
cursor = r === EXIT ? undefined : r;
|
|
2256
2398
|
continue;
|
|
2257
2399
|
}
|
|
2258
2400
|
if (node.type === T.switch) {
|
|
2259
2401
|
segs.push(emitSwitch(node, ipdom, scope, ctx, stop, refInto));
|
|
2402
|
+
flagDropped(node, ctx, segs);
|
|
2260
2403
|
const r = ipdom.get(node.id) ?? stop;
|
|
2261
2404
|
cursor = r === EXIT ? undefined : r;
|
|
2262
2405
|
continue;
|
|
@@ -2268,7 +2411,7 @@ function region(cursor, stop, scope, ipdom, ctx, refInto) {
|
|
|
2268
2411
|
// from the inner `start` handle. Break edges (body node -> container's
|
|
2269
2412
|
// `break` target) decompile to `.break()` inside the body region.
|
|
2270
2413
|
const bodyEntry = portTarget(ctx.graph, node.id, 'start') ?? portTarget(ctx.graph, node.id, 'output');
|
|
2271
|
-
const bodyIpdom = bodyEntry ? postDominators(bodyEntry, bodyScope, ctx.graph) : new Map();
|
|
2414
|
+
const bodyIpdom = bodyEntry ? postDominators(bodyEntry, bodyScope, ctx.graph, node.id) : new Map();
|
|
2272
2415
|
const prevBreak = ctx.breakInto;
|
|
2273
2416
|
ctx.breakInto = node.id;
|
|
2274
2417
|
const bodySegs = region(bodyEntry, EXIT, bodyScope, bodyIpdom, ctx);
|
|
@@ -2280,7 +2423,7 @@ function region(cursor, stop, scope, ipdom, ctx, refInto) {
|
|
|
2280
2423
|
extra.push('parallel: true');
|
|
2281
2424
|
const completion = unwrapSource(inputs.completionCondition);
|
|
2282
2425
|
if (typeof completion === 'string' && completion.length) {
|
|
2283
|
-
extra.push(`completionCondition: ${emitBareExpr(
|
|
2426
|
+
extra.push(`completionCondition: ${emitBareExpr(wireExprBody(completion), ctx.imp, ctx.inputNames)}`);
|
|
2284
2427
|
}
|
|
2285
2428
|
}
|
|
2286
2429
|
const limit = node.type === T.doWhile ? unwrapPlain(inputs.limit) : undefined;
|
|
@@ -2293,14 +2436,14 @@ function region(cursor, stop, scope, ipdom, ctx, refInto) {
|
|
|
2293
2436
|
if (node.type === T.doWhile) {
|
|
2294
2437
|
const condRaw = unwrapSource(inputs.condition);
|
|
2295
2438
|
const cond = typeof condRaw === 'string' && condRaw.length
|
|
2296
|
-
? emitBareExpr(
|
|
2439
|
+
? emitBareExpr(wireExprBody(condRaw), ctx.imp, ctx.inputNames)
|
|
2297
2440
|
: `${ctx.imp.use('lit')}(true)`;
|
|
2298
2441
|
segs.push(`.doWhile(${tsLiteral(node.id)}, ${cond}, ${armCb('b', bodySegs)}${opts})`);
|
|
2299
2442
|
}
|
|
2300
2443
|
else {
|
|
2301
2444
|
const collRaw = unwrapSource(inputs.collection);
|
|
2302
2445
|
const collection = collRaw !== undefined
|
|
2303
|
-
? emitBareExpr(
|
|
2446
|
+
? emitBareExpr(wireExprBody(collRaw), ctx.imp, ctx.inputNames)
|
|
2304
2447
|
: `${ctx.imp.use('lit')}([])`;
|
|
2305
2448
|
segs.push(`.loop(${tsLiteral(node.id)}, ${collection}, ${armCb('b', bodySegs)}${opts})`);
|
|
2306
2449
|
}
|
|
@@ -2312,12 +2455,22 @@ function region(cursor, stop, scope, ipdom, ctx, refInto) {
|
|
|
2312
2455
|
const containerHandler = errorHandlerSeg(node.id, containerSucc, scope, ipdom, ctx);
|
|
2313
2456
|
if (containerHandler)
|
|
2314
2457
|
segs.push(containerHandler);
|
|
2315
|
-
|
|
2458
|
+
const next = continueOn(node, 'success', ctx, refInto);
|
|
2459
|
+
if (next.brk || next.cont) {
|
|
2460
|
+
segs.push(next.brk ? '.break()' : '.continue()');
|
|
2461
|
+
flagDropped(node, ctx, segs);
|
|
2462
|
+
return segs;
|
|
2463
|
+
}
|
|
2464
|
+
cursor = next.cursor;
|
|
2465
|
+
flagDropped(node, ctx, segs);
|
|
2316
2466
|
continue;
|
|
2317
2467
|
}
|
|
2318
2468
|
if (node.type === T.subflow) {
|
|
2319
2469
|
segs.push(`.step(${tsLiteral(node.id)}, ${emitSubflow(node, ctx)}${optsArg(node, ctx)})`);
|
|
2320
|
-
cursor = scopeSucc(ctx.graph, node.id, scope)[0];
|
|
2470
|
+
cursor = follow(ctx.graph, node.id, scopeSucc(ctx.graph, node.id, scope)[0]);
|
|
2471
|
+
if (cursor === undefined)
|
|
2472
|
+
accountImplicitTail(node, ctx, refInto);
|
|
2473
|
+
flagDropped(node, ctx, segs);
|
|
2321
2474
|
continue;
|
|
2322
2475
|
}
|
|
2323
2476
|
// A regular action step (mock / script / http / transform / connector / …).
|
|
@@ -2333,11 +2486,12 @@ function region(cursor, stop, scope, ipdom, ctx, refInto) {
|
|
|
2333
2486
|
if (exitPorts && exitPorts.length > 1) {
|
|
2334
2487
|
const r = ipdom.get(node.id) ?? stop;
|
|
2335
2488
|
const armSrcs = exitPorts.map(({ value, port }) => {
|
|
2336
|
-
const armSegs =
|
|
2489
|
+
const armSegs = edgeRegion(portEdge(ctx.graph, node.id, port), r, scope, ipdom, ctx, refInto);
|
|
2337
2490
|
return `{ value: ${tsLiteral(value)}, body: ${armCb('b', armSegs)} }`;
|
|
2338
2491
|
});
|
|
2339
2492
|
const armList = `[\n${armSrcs.map((a) => reindent(a, ' ')).join(',\n')},\n]`;
|
|
2340
2493
|
segs.push(`.stepSwitch(${tsLiteral(node.id)}, ${spec}, ${armList}${optsArg(node, ctx)})`);
|
|
2494
|
+
flagDropped(node, ctx, segs);
|
|
2341
2495
|
cursor = r === EXIT ? undefined : r;
|
|
2342
2496
|
continue;
|
|
2343
2497
|
}
|
|
@@ -2352,7 +2506,7 @@ function region(cursor, stop, scope, ipdom, ctx, refInto) {
|
|
|
2352
2506
|
// machinery an error handler uses.
|
|
2353
2507
|
for (const be of ctx.graph.outEdges(node.id).filter((e) => e.sourcePort.startsWith('branch-'))) {
|
|
2354
2508
|
const mainReach = forwardReach(succ, scope, ctx.graph);
|
|
2355
|
-
const branchSegs =
|
|
2509
|
+
const branchSegs = edgeRegion(be, EXIT, scope, ipdom, ctx, mainReach);
|
|
2356
2510
|
if (branchSegs.length === 0)
|
|
2357
2511
|
branchSegs.push('.return({})');
|
|
2358
2512
|
segs.push(`.stepToList(${tsLiteral(be.sourcePort)}, ${armCb('p', branchSegs)})`);
|
|
@@ -2362,12 +2516,18 @@ function region(cursor, stop, scope, ipdom, ctx, refInto) {
|
|
|
2362
2516
|
// already a successor (see Graph.primaryOutcomePort).
|
|
2363
2517
|
// A converged fan-out has none: every outcome continues to the one
|
|
2364
2518
|
// successor, which is what `.step()` recompiles to.
|
|
2365
|
-
if (
|
|
2519
|
+
if (ctx.graph.convergedFanOut(node.id)) {
|
|
2520
|
+
// `.step()` with `outcomePorts: false` recompiles every one of these edges.
|
|
2521
|
+
for (const oe of ctx.graph.outEdges(node.id))
|
|
2522
|
+
if (oe.sourcePort.startsWith('outcome-'))
|
|
2523
|
+
ctx.graph.accounted.add(oe);
|
|
2524
|
+
}
|
|
2525
|
+
else {
|
|
2366
2526
|
const primary = ctx.graph.primaryOutcomePort(node.id);
|
|
2367
2527
|
for (const oe of ctx.graph.outEdges(node.id)
|
|
2368
2528
|
.filter((e) => e.sourcePort.startsWith('outcome-') && e.sourcePort !== primary)) {
|
|
2369
2529
|
const mainReach = forwardReach(succ, scope, ctx.graph);
|
|
2370
|
-
const outSegs =
|
|
2530
|
+
const outSegs = edgeRegion(oe, EXIT, scope, ipdom, ctx, mainReach);
|
|
2371
2531
|
if (outSegs.length === 0)
|
|
2372
2532
|
outSegs.push('.return({})');
|
|
2373
2533
|
segs.push(`.stepToList(${tsLiteral(oe.sourcePort)}, ${armCb('p', outSegs)})`);
|
|
@@ -2376,30 +2536,64 @@ function region(cursor, stop, scope, ipdom, ctx, refInto) {
|
|
|
2376
2536
|
// A body edge into the enclosing container's break handle: the path exits
|
|
2377
2537
|
// the loop here. Emit `.break()` and end the region — recompiling recreates
|
|
2378
2538
|
// exactly this edge and adds no continue back-edge for the path.
|
|
2379
|
-
|
|
2380
|
-
|
|
2539
|
+
const breakEdges = ctx.breakInto === undefined
|
|
2540
|
+
? []
|
|
2541
|
+
: ctx.graph.outEdges(node.id).filter((e) => e.targetNodeId === ctx.breakInto && e.targetPort === 'break');
|
|
2542
|
+
if (breakEdges.length > 0) {
|
|
2543
|
+
for (const be of breakEdges)
|
|
2544
|
+
ctx.graph.accounted.add(be);
|
|
2381
2545
|
segs.push('.break()');
|
|
2546
|
+
flagDropped(node, ctx, segs);
|
|
2547
|
+
return segs;
|
|
2548
|
+
}
|
|
2549
|
+
// A SIDE-path edge into the container's continue handle (an `.onError`
|
|
2550
|
+
// handler, an HTTP branch or outcome list that goes on to the next item):
|
|
2551
|
+
// `.continue()`. On the body's main path the same edge is the one every body
|
|
2552
|
+
// tail gets, so it stays implicit there.
|
|
2553
|
+
const continueEdges = ctx.breakInto === undefined || refInto === undefined
|
|
2554
|
+
? []
|
|
2555
|
+
: ctx.graph.outEdges(node.id).filter((e) => e.targetNodeId === ctx.breakInto
|
|
2556
|
+
&& (e.targetPort === 'continue' || e.targetPort === 'loopBack'));
|
|
2557
|
+
if (continueEdges.length > 0) {
|
|
2558
|
+
for (const ce of continueEdges)
|
|
2559
|
+
ctx.graph.accounted.add(ce);
|
|
2560
|
+
segs.push('.continue()');
|
|
2561
|
+
flagDropped(node, ctx, segs);
|
|
2382
2562
|
return segs;
|
|
2383
2563
|
}
|
|
2384
2564
|
// Continuation: 0 → terminal; 1 → follow; N same-port → parallel fork.
|
|
2385
|
-
if (succ.length === 0)
|
|
2565
|
+
if (succ.length === 0) {
|
|
2566
|
+
accountImplicitTail(node, ctx, refInto);
|
|
2567
|
+
flagDropped(node, ctx, segs);
|
|
2386
2568
|
return segs;
|
|
2569
|
+
}
|
|
2387
2570
|
if (succ.length === 1) {
|
|
2388
|
-
cursor = succ[0];
|
|
2571
|
+
cursor = follow(ctx.graph, node.id, succ[0]);
|
|
2572
|
+
flagDropped(node, ctx, segs);
|
|
2389
2573
|
continue;
|
|
2390
2574
|
}
|
|
2391
2575
|
// Parallel: the fork's ipdom is the merge join; each successor is one arm.
|
|
2392
2576
|
const join = ipdom.get(node.id);
|
|
2393
2577
|
if (join && join !== EXIT && ctx.graph.byId.get(join)?.type === T.merge) {
|
|
2394
|
-
const arms = succ.map((t, i) => armCb(`a${i}`, region(t, join, scope, ipdom, ctx, refInto)));
|
|
2578
|
+
const arms = succ.map((t, i) => armCb(`a${i}`, region(follow(ctx.graph, node.id, t), join, scope, ipdom, ctx, refInto)));
|
|
2395
2579
|
segs.push(`.parallel(${tsLiteral(join)}, [\n${arms.map((a) => reindent(a, ' ')).join(',\n')},\n]${verArg(ctx.graph.byId.get(join))})`);
|
|
2396
|
-
|
|
2580
|
+
flagDropped(node, ctx, segs);
|
|
2581
|
+
const next = continueOn(ctx.graph.byId.get(join), 'output', ctx, refInto);
|
|
2582
|
+
if (next.brk || next.cont) {
|
|
2583
|
+
segs.push(next.brk ? '.break()' : '.continue()');
|
|
2584
|
+
return segs;
|
|
2585
|
+
}
|
|
2586
|
+
cursor = next.cursor;
|
|
2397
2587
|
continue;
|
|
2398
2588
|
}
|
|
2399
|
-
// Unstructured fan-out we can't fold — best-effort marker.
|
|
2589
|
+
// Unstructured fan-out we can't fold — best-effort marker. Its successor
|
|
2590
|
+
// edges are what the marker reports, so they are not reported twice.
|
|
2400
2591
|
if (ctx.o.bestEffort === false)
|
|
2401
2592
|
throw new Error(`decompile: unstructured fan-out at "${node.id}".`);
|
|
2402
2593
|
segs.push(`/* TODO: unstructured fan-out at ${commentText(node.id)} (${commentText(succ.length)} successors) */`);
|
|
2594
|
+
for (const t of succ)
|
|
2595
|
+
follow(ctx.graph, node.id, t);
|
|
2596
|
+
flagDropped(node, ctx, segs);
|
|
2403
2597
|
return segs;
|
|
2404
2598
|
}
|
|
2405
2599
|
return segs;
|
|
@@ -2485,6 +2679,88 @@ function declaredExitsOfNode(node) {
|
|
|
2485
2679
|
}
|
|
2486
2680
|
return undefined;
|
|
2487
2681
|
}
|
|
2682
|
+
/**
|
|
2683
|
+
* Split a switch case expression into `<discriminant> === <literal>`, the only
|
|
2684
|
+
* form `.switch(on, [{ value }])` models. Returns `undefined` for any other
|
|
2685
|
+
* condition, so the caller can refuse it rather than emit a different one:
|
|
2686
|
+
*
|
|
2687
|
+
* - the split is at the LAST top-level `===` (equality is left-associative,
|
|
2688
|
+
* so that `===` is the outermost one); `==`, `!==` and `!=` never match;
|
|
2689
|
+
* - the right side must be one literal: JSON, or a single-quoted string;
|
|
2690
|
+
* - the left side must hold no top-level operator that binds looser than
|
|
2691
|
+
* `===` (`&&`, `||`, `??`, `?:`, `&`, `|`, `^`, `,`, `=`, `=>`), since
|
|
2692
|
+
* `a && b === 1` means `a && (b === 1)`, not `(a && b) === 1`.
|
|
2693
|
+
*
|
|
2694
|
+
* Strings, template literals and bracketed groups are skipped when scanning.
|
|
2695
|
+
*/
|
|
2696
|
+
function parseSwitchCase(expr) {
|
|
2697
|
+
let depth = 0;
|
|
2698
|
+
let lastEq = -1;
|
|
2699
|
+
const loose = [];
|
|
2700
|
+
for (let i = 0; i < expr.length; i++) {
|
|
2701
|
+
const ch = expr[i];
|
|
2702
|
+
if (ch === '"' || ch === "'" || ch === '`') {
|
|
2703
|
+
for (i++; i < expr.length && expr[i] !== ch; i++)
|
|
2704
|
+
if (expr[i] === '\\')
|
|
2705
|
+
i++;
|
|
2706
|
+
continue;
|
|
2707
|
+
}
|
|
2708
|
+
if (ch === '(' || ch === '[' || ch === '{') {
|
|
2709
|
+
depth++;
|
|
2710
|
+
continue;
|
|
2711
|
+
}
|
|
2712
|
+
if (ch === ')' || ch === ']' || ch === '}') {
|
|
2713
|
+
depth--;
|
|
2714
|
+
continue;
|
|
2715
|
+
}
|
|
2716
|
+
if (depth !== 0)
|
|
2717
|
+
continue;
|
|
2718
|
+
const two = expr.slice(i, i + 2);
|
|
2719
|
+
if (expr.startsWith('===', i)) {
|
|
2720
|
+
lastEq = i;
|
|
2721
|
+
i += 2;
|
|
2722
|
+
}
|
|
2723
|
+
else if (expr.startsWith('!==', i)) {
|
|
2724
|
+
i += 2;
|
|
2725
|
+
}
|
|
2726
|
+
else if (two === '?.' && !/[0-9]/.test(expr[i + 2] ?? '')) {
|
|
2727
|
+
i += 1; // optional chaining, not a conditional
|
|
2728
|
+
}
|
|
2729
|
+
else if (two === '==' || two === '!=' || two === '<=' || two === '>=') {
|
|
2730
|
+
i += 1;
|
|
2731
|
+
}
|
|
2732
|
+
else if (two === '&&' || two === '||' || two === '??' || two === '=>') {
|
|
2733
|
+
loose.push(i);
|
|
2734
|
+
i += 1;
|
|
2735
|
+
}
|
|
2736
|
+
else if ('?&|^,='.includes(ch)) {
|
|
2737
|
+
loose.push(i);
|
|
2738
|
+
}
|
|
2739
|
+
}
|
|
2740
|
+
if (lastEq < 0)
|
|
2741
|
+
return undefined;
|
|
2742
|
+
const lhs = expr.slice(0, lastEq).trim();
|
|
2743
|
+
const literal = expr.slice(lastEq + 3).trim();
|
|
2744
|
+
if (!lhs || loose.some((at) => at < lastEq))
|
|
2745
|
+
return undefined;
|
|
2746
|
+
try {
|
|
2747
|
+
return { lhs, literal, value: tsLiteral(JSON.parse(literal)) };
|
|
2748
|
+
}
|
|
2749
|
+
catch {
|
|
2750
|
+
const q = literal.match(/^'((?:[^'\\]|\\.)*)'$/);
|
|
2751
|
+
if (!q)
|
|
2752
|
+
return undefined;
|
|
2753
|
+
// The string's value, escapes resolved (`'it\'s'` is `it's`): rewrite it as
|
|
2754
|
+
// a JSON string, which shares JS's escapes except `\'`.
|
|
2755
|
+
const json = `"${q[1].replace(/\\(.)|"/g, (m, c) => (c === undefined ? '\\"' : c === "'" ? "'" : m))}"`;
|
|
2756
|
+
try {
|
|
2757
|
+
return { lhs, literal, value: tsLiteral(JSON.parse(json)) };
|
|
2758
|
+
}
|
|
2759
|
+
catch {
|
|
2760
|
+
return undefined;
|
|
2761
|
+
}
|
|
2762
|
+
}
|
|
2763
|
+
}
|
|
2488
2764
|
/**
|
|
2489
2765
|
* Emit a `.switch(...)` from a `core.logic.switch` node. Each case's serialized
|
|
2490
2766
|
* expression is `<discriminant> === <JSON-literal>`; the discriminant (shared by
|
|
@@ -2494,62 +2770,74 @@ function declaredExitsOfNode(node) {
|
|
|
2494
2770
|
function emitSwitch(node, ipdom, scope, ctx, stop, refInto) {
|
|
2495
2771
|
const r = ipdom.get(node.id) ?? stop;
|
|
2496
2772
|
const cases = (node.inputs ?? {}).cases ?? [];
|
|
2773
|
+
let caseIndex = 0;
|
|
2497
2774
|
let disc;
|
|
2498
2775
|
let nonUniform = false; // a case whose expr isn't `<disc> === <literal>`, or a differing discriminant
|
|
2499
2776
|
const caseSrcs = [];
|
|
2500
2777
|
for (const c of cases) {
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
if (m && lhs) {
|
|
2778
|
+
const parsed = parseSwitchCase(String(c.expression ?? ''));
|
|
2779
|
+
let value = '0';
|
|
2780
|
+
let valueLiteral = '';
|
|
2781
|
+
if (parsed) {
|
|
2506
2782
|
if (disc === undefined)
|
|
2507
|
-
disc = lhs;
|
|
2508
|
-
else if (disc !== lhs)
|
|
2783
|
+
disc = parsed.lhs;
|
|
2784
|
+
else if (disc !== parsed.lhs)
|
|
2509
2785
|
nonUniform = true;
|
|
2786
|
+
valueLiteral = parsed.literal;
|
|
2787
|
+
value = parsed.value;
|
|
2510
2788
|
}
|
|
2511
2789
|
else {
|
|
2512
2790
|
nonUniform = true;
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2791
|
+
// Keep the best-effort output the TODO comment points at.
|
|
2792
|
+
const m = String(c.expression ?? '').match(/^(.*?)\s*===?\s*(.*)$/);
|
|
2793
|
+
if (m && m[1].trim() && disc === undefined)
|
|
2794
|
+
disc = m[1].trim();
|
|
2795
|
+
if (m) {
|
|
2796
|
+
valueLiteral = m[2].trim();
|
|
2797
|
+
try {
|
|
2798
|
+
value = tsLiteral(JSON.parse(valueLiteral));
|
|
2799
|
+
}
|
|
2800
|
+
catch {
|
|
2801
|
+
const q = valueLiteral.match(/^'(.*)'$/);
|
|
2802
|
+
value = q ? tsLiteral(q[1]) : tsLiteral(valueLiteral);
|
|
2803
|
+
}
|
|
2525
2804
|
}
|
|
2526
2805
|
}
|
|
2527
|
-
|
|
2528
|
-
nonUniform = true;
|
|
2529
|
-
}
|
|
2530
|
-
const armSegs = region(portTarget(ctx.graph, node.id, `case-${c.id}`), r, scope, ipdom, ctx, refInto);
|
|
2806
|
+
const armSegs = edgeRegion(portEdge(ctx.graph, node.id, `case-${c.id}`), r, scope, ipdom, ctx, refInto);
|
|
2531
2807
|
const defaultLabel = `Case ${valueLiteral}`;
|
|
2532
2808
|
const labelPart = c.label && c.label !== defaultLabel ? `, label: ${tsLiteral(c.label)}` : '';
|
|
2533
|
-
|
|
2809
|
+
// The file's case id comes back when it is not the positional one serialize
|
|
2810
|
+
// derives (a designer-drawn switch uses `default-1`, …), so the recompiled
|
|
2811
|
+
// edges leave from the ORIGINAL node's handles, which merge keeps (#1130).
|
|
2812
|
+
caseIndex += 1;
|
|
2813
|
+
const idPart = typeof c.id === 'string' && c.id !== '' && c.id !== `case_${caseIndex}` ? `, id: ${tsLiteral(c.id)}` : '';
|
|
2814
|
+
caseSrcs.push(`{ value: ${value}${labelPart}${idPart}, body: ${armCb('b', armSegs)} }`);
|
|
2534
2815
|
}
|
|
2535
2816
|
const on = emitBareExpr(disc ?? '', ctx.imp, ctx.inputNames);
|
|
2536
|
-
const hasDefault = (node.inputs ?? {}).hasDefault === true ||
|
|
2817
|
+
const hasDefault = (node.inputs ?? {}).hasDefault === true || portEdge(ctx.graph, node.id, 'default') !== undefined;
|
|
2537
2818
|
const defArm = hasDefault
|
|
2538
|
-
? `, ${armCb('d',
|
|
2819
|
+
? `, ${armCb('d', edgeRegion(portEdge(ctx.graph, node.id, 'default'), r, scope, ipdom, ctx, refInto))}`
|
|
2539
2820
|
: '';
|
|
2540
2821
|
const caseList = `[\n${caseSrcs.map((c) => reindent(c, ' ')).join(',\n')},\n]`;
|
|
2541
2822
|
// The builder's `.switch(on, [{value}])` models `on === value` only. A flow whose
|
|
2542
2823
|
// cases carry arbitrary per-case boolean expressions (or a non-uniform
|
|
2543
2824
|
// discriminant) cannot round-trip through it — flag it rather than emit silently
|
|
2544
2825
|
// wrong values.
|
|
2826
|
+
if (nonUniform) {
|
|
2827
|
+
const listing = cases.map((c) => `${tsLiteral(c.label ?? c.id)}: ${tsLiteral(String(c.expression ?? ''))}`).join(', ');
|
|
2828
|
+
const message = `switch ${tsLiteral(node.id)} has per-case conditions that are not one shared \`<discriminant> === <value>\` `
|
|
2829
|
+
+ `(${listing}). \`.switch()\` models only that form, so the decompiled \`on\`/\`value\` are placeholders: `
|
|
2830
|
+
+ 'recompiling either fails or changes which case runs. Edit this switch in the .flow JSON directly.';
|
|
2831
|
+
if (runStrict)
|
|
2832
|
+
throw new Error(`decompile: ${message}`);
|
|
2833
|
+
warnOnce(message);
|
|
2834
|
+
}
|
|
2545
2835
|
const warn = nonUniform
|
|
2546
2836
|
? `\n/* TODO: this switch's case expressions are not a uniform \`<discriminant> === <value>\`; the recovered on/value are best-effort and need review. */`
|
|
2547
2837
|
: '';
|
|
2548
2838
|
const ver = verArg(node);
|
|
2549
2839
|
return `.switch(${tsLiteral(node.id)}, ${on}, ${caseList}${defArm}${ver && !defArm ? ', undefined' : ''}${ver})${warn}`;
|
|
2550
2840
|
}
|
|
2551
|
-
/** The manifest-less annotation node type — the only annotation `.flow` has. */
|
|
2552
|
-
const STICKY_NOTE_TYPE = 'stickyNote';
|
|
2553
2841
|
/**
|
|
2554
2842
|
* Recover `.stickyNotes([...])` from the scope's `stickyNote` nodes.
|
|
2555
2843
|
*
|
|
@@ -2617,12 +2905,21 @@ function stickyNotesSeg(nodes, flow) {
|
|
|
2617
2905
|
*/
|
|
2618
2906
|
function layoutSeg(flow, nodes, edges) {
|
|
2619
2907
|
const hoisted = (flow.layout?.nodes ?? {});
|
|
2908
|
+
// A layout key must name a node the recompiled flow will HAVE. A resource
|
|
2909
|
+
// folded into its agent comes back under the id compile mints for it; when
|
|
2910
|
+
// that is not the file's id (a canvas- or CLI-placed resource is named after
|
|
2911
|
+
// its index or tool, not `<agent>Context`), the file's id names nothing and
|
|
2912
|
+
// compile refuses the whole `.layout()`. So its entry, and any edge route that
|
|
2913
|
+
// ends at it, is left out and compile places the node itself.
|
|
2914
|
+
const renamed = (id) => runAbsorbed.has(id) && runAbsorbed.get(id) !== id;
|
|
2620
2915
|
const nodeEntries = [];
|
|
2621
2916
|
for (const n of nodes) {
|
|
2622
2917
|
// A sticky note carries its own geometry in `.stickyNotes()`; a layout entry
|
|
2623
2918
|
// for it would be a second, competing source for the same coordinates.
|
|
2624
2919
|
if (n.type === STICKY_NOTE_TYPE)
|
|
2625
2920
|
continue;
|
|
2921
|
+
if (renamed(n.id))
|
|
2922
|
+
continue;
|
|
2626
2923
|
const ui = { ...(n.ui ?? {}), ...(hoisted[n.id] ?? {}) };
|
|
2627
2924
|
const pos = ui.position;
|
|
2628
2925
|
if (!pos || typeof pos.x !== 'number' || typeof pos.y !== 'number')
|
|
@@ -2659,6 +2956,8 @@ function layoutSeg(flow, nodes, edges) {
|
|
|
2659
2956
|
}
|
|
2660
2957
|
if (!parts.length)
|
|
2661
2958
|
continue;
|
|
2959
|
+
if (renamed(e.sourceNodeId) || renamed(e.targetNodeId))
|
|
2960
|
+
continue;
|
|
2662
2961
|
const pair = `${e.sourceNodeId}->${e.targetNodeId}`;
|
|
2663
2962
|
if ((pairCount.get(pair) ?? 0) !== 1)
|
|
2664
2963
|
continue; // ambiguous — see the note above
|
|
@@ -2721,10 +3020,11 @@ function emitChildFlow(name, child, ctx) {
|
|
|
2721
3020
|
// Body: the child's trigger is its own manual Start; walk from its successor.
|
|
2722
3021
|
const trigger = (child.nodes ?? []).find((n) => isTrigger(n.type) && !(child.edges ?? []).some((e) => e.targetNodeId === n.id));
|
|
2723
3022
|
const scope = scopeIds(child.nodes ?? [], undefined);
|
|
2724
|
-
const entry = trigger ? scopeSucc(childCtx.graph, trigger.id, scope)[0] : undefined;
|
|
3023
|
+
const entry = trigger ? follow(childCtx.graph, trigger.id, scopeSucc(childCtx.graph, trigger.id, scope)[0]) : undefined;
|
|
2725
3024
|
const ipdom = entry ? postDominators(entry, scope, childCtx.graph) : new Map();
|
|
2726
3025
|
const bodySegs = entry ? region(entry, EXIT, scope, ipdom, childCtx) : [];
|
|
2727
3026
|
parts.push(...bodySegs);
|
|
3027
|
+
parts.push(...sweepDroppedEdges(childCtx.graph, new Set()));
|
|
2728
3028
|
// A subflow carries its own scoped layout, resolved against its own node ids.
|
|
2729
3029
|
const childNotes = stickyNotesSeg(child.nodes ?? [], child);
|
|
2730
3030
|
if (childNotes)
|
|
@@ -2747,11 +3047,13 @@ export function decompile(flow, options = {}) {
|
|
|
2747
3047
|
runWarn = options.warn;
|
|
2748
3048
|
runReadSidecar = options.readSidecar;
|
|
2749
3049
|
runWarned = new Set();
|
|
3050
|
+
runDropped = [];
|
|
2750
3051
|
runDefPairs = new Set((flow.definitions ?? [])
|
|
2751
3052
|
.filter((d) => typeof d?.nodeType === 'string' && typeof d?.version === 'string')
|
|
2752
3053
|
.map((d) => `${d.nodeType}@${d.version}`));
|
|
2753
3054
|
runDefs = new Map();
|
|
2754
3055
|
runRawManifests = [];
|
|
3056
|
+
runAbsorbed = new Map();
|
|
2755
3057
|
for (const d of (flow.definitions ?? [])) {
|
|
2756
3058
|
if (typeof d?.nodeType !== 'string')
|
|
2757
3059
|
continue;
|
|
@@ -2842,7 +3144,7 @@ export function decompile(flow, options = {}) {
|
|
|
2842
3144
|
const ctx = { graph, flow, imp, inputNames, o: options, emitted: new Set(), shared };
|
|
2843
3145
|
const scope = scopeIds(flow.nodes ?? [], undefined);
|
|
2844
3146
|
const startId = trigger?.id ?? 'start';
|
|
2845
|
-
const first = scopeSucc(graph, startId, scope)[0];
|
|
3147
|
+
const first = follow(graph, startId, scopeSucc(graph, startId, scope)[0]);
|
|
2846
3148
|
const ipdom = first !== undefined ? postDominators(first, scope, graph) : new Map();
|
|
2847
3149
|
const bodySegs = first !== undefined ? region(first, EXIT, scope, ipdom, ctx) : [];
|
|
2848
3150
|
// Additional roots decompile to .entryPoint(...): scoped inputs from the
|
|
@@ -2855,6 +3157,15 @@ export function decompile(flow, options = {}) {
|
|
|
2855
3157
|
continue;
|
|
2856
3158
|
extraEps.push(entryPointSeg(t, scope, ipdom, ctx));
|
|
2857
3159
|
}
|
|
3160
|
+
// Edge accounting (#1125): the walk above has claimed every edge it emitted
|
|
3161
|
+
// or that serialize recreates on its own. The rest are not in the source.
|
|
3162
|
+
const unreached = sweepDroppedEdges(graph, detachedIds);
|
|
3163
|
+
for (const line of unreached)
|
|
3164
|
+
e.line(d, line);
|
|
3165
|
+
if (runStrict && runDropped.length > 0) {
|
|
3166
|
+
throw new Error(`decompile: ${runDropped.length} edge(s) have no spelling in the decompiled source, so recompiling would drop them: `
|
|
3167
|
+
+ `${runDropped.map(edgeName).join(', ')}.`);
|
|
3168
|
+
}
|
|
2858
3169
|
const header = e.toString();
|
|
2859
3170
|
const body = bodySegs.map((s) => reindent(s, ' ')).join('\n');
|
|
2860
3171
|
const eps = extraEps.map((s) => reindent(s, ' ')).join('\n');
|
|
@@ -2925,7 +3236,7 @@ function entryPointSeg(t, scope, ipdom, ctx) {
|
|
|
2925
3236
|
optParts.push(`inputs: { ${scoped.map((g) => `${key(g.name ?? g.id)}: ${declExpr(g, true, ctx.imp, sharedOf(g))}`).join(', ')} }`);
|
|
2926
3237
|
}
|
|
2927
3238
|
optParts.push(...triggerOptParts(t));
|
|
2928
|
-
const first = scopeSucc(ctx.graph, t.id, scope)[0];
|
|
3239
|
+
const first = follow(ctx.graph, t.id, scopeSucc(ctx.graph, t.id, scope)[0]);
|
|
2929
3240
|
const prefixSegs = first !== undefined ? region(first, EXIT, scope, ipdom, ctx) : [];
|
|
2930
3241
|
const optArgText = optParts.length ? `, { ${optParts.join(', ')} }` : (prefixSegs.length ? ', undefined' : '');
|
|
2931
3242
|
const prefix = prefixSegs.length ? `, ${armCb('b', prefixSegs)}` : '';
|