@uipath/maestro-builder-sdk 6.19.0 → 6.20.1
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 +107 -107
- package/dist/api-members.md +358 -356
- package/dist/check.js +181 -45
- package/dist/cli/commands/flow.js +3 -0
- package/dist/compile-cli.js +51 -0
- package/dist/core/actions.d.ts +13 -5
- package/dist/core/actions.js +1 -1
- 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/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/process-resource-sort-order.d.ts +40 -0
- package/dist/core/process-resource-sort-order.js +44 -0
- 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 +680 -104
- package/dist/flow-expr-check.js +1 -1
- package/dist/flow-sdk.d.ts +33 -1
- package/dist/flow-sdk.js +21 -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/transform/flow/node-type.js +2 -1
- package/dist/registry/uip/pipeline-front-door.js +5 -3
- package/dist/serialize.js +112 -61
- package/package.json +2 -2
package/dist/decompile.js
CHANGED
|
@@ -30,17 +30,21 @@
|
|
|
30
30
|
*/
|
|
31
31
|
import { FLOW_SCHEMA_VERSIONS } from './format-profile.js';
|
|
32
32
|
import { preserveSchemaVersion } from './schema-version.js';
|
|
33
|
-
import { BUNDLED_DEFINITIONS, bundledVersions
|
|
33
|
+
import { BUNDLED_DEFINITIONS, bundledVersions } from './flow-definitions.js';
|
|
34
34
|
import { DEFAULT_TRIGGER_ID, Expr } from './core/expr.js';
|
|
35
35
|
import { FLOW_NODE_TYPES } from './flow-node-registry.js';
|
|
36
36
|
import { commentText } from './core/comment-text.js';
|
|
37
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';
|
|
38
40
|
import { parseIxpProjectType } from './core/actions.js';
|
|
39
41
|
import { readFlowGlobals } from './flow-variables.js';
|
|
40
42
|
import { missingBindingLabels } from './core/binding-messages.js';
|
|
41
43
|
import { prepareCommand } from './core/cli-spelling.js';
|
|
42
|
-
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';
|
|
43
46
|
import { eventFilterFromTreeLeaf } from './core/event-filters.js';
|
|
47
|
+
import { sdkFormFieldType } from './core/form-field-type.js';
|
|
44
48
|
/**
|
|
45
49
|
* The trigger id of the flow currently being decompiled, so a reference through a
|
|
46
50
|
* RENAMED trigger is still recognized as `input('x')` rather than falling back to
|
|
@@ -79,6 +83,17 @@ let runStrict = false;
|
|
|
79
83
|
/** `DecompileOptions.warn` for the current run, and the messages already sent to it (one line per finding). */
|
|
80
84
|
let runWarn;
|
|
81
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();
|
|
82
97
|
/** `DecompileOptions.readSidecar` for the current run. */
|
|
83
98
|
let runReadSidecar;
|
|
84
99
|
function warnOnce(message) {
|
|
@@ -112,19 +127,33 @@ function verClause(node) {
|
|
|
112
127
|
+ `the file's definitions[] disagrees with the instance.`);
|
|
113
128
|
}
|
|
114
129
|
}
|
|
115
|
-
//
|
|
116
|
-
//
|
|
117
|
-
//
|
|
118
|
-
//
|
|
119
|
-
//
|
|
120
|
-
//
|
|
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.
|
|
121
144
|
const registered = FLOW_NODE_TYPES.byTypeName(node.type);
|
|
122
|
-
const shipped = registered ? Object.keys(registered.type.versions)
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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 '';
|
|
128
157
|
}
|
|
129
158
|
return `version: ${tsLiteral(tv)}`;
|
|
130
159
|
}
|
|
@@ -393,7 +422,19 @@ class Graph {
|
|
|
393
422
|
byId = new Map();
|
|
394
423
|
out = new Map();
|
|
395
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;
|
|
396
436
|
constructor(nodes, edges) {
|
|
437
|
+
this.edges = edges;
|
|
397
438
|
for (const n of nodes)
|
|
398
439
|
this.byId.set(n.id, n);
|
|
399
440
|
for (const e of edges) {
|
|
@@ -504,9 +545,8 @@ function specValue(raw) {
|
|
|
504
545
|
* Print a node whose type is registered (flow-node-registry.ts): the node type
|
|
505
546
|
* deserializes it to its spec and prints the factory call. A node at a version
|
|
506
547
|
* the type does not register is lifted by its default version's implementation —
|
|
507
|
-
* as every built-in family lifts any version
|
|
508
|
-
* `{ version }`
|
|
509
|
-
* decompile time that it will. `undefined` when the type is not registered, or the node type declines
|
|
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
|
|
510
550
|
* the node; the caller's branches take it from there.
|
|
511
551
|
*/
|
|
512
552
|
function emitRegisteredSpec(node, imp, inputNames) {
|
|
@@ -780,7 +820,7 @@ function emitHttp(node, managed, imp, inputNames) {
|
|
|
780
820
|
const branchesSrc = branchRows.length
|
|
781
821
|
? `[${branchRows.map((b) => {
|
|
782
822
|
const cond = unwrapSource(b.conditionExpression) ?? '';
|
|
783
|
-
const expr = emitBareExpr(
|
|
823
|
+
const expr = emitBareExpr(wireExprBody(cond), imp, inputNames);
|
|
784
824
|
return `{ name: ${tsText(b.name ?? b.id)}, condition: ${expr} }`;
|
|
785
825
|
}).join(', ')}]`
|
|
786
826
|
: undefined;
|
|
@@ -938,6 +978,72 @@ function sidecarAgentInputs(node) {
|
|
|
938
978
|
...(isVoice && settings.voice && typeof settings.voice === 'object' ? { voice: settings.voice } : {}),
|
|
939
979
|
};
|
|
940
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
|
+
}
|
|
941
1047
|
/**
|
|
942
1048
|
* An AGENT and its attached resources — the inline autonomous agent, the
|
|
943
1049
|
* conversational agent, and the voice agent.
|
|
@@ -991,18 +1097,47 @@ function emitAgentCluster(node, imp, inputNames, graph) {
|
|
|
991
1097
|
.slice()
|
|
992
1098
|
.sort((a, b) => vars.findIndex((v) => v.id === a.flatName) - vars.findIndex((v) => v.id === b.flatName))
|
|
993
1099
|
.map((r) => {
|
|
994
|
-
|
|
995
|
-
return [r.flatName, emitBareExpr(js, imp, inputNames)];
|
|
1100
|
+
return [r.flatName, emitBareExpr(wireExprBody(r.binding), imp, inputNames)];
|
|
996
1101
|
});
|
|
997
1102
|
const outVars = (Array.isArray(i.agentOutputVariables) ? i.agentOutputVariables : []);
|
|
998
1103
|
const returns = outVars.length
|
|
999
1104
|
? objLit(outVars.map((v) => [String(v.id ?? ''), tsLiteral(String(v.type ?? 'string'))]))
|
|
1000
1105
|
: undefined;
|
|
1001
|
-
// The resources, from the agent's own artifact edges.
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
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
|
+
}
|
|
1006
1141
|
const rInputs = (n) => (n.inputs ?? {});
|
|
1007
1142
|
const rPlain = (n, k) => {
|
|
1008
1143
|
const raw = unwrapSource(rInputs(n)[k]);
|
|
@@ -1113,10 +1248,11 @@ function emitAgentCluster(node, imp, inputNames, graph) {
|
|
|
1113
1248
|
: typeof q?.textValue === 'string' && q.textValue !== '' ? q.textValue
|
|
1114
1249
|
: typeof q === 'string' && q !== '' ? q
|
|
1115
1250
|
: undefined;
|
|
1251
|
+
const { name, id, folderPath } = contextIndexIdentity(node, c, rPlain);
|
|
1116
1252
|
return opts([
|
|
1117
|
-
['name', tsLiteral(
|
|
1118
|
-
['id', tsLiteral(
|
|
1119
|
-
['folderPath',
|
|
1253
|
+
['name', tsLiteral(name)],
|
|
1254
|
+
['id', tsLiteral(id)],
|
|
1255
|
+
['folderPath', folderPath !== undefined ? tsLiteral(folderPath) : undefined],
|
|
1120
1256
|
['folderKey', rPlain(c, 'folderKey') !== undefined ? tsLiteral(rPlain(c, 'folderKey')) : undefined],
|
|
1121
1257
|
['query', queryText !== undefined ? tsLiteral(queryText) : undefined],
|
|
1122
1258
|
]);
|
|
@@ -1139,7 +1275,7 @@ function emitAgentCluster(node, imp, inputNames, graph) {
|
|
|
1139
1275
|
['description', rPlain(e, 'description') !== undefined ? tsLiteral(rPlain(e, 'description')) : undefined],
|
|
1140
1276
|
['fields', rows.length ? `[${rows.map((f) => opts([
|
|
1141
1277
|
['id', tsText(f.id)],
|
|
1142
|
-
['type', tsLiteral(
|
|
1278
|
+
['type', tsLiteral(sdkFormFieldType(f.type ?? 'text') ?? String(f.type))],
|
|
1143
1279
|
['direction', tsLiteral(String(f.direction ?? 'output'))],
|
|
1144
1280
|
])).join(', ')}]` : undefined],
|
|
1145
1281
|
['app', app === undefined ? undefined : opts([
|
|
@@ -1154,7 +1290,7 @@ function emitAgentCluster(node, imp, inputNames, graph) {
|
|
|
1154
1290
|
['priority', typeof extra.priority === 'string' && extra.priority !== 'medium' ? tsLiteral(extra.priority) : undefined],
|
|
1155
1291
|
]);
|
|
1156
1292
|
});
|
|
1157
|
-
const mem = resource('memory')[0];
|
|
1293
|
+
const mem = resource('memory', 1)[0];
|
|
1158
1294
|
const memorySrc = mem === undefined ? undefined : (() => {
|
|
1159
1295
|
// The memory's id is the node type's tail; the def carries it too.
|
|
1160
1296
|
const tail = mem.type.slice('uipath.agent.resource.memory.'.length);
|
|
@@ -1189,8 +1325,9 @@ function emitAgentCluster(node, imp, inputNames, graph) {
|
|
|
1189
1325
|
['inputs', inputRows.length ? objLit(inputRows) : undefined],
|
|
1190
1326
|
['callContext', (() => {
|
|
1191
1327
|
const raw = plain(i.callContext) ?? '';
|
|
1192
|
-
const js =
|
|
1193
|
-
|
|
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);
|
|
1194
1331
|
})()],
|
|
1195
1332
|
['voice', voiceSrc],
|
|
1196
1333
|
['maxIterations', num(i.maxIterations)],
|
|
@@ -1292,7 +1429,7 @@ function emitHitl(node, imp, inputNames, graph) {
|
|
|
1292
1429
|
const raw = plain(v);
|
|
1293
1430
|
if (raw === undefined)
|
|
1294
1431
|
return undefined;
|
|
1295
|
-
const js =
|
|
1432
|
+
const js = wireExprBody(raw);
|
|
1296
1433
|
return emitBareExpr(js.startsWith('vars.') ? `$${js}` : js, imp, inputNames);
|
|
1297
1434
|
};
|
|
1298
1435
|
const rows = (Array.isArray(schema.fields) ? schema.fields : []);
|
|
@@ -1303,17 +1440,29 @@ function emitHitl(node, imp, inputNames, graph) {
|
|
|
1303
1440
|
['id', tsLiteral(id)],
|
|
1304
1441
|
// serialize defaults a row's label to its id.
|
|
1305
1442
|
['label', typeof f.label === 'string' && f.label !== id ? tsLiteral(f.label) : undefined],
|
|
1306
|
-
|
|
1443
|
+
// The product's own spelling (`"string"`, `"datetime"`, …) → the SDK's (#1143).
|
|
1444
|
+
['type', tsLiteral(sdkFormFieldType(f.type ?? 'text') ?? String(f.type))],
|
|
1307
1445
|
['direction', tsLiteral(direction)],
|
|
1308
1446
|
['value', direction === 'input' || direction === 'inOut' ? bindingExpr(f.binding) : undefined],
|
|
1309
1447
|
['required', f.required === true ? 'true' : undefined],
|
|
1310
1448
|
]);
|
|
1311
1449
|
});
|
|
1312
1450
|
const outcomeRows = (Array.isArray(schema.outcomes) ? schema.outcomes : []);
|
|
1313
|
-
const outcomes = outcomeRows.map((o) => {
|
|
1451
|
+
const outcomes = outcomeRows.map((o, idx) => {
|
|
1314
1452
|
const name = String(o.name ?? '');
|
|
1315
|
-
//
|
|
1316
|
-
|
|
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);
|
|
1317
1466
|
});
|
|
1318
1467
|
// Delivery: only an override comes back. The definition's own default is what
|
|
1319
1468
|
// serialize emits when the author wrote nothing.
|
|
@@ -1344,7 +1493,7 @@ function emitHitl(node, imp, inputNames, graph) {
|
|
|
1344
1493
|
const bindings = (i.appInputBindings ?? {});
|
|
1345
1494
|
const appInputs = Object.keys(bindings).length === 0 ? undefined : objLit(Object.entries(bindings).map(([k, v]) => {
|
|
1346
1495
|
const raw = String(v ?? '');
|
|
1347
|
-
return [k, raw.startsWith('=') ? emitBareExpr(
|
|
1496
|
+
return [k, raw.startsWith('=') ? emitBareExpr(wireExprBody(raw), imp, inputNames) : tsLiteral(raw)];
|
|
1348
1497
|
}));
|
|
1349
1498
|
const appSrc = app === undefined ? undefined : opts([
|
|
1350
1499
|
// `displayName` is serialize's echo of `name`; only `name` is authored.
|
|
@@ -1355,13 +1504,14 @@ function emitHitl(node, imp, inputNames, graph) {
|
|
|
1355
1504
|
: typeof app.appSystemName === 'string' ? tsLiteral(app.appSystemName) : undefined],
|
|
1356
1505
|
['inputs', appInputs],
|
|
1357
1506
|
]);
|
|
1358
|
-
// 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.
|
|
1359
1509
|
const stationExpr = (v) => {
|
|
1360
1510
|
const raw = plain(v);
|
|
1361
1511
|
if (raw === undefined)
|
|
1362
1512
|
return undefined;
|
|
1363
|
-
const js =
|
|
1364
|
-
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);
|
|
1365
1515
|
};
|
|
1366
1516
|
const render = plain(i.validationRenderType);
|
|
1367
1517
|
const documentSrc = node.type !== T.hitlDocValidation ? undefined : opts([
|
|
@@ -1839,7 +1989,7 @@ function scopeSucc(graph, id, scope) {
|
|
|
1839
1989
|
* a fork. Returns a map from node id → its ipdom (`EXIT` when its only join is
|
|
1840
1990
|
* the scope exit).
|
|
1841
1991
|
*/
|
|
1842
|
-
function postDominators(entry, scope, graph) {
|
|
1992
|
+
function postDominators(entry, scope, graph, container) {
|
|
1843
1993
|
// Forward successors within scope (EXIT for terminals / scope-exits), over the
|
|
1844
1994
|
// subgraph reachable from `entry`.
|
|
1845
1995
|
const succ = new Map();
|
|
@@ -1853,7 +2003,13 @@ function postDominators(entry, scope, graph) {
|
|
|
1853
2003
|
continue;
|
|
1854
2004
|
seen.add(n);
|
|
1855
2005
|
const s = scopeSucc(graph, n, scope);
|
|
1856
|
-
|
|
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;
|
|
1857
2013
|
succ.set(n, ss);
|
|
1858
2014
|
for (const t of ss) {
|
|
1859
2015
|
addP(n, t);
|
|
@@ -1912,9 +2068,241 @@ function postDominators(entry, scope, graph) {
|
|
|
1912
2068
|
}
|
|
1913
2069
|
return ipdom;
|
|
1914
2070
|
}
|
|
1915
|
-
/**
|
|
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
|
+
*/
|
|
1916
2076
|
function portTarget(graph, id, port) {
|
|
1917
|
-
|
|
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;
|
|
2229
|
+
}
|
|
2230
|
+
/** Every out-edge of `id` on port `port`, in edge order. */
|
|
2231
|
+
function portEdges(graph, id, port) {
|
|
2232
|
+
return graph.outEdges(id).filter((e) => e.sourcePort === port);
|
|
2233
|
+
}
|
|
2234
|
+
/** `id`'s successor edges whose target is inside `scope` (the edges `scopeSucc` follows). */
|
|
2235
|
+
function scopeSuccEdges(graph, id, scope) {
|
|
2236
|
+
return graph.successorEdges(id).filter((e) => e.targetPort !== 'loopBack' && scope.has(e.targetNodeId));
|
|
2237
|
+
}
|
|
2238
|
+
/**
|
|
2239
|
+
* The nearest node that post-dominates EVERY one of `targets` — where the paths
|
|
2240
|
+
* that start at them all meet again — or `undefined` when they only meet at the
|
|
2241
|
+
* scope exit. Walks each target's `ipdom` chain (the target itself first).
|
|
2242
|
+
*/
|
|
2243
|
+
function commonPostDominator(targets, ipdom) {
|
|
2244
|
+
const chain = (t) => {
|
|
2245
|
+
const out = [];
|
|
2246
|
+
for (let n = t; n !== undefined && n !== EXIT && !out.includes(n); n = ipdom.get(n))
|
|
2247
|
+
out.push(n);
|
|
2248
|
+
return out;
|
|
2249
|
+
};
|
|
2250
|
+
const [head, ...rest] = targets.map(chain);
|
|
2251
|
+
const others = rest.map((c) => new Set(c));
|
|
2252
|
+
return head?.find((n) => others.every((c) => c.has(n)));
|
|
2253
|
+
}
|
|
2254
|
+
/**
|
|
2255
|
+
* The region that starts with `edges`, the edges ONE source port (or a
|
|
2256
|
+
* trigger) has.
|
|
2257
|
+
*
|
|
2258
|
+
* One edge is an ordinary region (`edgeRegion`). Two or more are a fork, which
|
|
2259
|
+
* is what serialize emits for a `.parallel(...)` that is the first thing in a
|
|
2260
|
+
* flow, a subflow, an entry point's prefix or a branch / switch arm: the trigger
|
|
2261
|
+
* (or the arm's port) gets one edge per parallel arm, and the arms meet at the
|
|
2262
|
+
* parallel's merge node. When the targets meet at a merge node the fork
|
|
2263
|
+
* decompiles to that `.parallel(...)`, then the region continues from the
|
|
2264
|
+
* merge's output (flow-builder-sdk#1155).
|
|
2265
|
+
*
|
|
2266
|
+
* Every edge in `edges` is accounted (edge accounting, #1125): the fold
|
|
2267
|
+
* recreates each one. A fork whose arms never meet at a merge node has no
|
|
2268
|
+
* builder spelling. Like the same shape mid-flow (`region`), it throws under
|
|
2269
|
+
* `bestEffort: false`, and otherwise keeps the first arm behind a
|
|
2270
|
+
* `TODO: unstructured fan-out` marker, which is what reports the other edges.
|
|
2271
|
+
*/
|
|
2272
|
+
function forkRegion(edges, stop, scope, ipdom, ctx, refInto) {
|
|
2273
|
+
if (edges.length <= 1)
|
|
2274
|
+
return edgeRegion(edges[0], stop, scope, ipdom, ctx, refInto);
|
|
2275
|
+
const targets = edges.map((e) => e.targetNodeId);
|
|
2276
|
+
const join = commonPostDominator(targets, ipdom);
|
|
2277
|
+
const joinNode = join !== undefined ? ctx.graph.byId.get(join) : undefined;
|
|
2278
|
+
if (join === undefined || join === stop || joinNode?.type !== T.merge) {
|
|
2279
|
+
if (ctx.o.bestEffort === false)
|
|
2280
|
+
throw new Error(`decompile: unstructured fan-out (${edges.length} edges).`);
|
|
2281
|
+
for (const e of edges)
|
|
2282
|
+
ctx.graph.accounted.add(e);
|
|
2283
|
+
return [
|
|
2284
|
+
`/* TODO: unstructured fan-out (${commentText(edges.length)} edges) */`,
|
|
2285
|
+
...region(targets[0], stop, scope, ipdom, ctx, refInto),
|
|
2286
|
+
];
|
|
2287
|
+
}
|
|
2288
|
+
for (const e of edges)
|
|
2289
|
+
ctx.graph.accounted.add(e);
|
|
2290
|
+
const fork = parallelSeg(join, joinNode, targets, scope, ipdom, ctx, refInto);
|
|
2291
|
+
if (fork.next.brk || fork.next.cont)
|
|
2292
|
+
return [fork.seg, fork.next.brk ? '.break()' : '.continue()'];
|
|
2293
|
+
return [fork.seg, ...region(fork.next.cursor, stop, scope, ipdom, ctx, refInto)];
|
|
2294
|
+
}
|
|
2295
|
+
/**
|
|
2296
|
+
* The `.parallel(join, [arms])` segment for a fork whose `targets` (one per
|
|
2297
|
+
* edge; the caller accounts those edges) meet at the merge node `join`. Each
|
|
2298
|
+
* arm is the region up to `join`. `next` is where the walk goes after the
|
|
2299
|
+
* merge: its `output` edge, accounted by `continueOn` (`brk` / `cont` when
|
|
2300
|
+
* that edge leaves through the enclosing loop's `break` / spelled `continue`).
|
|
2301
|
+
*/
|
|
2302
|
+
function parallelSeg(join, joinNode, targets, scope, ipdom, ctx, refInto) {
|
|
2303
|
+
const arms = targets.map((t, i) => armCb(`a${i}`, region(t, join, scope, ipdom, ctx, refInto)));
|
|
2304
|
+
const seg = `.parallel(${tsLiteral(join)}, [\n${arms.map((a) => reindent(a, ' ')).join(',\n')},\n]${verArg(joinNode)})`;
|
|
2305
|
+
return { seg, next: continueOn(joinNode, 'output', ctx, refInto) };
|
|
1918
2306
|
}
|
|
1919
2307
|
/** Re-indent every non-empty line of a (possibly multi-line) segment by `pad`. */
|
|
1920
2308
|
function reindent(seg, pad) {
|
|
@@ -2015,7 +2403,7 @@ function errorHandlerSeg(nodeId, succ, scope, ipdom, ctx) {
|
|
|
2015
2403
|
if (!errEdge)
|
|
2016
2404
|
return undefined;
|
|
2017
2405
|
const mainReach = forwardReach(succ, scope, ctx.graph);
|
|
2018
|
-
const handlerSegs =
|
|
2406
|
+
const handlerSegs = edgeRegion(errEdge, EXIT, scope, ipdom, ctx, mainReach);
|
|
2019
2407
|
if (handlerSegs.length === 0)
|
|
2020
2408
|
handlerSegs.push('.return({})');
|
|
2021
2409
|
return `.onError(${armCb('h', handlerSegs)})`;
|
|
@@ -2050,34 +2438,45 @@ function region(cursor, stop, scope, ipdom, ctx, refInto) {
|
|
|
2050
2438
|
ctx.emitted.add(cursor);
|
|
2051
2439
|
if (node.type === T.end) {
|
|
2052
2440
|
segs.push(returnSeg(node, ctx));
|
|
2441
|
+
flagDropped(node, ctx, segs);
|
|
2053
2442
|
return segs;
|
|
2054
2443
|
}
|
|
2055
2444
|
if (node.type === T.terminate) {
|
|
2056
2445
|
segs.push(`.terminate(${tsLiteral(node.id)}, undefined${optsArg(node, ctx)})`);
|
|
2446
|
+
flagDropped(node, ctx, segs);
|
|
2057
2447
|
return segs;
|
|
2058
2448
|
}
|
|
2059
2449
|
if (node.type === T.merge) {
|
|
2060
2450
|
// A bare merge reached on the main path (its fork was not detected as a
|
|
2061
2451
|
// parallel). Skip it and continue from its output — best effort.
|
|
2062
|
-
|
|
2452
|
+
const next = continueOn(node, 'output', ctx, refInto);
|
|
2453
|
+
if (next.brk || next.cont) {
|
|
2454
|
+
segs.push(next.brk ? '.break()' : '.continue()');
|
|
2455
|
+
flagDropped(node, ctx, segs);
|
|
2456
|
+
return segs;
|
|
2457
|
+
}
|
|
2458
|
+
cursor = next.cursor;
|
|
2459
|
+
flagDropped(node, ctx, segs);
|
|
2063
2460
|
continue;
|
|
2064
2461
|
}
|
|
2065
2462
|
if (node.type === T.decision) {
|
|
2066
2463
|
const r = ipdom.get(node.id) ?? stop;
|
|
2067
2464
|
const cond = emitBareExpr(String((node.inputs ?? {}).expression ?? ''), ctx.imp, ctx.inputNames);
|
|
2068
|
-
const thenSegs =
|
|
2069
|
-
const elseSegs =
|
|
2465
|
+
const thenSegs = forkRegion(portEdges(ctx.graph, node.id, 'true'), r, scope, ipdom, ctx, refInto);
|
|
2466
|
+
const elseSegs = forkRegion(portEdges(ctx.graph, node.id, 'false'), r, scope, ipdom, ctx, refInto);
|
|
2070
2467
|
const labels = armLabels(node);
|
|
2071
2468
|
if (labels.trueLabel)
|
|
2072
2469
|
thenSegs.unshift(`.label(${tsLiteral(labels.trueLabel)})`);
|
|
2073
2470
|
if (labels.falseLabel)
|
|
2074
2471
|
elseSegs.unshift(`.label(${tsLiteral(labels.falseLabel)})`);
|
|
2075
2472
|
segs.push(`.branch(${tsLiteral(node.id)}, ${cond},\n${reindent(armCb('t', thenSegs), ' ')},\n${reindent(armCb('e', elseSegs), ' ')}${verArg(node)})`);
|
|
2473
|
+
flagDropped(node, ctx, segs);
|
|
2076
2474
|
cursor = r === EXIT ? undefined : r;
|
|
2077
2475
|
continue;
|
|
2078
2476
|
}
|
|
2079
2477
|
if (node.type === T.switch) {
|
|
2080
2478
|
segs.push(emitSwitch(node, ipdom, scope, ctx, stop, refInto));
|
|
2479
|
+
flagDropped(node, ctx, segs);
|
|
2081
2480
|
const r = ipdom.get(node.id) ?? stop;
|
|
2082
2481
|
cursor = r === EXIT ? undefined : r;
|
|
2083
2482
|
continue;
|
|
@@ -2089,7 +2488,7 @@ function region(cursor, stop, scope, ipdom, ctx, refInto) {
|
|
|
2089
2488
|
// from the inner `start` handle. Break edges (body node -> container's
|
|
2090
2489
|
// `break` target) decompile to `.break()` inside the body region.
|
|
2091
2490
|
const bodyEntry = portTarget(ctx.graph, node.id, 'start') ?? portTarget(ctx.graph, node.id, 'output');
|
|
2092
|
-
const bodyIpdom = bodyEntry ? postDominators(bodyEntry, bodyScope, ctx.graph) : new Map();
|
|
2491
|
+
const bodyIpdom = bodyEntry ? postDominators(bodyEntry, bodyScope, ctx.graph, node.id) : new Map();
|
|
2093
2492
|
const prevBreak = ctx.breakInto;
|
|
2094
2493
|
ctx.breakInto = node.id;
|
|
2095
2494
|
const bodySegs = region(bodyEntry, EXIT, bodyScope, bodyIpdom, ctx);
|
|
@@ -2101,7 +2500,7 @@ function region(cursor, stop, scope, ipdom, ctx, refInto) {
|
|
|
2101
2500
|
extra.push('parallel: true');
|
|
2102
2501
|
const completion = unwrapSource(inputs.completionCondition);
|
|
2103
2502
|
if (typeof completion === 'string' && completion.length) {
|
|
2104
|
-
extra.push(`completionCondition: ${emitBareExpr(
|
|
2503
|
+
extra.push(`completionCondition: ${emitBareExpr(wireExprBody(completion), ctx.imp, ctx.inputNames)}`);
|
|
2105
2504
|
}
|
|
2106
2505
|
}
|
|
2107
2506
|
const limit = node.type === T.doWhile ? unwrapPlain(inputs.limit) : undefined;
|
|
@@ -2114,14 +2513,14 @@ function region(cursor, stop, scope, ipdom, ctx, refInto) {
|
|
|
2114
2513
|
if (node.type === T.doWhile) {
|
|
2115
2514
|
const condRaw = unwrapSource(inputs.condition);
|
|
2116
2515
|
const cond = typeof condRaw === 'string' && condRaw.length
|
|
2117
|
-
? emitBareExpr(
|
|
2516
|
+
? emitBareExpr(wireExprBody(condRaw), ctx.imp, ctx.inputNames)
|
|
2118
2517
|
: `${ctx.imp.use('lit')}(true)`;
|
|
2119
2518
|
segs.push(`.doWhile(${tsLiteral(node.id)}, ${cond}, ${armCb('b', bodySegs)}${opts})`);
|
|
2120
2519
|
}
|
|
2121
2520
|
else {
|
|
2122
2521
|
const collRaw = unwrapSource(inputs.collection);
|
|
2123
2522
|
const collection = collRaw !== undefined
|
|
2124
|
-
? emitBareExpr(
|
|
2523
|
+
? emitBareExpr(wireExprBody(collRaw), ctx.imp, ctx.inputNames)
|
|
2125
2524
|
: `${ctx.imp.use('lit')}([])`;
|
|
2126
2525
|
segs.push(`.loop(${tsLiteral(node.id)}, ${collection}, ${armCb('b', bodySegs)}${opts})`);
|
|
2127
2526
|
}
|
|
@@ -2133,12 +2532,22 @@ function region(cursor, stop, scope, ipdom, ctx, refInto) {
|
|
|
2133
2532
|
const containerHandler = errorHandlerSeg(node.id, containerSucc, scope, ipdom, ctx);
|
|
2134
2533
|
if (containerHandler)
|
|
2135
2534
|
segs.push(containerHandler);
|
|
2136
|
-
|
|
2535
|
+
const next = continueOn(node, 'success', ctx, refInto);
|
|
2536
|
+
if (next.brk || next.cont) {
|
|
2537
|
+
segs.push(next.brk ? '.break()' : '.continue()');
|
|
2538
|
+
flagDropped(node, ctx, segs);
|
|
2539
|
+
return segs;
|
|
2540
|
+
}
|
|
2541
|
+
cursor = next.cursor;
|
|
2542
|
+
flagDropped(node, ctx, segs);
|
|
2137
2543
|
continue;
|
|
2138
2544
|
}
|
|
2139
2545
|
if (node.type === T.subflow) {
|
|
2140
2546
|
segs.push(`.step(${tsLiteral(node.id)}, ${emitSubflow(node, ctx)}${optsArg(node, ctx)})`);
|
|
2141
|
-
cursor = scopeSucc(ctx.graph, node.id, scope)[0];
|
|
2547
|
+
cursor = follow(ctx.graph, node.id, scopeSucc(ctx.graph, node.id, scope)[0]);
|
|
2548
|
+
if (cursor === undefined)
|
|
2549
|
+
accountImplicitTail(node, ctx, refInto);
|
|
2550
|
+
flagDropped(node, ctx, segs);
|
|
2142
2551
|
continue;
|
|
2143
2552
|
}
|
|
2144
2553
|
// A regular action step (mock / script / http / transform / connector / …).
|
|
@@ -2154,11 +2563,12 @@ function region(cursor, stop, scope, ipdom, ctx, refInto) {
|
|
|
2154
2563
|
if (exitPorts && exitPorts.length > 1) {
|
|
2155
2564
|
const r = ipdom.get(node.id) ?? stop;
|
|
2156
2565
|
const armSrcs = exitPorts.map(({ value, port }) => {
|
|
2157
|
-
const armSegs =
|
|
2566
|
+
const armSegs = edgeRegion(portEdge(ctx.graph, node.id, port), r, scope, ipdom, ctx, refInto);
|
|
2158
2567
|
return `{ value: ${tsLiteral(value)}, body: ${armCb('b', armSegs)} }`;
|
|
2159
2568
|
});
|
|
2160
2569
|
const armList = `[\n${armSrcs.map((a) => reindent(a, ' ')).join(',\n')},\n]`;
|
|
2161
2570
|
segs.push(`.stepSwitch(${tsLiteral(node.id)}, ${spec}, ${armList}${optsArg(node, ctx)})`);
|
|
2571
|
+
flagDropped(node, ctx, segs);
|
|
2162
2572
|
cursor = r === EXIT ? undefined : r;
|
|
2163
2573
|
continue;
|
|
2164
2574
|
}
|
|
@@ -2173,7 +2583,7 @@ function region(cursor, stop, scope, ipdom, ctx, refInto) {
|
|
|
2173
2583
|
// machinery an error handler uses.
|
|
2174
2584
|
for (const be of ctx.graph.outEdges(node.id).filter((e) => e.sourcePort.startsWith('branch-'))) {
|
|
2175
2585
|
const mainReach = forwardReach(succ, scope, ctx.graph);
|
|
2176
|
-
const branchSegs =
|
|
2586
|
+
const branchSegs = edgeRegion(be, EXIT, scope, ipdom, ctx, mainReach);
|
|
2177
2587
|
if (branchSegs.length === 0)
|
|
2178
2588
|
branchSegs.push('.return({})');
|
|
2179
2589
|
segs.push(`.stepToList(${tsLiteral(be.sourcePort)}, ${armCb('p', branchSegs)})`);
|
|
@@ -2183,12 +2593,18 @@ function region(cursor, stop, scope, ipdom, ctx, refInto) {
|
|
|
2183
2593
|
// already a successor (see Graph.primaryOutcomePort).
|
|
2184
2594
|
// A converged fan-out has none: every outcome continues to the one
|
|
2185
2595
|
// successor, which is what `.step()` recompiles to.
|
|
2186
|
-
if (
|
|
2596
|
+
if (ctx.graph.convergedFanOut(node.id)) {
|
|
2597
|
+
// `.step()` with `outcomePorts: false` recompiles every one of these edges.
|
|
2598
|
+
for (const oe of ctx.graph.outEdges(node.id))
|
|
2599
|
+
if (oe.sourcePort.startsWith('outcome-'))
|
|
2600
|
+
ctx.graph.accounted.add(oe);
|
|
2601
|
+
}
|
|
2602
|
+
else {
|
|
2187
2603
|
const primary = ctx.graph.primaryOutcomePort(node.id);
|
|
2188
2604
|
for (const oe of ctx.graph.outEdges(node.id)
|
|
2189
2605
|
.filter((e) => e.sourcePort.startsWith('outcome-') && e.sourcePort !== primary)) {
|
|
2190
2606
|
const mainReach = forwardReach(succ, scope, ctx.graph);
|
|
2191
|
-
const outSegs =
|
|
2607
|
+
const outSegs = edgeRegion(oe, EXIT, scope, ipdom, ctx, mainReach);
|
|
2192
2608
|
if (outSegs.length === 0)
|
|
2193
2609
|
outSegs.push('.return({})');
|
|
2194
2610
|
segs.push(`.stepToList(${tsLiteral(oe.sourcePort)}, ${armCb('p', outSegs)})`);
|
|
@@ -2197,30 +2613,64 @@ function region(cursor, stop, scope, ipdom, ctx, refInto) {
|
|
|
2197
2613
|
// A body edge into the enclosing container's break handle: the path exits
|
|
2198
2614
|
// the loop here. Emit `.break()` and end the region — recompiling recreates
|
|
2199
2615
|
// exactly this edge and adds no continue back-edge for the path.
|
|
2200
|
-
|
|
2201
|
-
|
|
2616
|
+
const breakEdges = ctx.breakInto === undefined
|
|
2617
|
+
? []
|
|
2618
|
+
: ctx.graph.outEdges(node.id).filter((e) => e.targetNodeId === ctx.breakInto && e.targetPort === 'break');
|
|
2619
|
+
if (breakEdges.length > 0) {
|
|
2620
|
+
for (const be of breakEdges)
|
|
2621
|
+
ctx.graph.accounted.add(be);
|
|
2202
2622
|
segs.push('.break()');
|
|
2623
|
+
flagDropped(node, ctx, segs);
|
|
2624
|
+
return segs;
|
|
2625
|
+
}
|
|
2626
|
+
// A SIDE-path edge into the container's continue handle (an `.onError`
|
|
2627
|
+
// handler, an HTTP branch or outcome list that goes on to the next item):
|
|
2628
|
+
// `.continue()`. On the body's main path the same edge is the one every body
|
|
2629
|
+
// tail gets, so it stays implicit there.
|
|
2630
|
+
const continueEdges = ctx.breakInto === undefined || refInto === undefined
|
|
2631
|
+
? []
|
|
2632
|
+
: ctx.graph.outEdges(node.id).filter((e) => e.targetNodeId === ctx.breakInto
|
|
2633
|
+
&& (e.targetPort === 'continue' || e.targetPort === 'loopBack'));
|
|
2634
|
+
if (continueEdges.length > 0) {
|
|
2635
|
+
for (const ce of continueEdges)
|
|
2636
|
+
ctx.graph.accounted.add(ce);
|
|
2637
|
+
segs.push('.continue()');
|
|
2638
|
+
flagDropped(node, ctx, segs);
|
|
2203
2639
|
return segs;
|
|
2204
2640
|
}
|
|
2205
2641
|
// Continuation: 0 → terminal; 1 → follow; N same-port → parallel fork.
|
|
2206
|
-
if (succ.length === 0)
|
|
2642
|
+
if (succ.length === 0) {
|
|
2643
|
+
accountImplicitTail(node, ctx, refInto);
|
|
2644
|
+
flagDropped(node, ctx, segs);
|
|
2207
2645
|
return segs;
|
|
2646
|
+
}
|
|
2208
2647
|
if (succ.length === 1) {
|
|
2209
|
-
cursor = succ[0];
|
|
2648
|
+
cursor = follow(ctx.graph, node.id, succ[0]);
|
|
2649
|
+
flagDropped(node, ctx, segs);
|
|
2210
2650
|
continue;
|
|
2211
2651
|
}
|
|
2212
2652
|
// Parallel: the fork's ipdom is the merge join; each successor is one arm.
|
|
2213
2653
|
const join = ipdom.get(node.id);
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2654
|
+
const joinNode = join !== undefined ? ctx.graph.byId.get(join) : undefined;
|
|
2655
|
+
if (join && join !== EXIT && joinNode?.type === T.merge) {
|
|
2656
|
+
const fork = parallelSeg(join, joinNode, succ.map((t) => follow(ctx.graph, node.id, t)), scope, ipdom, ctx, refInto);
|
|
2657
|
+
segs.push(fork.seg);
|
|
2658
|
+
flagDropped(node, ctx, segs);
|
|
2659
|
+
if (fork.next.brk || fork.next.cont) {
|
|
2660
|
+
segs.push(fork.next.brk ? '.break()' : '.continue()');
|
|
2661
|
+
return segs;
|
|
2662
|
+
}
|
|
2663
|
+
cursor = fork.next.cursor;
|
|
2218
2664
|
continue;
|
|
2219
2665
|
}
|
|
2220
|
-
// Unstructured fan-out we can't fold — best-effort marker.
|
|
2666
|
+
// Unstructured fan-out we can't fold — best-effort marker. Its successor
|
|
2667
|
+
// edges are what the marker reports, so they are not reported twice.
|
|
2221
2668
|
if (ctx.o.bestEffort === false)
|
|
2222
2669
|
throw new Error(`decompile: unstructured fan-out at "${node.id}".`);
|
|
2223
2670
|
segs.push(`/* TODO: unstructured fan-out at ${commentText(node.id)} (${commentText(succ.length)} successors) */`);
|
|
2671
|
+
for (const t of succ)
|
|
2672
|
+
follow(ctx.graph, node.id, t);
|
|
2673
|
+
flagDropped(node, ctx, segs);
|
|
2224
2674
|
return segs;
|
|
2225
2675
|
}
|
|
2226
2676
|
return segs;
|
|
@@ -2306,6 +2756,88 @@ function declaredExitsOfNode(node) {
|
|
|
2306
2756
|
}
|
|
2307
2757
|
return undefined;
|
|
2308
2758
|
}
|
|
2759
|
+
/**
|
|
2760
|
+
* Split a switch case expression into `<discriminant> === <literal>`, the only
|
|
2761
|
+
* form `.switch(on, [{ value }])` models. Returns `undefined` for any other
|
|
2762
|
+
* condition, so the caller can refuse it rather than emit a different one:
|
|
2763
|
+
*
|
|
2764
|
+
* - the split is at the LAST top-level `===` (equality is left-associative,
|
|
2765
|
+
* so that `===` is the outermost one); `==`, `!==` and `!=` never match;
|
|
2766
|
+
* - the right side must be one literal: JSON, or a single-quoted string;
|
|
2767
|
+
* - the left side must hold no top-level operator that binds looser than
|
|
2768
|
+
* `===` (`&&`, `||`, `??`, `?:`, `&`, `|`, `^`, `,`, `=`, `=>`), since
|
|
2769
|
+
* `a && b === 1` means `a && (b === 1)`, not `(a && b) === 1`.
|
|
2770
|
+
*
|
|
2771
|
+
* Strings, template literals and bracketed groups are skipped when scanning.
|
|
2772
|
+
*/
|
|
2773
|
+
function parseSwitchCase(expr) {
|
|
2774
|
+
let depth = 0;
|
|
2775
|
+
let lastEq = -1;
|
|
2776
|
+
const loose = [];
|
|
2777
|
+
for (let i = 0; i < expr.length; i++) {
|
|
2778
|
+
const ch = expr[i];
|
|
2779
|
+
if (ch === '"' || ch === "'" || ch === '`') {
|
|
2780
|
+
for (i++; i < expr.length && expr[i] !== ch; i++)
|
|
2781
|
+
if (expr[i] === '\\')
|
|
2782
|
+
i++;
|
|
2783
|
+
continue;
|
|
2784
|
+
}
|
|
2785
|
+
if (ch === '(' || ch === '[' || ch === '{') {
|
|
2786
|
+
depth++;
|
|
2787
|
+
continue;
|
|
2788
|
+
}
|
|
2789
|
+
if (ch === ')' || ch === ']' || ch === '}') {
|
|
2790
|
+
depth--;
|
|
2791
|
+
continue;
|
|
2792
|
+
}
|
|
2793
|
+
if (depth !== 0)
|
|
2794
|
+
continue;
|
|
2795
|
+
const two = expr.slice(i, i + 2);
|
|
2796
|
+
if (expr.startsWith('===', i)) {
|
|
2797
|
+
lastEq = i;
|
|
2798
|
+
i += 2;
|
|
2799
|
+
}
|
|
2800
|
+
else if (expr.startsWith('!==', i)) {
|
|
2801
|
+
i += 2;
|
|
2802
|
+
}
|
|
2803
|
+
else if (two === '?.' && !/[0-9]/.test(expr[i + 2] ?? '')) {
|
|
2804
|
+
i += 1; // optional chaining, not a conditional
|
|
2805
|
+
}
|
|
2806
|
+
else if (two === '==' || two === '!=' || two === '<=' || two === '>=') {
|
|
2807
|
+
i += 1;
|
|
2808
|
+
}
|
|
2809
|
+
else if (two === '&&' || two === '||' || two === '??' || two === '=>') {
|
|
2810
|
+
loose.push(i);
|
|
2811
|
+
i += 1;
|
|
2812
|
+
}
|
|
2813
|
+
else if ('?&|^,='.includes(ch)) {
|
|
2814
|
+
loose.push(i);
|
|
2815
|
+
}
|
|
2816
|
+
}
|
|
2817
|
+
if (lastEq < 0)
|
|
2818
|
+
return undefined;
|
|
2819
|
+
const lhs = expr.slice(0, lastEq).trim();
|
|
2820
|
+
const literal = expr.slice(lastEq + 3).trim();
|
|
2821
|
+
if (!lhs || loose.some((at) => at < lastEq))
|
|
2822
|
+
return undefined;
|
|
2823
|
+
try {
|
|
2824
|
+
return { lhs, literal, value: tsLiteral(JSON.parse(literal)) };
|
|
2825
|
+
}
|
|
2826
|
+
catch {
|
|
2827
|
+
const q = literal.match(/^'((?:[^'\\]|\\.)*)'$/);
|
|
2828
|
+
if (!q)
|
|
2829
|
+
return undefined;
|
|
2830
|
+
// The string's value, escapes resolved (`'it\'s'` is `it's`): rewrite it as
|
|
2831
|
+
// a JSON string, which shares JS's escapes except `\'`.
|
|
2832
|
+
const json = `"${q[1].replace(/\\(.)|"/g, (m, c) => (c === undefined ? '\\"' : c === "'" ? "'" : m))}"`;
|
|
2833
|
+
try {
|
|
2834
|
+
return { lhs, literal, value: tsLiteral(JSON.parse(json)) };
|
|
2835
|
+
}
|
|
2836
|
+
catch {
|
|
2837
|
+
return undefined;
|
|
2838
|
+
}
|
|
2839
|
+
}
|
|
2840
|
+
}
|
|
2309
2841
|
/**
|
|
2310
2842
|
* Emit a `.switch(...)` from a `core.logic.switch` node. Each case's serialized
|
|
2311
2843
|
* expression is `<discriminant> === <JSON-literal>`; the discriminant (shared by
|
|
@@ -2315,62 +2847,74 @@ function declaredExitsOfNode(node) {
|
|
|
2315
2847
|
function emitSwitch(node, ipdom, scope, ctx, stop, refInto) {
|
|
2316
2848
|
const r = ipdom.get(node.id) ?? stop;
|
|
2317
2849
|
const cases = (node.inputs ?? {}).cases ?? [];
|
|
2850
|
+
let caseIndex = 0;
|
|
2318
2851
|
let disc;
|
|
2319
2852
|
let nonUniform = false; // a case whose expr isn't `<disc> === <literal>`, or a differing discriminant
|
|
2320
2853
|
const caseSrcs = [];
|
|
2321
2854
|
for (const c of cases) {
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
if (m && lhs) {
|
|
2855
|
+
const parsed = parseSwitchCase(String(c.expression ?? ''));
|
|
2856
|
+
let value = '0';
|
|
2857
|
+
let valueLiteral = '';
|
|
2858
|
+
if (parsed) {
|
|
2327
2859
|
if (disc === undefined)
|
|
2328
|
-
disc = lhs;
|
|
2329
|
-
else if (disc !== lhs)
|
|
2860
|
+
disc = parsed.lhs;
|
|
2861
|
+
else if (disc !== parsed.lhs)
|
|
2330
2862
|
nonUniform = true;
|
|
2863
|
+
valueLiteral = parsed.literal;
|
|
2864
|
+
value = parsed.value;
|
|
2331
2865
|
}
|
|
2332
2866
|
else {
|
|
2333
2867
|
nonUniform = true;
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2868
|
+
// Keep the best-effort output the TODO comment points at.
|
|
2869
|
+
const m = String(c.expression ?? '').match(/^(.*?)\s*===?\s*(.*)$/);
|
|
2870
|
+
if (m && m[1].trim() && disc === undefined)
|
|
2871
|
+
disc = m[1].trim();
|
|
2872
|
+
if (m) {
|
|
2873
|
+
valueLiteral = m[2].trim();
|
|
2874
|
+
try {
|
|
2875
|
+
value = tsLiteral(JSON.parse(valueLiteral));
|
|
2876
|
+
}
|
|
2877
|
+
catch {
|
|
2878
|
+
const q = valueLiteral.match(/^'(.*)'$/);
|
|
2879
|
+
value = q ? tsLiteral(q[1]) : tsLiteral(valueLiteral);
|
|
2880
|
+
}
|
|
2346
2881
|
}
|
|
2347
2882
|
}
|
|
2348
|
-
|
|
2349
|
-
nonUniform = true;
|
|
2350
|
-
}
|
|
2351
|
-
const armSegs = region(portTarget(ctx.graph, node.id, `case-${c.id}`), r, scope, ipdom, ctx, refInto);
|
|
2883
|
+
const armSegs = forkRegion(portEdges(ctx.graph, node.id, `case-${c.id}`), r, scope, ipdom, ctx, refInto);
|
|
2352
2884
|
const defaultLabel = `Case ${valueLiteral}`;
|
|
2353
2885
|
const labelPart = c.label && c.label !== defaultLabel ? `, label: ${tsLiteral(c.label)}` : '';
|
|
2354
|
-
|
|
2886
|
+
// The file's case id comes back when it is not the positional one serialize
|
|
2887
|
+
// derives (a designer-drawn switch uses `default-1`, …), so the recompiled
|
|
2888
|
+
// edges leave from the ORIGINAL node's handles, which merge keeps (#1130).
|
|
2889
|
+
caseIndex += 1;
|
|
2890
|
+
const idPart = typeof c.id === 'string' && c.id !== '' && c.id !== `case_${caseIndex}` ? `, id: ${tsLiteral(c.id)}` : '';
|
|
2891
|
+
caseSrcs.push(`{ value: ${value}${labelPart}${idPart}, body: ${armCb('b', armSegs)} }`);
|
|
2355
2892
|
}
|
|
2356
2893
|
const on = emitBareExpr(disc ?? '', ctx.imp, ctx.inputNames);
|
|
2357
|
-
const hasDefault = (node.inputs ?? {}).hasDefault === true ||
|
|
2894
|
+
const hasDefault = (node.inputs ?? {}).hasDefault === true || portEdge(ctx.graph, node.id, 'default') !== undefined;
|
|
2358
2895
|
const defArm = hasDefault
|
|
2359
|
-
? `, ${armCb('d',
|
|
2896
|
+
? `, ${armCb('d', forkRegion(portEdges(ctx.graph, node.id, 'default'), r, scope, ipdom, ctx, refInto))}`
|
|
2360
2897
|
: '';
|
|
2361
2898
|
const caseList = `[\n${caseSrcs.map((c) => reindent(c, ' ')).join(',\n')},\n]`;
|
|
2362
2899
|
// The builder's `.switch(on, [{value}])` models `on === value` only. A flow whose
|
|
2363
2900
|
// cases carry arbitrary per-case boolean expressions (or a non-uniform
|
|
2364
2901
|
// discriminant) cannot round-trip through it — flag it rather than emit silently
|
|
2365
2902
|
// wrong values.
|
|
2903
|
+
if (nonUniform) {
|
|
2904
|
+
const listing = cases.map((c) => `${tsLiteral(c.label ?? c.id)}: ${tsLiteral(String(c.expression ?? ''))}`).join(', ');
|
|
2905
|
+
const message = `switch ${tsLiteral(node.id)} has per-case conditions that are not one shared \`<discriminant> === <value>\` `
|
|
2906
|
+
+ `(${listing}). \`.switch()\` models only that form, so the decompiled \`on\`/\`value\` are placeholders: `
|
|
2907
|
+
+ 'recompiling either fails or changes which case runs. Edit this switch in the .flow JSON directly.';
|
|
2908
|
+
if (runStrict)
|
|
2909
|
+
throw new Error(`decompile: ${message}`);
|
|
2910
|
+
warnOnce(message);
|
|
2911
|
+
}
|
|
2366
2912
|
const warn = nonUniform
|
|
2367
2913
|
? `\n/* TODO: this switch's case expressions are not a uniform \`<discriminant> === <value>\`; the recovered on/value are best-effort and need review. */`
|
|
2368
2914
|
: '';
|
|
2369
2915
|
const ver = verArg(node);
|
|
2370
2916
|
return `.switch(${tsLiteral(node.id)}, ${on}, ${caseList}${defArm}${ver && !defArm ? ', undefined' : ''}${ver})${warn}`;
|
|
2371
2917
|
}
|
|
2372
|
-
/** The manifest-less annotation node type — the only annotation `.flow` has. */
|
|
2373
|
-
const STICKY_NOTE_TYPE = 'stickyNote';
|
|
2374
2918
|
/**
|
|
2375
2919
|
* Recover `.stickyNotes([...])` from the scope's `stickyNote` nodes.
|
|
2376
2920
|
*
|
|
@@ -2438,12 +2982,21 @@ function stickyNotesSeg(nodes, flow) {
|
|
|
2438
2982
|
*/
|
|
2439
2983
|
function layoutSeg(flow, nodes, edges) {
|
|
2440
2984
|
const hoisted = (flow.layout?.nodes ?? {});
|
|
2985
|
+
// A layout key must name a node the recompiled flow will HAVE. A resource
|
|
2986
|
+
// folded into its agent comes back under the id compile mints for it; when
|
|
2987
|
+
// that is not the file's id (a canvas- or CLI-placed resource is named after
|
|
2988
|
+
// its index or tool, not `<agent>Context`), the file's id names nothing and
|
|
2989
|
+
// compile refuses the whole `.layout()`. So its entry, and any edge route that
|
|
2990
|
+
// ends at it, is left out and compile places the node itself.
|
|
2991
|
+
const renamed = (id) => runAbsorbed.has(id) && runAbsorbed.get(id) !== id;
|
|
2441
2992
|
const nodeEntries = [];
|
|
2442
2993
|
for (const n of nodes) {
|
|
2443
2994
|
// A sticky note carries its own geometry in `.stickyNotes()`; a layout entry
|
|
2444
2995
|
// for it would be a second, competing source for the same coordinates.
|
|
2445
2996
|
if (n.type === STICKY_NOTE_TYPE)
|
|
2446
2997
|
continue;
|
|
2998
|
+
if (renamed(n.id))
|
|
2999
|
+
continue;
|
|
2447
3000
|
const ui = { ...(n.ui ?? {}), ...(hoisted[n.id] ?? {}) };
|
|
2448
3001
|
const pos = ui.position;
|
|
2449
3002
|
if (!pos || typeof pos.x !== 'number' || typeof pos.y !== 'number')
|
|
@@ -2480,6 +3033,8 @@ function layoutSeg(flow, nodes, edges) {
|
|
|
2480
3033
|
}
|
|
2481
3034
|
if (!parts.length)
|
|
2482
3035
|
continue;
|
|
3036
|
+
if (renamed(e.sourceNodeId) || renamed(e.targetNodeId))
|
|
3037
|
+
continue;
|
|
2483
3038
|
const pair = `${e.sourceNodeId}->${e.targetNodeId}`;
|
|
2484
3039
|
if ((pairCount.get(pair) ?? 0) !== 1)
|
|
2485
3040
|
continue; // ambiguous — see the note above
|
|
@@ -2542,10 +3097,13 @@ function emitChildFlow(name, child, ctx) {
|
|
|
2542
3097
|
// Body: the child's trigger is its own manual Start; walk from its successor.
|
|
2543
3098
|
const trigger = (child.nodes ?? []).find((n) => isTrigger(n.type) && !(child.edges ?? []).some((e) => e.targetNodeId === n.id));
|
|
2544
3099
|
const scope = scopeIds(child.nodes ?? [], undefined);
|
|
2545
|
-
const
|
|
2546
|
-
const ipdom =
|
|
2547
|
-
|
|
3100
|
+
const entries = trigger ? scopeSuccEdges(childCtx.graph, trigger.id, scope) : [];
|
|
3101
|
+
const ipdom = entries.length
|
|
3102
|
+
? postDominators(entries.length > 1 ? trigger.id : entries[0].targetNodeId, scope, childCtx.graph)
|
|
3103
|
+
: new Map();
|
|
3104
|
+
const bodySegs = entries.length ? forkRegion(entries, EXIT, scope, ipdom, childCtx) : [];
|
|
2548
3105
|
parts.push(...bodySegs);
|
|
3106
|
+
parts.push(...sweepDroppedEdges(childCtx.graph, new Set()));
|
|
2549
3107
|
// A subflow carries its own scoped layout, resolved against its own node ids.
|
|
2550
3108
|
const childNotes = stickyNotesSeg(child.nodes ?? [], child);
|
|
2551
3109
|
if (childNotes)
|
|
@@ -2568,11 +3126,13 @@ export function decompile(flow, options = {}) {
|
|
|
2568
3126
|
runWarn = options.warn;
|
|
2569
3127
|
runReadSidecar = options.readSidecar;
|
|
2570
3128
|
runWarned = new Set();
|
|
3129
|
+
runDropped = [];
|
|
2571
3130
|
runDefPairs = new Set((flow.definitions ?? [])
|
|
2572
3131
|
.filter((d) => typeof d?.nodeType === 'string' && typeof d?.version === 'string')
|
|
2573
3132
|
.map((d) => `${d.nodeType}@${d.version}`));
|
|
2574
3133
|
runDefs = new Map();
|
|
2575
3134
|
runRawManifests = [];
|
|
3135
|
+
runAbsorbed = new Map();
|
|
2576
3136
|
for (const d of (flow.definitions ?? [])) {
|
|
2577
3137
|
if (typeof d?.nodeType !== 'string')
|
|
2578
3138
|
continue;
|
|
@@ -2663,9 +3223,13 @@ export function decompile(flow, options = {}) {
|
|
|
2663
3223
|
const ctx = { graph, flow, imp, inputNames, o: options, emitted: new Set(), shared };
|
|
2664
3224
|
const scope = scopeIds(flow.nodes ?? [], undefined);
|
|
2665
3225
|
const startId = trigger?.id ?? 'start';
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
const
|
|
3226
|
+
// Walked from the trigger's successors together: a `.parallel` placed first
|
|
3227
|
+
// gives the trigger one edge per arm (#1155).
|
|
3228
|
+
const firsts = scopeSuccEdges(graph, startId, scope);
|
|
3229
|
+
const ipdom = firsts.length
|
|
3230
|
+
? postDominators(firsts.length > 1 ? startId : firsts[0].targetNodeId, scope, graph)
|
|
3231
|
+
: new Map();
|
|
3232
|
+
const bodySegs = firsts.length ? forkRegion(firsts, EXIT, scope, ipdom, ctx) : [];
|
|
2669
3233
|
// Additional roots decompile to .entryPoint(...): scoped inputs from the
|
|
2670
3234
|
// globals bound to them, and a prefix region that hands off into the shared
|
|
2671
3235
|
// body — the shared nodes are in ctx.emitted by now, so the walk naturally
|
|
@@ -2676,6 +3240,15 @@ export function decompile(flow, options = {}) {
|
|
|
2676
3240
|
continue;
|
|
2677
3241
|
extraEps.push(entryPointSeg(t, scope, ipdom, ctx));
|
|
2678
3242
|
}
|
|
3243
|
+
// Edge accounting (#1125): the walk above has claimed every edge it emitted
|
|
3244
|
+
// or that serialize recreates on its own. The rest are not in the source.
|
|
3245
|
+
const unreached = sweepDroppedEdges(graph, detachedIds);
|
|
3246
|
+
for (const line of unreached)
|
|
3247
|
+
e.line(d, line);
|
|
3248
|
+
if (runStrict && runDropped.length > 0) {
|
|
3249
|
+
throw new Error(`decompile: ${runDropped.length} edge(s) have no spelling in the decompiled source, so recompiling would drop them: `
|
|
3250
|
+
+ `${runDropped.map(edgeName).join(', ')}.`);
|
|
3251
|
+
}
|
|
2679
3252
|
const header = e.toString();
|
|
2680
3253
|
const body = bodySegs.map((s) => reindent(s, ' ')).join('\n');
|
|
2681
3254
|
const eps = extraEps.map((s) => reindent(s, ' ')).join('\n');
|
|
@@ -2746,8 +3319,11 @@ function entryPointSeg(t, scope, ipdom, ctx) {
|
|
|
2746
3319
|
optParts.push(`inputs: { ${scoped.map((g) => `${key(g.name ?? g.id)}: ${declExpr(g, true, ctx.imp, sharedOf(g))}`).join(', ')} }`);
|
|
2747
3320
|
}
|
|
2748
3321
|
optParts.push(...triggerOptParts(t));
|
|
2749
|
-
const
|
|
2750
|
-
|
|
3322
|
+
const firsts = scopeSuccEdges(ctx.graph, t.id, scope);
|
|
3323
|
+
// A prefix that forks needs post-dominators over ITS nodes, which the root's
|
|
3324
|
+
// map (computed from the default trigger) does not cover.
|
|
3325
|
+
const epIpdom = firsts.length > 1 ? postDominators(t.id, scope, ctx.graph) : ipdom;
|
|
3326
|
+
const prefixSegs = firsts.length ? forkRegion(firsts, EXIT, scope, epIpdom, ctx) : [];
|
|
2751
3327
|
const optArgText = optParts.length ? `, { ${optParts.join(', ')} }` : (prefixSegs.length ? ', undefined' : '');
|
|
2752
3328
|
const prefix = prefixSegs.length ? `, ${armCb('b', prefixSegs)}` : '';
|
|
2753
3329
|
return `.entryPoint(${tsLiteral(t.id)}, ${factory}${optArgText}${prefix})`;
|