@uipath/maestro-builder-sdk 5.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (238) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +322 -0
  3. package/dist/bindings-v2.d.ts +37 -0
  4. package/dist/bindings-v2.js +155 -0
  5. package/dist/bindings.d.ts +6 -0
  6. package/dist/bindings.js +6 -0
  7. package/dist/bpmn/bpmn-expr-check.d.ts +41 -0
  8. package/dist/bpmn/bpmn-expr-check.js +218 -0
  9. package/dist/bpmn/bpmn-sdk.d.ts +2257 -0
  10. package/dist/bpmn/bpmn-sdk.js +1476 -0
  11. package/dist/bpmn/check-cli.d.ts +3 -0
  12. package/dist/bpmn/check-cli.js +32 -0
  13. package/dist/bpmn/check.d.ts +25 -0
  14. package/dist/bpmn/check.js +430 -0
  15. package/dist/bpmn/compile-cli.d.ts +3 -0
  16. package/dist/bpmn/compile-cli.js +60 -0
  17. package/dist/bpmn/connector.d.ts +56 -0
  18. package/dist/bpmn/connector.js +199 -0
  19. package/dist/bpmn/decompile-cli.d.ts +8 -0
  20. package/dist/bpmn/decompile-cli.js +54 -0
  21. package/dist/bpmn/decompile.d.ts +6 -0
  22. package/dist/bpmn/decompile.js +1108 -0
  23. package/dist/bpmn/format-profile.d.ts +27 -0
  24. package/dist/bpmn/format-profile.js +11 -0
  25. package/dist/bpmn/intsvc.d.ts +77 -0
  26. package/dist/bpmn/intsvc.js +155 -0
  27. package/dist/bpmn/load.d.ts +4 -0
  28. package/dist/bpmn/load.js +22 -0
  29. package/dist/bpmn/merge-cli.d.ts +3 -0
  30. package/dist/bpmn/merge-cli.js +45 -0
  31. package/dist/bpmn/merge.d.ts +19 -0
  32. package/dist/bpmn/merge.js +293 -0
  33. package/dist/bpmn/registry-types.json +1869 -0
  34. package/dist/bpmn/serialize.d.ts +28 -0
  35. package/dist/bpmn/serialize.js +757 -0
  36. package/dist/bpmn/tidy-cli.d.ts +2 -0
  37. package/dist/bpmn/tidy-cli.js +39 -0
  38. package/dist/bpmn/typed-node.d.ts +382 -0
  39. package/dist/bpmn/typed-node.js +551 -0
  40. package/dist/bpmn/uipath-moddle.v1.json +823 -0
  41. package/dist/bpmn/xml-ranges.d.ts +45 -0
  42. package/dist/bpmn/xml-ranges.js +151 -0
  43. package/dist/case/case-expr-check.d.ts +35 -0
  44. package/dist/case/case-expr-check.js +85 -0
  45. package/dist/case/case-sdk.d.ts +1282 -0
  46. package/dist/case/case-sdk.js +923 -0
  47. package/dist/case/check-cli.d.ts +3 -0
  48. package/dist/case/check-cli.js +31 -0
  49. package/dist/case/check.d.ts +18 -0
  50. package/dist/case/check.js +456 -0
  51. package/dist/case/compile-cli.d.ts +3 -0
  52. package/dist/case/compile-cli.js +291 -0
  53. package/dist/case/decompile-cli.d.ts +9 -0
  54. package/dist/case/decompile-cli.js +117 -0
  55. package/dist/case/decompile.d.ts +187 -0
  56. package/dist/case/decompile.js +1170 -0
  57. package/dist/case/entry-points.d.ts +79 -0
  58. package/dist/case/entry-points.js +118 -0
  59. package/dist/case/format-profile.d.ts +60 -0
  60. package/dist/case/format-profile.js +79 -0
  61. package/dist/case/ids.d.ts +24 -0
  62. package/dist/case/ids.js +76 -0
  63. package/dist/case/load.d.ts +12 -0
  64. package/dist/case/load.js +32 -0
  65. package/dist/case/preserve.d.ts +133 -0
  66. package/dist/case/preserve.js +105 -0
  67. package/dist/case/serialize.d.ts +10 -0
  68. package/dist/case/serialize.js +1414 -0
  69. package/dist/check-cli.d.ts +3 -0
  70. package/dist/check-cli.js +118 -0
  71. package/dist/check.d.ts +146 -0
  72. package/dist/check.js +4898 -0
  73. package/dist/cli/commands/bpmn.d.ts +2 -0
  74. package/dist/cli/commands/bpmn.js +73 -0
  75. package/dist/cli/commands/case.d.ts +2 -0
  76. package/dist/cli/commands/case.js +60 -0
  77. package/dist/cli/commands/flow.d.ts +2 -0
  78. package/dist/cli/commands/flow.js +99 -0
  79. package/dist/cli/commands/registry.d.ts +10 -0
  80. package/dist/cli/commands/registry.js +170 -0
  81. package/dist/cli/common.d.ts +30 -0
  82. package/dist/cli/common.js +92 -0
  83. package/dist/cli/index.d.ts +2 -0
  84. package/dist/cli/index.js +18 -0
  85. package/dist/cli-run.d.ts +2 -0
  86. package/dist/cli-run.js +42 -0
  87. package/dist/compile-cli.d.ts +3 -0
  88. package/dist/compile-cli.js +160 -0
  89. package/dist/config.d.ts +47 -0
  90. package/dist/config.js +201 -0
  91. package/dist/core/actions.d.ts +3244 -0
  92. package/dist/core/actions.js +1062 -0
  93. package/dist/core/binding-messages.d.ts +1 -0
  94. package/dist/core/binding-messages.js +16 -0
  95. package/dist/core/bindings.d.ts +12 -0
  96. package/dist/core/bindings.js +67 -0
  97. package/dist/core/ceql.d.ts +73 -0
  98. package/dist/core/ceql.js +272 -0
  99. package/dist/core/cli-spelling.d.ts +39 -0
  100. package/dist/core/cli-spelling.js +62 -0
  101. package/dist/core/connector-raw-node.d.ts +21 -0
  102. package/dist/core/connector-raw-node.js +47 -0
  103. package/dist/core/connectors.d.ts +184 -0
  104. package/dist/core/connectors.js +128 -0
  105. package/dist/core/expr-check.d.ts +93 -0
  106. package/dist/core/expr-check.js +186 -0
  107. package/dist/core/expr.d.ts +323 -0
  108. package/dist/core/expr.js +400 -0
  109. package/dist/core/index.d.ts +10 -0
  110. package/dist/core/index.js +10 -0
  111. package/dist/core/library.d.ts +440 -0
  112. package/dist/core/library.js +1183 -0
  113. package/dist/core/list-envelope.d.ts +45 -0
  114. package/dist/core/list-envelope.js +97 -0
  115. package/dist/core/lookup-store.d.ts +22 -0
  116. package/dist/core/lookup-store.js +85 -0
  117. package/dist/core/lookups.d.ts +227 -0
  118. package/dist/core/lookups.js +231 -0
  119. package/dist/core/node-classes.d.ts +234 -0
  120. package/dist/core/node-classes.js +158 -0
  121. package/dist/core/script-return.d.ts +68 -0
  122. package/dist/core/script-return.js +197 -0
  123. package/dist/core/stable-id.d.ts +28 -0
  124. package/dist/core/stable-id.js +37 -0
  125. package/dist/core-definitions.json +16249 -0
  126. package/dist/decompile-cli.d.ts +3 -0
  127. package/dist/decompile-cli.js +131 -0
  128. package/dist/decompile.d.ts +138 -0
  129. package/dist/decompile.js +2482 -0
  130. package/dist/emit.d.ts +10 -0
  131. package/dist/emit.js +60 -0
  132. package/dist/event-filters.d.ts +130 -0
  133. package/dist/event-filters.js +137 -0
  134. package/dist/flow-expr-check.d.ts +17 -0
  135. package/dist/flow-expr-check.js +193 -0
  136. package/dist/flow-sdk.d.ts +1341 -0
  137. package/dist/flow-sdk.js +1164 -0
  138. package/dist/flow-variables.d.ts +13 -0
  139. package/dist/flow-variables.js +42 -0
  140. package/dist/format-profile.d.ts +72 -0
  141. package/dist/format-profile.js +97 -0
  142. package/dist/generators/_connections.py +287 -0
  143. package/dist/generators/_events.py +355 -0
  144. package/dist/generators/_lookups.py +114 -0
  145. package/dist/generators/_naming.py +185 -0
  146. package/dist/generators/_objects.py +299 -0
  147. package/dist/generators/_resolve.py +273 -0
  148. package/dist/generators/convert_library_to_md.py +1441 -0
  149. package/dist/generators/extract_library.py +125 -0
  150. package/dist/generators/generate_connectors_ts.py +936 -0
  151. package/dist/generators/generate_library_json.py +2527 -0
  152. package/dist/generators/generate_triggers_ts.py +110 -0
  153. package/dist/generators/materialize_registry.py +155 -0
  154. package/dist/generators/prepare_connector.py +1688 -0
  155. package/dist/index.d.ts +35 -0
  156. package/dist/index.js +35 -0
  157. package/dist/ixp-definition.json +1982 -0
  158. package/dist/library.d.ts +6 -0
  159. package/dist/library.js +6 -0
  160. package/dist/load.d.ts +16 -0
  161. package/dist/load.js +95 -0
  162. package/dist/merge-cli.d.ts +3 -0
  163. package/dist/merge-cli.js +47 -0
  164. package/dist/merge.d.ts +125 -0
  165. package/dist/merge.js +0 -0
  166. package/dist/migrate.d.ts +15 -0
  167. package/dist/migrate.js +245 -0
  168. package/dist/node-runtime.d.ts +3 -0
  169. package/dist/node-runtime.js +22 -0
  170. package/dist/package-name.d.ts +17 -0
  171. package/dist/package-name.js +17 -0
  172. package/dist/ref-resolve.d.ts +77 -0
  173. package/dist/ref-resolve.js +170 -0
  174. package/dist/registry/cache.d.ts +55 -0
  175. package/dist/registry/cache.js +134 -0
  176. package/dist/registry/curated-overlay.d.ts +16 -0
  177. package/dist/registry/curated-overlay.js +62 -0
  178. package/dist/registry/extractor.d.ts +22 -0
  179. package/dist/registry/extractor.js +18 -0
  180. package/dist/registry/materialize-bin.d.ts +2 -0
  181. package/dist/registry/materialize-bin.js +7 -0
  182. package/dist/registry/materialize-cli.d.ts +1 -0
  183. package/dist/registry/materialize-cli.js +46 -0
  184. package/dist/registry/materializer.d.ts +23 -0
  185. package/dist/registry/materializer.js +34 -0
  186. package/dist/registry/path-cli.d.ts +1 -0
  187. package/dist/registry/path-cli.js +56 -0
  188. package/dist/registry/prepare-cli.d.ts +62 -0
  189. package/dist/registry/prepare-cli.js +185 -0
  190. package/dist/registry/pull-cli.d.ts +1 -0
  191. package/dist/registry/pull-cli.js +274 -0
  192. package/dist/registry/python.d.ts +26 -0
  193. package/dist/registry/python.js +46 -0
  194. package/dist/registry/search-cli.d.ts +52 -0
  195. package/dist/registry/search-cli.js +159 -0
  196. package/dist/schema-version.d.ts +165 -0
  197. package/dist/schema-version.js +148 -0
  198. package/dist/serialize.d.ts +64 -0
  199. package/dist/serialize.js +6398 -0
  200. package/lib/README.md +112 -0
  201. package/lib/library-json.samples/index.json +158 -0
  202. package/lib/library-json.samples/invariant-collections.json +83 -0
  203. package/lib/library-json.samples/list-envelopes.json +123 -0
  204. package/lib/library-json.samples/uipath-atlassian-jira/create-issue@1.0.0.json +131 -0
  205. package/lib/library-json.samples/uipath-atlassian-jira/create-issue@1.0.0.v1def.json +306 -0
  206. package/lib/library-json.samples/uipath-atlassian-jira/generic/create_issue/create@1.0.0.json +86 -0
  207. package/lib/library-json.samples/uipath-atlassian-jira/generic/create_issue/create@1.0.0.v1def.json +208 -0
  208. package/lib/library-json.samples/uipath-atlassian-jira/get-issue@1.0.0.json +2040 -0
  209. package/lib/library-json.samples/uipath-atlassian-jira/get-issue@1.0.0.v1def.json +195 -0
  210. package/lib/library-json.samples/uipath-google-vertex/execute-google-vertex-agent@1.0.0.json +169 -0
  211. package/lib/library-json.samples/uipath-google-vertex/execute-google-vertex-agent@1.0.0.v1def.json +209 -0
  212. package/lib/library-json.samples/uipath-http-webhook/event-http-webhook@1.0.0.json +63 -0
  213. package/lib/library-json.samples/uipath-http-webhook/event-http-webhook@1.0.0.v1def.json +262 -0
  214. package/lib/library-json.samples/uipath-http-webhook/trigger-http-webhook@1.0.0.json +63 -0
  215. package/lib/library-json.samples/uipath-http-webhook/trigger-http-webhook@1.0.0.v1def.json +213 -0
  216. package/lib/library-json.samples/uipath-microsoft-azureactivedirectory/list-groups@1.0.0.json +227 -0
  217. package/lib/library-json.samples/uipath-microsoft-azureactivedirectory/list-groups@1.0.0.v1def.json +373 -0
  218. package/lib/library-json.samples/uipath-microsoft-onedrive/event-file-created@1.0.0.json +116 -0
  219. package/lib/library-json.samples/uipath-microsoft-onedrive/event-file-created@1.0.0.v1def.json +369 -0
  220. package/lib/library-json.samples/uipath-microsoft-onedrive/event-list-item-added@1.0.0.json +56 -0
  221. package/lib/library-json.samples/uipath-microsoft-onedrive/event-list-item-added@1.0.0.v1def.json +256 -0
  222. package/lib/library-json.samples/uipath-microsoft-onedrive/trigger-file-created@1.0.0.json +116 -0
  223. package/lib/library-json.samples/uipath-microsoft-onedrive/trigger-file-created@1.0.0.v1def.json +320 -0
  224. package/lib/library-json.samples/uipath-microsoft-onedrive/trigger-list-item-added@1.0.0.json +56 -0
  225. package/lib/library-json.samples/uipath-microsoft-onedrive/trigger-list-item-added@1.0.0.v1def.json +207 -0
  226. package/lib/library-json.samples/uipath-microsoft-outlook365/event-email-received@1.0.0.json +107 -0
  227. package/lib/library-json.samples/uipath-microsoft-outlook365/event-email-received@1.0.0.v1def.json +264 -0
  228. package/lib/library-json.samples/uipath-microsoft-outlook365/send-email@1.0.0.json +142 -0
  229. package/lib/library-json.samples/uipath-microsoft-outlook365/send-email@1.0.0.v1def.json +306 -0
  230. package/lib/library-json.samples/uipath-microsoft-outlook365/trigger-email-received@1.0.0.json +107 -0
  231. package/lib/library-json.samples/uipath-microsoft-outlook365/trigger-email-received@1.0.0.v1def.json +217 -0
  232. package/lib/library-json.samples/uipath-microsoft-powerautomate/invoke-a-microsoft-power-automate-flow@1.0.0.json +85 -0
  233. package/lib/library-json.samples/uipath-microsoft-powerautomate/invoke-a-microsoft-power-automate-flow@1.0.0.v1def.json +209 -0
  234. package/lib/library-json.samples/uipath-salesforce-slack/send-message-to-user@1.0.0.json +755 -0
  235. package/lib/library-json.samples/uipath-salesforce-slack/send-message-to-user@1.0.0.v1def.json +513 -0
  236. package/lib/library-json.samples/uipath-uipath-dataservice/query-entity-records@1.0.0.json +122 -0
  237. package/lib/library-json.samples/uipath-uipath-dataservice/query-entity-records@1.0.0.v1def.json +227 -0
  238. package/package.json +85 -0
@@ -0,0 +1,2482 @@
1
+ /**
2
+ * decompile — the reverse of `serialize.ts`: turn a real UiPath `.flow` JSON
3
+ * back into Flow SDK builder source (`.flow.ts`) that, when re-compiled,
4
+ * serializes to the **same** graph (same node ids, same logical edges).
5
+ *
6
+ * Unlike the case decompiler (`case/decompile.ts`), a flow is a directed GRAPH
7
+ * (`nodes[]` + `edges[]` with `sourcePort`/`targetPort`), not a hierarchy, and
8
+ * the builder is structured (`.branch()` / `.switch()` / `.loop()` /
9
+ * `.parallel()` / `.onError()` / `.return()`). Decompiling therefore means
10
+ * reconstructing structured control flow from the graph — the inverse of
11
+ * serialize's `emit()` walker. This module is built in layers:
12
+ *
13
+ * - Phase 1 (here): the linear core — header, variables, trigger, and a
14
+ * straight-line chain of action steps ending in `.return()` / `.terminate()`.
15
+ * - Phase 2: the full node reverse-map (http / transform / connectors / hitl /
16
+ * agents / queue / …) and connector reverse-lookup.
17
+ * - Phase 3: control-flow reconstruction (branch / switch / loop / parallel /
18
+ * onError / stepToRef / subflow) via graph analysis.
19
+ *
20
+ * The linchpin is **node-id stability**: serialize derives each node id from the
21
+ * author's step NAME (`claimId`: `[^A-Za-z0-9_]→_`, then de-dup `base`/`base2`/…),
22
+ * so if we name every step with its original node id, serialize reproduces
23
+ * byte-identical ids. A happy consequence: serialize's `renameRefs`
24
+ * (`$vars.<stepName>`→`$vars.<nodeId>`) becomes the identity, so expression
25
+ * references need no un-renaming.
26
+ *
27
+ * The round-trip is **semantically** lossless, not byte-for-byte: layout,
28
+ * display labels on plain steps, and formatting are not preserved (layout is
29
+ * re-attached by `flow-merge`; see `docs/FLOW_PARITY.md`).
30
+ */
31
+ import { readFileSync } from 'node:fs';
32
+ import { FLOW_SCHEMA_VERSIONS } from './format-profile.js';
33
+ import { preserveSchemaVersion } from './schema-version.js';
34
+ import { dirname, join } from 'node:path';
35
+ import { fileURLToPath } from 'node:url';
36
+ import { inferScriptShape } from './core/script-return.js';
37
+ import { DEFAULT_TRIGGER_ID } from './core/expr.js';
38
+ import { parseIxpProjectType } from './core/actions.js';
39
+ import { readFlowGlobals } from './flow-variables.js';
40
+ import { eventFilterFromTreeLeaf } from './event-filters.js';
41
+ /**
42
+ * The trigger id of the flow currently being decompiled, so a reference through a
43
+ * RENAMED trigger is still recognized as `input('x')` rather than falling back to
44
+ * a raw `js` template. Run-scoped rather than threaded: `decompile()` is
45
+ * synchronous and single-shot, and `inputNames` is already carried to all 34 call
46
+ * sites that would otherwise need a second argument.
47
+ */
48
+ let currentTriggerId = DEFAULT_TRIGGER_ID;
49
+ /**
50
+ * Version preservation (design §5.3), run-scoped like `currentTriggerId`:
51
+ * every emitted node method carries an explicit `{ version }` read from the
52
+ * instance's `typeVersion`, verified against the file's own embedded
53
+ * `definitions[]`. Strict mode (bestEffort: false) FAILS on a missing or
54
+ * disagreeing pair rather than normalizing either side; best-effort still
55
+ * emits the original version so recompilation cannot silently downgrade it.
56
+ */
57
+ let runDefPairs = new Set();
58
+ /**
59
+ * The file's `definitions[]`, by `<type>@<version>` and by bare type.
60
+ *
61
+ * Several families keep the author's identity on the DEFINITION rather than the
62
+ * instance — the published-resource families put `name`/`folderPath` in
63
+ * `model.bindings.values` and the declared `returns` in
64
+ * `outputDefinition.output.schema` — so recovering them means reading the
65
+ * definition, not just the node.
66
+ */
67
+ let runDefs = new Map();
68
+ /**
69
+ * Manifests hoisted out of the step chain for `rawNode(...)` calls — `[const
70
+ * name, JSON source]`. A definition can be 18 KB (an IxP extractor's is), and a
71
+ * blob that size inline in the middle of a builder chain is unreadable for the
72
+ * agent that has to edit the file next.
73
+ */
74
+ let runRawManifests = [];
75
+ let runStrict = false;
76
+ /** `DecompileOptions.warn` for the current run, and the messages already sent to it (one line per finding). */
77
+ let runWarn;
78
+ let runWarned = new Set();
79
+ function warnOnce(message) {
80
+ if (runWarned.has(message))
81
+ return;
82
+ runWarned.add(message);
83
+ runWarn?.(message);
84
+ }
85
+ /**
86
+ * The `<nodeType>@<version>` pairs this SDK bundles for core nodes — the same
87
+ * file `serialize` resolves an explicit `{ version }` against (design §2.6:
88
+ * exact, no floating). Decompile preserves the file's pair (design §5.3), so
89
+ * when the file pins a version the bundle lacks, `compile` WILL refuse the
90
+ * emitted source; saying so here is the difference between a loop that fails
91
+ * at step 2 with a hint and one that fails at step 2 with a puzzle.
92
+ */
93
+ const CORE_DEFINITION_KEYS = new Set(Object.keys(JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), 'core-definitions.json'), 'utf8'))));
94
+ function verClause(node) {
95
+ const tv = node.typeVersion;
96
+ if (!tv) {
97
+ if (runStrict) {
98
+ throw new Error(`decompile: node "${node.id}" (${node.type}) carries no typeVersion.`);
99
+ }
100
+ return '';
101
+ }
102
+ if (runDefPairs.size > 0 && !runDefPairs.has(`${node.type}@${tv}`)) {
103
+ if (runStrict) {
104
+ throw new Error(`decompile: no embedded definition matches (${node.type}, ${tv}) for node "${node.id}" — `
105
+ + `the file's definitions[] disagrees with the instance.`);
106
+ }
107
+ }
108
+ if (node.type.startsWith('core.') && !CORE_DEFINITION_KEYS.has(`${node.type}@${tv}`)) {
109
+ const shipped = [...CORE_DEFINITION_KEYS]
110
+ .filter((k) => k.startsWith(`${node.type}@`))
111
+ .map((k) => k.slice(node.type.length + 1));
112
+ warnOnce(`node ${str(node.id)} pins ${node.type}@${tv}, but this SDK bundles ${node.type}@${shipped.join(', ') || '<none>'} only; `
113
+ + `\`compile\` will refuse the emitted { version: ${str(tv)} } (versions resolve exactly, flow-builder-sdk#71). `
114
+ + `The source keeps the file's pair; to recompile, drop that { version } and the node takes the SDK's pinned version.`);
115
+ }
116
+ return `version: ${str(tv)}`;
117
+ }
118
+ function verArg(node) {
119
+ const clause = verClause(node);
120
+ return clause ? `, { ${clause} }` : '';
121
+ }
122
+ /**
123
+ * The full node-options argument for steps that can carry both an exact
124
+ * `version` and mid-flow `updates` (the format's `variableUpdates[nodeId]`,
125
+ * for every node that is not an End — an End's updates are `.return()` values).
126
+ */
127
+ function optsArg(node, ctx) {
128
+ return optsArgWith(node, ctx, []);
129
+ }
130
+ /** `optsArg` plus construct-specific option clauses (loop/do-while recovery). */
131
+ function optsArgWith(node, ctx, extra) {
132
+ const parts = [...extra];
133
+ const ver = verClause(node);
134
+ if (ver)
135
+ parts.push(ver);
136
+ // A canvas label the author chose. Decompile names every step by its node id,
137
+ // so serialize would re-derive exactly `node.id` — anything else is the
138
+ // designer's or the author's and has to be carried, or it is lost on the next
139
+ // compile. Equal-to-default is omitted so ordinary flows stay quiet.
140
+ const label = node.display?.label;
141
+ if (typeof label === 'string' && label !== node.id)
142
+ parts.push(`label: ${str(label)}`);
143
+ const updates = ctx.flow.variables?.variableUpdates?.[node.id] ?? [];
144
+ if (updates.length) {
145
+ const entries = updates.map((u) => `${key(u.variableId)}: ${emitVal(u.expression, ctx.imp, ctx.inputNames)}`);
146
+ parts.push(`updates: { ${entries.join(', ')} }`);
147
+ }
148
+ return parts.length ? `, { ${parts.join(', ')} }` : '';
149
+ }
150
+ // ─── Node type constants (mirror of serialize.ts NODE_TYPE) ───────────────────
151
+ const T = {
152
+ triggerManual: 'core.trigger.manual',
153
+ triggerScheduled: 'core.trigger.scheduled',
154
+ triggerForm: 'core.trigger.form',
155
+ triggerConversation: 'core.trigger.conversation',
156
+ hitl: 'uipath.human-in-the-loop',
157
+ inlineAgent: 'uipath.agent.autonomous',
158
+ conversationalAgent: 'uipath.agent.conversational',
159
+ voiceAgent: 'uipath.agent.voice',
160
+ hitlQuickForm: 'uipath.human-in-the-loop.quick-form',
161
+ hitlActionApp: 'uipath.human-in-the-loop.coded-action-app',
162
+ hitlDocValidation: 'uipath.human-in-the-loop.document-validation',
163
+ queueCreate: 'core.action.queue.create',
164
+ queueCreateAndWait: 'core.action.queue.create-and-wait',
165
+ summarize: 'uipath.pattern.deep-rag',
166
+ batchTransform: 'uipath.pattern.batch-transform',
167
+ sendMessage: 'uipath.conversational.send-message',
168
+ waitForMessage: 'uipath.conversational.wait-for-message',
169
+ conversationContext: 'uipath.conversational.get-conversation-context',
170
+ triggerVoice: 'core.trigger.voice',
171
+ createOutgoingCall: 'uipath.conversational.voice.create-outgoing-call',
172
+ endCall: 'uipath.conversational.voice.end-call',
173
+ http: 'core.action.http',
174
+ httpV2: 'core.action.http.v2',
175
+ script: 'core.action.script',
176
+ transform: 'core.action.transform',
177
+ delay: 'core.logic.delay',
178
+ mock: 'core.logic.mock',
179
+ decision: 'core.logic.decision',
180
+ switch: 'core.logic.switch',
181
+ merge: 'core.logic.merge',
182
+ loop: 'core.logic.loop',
183
+ doWhile: 'core.logic.dowhile',
184
+ dataFabricRead: 'core.datafabric.read',
185
+ documentClassify: 'uipath.document.classify',
186
+ dynamicExtract: 'uipath.ixp.extract-document-builder',
187
+ dataFabricUpdate: 'core.datafabric.update',
188
+ subflow: 'core.subflow',
189
+ terminate: 'core.logic.terminate',
190
+ end: 'core.control.end',
191
+ };
192
+ /** A trigger node's type (its id is always `start`). */
193
+ function isTrigger(type) {
194
+ return type.startsWith('core.trigger.') || type.startsWith('uipath.connector.event.') || type.startsWith('uipath.connector.trigger.');
195
+ }
196
+ // ─── Injection-safe source emitters (ported verbatim from case/decompile.ts) ──
197
+ /**
198
+ * A JS/TS string literal (double-quoted, correctly escaped). `JSON.stringify`
199
+ * escapes quotes, backslashes and control chars — so untrusted `.flow` values
200
+ * (labels, expressions, code) can never break out of the literal. We also escape
201
+ * U+2028/U+2029 (legal in JSON, historically illegal in JS string literals).
202
+ * Non-string input still serializes to inert JSON (never executable).
203
+ */
204
+ function str(s) {
205
+ return JSON.stringify(s).replace(/[\u2028\u2029]/g, (c) => '\\u' + c.charCodeAt(0).toString(16));
206
+ }
207
+ /** A boolean literal, coerced so an untrusted non-boolean can never reach source verbatim. */
208
+ function boolLit(v) {
209
+ return v ? 'true' : 'false';
210
+ }
211
+ /** An object key: a bare identifier when legal, otherwise a quoted string. */
212
+ function key(k) {
213
+ return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(k) ? k : str(k);
214
+ }
215
+ /** An arbitrary JSON-serializable value as a TS literal (used for var defaults). */
216
+ function literal(v) {
217
+ return JSON.stringify(v);
218
+ }
219
+ /** Render an options object from present entries; '' when nothing is present. */
220
+ function opts(entries) {
221
+ const parts = entries.filter(([, v]) => v !== undefined).map(([k, v]) => `${key(k)}: ${v}`);
222
+ return parts.length ? `{ ${parts.join(', ')} }` : '';
223
+ }
224
+ /** A collector for indented source lines. */
225
+ class Emit {
226
+ indentUnit;
227
+ lines = [];
228
+ constructor(indentUnit = ' ') {
229
+ this.indentUnit = indentUnit;
230
+ }
231
+ line(depth, text) {
232
+ this.lines.push(this.indentUnit.repeat(depth) + text);
233
+ }
234
+ toString() {
235
+ return this.lines.join('\n');
236
+ }
237
+ }
238
+ /** Tracks which SDK identifiers the emitted source uses, so the import is exact. */
239
+ class Imports {
240
+ used = new Set(['flow']);
241
+ use(name) {
242
+ this.used.add(name);
243
+ return name;
244
+ }
245
+ line(specifier) {
246
+ const names = [...this.used].sort();
247
+ return `import { ${names.join(', ')} } from ${str(specifier)};`;
248
+ }
249
+ }
250
+ // ─── Expression inversion ─────────────────────────────────────────────────────
251
+ /**
252
+ * Unwrap a node input / output `source` value into a raw expression string.
253
+ * `migrate.ts` wraps dynamic values as `{type:'jsExpression', expression, …}`
254
+ * (the `=js:` prefix stripped) and literals as `{type:'literal', expression}`;
255
+ * this is the inverse. A plain string is returned as-is.
256
+ */
257
+ function unwrapSource(v) {
258
+ if (typeof v === 'string')
259
+ return v;
260
+ if (v && typeof v === 'object' && 'expression' in v) {
261
+ const o = v;
262
+ if (o.type === 'jsExpression')
263
+ return '=js:' + o.expression;
264
+ return o.expression;
265
+ }
266
+ return undefined;
267
+ }
268
+ /** A plain (boolean/number) input value, unwrapping a literal EV envelope if present. */
269
+ function unwrapPlain(v) {
270
+ if (v && typeof v === 'object' && 'expression' in v && v.type === 'literal') {
271
+ try {
272
+ return JSON.parse(v.expression);
273
+ }
274
+ catch {
275
+ return undefined;
276
+ }
277
+ }
278
+ return v;
279
+ }
280
+ /** True when any node in the body scope wires the container's break handle. */
281
+ function bodyBreaks(graph, scope, containerId) {
282
+ return [...scope].some((id) => graph.outEdges(id).some((e) => e.targetNodeId === containerId && e.targetPort === 'break'));
283
+ }
284
+ /** Escape a raw JS expression body for embedding in a `` js`…` `` tagged template. */
285
+ function jsTemplate(body) {
286
+ const esc = body.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$\{/g, '\\${');
287
+ return '`' + esc + '`';
288
+ }
289
+ /**
290
+ * Emit builder source for a dynamic reference or computed expression (the JS
291
+ * body of an `=js:` value, or a bare decision/switch expression). Recognizes the
292
+ * common single-reference shapes and lowers them to the readable helpers
293
+ * (`input` / `v` / `out` / `err` / `ran`); anything composite falls back to a
294
+ * `` js`…` `` template (which reproduces the same `Expr.js`).
295
+ */
296
+ function emitRef(jsBody, imp, inputNames) {
297
+ const body = jsBody.trim();
298
+ let m;
299
+ // The path/field grammar of a SINGLE reference: an identifier followed by any
300
+ // number of `.identifier` or `[index]` selectors — and NOTHING else. Anchoring
301
+ // to exactly this is what keeps a COMPOSITE expression like
302
+ // `$vars.p.output.ageDays <= 7` from being mis-read as `out('p','ageDays <= 7')`:
303
+ // the trailing ` <= 7` is not a path segment, so the whole thing falls through
304
+ // to the `js\`…\`` template below.
305
+ const PATH = '[A-Za-z_]\\w*(?:\\.[A-Za-z_]\\w*|\\[\\d+\\])*';
306
+ // !!$vars.<step>.output → ran('step')
307
+ if ((m = body.match(/^!!\$vars\.([A-Za-z_]\w*)\.output$/)))
308
+ return `${imp.use('ran')}(${str(m[1])})`;
309
+ // $vars.<step>.output[.<path>] → out('step'[, 'path'])
310
+ if ((m = body.match(new RegExp(`^\\$vars\\.([A-Za-z_]\\w*)\\.output(?:\\.(${PATH}))?$`)))) {
311
+ // A flow INPUT is published as the trigger's output, so
312
+ // `$vars.start.output.<declared input>` is what `input('<name>')` lowers to.
313
+ // Recover the author's spelling rather than the mechanical `out('start', …)`;
314
+ // both re-serialize identically, but only one reads like the source.
315
+ if (m[1] === currentTriggerId && m[2] !== undefined && inputNames.has(m[2])) {
316
+ return `${imp.use('input')}(${str(m[2])})`;
317
+ }
318
+ return m[2] === undefined ? `${imp.use('out')}(${str(m[1])})` : `${imp.use('out')}(${str(m[1])}, ${str(m[2])})`;
319
+ }
320
+ // $vars.<step>.error[.<field>] → err('step'[, 'field'])
321
+ if ((m = body.match(new RegExp(`^\\$vars\\.([A-Za-z_]\\w*)\\.error(?:\\.(${PATH}))?$`)))) {
322
+ return m[2] === undefined ? `${imp.use('err')}(${str(m[1])})` : `${imp.use('err')}(${str(m[1])}, ${str(m[2])})`;
323
+ }
324
+ // $vars.<name> → input('name') for a declared input, else v('name').
325
+ // The bare spelling predates flow inputs moving to `<trigger>.output.<name>`;
326
+ // still read it so an older artifact decompiles.
327
+ if ((m = body.match(/^\$vars\.([A-Za-z_]\w*)$/))) {
328
+ const name = m[1];
329
+ return inputNames.has(name) ? `${imp.use('input')}(${str(name)})` : `${imp.use('v')}(${str(name)})`;
330
+ }
331
+ // Composite / unrecognized: a raw JS expression template.
332
+ return `${imp.use('js')}${jsTemplate(body)}`;
333
+ }
334
+ /**
335
+ * Emit builder source for ANY input value — a node input, a `.return()` value, a
336
+ * connector field. Handles the three shapes a serialized value takes: an
337
+ * expression envelope (`{type,expression,fieldType}`), a plain/`=js:` string, or
338
+ * a raw JSON literal (number / boolean / null / array / object). A `=js:` value
339
+ * lowers to a reference helper; everything else is a literal (a plain string, or
340
+ * `lit(...)` for structured/typed literals). All three are accepted anywhere the
341
+ * builder takes `Expr | string | unknown` (bare values pass through `toExpr`).
342
+ */
343
+ function emitVal(v, imp, inputNames) {
344
+ const raw = unwrapSource(v);
345
+ if (raw !== undefined && !Array.isArray(v)) {
346
+ if (raw.startsWith('=js:'))
347
+ return emitRef(raw.slice(4), imp, inputNames);
348
+ return str(raw); // a literal string (valid as Expr via toExpr, or as a raw string input)
349
+ }
350
+ // A raw JSON literal (number / boolean / null / array / object).
351
+ return literal(v);
352
+ }
353
+ /** A bare (unprefixed) JS expression, as decision/switch nodes store it. */
354
+ function emitBareExpr(raw, imp, inputNames) {
355
+ return emitRef(raw, imp, inputNames);
356
+ }
357
+ /** Render an object literal `{ k: <src>, … }` from pre-rendered value sources; '' when empty. */
358
+ function objLit(entries) {
359
+ if (entries.length === 0)
360
+ return '';
361
+ return `{ ${entries.map(([k, v]) => `${key(k)}: ${v}`).join(', ')} }`;
362
+ }
363
+ /** A map of `{name: value}` inputs (headers / query / connector fields) → object-literal source. */
364
+ function emitValMap(m, imp, inputNames) {
365
+ if (!m || typeof m !== 'object')
366
+ return '';
367
+ return objLit(Object.entries(m).map(([k, v]) => [k, emitVal(v, imp, inputNames)]));
368
+ }
369
+ /**
370
+ * An agent's ARTIFACT handles. Edges leaving these carry no control flow — they
371
+ * attach a resource (a tool, a grounding index, an escalation, a memory) that
372
+ * takes no input, produces no output and never reaches End. Treating one as a
373
+ * successor reads the agent as an unstructured fan-out and drops everything
374
+ * after it, so they are excluded from the walk and recovered as arguments on
375
+ * the agent's own spec instead.
376
+ */
377
+ const ARTIFACT_PORTS = new Set(['tool', 'context', 'escalation', 'memory']);
378
+ // ─── Graph model ──────────────────────────────────────────────────────────────
379
+ class Graph {
380
+ byId = new Map();
381
+ out = new Map();
382
+ inc = new Map();
383
+ constructor(nodes, edges) {
384
+ for (const n of nodes)
385
+ this.byId.set(n.id, n);
386
+ for (const e of edges) {
387
+ (this.out.get(e.sourceNodeId) ?? this.out.set(e.sourceNodeId, []).get(e.sourceNodeId)).push(e);
388
+ (this.inc.get(e.targetNodeId) ?? this.inc.set(e.targetNodeId, []).get(e.targetNodeId)).push(e);
389
+ }
390
+ }
391
+ outEdges(id) {
392
+ return this.out.get(id) ?? [];
393
+ }
394
+ inEdges(id) {
395
+ return this.inc.get(id) ?? [];
396
+ }
397
+ /**
398
+ * A per-outcome human task's PRIMARY exit — the one serialize continues the
399
+ * main path from (`outcome-<first outcome's id>`), so it is a successor while
400
+ * its siblings are side paths. `undefined` for every other node.
401
+ */
402
+ primaryOutcomePort(id) {
403
+ const n = this.byId.get(id);
404
+ const type = String(n?.type ?? '');
405
+ if (!type.startsWith('uipath.human-in-the-loop'))
406
+ return undefined;
407
+ const outcomes = (n?.inputs?.schema?.outcomes ?? []);
408
+ const first = outcomes[0]?.id;
409
+ return typeof first === 'string' && first !== '' ? `outcome-${first}` : undefined;
410
+ }
411
+ /**
412
+ * Out-edges excluding the side paths recovered as port lists: `error`,
413
+ * `branch-*`, and every `outcome-*` except the primary one.
414
+ */
415
+ successorEdges(id) {
416
+ const primary = this.primaryOutcomePort(id);
417
+ return this.outEdges(id).filter((e) => e.sourcePort !== 'error'
418
+ && !ARTIFACT_PORTS.has(e.sourcePort)
419
+ && !e.sourcePort.startsWith('branch-')
420
+ && (!e.sourcePort.startsWith('outcome-') || e.sourcePort === primary));
421
+ }
422
+ }
423
+ // ─── Node → step-spec source ──────────────────────────────────────────────────
424
+ /**
425
+ * Emit the action-spec argument for a `.step(id, <spec>)` call — e.g.
426
+ * `script({ code: "…" })`. Returns `undefined` for a node that is not a plain
427
+ * action (control-flow / trigger / end), which the traversal handles specially.
428
+ */
429
+ function emitStepSpec(node, imp, inputNames, o, graph) {
430
+ const inputs = node.inputs ?? {};
431
+ switch (node.type) {
432
+ case T.mock:
433
+ return `${imp.use('mock')}()`;
434
+ case T.script: {
435
+ const code = typeof inputs.script === 'string' ? inputs.script : '';
436
+ // `returns` is emitted only when the node's declared output type is
437
+ // something re-compiling the body would NOT produce on its own. Serialize
438
+ // infers from the body first (see `scriptOutputs`), so a script whose type
439
+ // is already plain in its code round-trips without it — emitting one there
440
+ // would add an argument the author never wrote to every such step.
441
+ return `${imp.use('script')}(${opts([['code', str(code)], ...scriptReturnsOpt(node, code)])})`;
442
+ }
443
+ case T.delay: {
444
+ // serialize's `delayInputs` writes `{timerType, timerPreset, timerValue?}`
445
+ // for a duration and `{timerType:'timeDate', timerDate}` for an absolute
446
+ // wait. Invert all three spellings.
447
+ if (inputs.timerType === 'timeDate') {
448
+ const until = typeof inputs.timerDate === 'string' ? inputs.timerDate : '';
449
+ return `${imp.use('delay')}(${opts([['until', str(until)]])})`;
450
+ }
451
+ const duration = inputs.timerPreset === 'custom'
452
+ ? (typeof inputs.timerValue === 'string' ? inputs.timerValue : '')
453
+ : (typeof inputs.timerPreset === 'string' ? inputs.timerPreset : '');
454
+ return `${imp.use('delay')}(${opts([['duration', str(duration)]])})`;
455
+ }
456
+ case T.documentClassify:
457
+ case T.dynamicExtract: {
458
+ const fr = unwrapSource(inputs.fileRef) ?? '';
459
+ const fileSrc = fr.startsWith('=js:') ? emitBareExpr(fr.slice(4), imp, inputNames) : str(fr);
460
+ const pageRange = typeof inputs.pageRange === 'string' && inputs.pageRange !== '' ? str(inputs.pageRange) : undefined;
461
+ if (node.type === T.documentClassify) {
462
+ const split = unwrapPlain(inputs.splitPages);
463
+ return `${imp.use('documentClassify')}(${opts([
464
+ ['fileRef', fileSrc],
465
+ ['pageRange', pageRange],
466
+ ['splitPages', split === true ? 'true' : undefined],
467
+ ])})`;
468
+ }
469
+ const schema = inputs.schemaDocument !== undefined ? literal(inputs.schemaDocument) : undefined;
470
+ return `${imp.use('dynamicExtract')}(${opts([
471
+ ['fileRef', fileSrc],
472
+ ['schema', schema],
473
+ ['pageRange', pageRange],
474
+ ])})`;
475
+ }
476
+ case T.dataFabricRead:
477
+ case T.dataFabricUpdate: {
478
+ const cfg = (inputs.entityConfig ?? {});
479
+ const entity = str(String(cfg.entityName ?? ''));
480
+ const folder = typeof cfg._folderKey === 'string' ? str(cfg._folderKey) : undefined;
481
+ const val = (v) => {
482
+ const raw = typeof v === 'string' ? v : String(v ?? '');
483
+ return raw.startsWith('=js:') ? emitBareExpr(raw.slice(4), imp, inputNames) : str(raw);
484
+ };
485
+ if (node.type === T.dataFabricRead) {
486
+ const rows = Array.isArray(cfg._filters) ? cfg._filters : [];
487
+ const filterRow = (r) => opts([
488
+ ['field', str(String(r.field ?? ''))],
489
+ ['operator', r.operator !== '=' ? str(String(r.operator ?? '')) : undefined],
490
+ ['value', val(r.value)],
491
+ ['or', r['join'] === 'OR' ? 'true' : undefined],
492
+ ]);
493
+ const filters = rows.length ? `[${rows.map(filterRow).join(', ')}]` : undefined;
494
+ return `${imp.use('dataFabricRead')}(${opts([['entity', entity], ['filters', filters], ['folderKey', folder]])})`;
495
+ }
496
+ const record = cfg.recordSource === 'fromRead'
497
+ ? `{ fromRead: ${str(String(cfg.readEntityNodeId ?? ''))} }`
498
+ : `{ byId: ${val(cfg.recordId)} }`;
499
+ const updates = Array.isArray(cfg.fieldUpdates) ? cfg.fieldUpdates : [];
500
+ const set = `{ ${updates.map((u) => `${key(String(u.field ?? ''))}: ${val(u.value)}`).join(', ')} }`;
501
+ return `${imp.use('dataFabricUpdate')}(${opts([['entity', entity], ['record', record], ['set', set], ['folderKey', folder]])})`;
502
+ }
503
+ case T.createOutgoingCall:
504
+ case T.endCall: {
505
+ const i = (node.inputs ?? {});
506
+ const v = (x) => {
507
+ const raw = typeof x === 'string' ? x : String(x?.expression ?? x ?? '');
508
+ return raw.startsWith('=js:') ? emitBareExpr(raw.slice(4), imp, inputNames)
509
+ : raw.startsWith('$vars.') ? emitBareExpr(raw, imp, inputNames)
510
+ : str(raw);
511
+ };
512
+ if (node.type === T.createOutgoingCall) {
513
+ return `${imp.use('createOutgoingCall')}(${opts([['from', v(i.from)], ['to', v(i.to)]])})`;
514
+ }
515
+ return `${imp.use('endCall')}(${opts([['callContext', v(i.callContext)]])})`;
516
+ }
517
+ case T.inlineAgent:
518
+ case T.conversationalAgent:
519
+ case T.voiceAgent:
520
+ return emitAgentCluster(node, imp, inputNames, graph);
521
+ case T.hitl:
522
+ case T.hitlQuickForm:
523
+ case T.hitlActionApp:
524
+ case T.hitlDocValidation:
525
+ return emitHitl(node, imp, inputNames);
526
+ case T.queueCreate:
527
+ case T.queueCreateAndWait: {
528
+ // `itemData` is the node's name for the surface's `item` (the reference
529
+ // documents the mapping); `queue` carries the three identity fields.
530
+ const q = (inputs.queue ?? {});
531
+ const rows = Object.entries((inputs.itemData ?? {}))
532
+ .map(([k, v]) => [k, emitVal(v, imp, inputNames)]);
533
+ const optStr = (key) => {
534
+ const raw = unwrapSource(inputs[key]);
535
+ // serialize writes '' for an omitted optional — that is not an authored
536
+ // value, so it must not come back as one.
537
+ if (raw === undefined || raw === '')
538
+ return undefined;
539
+ return raw.startsWith('=js:') ? emitBareExpr(raw.slice(4), imp, inputNames) : str(raw);
540
+ };
541
+ const priority = unwrapPlain(inputs.priority);
542
+ return `${imp.use('queueItem')}(${opts([
543
+ ['queue', str(String(q.name ?? ''))],
544
+ ['folderPath', str(String(q.folderPath ?? ''))],
545
+ ['key', str(String(q.key ?? ''))],
546
+ ['item', rows.length ? objLit(rows) : '{}'],
547
+ // `Normal` is the definition's default — only a departure was authored.
548
+ ['priority', typeof priority === 'string' && priority !== 'Normal' ? str(priority) : undefined],
549
+ ['reference', optStr('reference')],
550
+ ['deferDate', optStr('deferDate')],
551
+ ['dueDate', optStr('dueDate')],
552
+ ['wait', node.type === T.queueCreateAndWait ? 'true' : undefined],
553
+ ])})`;
554
+ }
555
+ case T.summarize:
556
+ case T.batchTransform: {
557
+ const attachment = emitVal(inputs.attachment, imp, inputNames);
558
+ const prompt = emitVal(inputs.prompt, imp, inputNames);
559
+ if (node.type === T.summarize) {
560
+ return `${imp.use('summarize')}(${opts([
561
+ ['attachment', attachment],
562
+ ['prompt', prompt],
563
+ // Both flags default to false in the emitted node; only `true` was authored.
564
+ ['returnCitations', unwrapPlain(inputs.returnCitations) === true ? 'true' : undefined],
565
+ ])})`;
566
+ }
567
+ const cols = Array.isArray(inputs.outputColumns) ? inputs.outputColumns : [];
568
+ const colSrc = `[${cols.map((c) => opts([
569
+ ['name', emitVal(c.name, imp, inputNames)],
570
+ ['description', emitVal(c.description, imp, inputNames)],
571
+ ])).join(', ')}]`;
572
+ return `${imp.use('batchTransform')}(${opts([
573
+ ['attachment', attachment],
574
+ ['prompt', prompt],
575
+ ['outputColumns', colSrc],
576
+ ['enableWebSearchGrounding', unwrapPlain(inputs.enableWebSearchGrounding) === true ? 'true' : undefined],
577
+ ])})`;
578
+ }
579
+ case T.sendMessage:
580
+ case T.waitForMessage:
581
+ case T.conversationContext: {
582
+ // The fixed enum fields (`role`/`mimeType`/`from`) come from the
583
+ // definition's defaults, so nothing of them is recovered — only what an
584
+ // author could have written.
585
+ const i = (node.inputs ?? {});
586
+ const v = (x) => {
587
+ const raw = typeof x === 'string' ? x : String(x?.expression ?? x ?? '');
588
+ return raw.startsWith('=js:') ? emitBareExpr(raw.slice(4), imp, inputNames)
589
+ : raw.startsWith('$vars.') ? emitBareExpr(raw, imp, inputNames)
590
+ : str(raw);
591
+ };
592
+ const num = (x) => {
593
+ if (x === undefined || x === null)
594
+ return undefined;
595
+ const n = Number(x?.expression ?? x);
596
+ return Number.isFinite(n) ? String(n) : undefined;
597
+ };
598
+ const cid = ['conversationId', v(i.conversationId)];
599
+ if (node.type === T.sendMessage) {
600
+ return `${imp.use('sendMessage')}(${opts([
601
+ cid,
602
+ ['exchangeId', v(i.exchangeId)],
603
+ ['content', v(i.content)],
604
+ // The definition defaults `endExchange` to false; only a departure
605
+ // from the default is something the author wrote.
606
+ ['endExchange', i.endExchange === true ? 'true' : undefined],
607
+ ])})`;
608
+ }
609
+ if (node.type === T.waitForMessage) {
610
+ return `${imp.use('waitForMessage')}(${opts([
611
+ cid,
612
+ ['numExchanges', num(i.numExchanges) === '20' ? undefined : num(i.numExchanges)],
613
+ ])})`;
614
+ }
615
+ return `${imp.use('conversationContext')}(${opts([
616
+ cid,
617
+ ['exchangeLimit', num(i.exchangeLimit) === '20' ? undefined : num(i.exchangeLimit)],
618
+ ])})`;
619
+ }
620
+ case T.http:
621
+ return emitHttp(node, false, imp, inputNames);
622
+ case T.httpV2:
623
+ return emitHttp(node, true, imp, inputNames);
624
+ default:
625
+ if (node.type.startsWith('core.action.transform'))
626
+ return emitTransform(node, imp, inputNames);
627
+ // A connector action node: `uipath.connector.<key>.<action>` (but NOT the
628
+ // event nodes `uipath.connector.event.*` / `.trigger.*`, which are triggers).
629
+ if (node.type.startsWith('uipath.connector.') && !node.type.startsWith('uipath.connector.event.') && !node.type.startsWith('uipath.connector.trigger.')) {
630
+ return emitConnector(node, imp, inputNames, o);
631
+ }
632
+ {
633
+ const published = emitPublishedResource(node, imp, inputNames);
634
+ if (published !== undefined)
635
+ return published;
636
+ }
637
+ // A mid-flow connector EVENT wait (`uipath.connector.event.<key>.<event>`
638
+ // placed as a step rather than as the start node).
639
+ if (node.type.startsWith('uipath.connector.event.')) {
640
+ const sub = emitEventSubscription(node, imp, inputNames);
641
+ if (sub !== undefined)
642
+ return `${imp.use('waitForEvent')}(${sub})`;
643
+ }
644
+ // A published IxP extractor: `uipath.ixp.<slug>.<versionId>-<folderKey>`.
645
+ // The document-processing pair (`uipath.ixp.extract-document-builder`,
646
+ // `uipath.document.classify`) are their own families, handled above and
647
+ // excluded here by the parse — their tails are not the GUID pair.
648
+ if (node.type.startsWith('uipath.ixp.') && parseIxpProjectType(node.type) !== undefined) {
649
+ const i = (node.inputs ?? {});
650
+ const plain = (k) => {
651
+ const v = unwrapSource(i[k]);
652
+ return v === undefined || v === '' ? undefined : v;
653
+ };
654
+ const fr = unwrapSource(i.fileRef) ?? '';
655
+ // The DEPLOYMENT's display name is nested in `inputs.model`, not at the
656
+ // top of `inputs` — the two are different values (the model block's
657
+ // `modelDisplayName` is 'InvoiceIXP'; `modelName` is the slug), so
658
+ // reading the wrong one recovers the slug as the name.
659
+ const model = (i.model ?? {});
660
+ const nested = (k) => typeof model[k] === 'string' && model[k] !== '' ? model[k] : undefined;
661
+ return `${imp.use('ixpExtract')}(${opts([
662
+ ['project', str(node.type)],
663
+ // `modelName`/`projectName` are one value in two fields; either recovers it.
664
+ ['modelName', str(plain('modelName') ?? plain('projectName') ?? nested('modelName') ?? '')],
665
+ ['name', str(nested('modelDisplayName') ?? plain('modelName') ?? '')],
666
+ ['folderName', str(plain('folderName') ?? nested('folderName') ?? '')],
667
+ ['fileRef', fr.startsWith('=js:') ? emitBareExpr(fr.slice(4), imp, inputNames) : str(fr)],
668
+ ['pageRange', plain('pageRange') !== undefined ? str(plain('pageRange')) : undefined],
669
+ ['versionTag', plain('versionTag') !== undefined ? str(plain('versionTag')) : undefined],
670
+ ['description', plain('description') ?? nested('description') !== undefined
671
+ ? str((plain('description') ?? nested('description'))) : undefined],
672
+ ])})`;
673
+ }
674
+ // Every family this SDK AUTHORS is recovered above — a guard test asserts
675
+ // it (`flow-decompile-families.test.ts`). Reaching here means a node type
676
+ // the SDK cannot author: a hand-written file, a newer platform family, or
677
+ // a UI-only node (a sticky note). Best effort keeps the node as a
678
+ // placeholder so the graph shape and `merge` write-back still work.
679
+ return unsupportedSpec(node, imp, o);
680
+ }
681
+ }
682
+ /**
683
+ * The `returns` argument for a script step, or nothing.
684
+ *
685
+ * Recovers what the author declared by DIFFERENCE: the node says what its output
686
+ * is, `inferScriptShape` says what re-compiling the body would declare on its
687
+ * own, and only a gap between the two needs writing down. Fields are recovered
688
+ * the same way — a property map the body does not produce is one the author gave
689
+ * as `returns: {…}`.
690
+ */
691
+ function scriptReturnsOpt(node, code) {
692
+ const out = node.outputs?.output;
693
+ const declared = typeof out?.type === 'string' ? out.type : undefined;
694
+ if (!declared)
695
+ return [];
696
+ const props = out?.properties;
697
+ // What re-compiling this body with NO `returns` would declare. Mirror
698
+ // `scriptOutputs` exactly: an inference of a bare `object` (or none at all)
699
+ // leaves the definition's own `{type:'object'}` standing, untouched.
700
+ const inferred = inferScriptShape(code);
701
+ const effective = inferred && !(inferred.type === 'object' && !inferred.properties)
702
+ ? inferred
703
+ : { type: 'object', properties: undefined };
704
+ const same = effective.type === declared
705
+ && JSON.stringify(sortKeys(effective.properties ?? {})) === JSON.stringify(sortKeys(props ?? {}));
706
+ if (same)
707
+ return [];
708
+ // Not what the body alone would say — so the author declared it, and it has to
709
+ // be written back or the round-trip loses it. That includes declaring the OPEN
710
+ // object over a body that looks typed, which is a real thing to want.
711
+ if (props && Object.keys(props).length > 0) {
712
+ // A field map is the richer statement, so it wins when there is one.
713
+ const entries = Object.entries(props)
714
+ .filter(([, v]) => typeof v?.type === 'string')
715
+ .map(([k, v]) => [k, str(v.type)]);
716
+ if (entries.length > 0)
717
+ return [['returns', opts(entries)]];
718
+ }
719
+ return [['returns', str(declared)]];
720
+ }
721
+ /** Key-sorted copy, so a property map compares by content and not by order. */
722
+ function sortKeys(o) {
723
+ return Object.fromEntries(Object.entries(o).sort(([a], [b]) => a.localeCompare(b)));
724
+ }
725
+ /** A validated numeric literal — never emit a raw JSON value that could carry code. */
726
+ function numLit(v) {
727
+ const n = Number(v);
728
+ if (!Number.isFinite(n))
729
+ throw new Error(`decompile: expected a number, got ${JSON.stringify(v)}.`);
730
+ return String(n);
731
+ }
732
+ /**
733
+ * Invert an HTTP node. The plain node (`core.action.http`) keeps its request at
734
+ * the top level (`url` / `method` / `headers` / `queryParams` / `body` /
735
+ * `contentType`); the managed node (`core.action.http.v2`) nests the request
736
+ * inside `detail.bodyParameters` and routes through IS. Request settings
737
+ * (`timeout` / `retryCount`) sit at the top level on both. Emitted only-when-off
738
+ * their definition defaults, exactly as serialize writes them, so the round-trip
739
+ * stays minimal. The response schema (`returns`) is a Phase-2 refinement.
740
+ */
741
+ function emitHttp(node, managed, imp, inputNames) {
742
+ const inputs = node.inputs ?? {};
743
+ const detail = managed
744
+ ? (inputs.detail ?? {})
745
+ : {};
746
+ const req = managed
747
+ ? detail.bodyParameters ?? {}
748
+ : inputs;
749
+ const method = typeof req.method === 'string' ? req.method : 'GET';
750
+ const headers = managed ? req.headers : inputs.headers;
751
+ const query = managed ? req.query : inputs.queryParams;
752
+ const bodySrc = req.body !== undefined && req.body !== '' ? emitVal(req.body, imp, inputNames) : undefined;
753
+ const headersSrc = emitValMap(headers, imp, inputNames) || undefined;
754
+ const querySrc = emitValMap(query, imp, inputNames) || undefined;
755
+ const contentType = !managed && typeof inputs.contentType === 'string' && inputs.contentType !== 'application/json'
756
+ ? str(inputs.contentType) : undefined;
757
+ const timeout = typeof inputs.timeout === 'string' && inputs.timeout !== 'PT15M' ? str(inputs.timeout) : undefined;
758
+ const retry = inputs.retryCount !== undefined && Number(inputs.retryCount) !== 0 ? numLit(inputs.retryCount) : undefined;
759
+ const branchRows = Array.isArray(inputs.branches) ? inputs.branches : [];
760
+ const branchesSrc = branchRows.length
761
+ ? `[${branchRows.map((b) => {
762
+ const cond = unwrapSource(b.conditionExpression) ?? '';
763
+ const expr = emitBareExpr(cond.startsWith('=js:') ? cond.slice(4) : cond, imp, inputNames);
764
+ return `{ name: ${str(String(b.name ?? b.id ?? ''))}, condition: ${expr} }`;
765
+ }).join(', ')}]`
766
+ : undefined;
767
+ const connectionValue = detail.connectionId ?? detail.connectionResourceId ?? req.connection;
768
+ const connection = managed && typeof connectionValue === 'string' && connectionValue !== 'ImplicitConnection'
769
+ ? str(connectionValue) : undefined;
770
+ const folder = managed && typeof detail.connectionFolderKey === 'string' && detail.connectionFolderKey.length > 0
771
+ ? str(detail.connectionFolderKey) : undefined;
772
+ const targetConnector = managed && typeof req.targetConnector === 'string'
773
+ && req.targetConnector !== 'uipath-uipath-http'
774
+ ? str(req.targetConnector) : undefined;
775
+ const spec = opts([
776
+ ['url', emitVal(req.url ?? '', imp, inputNames)],
777
+ ['method', method !== 'GET' ? str(method) : undefined],
778
+ ['headers', headersSrc],
779
+ ['query', querySrc],
780
+ ['body', bodySrc],
781
+ ['contentType', contentType],
782
+ ['managed', boolLit(managed)],
783
+ ['connection', connection],
784
+ ['folder', folder],
785
+ ['targetConnector', targetConnector],
786
+ ['timeout', timeout],
787
+ ['retryCount', retry],
788
+ ['branches', branchesSrc],
789
+ ]);
790
+ return `${imp.use('http')}(${spec})`;
791
+ }
792
+ /**
793
+ * Invert a Transform node (generic `core.action.transform` or a `.map` / `.filter`
794
+ * / `.group-by` variant). The `collection` is a bare `$vars.…` path; each
795
+ * operation's serialized `{id,type,config}` becomes a builder operation object.
796
+ * (The variant node types carry a `groupBy` operation spelled with a hyphen in
797
+ * the type — `group-by` — but `groupBy` in the operation, the platform's own
798
+ * mismatch, mirrored here.)
799
+ */
800
+ function emitTransform(node, imp, inputNames) {
801
+ const inputs = node.inputs ?? {};
802
+ const suffix = node.type.slice('core.action.transform'.length).replace(/^\./, '');
803
+ const variant = suffix || undefined; // 'map' | 'filter' | 'group-by' | undefined
804
+ const collRaw = unwrapSource(inputs.collection);
805
+ const collection = collRaw !== undefined
806
+ ? emitBareExpr(collRaw.startsWith('=js:') ? collRaw.slice(4) : collRaw, imp, inputNames)
807
+ : `${imp.use('lit')}([])`;
808
+ const ops = Array.isArray(inputs.operations) ? inputs.operations : [];
809
+ const opSrcs = ops.map((op) => {
810
+ const cfg = op?.config ?? {};
811
+ if (op?.type === 'filter') {
812
+ const filters = (cfg.filters ?? []).map((f) => objLit([
813
+ ['field', str(f.field)],
814
+ ['condition', str(f.condition)],
815
+ ...(f.value !== undefined ? [['value', literal(f.value)]] : []),
816
+ ]));
817
+ return objLit([
818
+ ['type', str('filter')],
819
+ ...(cfg.operation !== undefined ? [['operation', str(cfg.operation)]] : []),
820
+ ['filters', `[${filters.join(', ')}]`],
821
+ ]);
822
+ }
823
+ if (op?.type === 'map') {
824
+ const mappings = (cfg.mappings ?? []).map((m) => objLit([
825
+ ['field', str(m.field)],
826
+ ...(m.transformation && m.transformation !== 'copy' ? [['transformation', str(m.transformation)]] : []),
827
+ ...(m.renameTo ? [['renameTo', str(m.renameTo)]] : []),
828
+ ]));
829
+ return objLit([
830
+ ['type', str('map')],
831
+ ...(cfg.keepOriginalFields ? [['keepOriginalFields', boolLit(cfg.keepOriginalFields)]] : []),
832
+ ['mappings', `[${mappings.join(', ')}]`],
833
+ ]);
834
+ }
835
+ // groupBy
836
+ const aggs = (cfg.aggregations ?? []).map((a) => objLit([
837
+ ['operation', str(a.operation)],
838
+ ['alias', str(a.alias)],
839
+ ...(a.field ? [['field', str(a.field)]] : []),
840
+ ]));
841
+ return objLit([
842
+ ['type', str('groupBy')],
843
+ ['groupByField', str(cfg.groupByField)],
844
+ ['aggregations', `[${aggs.join(', ')}]`],
845
+ ]);
846
+ });
847
+ const spec = opts([
848
+ ['collection', collection],
849
+ ['variant', variant ? str(variant) : undefined],
850
+ ['operations', `[${opSrcs.join(', ')}]`],
851
+ ]);
852
+ return `${imp.use('transform')}(${spec})`;
853
+ }
854
+ /**
855
+ * Split a connector `nodeType` (`uipath.connector.<key>.<action>`) into key +
856
+ * action. Mirrors `core/connectors.ts`'s `splitConnectorNodeType`.
857
+ */
858
+ function splitConnector(nodeType) {
859
+ const rest = nodeType.replace(/^uipath\.connector\./, '');
860
+ const dot = rest.lastIndexOf('.');
861
+ if (dot <= 0)
862
+ return undefined;
863
+ return { key: rest.slice(0, dot), action: rest.slice(dot + 1) };
864
+ }
865
+ /** Pull `objectName` out of a connector's `=jsonString:` configuration blob, if present. */
866
+ function connectorObjectName(configuration) {
867
+ if (typeof configuration !== 'string' || !configuration.startsWith('=jsonString:'))
868
+ return undefined;
869
+ try {
870
+ const cfg = JSON.parse(configuration.slice('=jsonString:'.length));
871
+ const name = cfg?.instanceParameters?.objectName ?? cfg?.objectName;
872
+ return typeof name === 'string' ? name : undefined;
873
+ }
874
+ catch {
875
+ return undefined;
876
+ }
877
+ }
878
+ /**
879
+ * Invert a connector action node into a `connector(key, action, inputs, opts)`
880
+ * call. serialize routes author inputs into `detail.bodyParameters` /
881
+ * `queryParameters` / `pathParameters` or the `multipartParameters` list; the
882
+ * inverse merges values from those slots back into one flat inputs map —
883
+ * `buildConnectorInputs` re-routes them by field location on recompile. Static
884
+ * parts of `detail` (endpoint, configuration, telemetry) are regenerated from
885
+ * the library, so they are not carried through the code.
886
+ *
887
+ * NOTE: connector nodes need the connector library to (re)compile, which is not
888
+ * present in this checkout — it is baked into the e2e image — so this inversion
889
+ * is validated in-image, not by the local round-trip tests.
890
+ */
891
+ /**
892
+ * The PUBLISHED-RESOURCE families: RPA workflow, API workflow, published
893
+ * function, and the three Agentic Process forms (bpmn / flow / case).
894
+ *
895
+ * All five node types are `<prefix><key>`, and all of them keep the author's
896
+ * identity where the instance cannot: `name` and `folderPath` live in the
897
+ * definition's `model.bindings.values`, and the declared `returns` in its
898
+ * `outputDefinition.output.schema`. So this reads the definition and inverts
899
+ * the one emit path they share (`serialize`'s four-family arm).
900
+ *
901
+ * Returns `undefined` when the node is not one of them, so the caller can fall
902
+ * through to the next arm.
903
+ */
904
+ /**
905
+ * An AGENT and its attached resources — the inline autonomous agent, the
906
+ * conversational agent, and the voice agent.
907
+ *
908
+ * The cluster is a node plus artifact nodes on its `tool` / `context` /
909
+ * `escalation` / `memory` handles, so this walks those edges and recovers each
910
+ * resource as an argument rather than as a step (see {@link ARTIFACT_PORTS}).
911
+ *
912
+ * **The author's input NAMES are not recoverable, and that is by design.** The
913
+ * platform does not send an agent the names you declared: it keys each argument
914
+ * by the reference PATH, `__`-joined (`$vars.prep.output` → `prep__output`), and
915
+ * only those flat names survive in the emitted file. Recovery therefore uses
916
+ * them for both the `inputs` keys and the `{{input.…}}` placeholders — the same
917
+ * arguments reach the model, spelled the platform's way. (Node ids are
918
+ * preserved exactly; only these logical names change.)
919
+ */
920
+ function emitAgentCluster(node, imp, inputNames, graph) {
921
+ const i = (node.inputs ?? {});
922
+ const plain = (v) => {
923
+ const raw = unwrapSource(v);
924
+ return raw === undefined || raw === '' ? undefined : raw;
925
+ };
926
+ const num = (v) => {
927
+ const raw = unwrapPlain(v);
928
+ return typeof raw === 'number' ? String(raw) : undefined;
929
+ };
930
+ // Prompts: invert `renderInlineAgentPrompt`. It replaced each
931
+ // `{{input.<name>}}` with that input's rendered binding, so the bindings in
932
+ // `agentInputVariables` are exactly what to put back — longest first, so a
933
+ // shorter binding cannot eat a longer one's prefix.
934
+ const vars = (Array.isArray(i.agentInputVariables) ? i.agentInputVariables : []);
935
+ const refs = vars
936
+ .filter((v) => typeof v.id === 'string' && typeof v.binding === 'string')
937
+ .map((v) => ({ flatName: v.id, binding: v.binding }))
938
+ .sort((a, b) => b.binding.length - a.binding.length);
939
+ const unrender = (text) => {
940
+ let out = text;
941
+ for (const r of refs)
942
+ out = out.split(r.binding).join(`{{input.${r.flatName}}}`);
943
+ return out;
944
+ };
945
+ const promptSrc = (v) => {
946
+ const raw = plain(v);
947
+ return raw === undefined ? undefined : str(unrender(raw));
948
+ };
949
+ const inputRows = refs
950
+ // Emit in the file's own order, not the length-sorted one.
951
+ .slice()
952
+ .sort((a, b) => vars.findIndex((v) => v.id === a.flatName) - vars.findIndex((v) => v.id === b.flatName))
953
+ .map((r) => {
954
+ const js = r.binding.startsWith('=js:') ? r.binding.slice(4) : r.binding;
955
+ return [r.flatName, emitBareExpr(js, imp, inputNames)];
956
+ });
957
+ const outVars = (Array.isArray(i.agentOutputVariables) ? i.agentOutputVariables : []);
958
+ const returns = outVars.length
959
+ ? objLit(outVars.map((v) => [String(v.id ?? ''), str(String(v.type ?? 'string'))]))
960
+ : undefined;
961
+ // The resources, from the agent's own artifact edges.
962
+ const resource = (port) => (graph?.outEdges(node.id) ?? [])
963
+ .filter((e) => e.sourcePort === port)
964
+ .map((e) => graph.byId.get(e.targetNodeId))
965
+ .filter((n) => n !== undefined);
966
+ const rInputs = (n) => (n.inputs ?? {});
967
+ const rPlain = (n, k) => {
968
+ const raw = unwrapSource(rInputs(n)[k]);
969
+ return raw === undefined || raw === '' ? undefined : raw;
970
+ };
971
+ const toolSrc = resource('tool').map((t) => {
972
+ const ti = rInputs(t);
973
+ const type = t.type;
974
+ if (type === 'uipath.agent.resource.tool.clientside') {
975
+ const schemaTypes = (v) => {
976
+ const props = v?.properties;
977
+ if (!props || Object.keys(props).length === 0)
978
+ return undefined;
979
+ return objLit(Object.entries(props).map(([k, x]) => [k, str(String(x?.type ?? 'string'))]));
980
+ };
981
+ return opts([
982
+ ['kind', str('clientside')],
983
+ ['name', str(rPlain(t, 'name') ?? '')],
984
+ ['description', rPlain(t, 'description') !== undefined ? str(rPlain(t, 'description')) : undefined],
985
+ ['inputs', schemaTypes(ti.inputSchema)],
986
+ ['returns', schemaTypes(ti.outputSchema)],
987
+ ]);
988
+ }
989
+ if (type === 'uipath.agent.resource.tool.builtin.httprequest') {
990
+ // A field is authored only when it left prompt mode (text-builder).
991
+ const fixed = (k) => {
992
+ const row = ti[k];
993
+ if (!row || row.mode !== 'text-builder')
994
+ return undefined;
995
+ const v = row.textValue;
996
+ return typeof v === 'number' ? String(v) : typeof v === 'string' ? str(v) : JSON.stringify(v);
997
+ };
998
+ return opts([
999
+ ['kind', str('httpRequest')],
1000
+ ['url', fixed('url')], ['method', fixed('method')],
1001
+ ['headers', fixed('headers')], ['params', fixed('params')],
1002
+ ['body', fixed('body')], ['timeout', fixed('timeout')],
1003
+ ]);
1004
+ }
1005
+ if (type.startsWith('uipath.agent.resource.tool.mcp.')) {
1006
+ return opts([
1007
+ ['kind', str('mcp')],
1008
+ ['name', str(rPlain(t, 'name') ?? '')],
1009
+ ['key', str(rPlain(t, 'referenceKey') ?? '')],
1010
+ // The mapper's field name for the canonical server slug.
1011
+ ['slug', rPlain(t, 'serverUrl') !== undefined ? str(rPlain(t, 'serverUrl')) : undefined],
1012
+ ['folderPath', rPlain(t, 'folderPath') !== undefined ? str(rPlain(t, 'folderPath')) : undefined],
1013
+ ['mcpType', rPlain(t, 'mcpType') !== undefined ? str(rPlain(t, 'mcpType')) : undefined],
1014
+ ]);
1015
+ }
1016
+ if (type.startsWith('uipath.agent.resource.tool.a2a.')) {
1017
+ return opts([
1018
+ ['kind', str('a2a')],
1019
+ ['name', str(rPlain(t, 'name') ?? '')],
1020
+ ['key', str(rPlain(t, 'referenceKey') ?? '')],
1021
+ ['slug', str(rPlain(t, 'slug') ?? '')],
1022
+ ['folderPath', rPlain(t, 'folderPath') !== undefined ? str(rPlain(t, 'folderPath')) : undefined],
1023
+ ]);
1024
+ }
1025
+ if (type.startsWith('uipath.agent.resource.tool.builtin.')) {
1026
+ return opts([
1027
+ ['kind', str('builtin')],
1028
+ ['tool', str(type.slice('uipath.agent.resource.tool.builtin.'.length))],
1029
+ ['description', rPlain(t, 'description') !== undefined ? str(rPlain(t, 'description')) : undefined],
1030
+ ]);
1031
+ }
1032
+ if (type.startsWith('uipath.agent.resource.tool.ixp.')) {
1033
+ const tail = type.slice('uipath.agent.resource.tool.ixp.'.length);
1034
+ return opts([
1035
+ ['kind', str('ixp')],
1036
+ ['projectId', str(tail.slice(tail.indexOf('.') + 1))],
1037
+ ['name', str(rPlain(t, 'name') ?? '')],
1038
+ ]);
1039
+ }
1040
+ // The per-instance resource kinds (process / api / flow / maestro / agent /
1041
+ // function): the key is the node type's tail, the rest is a binding pair.
1042
+ const PROCESS_NS = [
1043
+ ['uipath.agent.resource.tool.processorchestration.', 'maestro'],
1044
+ ['uipath.agent.resource.tool.process.', 'process'],
1045
+ ['uipath.agent.resource.tool.function.', 'function'],
1046
+ ['uipath.agent.resource.tool.api.', 'api'],
1047
+ ['uipath.agent.resource.tool.flow.', 'flow'],
1048
+ ['uipath.agent.resource.tool.agent.', 'agent'],
1049
+ ];
1050
+ const ns = PROCESS_NS.find(([prefix]) => type.startsWith(prefix));
1051
+ if (ns) {
1052
+ const def = runDefs.get(`${type}@${t.typeVersion}`) ?? runDefs.get(type);
1053
+ const values = (def?.model?.bindings?.values ?? []);
1054
+ const bound = (n) => {
1055
+ const row = values.find((v) => v.name === n);
1056
+ return typeof row?.default === 'string' ? row.default : undefined;
1057
+ };
1058
+ return opts([
1059
+ ['kind', str(ns[1])],
1060
+ ['key', str(type.slice(ns[0].length))],
1061
+ ['name', str(bound('name') ?? rPlain(t, 'name') ?? '')],
1062
+ ['folderPath', str(bound('folderPath') ?? '')],
1063
+ ]);
1064
+ }
1065
+ return opts([['kind', str('builtin')], ['tool', str('summarize')]]); // unreachable in practice
1066
+ });
1067
+ const contextSrc = resource('context').map((c) => {
1068
+ // `query` is a MODE TRIPLE, not a string: the author's value lands in
1069
+ // `promptValue` (prompt mode) or `textValue` (text-builder). Reading the
1070
+ // field itself finds an object and silently recovers nothing.
1071
+ const q = rInputs(c).query;
1072
+ const queryText = typeof q?.promptValue === 'string' && q.promptValue !== '' ? q.promptValue
1073
+ : typeof q?.textValue === 'string' && q.textValue !== '' ? q.textValue
1074
+ : typeof q === 'string' && q !== '' ? q
1075
+ : undefined;
1076
+ return opts([
1077
+ ['name', str(rPlain(c, 'indexName') ?? String(c.display?.label ?? ''))],
1078
+ ['id', str(rPlain(c, 'indexId') ?? '')],
1079
+ ['folderPath', rPlain(c, 'folderPath') !== undefined ? str(rPlain(c, 'folderPath')) : undefined],
1080
+ ['folderKey', rPlain(c, 'folderKey') !== undefined ? str(rPlain(c, 'folderKey')) : undefined],
1081
+ ['query', queryText !== undefined ? str(queryText) : undefined],
1082
+ ]);
1083
+ });
1084
+ const escalationSrc = resource('escalation').map((e) => {
1085
+ const ei = rInputs(e);
1086
+ const recipients = Array.isArray(ei.recipients)
1087
+ ? ei.recipients.map((r) => str(String(r.value ?? '')))
1088
+ : [];
1089
+ const outcomeNames = Object.keys((ei.outcomeMapping ?? {}));
1090
+ const isQuickForm = e.type === 'uipath.agent.resource.escalation.quick-form';
1091
+ const rows = isQuickForm
1092
+ ? (ei.schema?.fields ?? [])
1093
+ : [];
1094
+ const app = ei.app;
1095
+ const extra = (ei._additionalProps ?? {});
1096
+ return opts([
1097
+ ['variant', isQuickForm ? str('quick-form') : undefined],
1098
+ ['name', str(rPlain(e, 'name') ?? '')],
1099
+ ['description', rPlain(e, 'description') !== undefined ? str(rPlain(e, 'description')) : undefined],
1100
+ ['fields', rows.length ? `[${rows.map((f) => opts([
1101
+ ['id', str(String(f.id ?? ''))],
1102
+ ['type', str(String(f.type ?? 'text'))],
1103
+ ['direction', str(String(f.direction ?? 'output'))],
1104
+ ])).join(', ')}]` : undefined],
1105
+ ['app', app === undefined ? undefined : opts([
1106
+ ['key', str(String(app.key ?? ''))],
1107
+ ['name', str(String(app.name ?? ''))],
1108
+ ['folderPath', typeof app.folderPath === 'string' && app.folderPath !== '' ? str(app.folderPath) : undefined],
1109
+ ])],
1110
+ ['recipients', recipients.length ? `[${recipients.join(', ')}]` : undefined],
1111
+ ['outcomes', outcomeNames.length ? `[${outcomeNames.map((o) => str(o)).join(', ')}]` : undefined],
1112
+ ['taskTitle', typeof extra.taskTitle === 'string' && extra.taskTitle !== '' ? str(extra.taskTitle) : undefined],
1113
+ // 'medium' is the platform default.
1114
+ ['priority', typeof extra.priority === 'string' && extra.priority !== 'medium' ? str(extra.priority) : undefined],
1115
+ ]);
1116
+ });
1117
+ const mem = resource('memory')[0];
1118
+ const memorySrc = mem === undefined ? undefined : (() => {
1119
+ // The memory's id is the node type's tail; the def carries it too.
1120
+ const tail = mem.type.slice('uipath.agent.resource.memory.'.length);
1121
+ const id = tail.slice(tail.indexOf('.') + 1);
1122
+ const mi = rInputs(mem);
1123
+ return opts([
1124
+ ['name', str(rPlain(mem, 'name') ?? '')],
1125
+ ['id', str(id)],
1126
+ ['description', rPlain(mem, 'description') !== undefined ? str(rPlain(mem, 'description')) : undefined],
1127
+ // The platform defaults: few-shot on, similarity 0, k 3, hybrid search.
1128
+ ['dynamicFewShotLearning', unwrapPlain(mi.dynamicFewShotLearning) === false ? 'false' : undefined],
1129
+ ['semanticSimilarity', num(mi.semanticSimilarity) !== undefined && num(mi.semanticSimilarity) !== '0' ? num(mi.semanticSimilarity) : undefined],
1130
+ ['kValue', num(mi.kValue) !== undefined && num(mi.kValue) !== '3' ? num(mi.kValue) : undefined],
1131
+ ['searchMode', rPlain(mem, 'searchMode') !== undefined && rPlain(mem, 'searchMode') !== 'hybrid'
1132
+ ? str(rPlain(mem, 'searchMode')) : undefined],
1133
+ ]);
1134
+ })();
1135
+ const guardrails = Array.isArray(i.guardrails) && i.guardrails.length > 0
1136
+ ? JSON.stringify(i.guardrails)
1137
+ : undefined;
1138
+ const list = (items) => (items.length ? `[${items.join(', ')}]` : undefined);
1139
+ if (node.type === T.voiceAgent) {
1140
+ const v = (i.voice ?? {});
1141
+ const voiceSrc = Object.keys(v).length === 0 ? undefined : opts([
1142
+ ['model', typeof v.model === 'string' ? str(v.model) : undefined],
1143
+ ['persona', typeof v.persona === 'string' ? str(v.persona) : undefined],
1144
+ ['temperature', num(v.temperature)],
1145
+ ['maxTokens', num(v.maxTokens)],
1146
+ ]);
1147
+ return `${imp.use('voiceAgent')}(${opts([
1148
+ ['systemPrompt', promptSrc(i.systemPrompt) ?? str('')],
1149
+ ['inputs', inputRows.length ? objLit(inputRows) : undefined],
1150
+ ['callContext', (() => {
1151
+ const raw = plain(i.callContext) ?? '';
1152
+ const js = raw.startsWith('=js:') ? raw.slice(4) : raw;
1153
+ return js.startsWith('$vars.') ? emitBareExpr(js, imp, inputNames) : str(js);
1154
+ })()],
1155
+ ['voice', voiceSrc],
1156
+ ['maxIterations', num(i.maxIterations)],
1157
+ ['source', str(plain(i.source) ?? '')],
1158
+ ])})`;
1159
+ }
1160
+ if (node.type === T.conversationalAgent) {
1161
+ const st = (i.conversationalAgentSettings ?? {});
1162
+ const settingExpr = (k) => {
1163
+ const raw = typeof st[k] === 'string' ? st[k] : undefined;
1164
+ if (raw === undefined || raw === '')
1165
+ return undefined;
1166
+ const js = raw.startsWith('=js:') ? raw.slice(4) : raw;
1167
+ return js.startsWith('$vars.') ? emitBareExpr(js, imp, inputNames) : str(js);
1168
+ };
1169
+ // `'simple'` mode DERIVES the four runtime fields from `context`, so only
1170
+ // the context binding is the author's; recovering the derived ones would
1171
+ // re-emit them as hand-bound custom fields.
1172
+ const mode = typeof st.mode === 'string' ? st.mode : 'simple';
1173
+ const settingsSrc = mode === 'custom'
1174
+ ? opts([
1175
+ ['mode', str('custom')],
1176
+ ['conversationId', settingExpr('conversationId')],
1177
+ ['exchangeId', settingExpr('exchangeId')],
1178
+ ['messages', settingExpr('messages')],
1179
+ ['userSettings', settingExpr('userSettings')],
1180
+ ])
1181
+ : opts([['context', settingExpr('context')]]);
1182
+ return `${imp.use('conversationalAgent')}(${opts([
1183
+ ['model', str(plain(i.model) ?? '')],
1184
+ ['systemPrompt', promptSrc(i.systemPrompt) ?? str('')],
1185
+ ['settings', settingsSrc],
1186
+ ['endExchange', unwrapPlain(i.endExchange) === false ? 'false' : undefined],
1187
+ ['temperature', num(i.temperature)],
1188
+ ['maxTokenPerResponse', num(i.maxTokenPerResponse)],
1189
+ ['modelMaxTokens', num(i.modelMaxTokens)],
1190
+ ['maxIterations', num(i.maxIterations)],
1191
+ ['guardrails', guardrails],
1192
+ ['source', str(plain(i.source) ?? '')],
1193
+ ])})`;
1194
+ }
1195
+ return `${imp.use('inlineAgent')}(${opts([
1196
+ ['model', str(plain(i.model) ?? '')],
1197
+ ['systemPrompt', promptSrc(i.systemPrompt) ?? str('')],
1198
+ ['userPrompt', promptSrc(i.userPrompt) ?? str('')],
1199
+ ['inputs', inputRows.length ? objLit(inputRows) : undefined],
1200
+ ['returns', returns],
1201
+ ['temperature', num(i.temperature)],
1202
+ ['maxTokenPerResponse', num(i.maxTokenPerResponse)],
1203
+ ['modelMaxTokens', num(i.modelMaxTokens)],
1204
+ ['maxIterations', num(i.maxIterations)],
1205
+ ['mode', plain(i.mode) !== undefined ? str(plain(i.mode)) : undefined],
1206
+ ['guardrails', guardrails],
1207
+ ['context', list(contextSrc)],
1208
+ ['tools', list(toolSrc)],
1209
+ ['escalation', list(escalationSrc)],
1210
+ ['memory', memorySrc],
1211
+ // The agent's directory uuid is derived from the flow and step ids, but a
1212
+ // decompiled flow must keep the ORIGINAL — the sidecar lives at
1213
+ // `<source>/agent.json`, so a re-derived one would point at nothing.
1214
+ ['source', str(plain(i.source) ?? '')],
1215
+ ])})`;
1216
+ }
1217
+ /**
1218
+ * A HUMAN TASK — the base node plus its three sub-typed variants.
1219
+ *
1220
+ * `hitlInputs` is the widest lowering in the serializer, so this inverts it
1221
+ * field by field. Three of its outputs are DERIVED and deliberately not
1222
+ * recovered: `schema.schemaId` (a stable hash of the flow and node ids),
1223
+ * `type: 'quick'` (the base node's discriminator), and the `variable`/`binding`
1224
+ * wiring each field row carries — all of them are re-emitted from what comes
1225
+ * back here.
1226
+ *
1227
+ * A value equal to what serialize would have DEFAULTED is omitted rather than
1228
+ * recovered: the step's own name for `title`, `'Low'` for `priority`, and the
1229
+ * definition's own `inputDefaults.recipient` for delivery. Recovering those
1230
+ * would add arguments the author never wrote to every task.
1231
+ */
1232
+ function emitHitl(node, imp, inputNames) {
1233
+ const i = (node.inputs ?? {});
1234
+ const schema = (i.schema ?? {});
1235
+ const plain = (v) => {
1236
+ const raw = unwrapSource(v);
1237
+ return raw === undefined || raw === '' ? undefined : raw;
1238
+ };
1239
+ const variant = node.type === T.hitlQuickForm ? 'quick-form'
1240
+ : node.type === T.hitlActionApp ? 'action-app'
1241
+ : node.type === T.hitlDocValidation ? 'document-validation'
1242
+ : undefined;
1243
+ // A field row's binding is a variable PATH (`vars.x`), not an expression —
1244
+ // serialize rewrote the author's `$vars.x` on the way out, so put it back.
1245
+ const bindingExpr = (v) => {
1246
+ const raw = plain(v);
1247
+ if (raw === undefined)
1248
+ return undefined;
1249
+ const js = raw.startsWith('=js:') ? raw.slice(4) : raw;
1250
+ return emitBareExpr(js.startsWith('vars.') ? `$${js}` : js, imp, inputNames);
1251
+ };
1252
+ const rows = (Array.isArray(schema.fields) ? schema.fields : []);
1253
+ const fields = rows.map((f) => {
1254
+ const id = String(f.id ?? '');
1255
+ const direction = String(f.direction ?? 'output');
1256
+ return opts([
1257
+ ['id', str(id)],
1258
+ // serialize defaults a row's label to its id.
1259
+ ['label', typeof f.label === 'string' && f.label !== id ? str(f.label) : undefined],
1260
+ ['type', str(String(f.type ?? 'text'))],
1261
+ ['direction', str(direction)],
1262
+ ['value', direction === 'input' || direction === 'inOut' ? bindingExpr(f.binding) : undefined],
1263
+ ['required', f.required === true ? 'true' : undefined],
1264
+ ]);
1265
+ });
1266
+ const outcomeRows = (Array.isArray(schema.outcomes) ? schema.outcomes : []);
1267
+ const outcomes = outcomeRows.map((o) => {
1268
+ const name = String(o.name ?? '');
1269
+ // A bare string is the shorthand; only `action: 'End'` needs the object.
1270
+ return o.action === 'End' ? opts([['name', str(name)], ['action', str('End')]]) : str(name);
1271
+ });
1272
+ // Delivery: only an override comes back. The definition's own default is what
1273
+ // serialize emits when the author wrote nothing.
1274
+ const def = runDefs.get(`${node.type}@${node.typeVersion}`) ?? runDefs.get(node.type);
1275
+ const defaultRecipient = JSON.stringify(def?.inputDefaults?.recipient ?? null);
1276
+ const rec = i.recipient;
1277
+ let recipientSrc;
1278
+ if (rec !== undefined && JSON.stringify(rec) !== defaultRecipient) {
1279
+ const assignee = (rec.assignee ?? {});
1280
+ const channels = Array.isArray(rec.channels) ? rec.channels : undefined;
1281
+ recipientSrc = opts([
1282
+ ['channels', channels ? `[${channels.map((c) => str(c)).join(', ')}]` : undefined],
1283
+ ['assignee', opts([
1284
+ ['type', str(String(assignee.type ?? 'user'))],
1285
+ ['value', typeof assignee.value === 'string' ? str(assignee.value) : undefined],
1286
+ ['displayName', typeof assignee.displayName === 'string' ? str(assignee.displayName) : undefined],
1287
+ ])],
1288
+ ]);
1289
+ }
1290
+ // The deployed app that owns the form (action-app, and a custom-render
1291
+ // validation station). `appInputBindings` maps the app's own parameter names
1292
+ // to `=`-prefixed expressions.
1293
+ const app = i.app;
1294
+ // `app.inputs` is a VALUE map — what to send the app, keyed by the app's own
1295
+ // parameter names — and it lowers to a SIBLING key, `appInputBindings`, whose
1296
+ // values are `=vars.…` paths (or literals). Not a type map, and not inside
1297
+ // `app`: both are easy to assume and both round-trip wrong.
1298
+ const bindings = (i.appInputBindings ?? {});
1299
+ const appInputs = Object.keys(bindings).length === 0 ? undefined : objLit(Object.entries(bindings).map(([k, v]) => {
1300
+ const raw = String(v ?? '');
1301
+ return [k, raw.startsWith('=') ? emitBareExpr(`$${raw.slice(1)}`, imp, inputNames) : str(raw)];
1302
+ }));
1303
+ const appSrc = app === undefined ? undefined : opts([
1304
+ // `displayName` is serialize's echo of `name`; only `name` is authored.
1305
+ ['name', str(String(app.name ?? ''))],
1306
+ ['key', str(String(app.key ?? ''))],
1307
+ ['folderPath', typeof app.folderPath === 'string' && app.folderPath !== '' ? str(app.folderPath) : undefined],
1308
+ ['appSystemName', app.appSystemName === null ? 'null'
1309
+ : typeof app.appSystemName === 'string' ? str(app.appSystemName) : undefined],
1310
+ ['inputs', appInputs],
1311
+ ]);
1312
+ // The validation station's own inputs ride as PLAIN `$vars.…` paths.
1313
+ const stationExpr = (v) => {
1314
+ const raw = plain(v);
1315
+ if (raw === undefined)
1316
+ return undefined;
1317
+ const js = raw.startsWith('=js:') ? raw.slice(4) : raw;
1318
+ return js.startsWith('$vars.') ? emitBareExpr(js, imp, inputNames) : str(js);
1319
+ };
1320
+ const render = plain(i.validationRenderType);
1321
+ const documentSrc = node.type !== T.hitlDocValidation ? undefined : opts([
1322
+ ['extractionResult', stationExpr(i.extractionResult) ?? str('')],
1323
+ ['storageBucket', plain(i.storageBucket) !== undefined ? str(plain(i.storageBucket)) : undefined],
1324
+ ['documentId', plain(i.documentId) !== undefined ? str(plain(i.documentId)) : undefined],
1325
+ // 'standard' is the definition's default.
1326
+ ['render', render !== undefined && render !== 'standard' ? str(render) : undefined],
1327
+ ['taxonomy', stationExpr(i.taxonomy)],
1328
+ ]);
1329
+ // Per-outcome exits and the error output are VERSION-selected: 1.1 routes per
1330
+ // outcome, 1.2 adds the error output (and implies the routing).
1331
+ const version = String(node.typeVersion ?? '');
1332
+ const exposeError = version === '1.2';
1333
+ const outcomePorts = version === '1.1';
1334
+ const title = plain(i.title);
1335
+ const priority = plain(i.priority);
1336
+ return `${imp.use('hitl')}(${opts([
1337
+ ['variant', variant !== undefined ? str(variant) : undefined],
1338
+ // serialize defaults the title to the step's own name.
1339
+ ['title', title !== undefined && title !== node.id ? str(title) : undefined],
1340
+ ['priority', priority !== undefined && priority !== 'Low' ? str(priority) : undefined],
1341
+ ['labels', plain(i.labels) !== undefined ? str(plain(i.labels)) : undefined],
1342
+ ['recipient', recipientSrc],
1343
+ ['app', appSrc],
1344
+ ['document', documentSrc],
1345
+ // An app task and a validation station own no inline form; an empty array
1346
+ // there is serialize's own, not the author's.
1347
+ ['fields', fields.length ? `[${fields.join(', ')}]`
1348
+ : (variant === 'action-app' || variant === 'document-validation') ? undefined : '[]'],
1349
+ ['outcomes', `[${outcomes.join(', ')}]`],
1350
+ ['outcomePorts', outcomePorts ? 'true' : undefined],
1351
+ ['exposeError', exposeError ? 'true' : undefined],
1352
+ ])})`;
1353
+ }
1354
+ /**
1355
+ * A connector EVENT subscription, shared by the trigger (`onEvent`) and the
1356
+ * mid-flow wait (`waitForEvent`) — the same node body in both places, so the
1357
+ * same inversion serves both.
1358
+ *
1359
+ * The `where` scope comes back from `detail.eventParameters`, which carries it
1360
+ * verbatim; the `contains` filters from the filter tree inside
1361
+ * `detail.configuration`. Both are also encoded into the generated
1362
+ * `filterExpression` string, but reading the structured copies avoids parsing a
1363
+ * string this file generated.
1364
+ */
1365
+ function emitEventSubscription(node, imp, inputNames) {
1366
+ const prefix = node.type.startsWith('uipath.connector.event.') ? 'uipath.connector.event.'
1367
+ : node.type.startsWith('uipath.connector.trigger.') ? 'uipath.connector.trigger.'
1368
+ : undefined;
1369
+ if (prefix === undefined)
1370
+ return undefined;
1371
+ const rest = node.type.slice(prefix.length);
1372
+ const dot = rest.lastIndexOf('.');
1373
+ if (dot <= 0)
1374
+ return undefined;
1375
+ const connector = rest.slice(0, dot);
1376
+ const event = rest.slice(dot + 1);
1377
+ const detail = (node.inputs ?? {}).detail;
1378
+ const where = (detail?.eventParameters ?? {});
1379
+ const whereRows = Object.entries(where)
1380
+ .filter(([, v]) => v !== undefined && v !== null && String(v) !== '')
1381
+ .map(([k, v]) => [k, emitVal(v, imp, inputNames)]);
1382
+ // The filter tree lives inside the `=jsonString:` configuration blob, and so
1383
+ // does the one fact that says whether `detail.objectName` was the AUTHOR's
1384
+ // choice: a GENERIC event (`activityType: GenericTrigger`) carries the object
1385
+ // the author named, a curated one carries its built-in object.
1386
+ let filterRows = [];
1387
+ let generic = false;
1388
+ const config = detail?.configuration;
1389
+ if (typeof config === 'string' && config.startsWith('=jsonString:')) {
1390
+ try {
1391
+ const parsed = JSON.parse(config.slice('=jsonString:'.length));
1392
+ generic = parsed?.essentialConfiguration?.instanceParameters?.activityType === 'GenericTrigger';
1393
+ const tree = parsed?.essentialConfiguration?.filter;
1394
+ const rows = (tree?.filters ?? []);
1395
+ filterRows = rows
1396
+ .map((f) => eventFilterFromTreeLeaf(f))
1397
+ .filter((f) => f !== undefined)
1398
+ .map((f) => opts([
1399
+ ['field', str(f.field)],
1400
+ [f.operator, typeof f.value === 'string' ? str(f.value) : JSON.stringify(f.value)],
1401
+ ]));
1402
+ }
1403
+ catch {
1404
+ // A hand-edited blob is not ours to guess at — the scope and identity
1405
+ // still recover, and the filters simply do not come back.
1406
+ filterRows = [];
1407
+ }
1408
+ }
1409
+ const object = generic && typeof detail?.objectName === 'string' && detail.objectName !== ''
1410
+ ? detail.objectName
1411
+ : undefined;
1412
+ const conn = typeof detail?.connectionId === 'string' ? detail.connectionId : undefined;
1413
+ const folder = typeof detail?.connectionFolderKey === 'string' ? detail.connectionFolderKey : undefined;
1414
+ return opts([
1415
+ ['connector', str(connector)],
1416
+ ['event', str(event)],
1417
+ ['where', whereRows.length ? objLit(whereRows) : undefined],
1418
+ ['object', object !== undefined ? str(object) : undefined],
1419
+ ['filters', filterRows.length ? `[${filterRows.join(', ')}]` : undefined],
1420
+ ['connection', conn !== undefined ? str(conn) : undefined],
1421
+ ['folder', folder !== undefined ? str(folder) : undefined],
1422
+ ]);
1423
+ }
1424
+ function emitPublishedResource(node, imp, inputNames) {
1425
+ const FAMILIES = [
1426
+ { prefix: 'uipath.core.rpa-workflow.', factory: 'rpaWorkflow' },
1427
+ { prefix: 'uipath.core.api-workflow.', factory: 'apiWorkflow' },
1428
+ { prefix: 'uipath.core.function.', factory: 'publishedFunction' },
1429
+ // The published AGENT resource — same shape as its siblings (identity in
1430
+ // the definition's binding pair, contract in its output schema).
1431
+ { prefix: 'uipath.core.agent.', factory: 'agent' },
1432
+ // The Agentic Process umbrella: one factory, three wire identities. `bpmn`
1433
+ // is the default form, so only the other two name it.
1434
+ { prefix: 'uipath.core.agentic-process.', factory: 'agenticProcess' },
1435
+ { prefix: 'uipath.core.flow.', factory: 'agenticProcess', form: 'flow' },
1436
+ { prefix: 'uipath.core.case-management.', factory: 'agenticProcess', form: 'case' },
1437
+ ];
1438
+ const family = FAMILIES.find((f) => node.type.startsWith(f.prefix));
1439
+ if (!family)
1440
+ return undefined;
1441
+ const key = node.type.slice(family.prefix.length);
1442
+ const def = runDefs.get(`${node.type}@${node.typeVersion}`) ?? runDefs.get(node.type);
1443
+ const values = (def?.model?.bindings?.values ?? []);
1444
+ const binding = (name) => {
1445
+ const row = values.find((v) => v.name === name);
1446
+ return typeof row?.default === 'string' ? row.default : undefined;
1447
+ };
1448
+ // The binding pair is the identity the runtime resolves by; without a
1449
+ // definition to read there is nothing to recover it from.
1450
+ const name = binding('name');
1451
+ const folderPath = binding('folderPath');
1452
+ const inputs = { ...(node.inputs ?? {}) };
1453
+ // The design-time fire-and-forget switch is an OPTION on the factory, not a
1454
+ // process argument — serialize adds it to `inputs` after rendering the
1455
+ // author's map, so it comes back off here.
1456
+ const fireAndForget = unwrapPlain(inputs.fireAndForget) === true;
1457
+ delete inputs.fireAndForget;
1458
+ const inputRows = Object.entries(inputs)
1459
+ .map(([k, v]) => [k, emitVal(v, imp, inputNames)]);
1460
+ const schema = def?.outputDefinition?.output?.schema?.properties;
1461
+ const returns = schema && Object.keys(schema).length > 0
1462
+ ? objLit(Object.entries(schema).map(([k, v]) => [k, str(String(v?.type ?? 'string'))]))
1463
+ : undefined;
1464
+ return `${imp.use(family.factory)}(${opts([
1465
+ ['key', str(key)],
1466
+ ['name', name !== undefined ? str(name) : undefined],
1467
+ ['folderPath', folderPath !== undefined ? str(folderPath) : undefined],
1468
+ ['inputs', inputRows.length ? objLit(inputRows) : undefined],
1469
+ ['returns', returns],
1470
+ ['form', family.form !== undefined ? str(family.form) : undefined],
1471
+ ['completion', fireAndForget ? str('fire-and-forget') : undefined],
1472
+ ])})`;
1473
+ }
1474
+ function emitConnector(node, imp, inputNames, o) {
1475
+ const split = splitConnector(node.type);
1476
+ const detail = (node.inputs?.detail ?? {});
1477
+ if (!split)
1478
+ return unsupportedSpec(node, imp, o);
1479
+ const slots = ['bodyParameters', 'queryParameters', 'pathParameters'];
1480
+ const flat = [];
1481
+ for (const s of slots) {
1482
+ const m = detail[s];
1483
+ if (m && typeof m === 'object' && !Array.isArray(m)) {
1484
+ for (const [k, v] of Object.entries(m))
1485
+ flat.push([k, emitVal(v, imp, inputNames)]);
1486
+ }
1487
+ }
1488
+ const multipart = detail.multipartParameters;
1489
+ if (Array.isArray(multipart)) {
1490
+ for (const part of multipart) {
1491
+ if (typeof part?.name === 'string' && Object.prototype.hasOwnProperty.call(part, 'value')) {
1492
+ flat.push([part.name, emitVal(part.value, imp, inputNames)]);
1493
+ }
1494
+ }
1495
+ }
1496
+ const connection = typeof detail.connectionId === 'string' ? detail.connectionId : undefined;
1497
+ const folder = typeof detail.connectionFolderKey === 'string' ? detail.connectionFolderKey : undefined;
1498
+ const object = connectorObjectName(detail.configuration);
1499
+ const optsSrc = opts([
1500
+ ['connection', connection !== undefined ? str(connection) : undefined],
1501
+ ['folder', folder !== undefined ? str(folder) : undefined],
1502
+ ['object', object !== undefined ? str(object) : undefined],
1503
+ ]);
1504
+ const inputsSrc = objLit(flat) || '{}';
1505
+ const args = [str(split.key), str(split.action), inputsSrc, ...(optsSrc ? [optsSrc] : [])];
1506
+ return `${imp.use('connector')}(${args.join(', ')})`;
1507
+ }
1508
+ /** Best-effort placeholder for a node type the reverse-map does not yet cover. */
1509
+ function unsupportedSpec(node, imp, o) {
1510
+ // The file's own definition for this node — the manifest the platform served.
1511
+ const def = runDefs.get(`${node.type}@${node.typeVersion}`) ?? runDefs.get(node.type);
1512
+ if (def !== undefined) {
1513
+ // A node type this SDK has no factory for is still expressible: `rawNode`
1514
+ // carries the definition verbatim, so the node keeps its TYPE, its VERSION
1515
+ // and its inputs through a round trip. `mock()` was the old fallback and it
1516
+ // LOST the type — it compiles to `core.logic.mock`, which only looked
1517
+ // harmless because `merge` restored the original node afterwards. Reserve
1518
+ // `mock()` for a flow that genuinely contains one.
1519
+ const constName = `${camel(node.id)}Definition`;
1520
+ if (!runRawManifests.some(([n]) => n === constName)) {
1521
+ runRawManifests.push([constName, JSON.stringify(def, null, 2)]);
1522
+ }
1523
+ const inputs = node.inputs ?? {};
1524
+ const inputsSrc = Object.keys(inputs).length > 0 ? JSON.stringify(inputs, null, 2) : undefined;
1525
+ return `${imp.use('rawNode')}(${opts([
1526
+ ['nodeType', str(node.type)],
1527
+ ['version', str(String(node.typeVersion ?? ''))],
1528
+ ['manifest', constName],
1529
+ ['inputs', inputsSrc],
1530
+ ])})`;
1531
+ }
1532
+ if (o.bestEffort === false) {
1533
+ throw new Error(`decompile: unsupported node type "${node.type}" (node "${node.id}") and the file carries no `
1534
+ + `definitions[] entry for it, so it cannot be expressed as a rawNode either.`);
1535
+ }
1536
+ // No definition to carry: a mock publishes nothing and takes no inputs, so it
1537
+ // is a safe structural stand-in, and the marker records what it replaced.
1538
+ return `${imp.use('mock')}() /* TODO: unsupported node type ${node.type} */`;
1539
+ }
1540
+ /** `some-node.id` → `someNodeId`, for a hoisted const name. */
1541
+ function camel(id) {
1542
+ const parts = String(id).split(/[^A-Za-z0-9]+/).filter(Boolean);
1543
+ if (parts.length === 0)
1544
+ return 'node';
1545
+ const [first, ...rest] = parts;
1546
+ const head = /^[0-9]/.test(first) ? `n${first}` : first;
1547
+ return head.charAt(0).toLowerCase() + head.slice(1) + rest.map((r) => r.charAt(0).toUpperCase() + r.slice(1)).join('');
1548
+ }
1549
+ // ─── Variables ────────────────────────────────────────────────────────────────
1550
+ const TYPE_NAMES = new Set([
1551
+ 'string', 'number', 'boolean', 'object', 'array', 'file', 'any',
1552
+ 'integer', 'float', 'double', 'date', 'datetime',
1553
+ ]);
1554
+ /** `types.<name>` when the type is a known descriptor, else a defensive string literal. */
1555
+ function typeExpr(type, imp) {
1556
+ if (TYPE_NAMES.has(type))
1557
+ return `${imp.use('types')}.${type}`;
1558
+ return str(type);
1559
+ }
1560
+ /**
1561
+ * A global carrying metadata decompiles to the rich VarSpec spelling
1562
+ * (`{ type: types.x, description, subType, schema }`); a bare one keeps the
1563
+ * compact `name: types.x` form.
1564
+ */
1565
+ function declExpr(g, withDefault, imp) {
1566
+ const meta = [];
1567
+ if (withDefault && g.defaultValue !== undefined)
1568
+ meta.push(`default: ${literal(g.defaultValue)}`);
1569
+ if (g.description !== undefined)
1570
+ meta.push(`description: ${str(g.description)}`);
1571
+ if (g.subType !== undefined)
1572
+ meta.push(`subType: ${str(g.subType)}`);
1573
+ if (g.schema !== undefined)
1574
+ meta.push(`schema: ${literal(g.schema)}`);
1575
+ if (meta.length === 0)
1576
+ return typeExpr(g.type, imp);
1577
+ return `{ type: ${typeExpr(g.type, imp)}, ${meta.join(', ')} }`;
1578
+ }
1579
+ function emitVariables(e, depth, flow, imp) {
1580
+ const globals = readFlowGlobals(flow.variables, runStrict);
1581
+ const inputs = globals.filter((g) => g.direction === 'in');
1582
+ const outputs = globals.filter((g) => g.direction === 'out');
1583
+ const inouts = globals.filter((g) => g.direction === 'inout');
1584
+ const shape = (list, withDefault) => `{ ${list.map((g) => `${key(g.name ?? g.id)}: ${declExpr(g, withDefault, imp)}`).join(', ')} }`;
1585
+ if (inputs.length)
1586
+ e.line(depth, `.input(${shape(inputs, true)})`);
1587
+ if (outputs.length)
1588
+ e.line(depth, `.output(${shape(outputs, false)})`);
1589
+ for (const g of inouts) {
1590
+ const rich = declExpr(g, false, imp);
1591
+ const def = g.defaultValue !== undefined ? `, ${literal(g.defaultValue)}` : '';
1592
+ e.line(depth, `.var(${str(g.name ?? g.id)}, ${rich}${def})`);
1593
+ }
1594
+ }
1595
+ // ─── Trigger ──────────────────────────────────────────────────────────────────
1596
+ function emitTrigger(e, depth, trigger, imp, o, inputNames = new Set()) {
1597
+ // The trigger's id is addressable — a flow's inputs are published as its
1598
+ // output — so a renamed one has to survive the round trip. `start` is the
1599
+ // default and stays implicit.
1600
+ if (trigger.id && trigger.id !== DEFAULT_TRIGGER_ID)
1601
+ e.line(depth, `.triggerId(${str(trigger.id)})`);
1602
+ if (trigger.type === T.triggerManual) {
1603
+ // The default trigger, spelled out: decompiled source is deliberately more
1604
+ // explicit than hand-authored source so the node's exact definition version
1605
+ // survives recompilation (design §5.3).
1606
+ e.line(depth, `.trigger(${imp.use('manual')}()${verArg(trigger)})`);
1607
+ return;
1608
+ }
1609
+ if (trigger.type === T.triggerScheduled) {
1610
+ e.line(depth, `.trigger(${imp.use('scheduled')}(${opts([['every', str(scheduleEvery(trigger))]])})${verArg(trigger)})`);
1611
+ return;
1612
+ }
1613
+ if (trigger.type === T.triggerForm) {
1614
+ // The schema is DERIVED from the flow's inputs at compile time, so nothing
1615
+ // of it needs recovering — the factory takes no arguments.
1616
+ e.line(depth, `.trigger(${imp.use('formTrigger')}()${verArg(trigger)})`);
1617
+ return;
1618
+ }
1619
+ if (trigger.type === T.triggerConversation) {
1620
+ e.line(depth, `.trigger(${imp.use('conversationTrigger')}()${verArg(trigger)})`);
1621
+ return;
1622
+ }
1623
+ if (trigger.type === T.triggerVoice) {
1624
+ e.line(depth, `.trigger(${imp.use('voiceTrigger')}()${verArg(trigger)})`);
1625
+ return;
1626
+ }
1627
+ // A connector EVENT trigger — the same subscription body as a mid-flow wait.
1628
+ {
1629
+ const sub = emitEventSubscription(trigger, imp, inputNames);
1630
+ if (sub !== undefined) {
1631
+ e.line(depth, `.trigger(${imp.use('onEvent')}(${sub})${verArg(trigger)})`);
1632
+ return;
1633
+ }
1634
+ }
1635
+ if (o.bestEffort === false) {
1636
+ throw new Error(`decompile: unsupported trigger type "${trigger.type}".`);
1637
+ }
1638
+ e.line(depth, `/* TODO: unsupported trigger type ${trigger.type} */`);
1639
+ }
1640
+ // ─── Control-flow reconstruction (Phase 3) ────────────────────────────────────
1641
+ //
1642
+ // serialize's `emit()` walker turns a structured builder tree into a graph; this
1643
+ // inverts it. A flow is a directed graph, so recovering the tree means finding,
1644
+ // for each fork (decision / switch / parallel), the RECONVERGENCE point where its
1645
+ // arms meet again — the fork's immediate post-dominator. We compute post-
1646
+ // dominators per SCOPE (the root, and each loop body — a `parentId` group), then
1647
+ // walk each scope emitting structured constructs, recursing into arm sub-regions
1648
+ // and loop bodies. Non-reducible / non-reconverging graphs fall back to a marker
1649
+ // rather than a hard failure (a validating flow must still yield usable code).
1650
+ const EXIT = '#EXIT#';
1651
+ /** The node ids in a scope: the `parentId` group (root scope = no `parentId`). */
1652
+ function scopeIds(nodes, parentId) {
1653
+ return new Set(nodes.filter((n) => (n.parentId ?? undefined) === parentId).map((n) => n.id));
1654
+ }
1655
+ /** In-scope successors of a node (by target id), excluding `error` and `loopBack` edges and out-of-scope targets. */
1656
+ function scopeSucc(graph, id, scope) {
1657
+ const out = [];
1658
+ for (const e of graph.successorEdges(id)) {
1659
+ if (e.targetPort === 'loopBack')
1660
+ continue;
1661
+ if (scope.has(e.targetNodeId))
1662
+ out.push(e.targetNodeId);
1663
+ }
1664
+ return out;
1665
+ }
1666
+ /**
1667
+ * Immediate post-dominators for a scope's CFG (Cooper-Harvey-Kennedy iterative
1668
+ * dominators, run on the REVERSE graph from a virtual EXIT). `ipdom(n)` is the
1669
+ * node where every path out of `n` first reconverges — the reconvergence point of
1670
+ * a fork. Returns a map from node id → its ipdom (`EXIT` when its only join is
1671
+ * the scope exit).
1672
+ */
1673
+ function postDominators(entry, scope, graph) {
1674
+ // Forward successors within scope (EXIT for terminals / scope-exits), over the
1675
+ // subgraph reachable from `entry`.
1676
+ const succ = new Map();
1677
+ const fpreds = new Map(); // forward predecessors: fpreds[t] = {n : n→t}
1678
+ const addP = (from, to) => (fpreds.get(to) ?? fpreds.set(to, []).get(to)).push(from);
1679
+ const seen = new Set();
1680
+ const stack = [entry];
1681
+ while (stack.length) {
1682
+ const n = stack.pop();
1683
+ if (seen.has(n) || !scope.has(n))
1684
+ continue;
1685
+ seen.add(n);
1686
+ const s = scopeSucc(graph, n, scope);
1687
+ const ss = s.length ? s : [EXIT];
1688
+ succ.set(n, ss);
1689
+ for (const t of ss) {
1690
+ addP(n, t);
1691
+ if (t !== EXIT && !seen.has(t))
1692
+ stack.push(t);
1693
+ }
1694
+ }
1695
+ // Post-dominators of the forward graph = dominators of the REVERSE graph rooted
1696
+ // at EXIT. In that reverse graph a node's successors are its forward
1697
+ // predecessors (`fpreds`) and its predecessors are its forward successors
1698
+ // (`succ`). Cooper-Harvey-Kennedy over that graph, in reverse-postorder.
1699
+ const post = [];
1700
+ const vis = new Set();
1701
+ const dfs = (n) => {
1702
+ if (vis.has(n))
1703
+ return;
1704
+ vis.add(n);
1705
+ for (const p of fpreds.get(n) ?? [])
1706
+ dfs(p);
1707
+ post.push(n);
1708
+ };
1709
+ dfs(EXIT);
1710
+ const rpo = [...post].reverse(); // EXIT (the root) first
1711
+ const num = new Map(post.map((n, i) => [n, i])); // postorder number: higher = closer to root
1712
+ const ipdom = new Map();
1713
+ ipdom.set(EXIT, EXIT);
1714
+ const intersect = (a, b) => {
1715
+ let x = a;
1716
+ let y = b;
1717
+ while (x !== y) {
1718
+ while ((num.get(x) ?? -1) < (num.get(y) ?? -1))
1719
+ x = ipdom.get(x);
1720
+ while ((num.get(y) ?? -1) < (num.get(x) ?? -1))
1721
+ y = ipdom.get(y);
1722
+ }
1723
+ return x;
1724
+ };
1725
+ let changed = true;
1726
+ while (changed) {
1727
+ changed = false;
1728
+ for (const b of rpo) {
1729
+ if (b === EXIT)
1730
+ continue;
1731
+ // Predecessors in the reverse graph = forward successors of b.
1732
+ const preds = (succ.get(b) ?? []).filter((p) => ipdom.has(p));
1733
+ if (preds.length === 0)
1734
+ continue;
1735
+ let ni = preds[0];
1736
+ for (const p of preds.slice(1))
1737
+ ni = intersect(p, ni);
1738
+ if (ipdom.get(b) !== ni) {
1739
+ ipdom.set(b, ni);
1740
+ changed = true;
1741
+ }
1742
+ }
1743
+ }
1744
+ return ipdom;
1745
+ }
1746
+ /** The target of the first out-edge of `id` on port `port` (in the whole graph). */
1747
+ function portTarget(graph, id, port) {
1748
+ return graph.outEdges(id).find((e) => e.sourcePort === port)?.targetNodeId;
1749
+ }
1750
+ /** Re-indent every non-empty line of a (possibly multi-line) segment by `pad`. */
1751
+ function reindent(seg, pad) {
1752
+ return seg.split('\n').map((l) => (l === '' ? l : pad + l)).join('\n');
1753
+ }
1754
+ /** Render a list of chain segments at an indent (each segment may be multi-line). */
1755
+ function renderSegs(segs, pad) {
1756
+ return segs.map((s) => reindent(s, pad)).join('\n');
1757
+ }
1758
+ /** An arm callback `(x) => x <chain>` (or just `(x) => x` for an empty arm). */
1759
+ function armCb(param, segs) {
1760
+ if (segs.length === 0)
1761
+ return `(${param}) => ${param}`;
1762
+ return `(${param}) => ${param}\n${renderSegs(segs, ' ')}`;
1763
+ }
1764
+ /**
1765
+ * The `.return({...})` segment for a `core.control.end` node — its explicit
1766
+ * `variableUpdates` (what `.return()` wrote), NOT the End's full `outputs` map
1767
+ * (which also carries auto fall-through). An End with no updates is the implicit
1768
+ * fall-off End the serializer always appends; it now decompiles to an explicit
1769
+ * `.return({}, { version })` so the End's exact definition version survives the
1770
+ * round trip (design §5.3) — recompiling emits the identical node.
1771
+ */
1772
+ function returnSeg(node, ctx) {
1773
+ const updates = ctx.flow.variables?.variableUpdates?.[node.id] ?? [];
1774
+ const opts = returnOptsArg(node);
1775
+ if (updates.length === 0)
1776
+ return `.return({}${opts})`;
1777
+ const entries = updates.map((u) => `${key(u.variableId)}: ${emitVal(u.expression, ctx.imp, ctx.inputNames)}`);
1778
+ return `.return({ ${entries.join(', ')} }${opts})`;
1779
+ }
1780
+ /**
1781
+ * The options argument for a `.return(...)` — `version`, plus the End's `name`
1782
+ * and `label` when they are not what the serializer would derive on its own.
1783
+ *
1784
+ * `name` is emitted for any id other than `end`, which is what a first End is
1785
+ * always given. That is deliberately not a prediction of the full `end`, `end2`,
1786
+ * `end3` sequence: ids are claimed globally in emission order, decompile walks
1787
+ * the graph in its own order, and a wrong guess would silently move a node. An
1788
+ * id that is already `end` needs no name; every other id gets one and round-trips
1789
+ * exactly. Naming an id the serializer would have chosen anyway is harmless.
1790
+ *
1791
+ * Without this the End's id was lost, and with it the `layout` block that keys on
1792
+ * it — compile then refused its own decompiled source with `layout: "endNiceDay"
1793
+ * names no step or node in this flow`.
1794
+ */
1795
+ function returnOptsArg(node) {
1796
+ const parts = [];
1797
+ if (node.id !== 'end')
1798
+ parts.push(`name: ${str(node.id)}`);
1799
+ const ver = verClause(node);
1800
+ if (ver)
1801
+ parts.push(ver);
1802
+ // The serializer labels an unlabelled End `End`, so that needs no carrying.
1803
+ const label = node.display?.label;
1804
+ if (typeof label === 'string' && label !== 'End')
1805
+ parts.push(`label: ${str(label)}`);
1806
+ return parts.length ? `, { ${parts.join(', ')} }` : '';
1807
+ }
1808
+ /**
1809
+ * `.onError(...)` for whatever node carries an `error` edge — an action step or a
1810
+ * `.loop()` container, which declares the handle too.
1811
+ *
1812
+ * Shared rather than duplicated because the reconstruction is identical: the
1813
+ * handler's rejoin targets are whatever the guarded node's success continuation
1814
+ * reaches, and a handler whose only content is a bare End reconstructs to an
1815
+ * empty region that `.onError()` would refuse — so it becomes an explicit
1816
+ * `.return({})`, the minimal legal handler, which serializes back to the same
1817
+ * "error path ends the run" shape (its End id is derived, so — like all End ids
1818
+ * — it is not preserved exactly).
1819
+ *
1820
+ * @returns The segment, or undefined when the node has no error edge.
1821
+ */
1822
+ function errorHandlerSeg(nodeId, succ, scope, ipdom, ctx) {
1823
+ const errEdge = ctx.graph.outEdges(nodeId).find((e) => e.sourcePort === 'error');
1824
+ if (!errEdge)
1825
+ return undefined;
1826
+ const mainReach = forwardReach(succ, scope, ctx.graph);
1827
+ const handlerSegs = region(errEdge.targetNodeId, EXIT, scope, ipdom, ctx, mainReach);
1828
+ if (handlerSegs.length === 0)
1829
+ handlerSegs.push('.return({})');
1830
+ return `.onError(${armCb('h', handlerSegs)})`;
1831
+ }
1832
+ /**
1833
+ * Emit a region — the chain of steps from `cursor` up to (but not including)
1834
+ * `stop` — as a list of segments. `stop` is the reconvergence node the enclosing
1835
+ * construct continues from (`EXIT`/`undefined` = run to a terminal). `refInto`
1836
+ * (set only inside a port list) names the main-path nodes a `.stepToRef(...)`
1837
+ * hands control back to.
1838
+ */
1839
+ function region(cursor, stop, scope, ipdom, ctx, refInto) {
1840
+ const segs = [];
1841
+ while (cursor !== undefined && cursor !== stop && cursor !== EXIT) {
1842
+ // A ref forward: this path re-enters a step the main path reaches later.
1843
+ if (refInto?.has(cursor)) {
1844
+ segs.push(`.stepToRef(${str(cursor)})`);
1845
+ return segs;
1846
+ }
1847
+ const node = ctx.graph.byId.get(cursor);
1848
+ if (!node)
1849
+ break;
1850
+ // A ref backward or sideways: this path has arrived somewhere already emitted.
1851
+ // Before, the walk simply stopped here — which silently dropped the rest of a
1852
+ // cycle, and a backward edge is now something the builder can author. Ends and
1853
+ // terminates are excluded: they are not named steps, so there is nothing a ref
1854
+ // could resolve against, and each path emitting its own is already correct.
1855
+ if (ctx.emitted.has(cursor) && node.type !== T.end && node.type !== T.terminate) {
1856
+ segs.push(`.stepToRef(${str(cursor)})`);
1857
+ return segs;
1858
+ }
1859
+ ctx.emitted.add(cursor);
1860
+ if (node.type === T.end) {
1861
+ segs.push(returnSeg(node, ctx));
1862
+ return segs;
1863
+ }
1864
+ if (node.type === T.terminate) {
1865
+ segs.push(`.terminate(${str(node.id)}, undefined${optsArg(node, ctx)})`);
1866
+ return segs;
1867
+ }
1868
+ if (node.type === T.merge) {
1869
+ // A bare merge reached on the main path (its fork was not detected as a
1870
+ // parallel). Skip it and continue from its output — best effort.
1871
+ cursor = portTarget(ctx.graph, node.id, 'output');
1872
+ continue;
1873
+ }
1874
+ if (node.type === T.decision) {
1875
+ const r = ipdom.get(node.id) ?? stop;
1876
+ const cond = emitBareExpr(String((node.inputs ?? {}).expression ?? ''), ctx.imp, ctx.inputNames);
1877
+ const thenSegs = region(portTarget(ctx.graph, node.id, 'true'), r, scope, ipdom, ctx, refInto);
1878
+ const elseSegs = region(portTarget(ctx.graph, node.id, 'false'), r, scope, ipdom, ctx, refInto);
1879
+ const labels = armLabels(node);
1880
+ if (labels.trueLabel)
1881
+ thenSegs.unshift(`.label(${str(labels.trueLabel)})`);
1882
+ if (labels.falseLabel)
1883
+ elseSegs.unshift(`.label(${str(labels.falseLabel)})`);
1884
+ segs.push(`.branch(${str(node.id)}, ${cond},\n${reindent(armCb('t', thenSegs), ' ')},\n${reindent(armCb('e', elseSegs), ' ')}${verArg(node)})`);
1885
+ cursor = r === EXIT ? undefined : r;
1886
+ continue;
1887
+ }
1888
+ if (node.type === T.switch) {
1889
+ segs.push(emitSwitch(node, ipdom, scope, ctx, stop, refInto));
1890
+ const r = ipdom.get(node.id) ?? stop;
1891
+ cursor = r === EXIT ? undefined : r;
1892
+ continue;
1893
+ }
1894
+ if (node.type === T.loop || node.type === T.doWhile) {
1895
+ const inputs = node.inputs ?? {};
1896
+ const bodyScope = scopeIds(ctx.flow.nodes ?? [], node.id);
1897
+ // v1.0.0 loops start the body from `output`; v2.x loops and do-whiles
1898
+ // from the inner `start` handle. Break edges (body node -> container's
1899
+ // `break` target) decompile to `.break()` inside the body region.
1900
+ const bodyEntry = portTarget(ctx.graph, node.id, 'start') ?? portTarget(ctx.graph, node.id, 'output');
1901
+ const bodyIpdom = bodyEntry ? postDominators(bodyEntry, bodyScope, ctx.graph) : new Map();
1902
+ const prevBreak = ctx.breakInto;
1903
+ ctx.breakInto = node.id;
1904
+ const bodySegs = region(bodyEntry, EXIT, bodyScope, bodyIpdom, ctx);
1905
+ ctx.breakInto = prevBreak;
1906
+ const extra = [];
1907
+ if (node.type === T.loop) {
1908
+ const parallel = unwrapPlain(inputs.parallel);
1909
+ if (parallel === true)
1910
+ extra.push('parallel: true');
1911
+ const completion = unwrapSource(inputs.completionCondition);
1912
+ if (typeof completion === 'string' && completion.length) {
1913
+ extra.push(`completionCondition: ${emitBareExpr(completion.startsWith('=js:') ? completion.slice(4) : completion, ctx.imp, ctx.inputNames)}`);
1914
+ }
1915
+ }
1916
+ const limit = node.type === T.doWhile ? unwrapPlain(inputs.limit) : undefined;
1917
+ if (typeof limit === 'number')
1918
+ extra.push(`limit: ${limit}`);
1919
+ if (unwrapPlain(inputs.breakEnabled) === true && !bodyBreaks(ctx.graph, bodyScope, node.id)) {
1920
+ extra.push('breakEnabled: true');
1921
+ }
1922
+ const opts = optsArgWith(node, ctx, extra);
1923
+ if (node.type === T.doWhile) {
1924
+ const condRaw = unwrapSource(inputs.condition);
1925
+ const cond = typeof condRaw === 'string' && condRaw.length
1926
+ ? emitBareExpr(condRaw.startsWith('=js:') ? condRaw.slice(4) : condRaw, ctx.imp, ctx.inputNames)
1927
+ : `${ctx.imp.use('lit')}(true)`;
1928
+ segs.push(`.doWhile(${str(node.id)}, ${cond}, ${armCb('b', bodySegs)}${opts})`);
1929
+ }
1930
+ else {
1931
+ const collRaw = unwrapSource(inputs.collection);
1932
+ const collection = collRaw !== undefined
1933
+ ? emitBareExpr(collRaw.startsWith('=js:') ? collRaw.slice(4) : collRaw, ctx.imp, ctx.inputNames)
1934
+ : `${ctx.imp.use('lit')}([])`;
1935
+ segs.push(`.loop(${str(node.id)}, ${collection}, ${armCb('b', bodySegs)}${opts})`);
1936
+ }
1937
+ // A container can carry an error handler too — `core.logic.loop` declares
1938
+ // the handle, and `.onError()` after `.loop(...)` authors it. Without this
1939
+ // the edge and its whole handler region were dropped on decompile, which
1940
+ // would delete a handler from any brownfield flow that had one.
1941
+ const containerSucc = scopeSucc(ctx.graph, node.id, scope);
1942
+ const containerHandler = errorHandlerSeg(node.id, containerSucc, scope, ipdom, ctx);
1943
+ if (containerHandler)
1944
+ segs.push(containerHandler);
1945
+ cursor = portTarget(ctx.graph, node.id, 'success');
1946
+ continue;
1947
+ }
1948
+ if (node.type === T.subflow) {
1949
+ segs.push(`.step(${str(node.id)}, ${emitSubflow(node, ctx)}${optsArg(node, ctx)})`);
1950
+ cursor = scopeSucc(ctx.graph, node.id, scope)[0];
1951
+ continue;
1952
+ }
1953
+ // A regular action step (mock / script / http / transform / connector / …).
1954
+ const spec = emitStepSpec(node, ctx.imp, ctx.inputNames, ctx.o, ctx.graph);
1955
+ segs.push(`.step(${str(node.id)}, ${spec}${optsArg(node, ctx)})`);
1956
+ // An error handler on this step (source port `error` + `errorHandlingEnabled`).
1957
+ const succ = scopeSucc(ctx.graph, node.id, scope);
1958
+ const handler = errorHandlerSeg(node.id, succ, scope, ipdom, ctx);
1959
+ if (handler)
1960
+ segs.push(handler);
1961
+ // HTTP response branches: each branch-* edge is its own side path,
1962
+ // recovered as .stepToList('branch-<name>', …) with the same rejoin
1963
+ // machinery an error handler uses.
1964
+ for (const be of ctx.graph.outEdges(node.id).filter((e) => e.sourcePort.startsWith('branch-'))) {
1965
+ const mainReach = forwardReach(succ, scope, ctx.graph);
1966
+ const branchSegs = region(be.targetNodeId, EXIT, scope, ipdom, ctx, mainReach);
1967
+ if (branchSegs.length === 0)
1968
+ branchSegs.push('.return({})');
1969
+ segs.push(`.stepToList(${str(be.sourcePort)}, ${armCb('p', branchSegs)})`);
1970
+ }
1971
+ // A per-outcome human task routes each NON-primary outcome from its own
1972
+ // `outcome-<slug>` exit; the primary one continues the main path, so it is
1973
+ // already a successor (see Graph.primaryOutcomePort).
1974
+ {
1975
+ const primary = ctx.graph.primaryOutcomePort(node.id);
1976
+ for (const oe of ctx.graph.outEdges(node.id)
1977
+ .filter((e) => e.sourcePort.startsWith('outcome-') && e.sourcePort !== primary)) {
1978
+ const mainReach = forwardReach(succ, scope, ctx.graph);
1979
+ const outSegs = region(oe.targetNodeId, EXIT, scope, ipdom, ctx, mainReach);
1980
+ if (outSegs.length === 0)
1981
+ outSegs.push('.return({})');
1982
+ segs.push(`.stepToList(${str(oe.sourcePort)}, ${armCb('p', outSegs)})`);
1983
+ }
1984
+ }
1985
+ // A body edge into the enclosing container's break handle: the path exits
1986
+ // the loop here. Emit `.break()` and end the region — recompiling recreates
1987
+ // exactly this edge and adds no continue back-edge for the path.
1988
+ if (ctx.breakInto !== undefined
1989
+ && ctx.graph.outEdges(node.id).some((e) => e.targetNodeId === ctx.breakInto && e.targetPort === 'break')) {
1990
+ segs.push('.break()');
1991
+ return segs;
1992
+ }
1993
+ // Continuation: 0 → terminal; 1 → follow; N same-port → parallel fork.
1994
+ if (succ.length === 0)
1995
+ return segs;
1996
+ if (succ.length === 1) {
1997
+ cursor = succ[0];
1998
+ continue;
1999
+ }
2000
+ // Parallel: the fork's ipdom is the merge join; each successor is one arm.
2001
+ const join = ipdom.get(node.id);
2002
+ if (join && join !== EXIT && ctx.graph.byId.get(join)?.type === T.merge) {
2003
+ const arms = succ.map((t, i) => armCb(`a${i}`, region(t, join, scope, ipdom, ctx, refInto)));
2004
+ segs.push(`.parallel(${str(join)}, [\n${arms.map((a) => reindent(a, ' ')).join(',\n')},\n]${verArg(ctx.graph.byId.get(join))})`);
2005
+ cursor = portTarget(ctx.graph, join, 'output');
2006
+ continue;
2007
+ }
2008
+ // Unstructured fan-out we can't fold — best-effort marker.
2009
+ if (ctx.o.bestEffort === false)
2010
+ throw new Error(`decompile: unstructured fan-out at "${node.id}".`);
2011
+ segs.push(`/* TODO: unstructured fan-out at ${node.id} (${succ.length} successors) */`);
2012
+ return segs;
2013
+ }
2014
+ return segs;
2015
+ }
2016
+ /** A branch/switch node's arm labels (the designer edge text), when non-default. */
2017
+ function armLabels(node) {
2018
+ const i = node.inputs ?? {};
2019
+ const out = {};
2020
+ if (typeof i.trueLabel === 'string' && i.trueLabel !== 'True')
2021
+ out.trueLabel = i.trueLabel;
2022
+ if (typeof i.falseLabel === 'string' && i.falseLabel !== 'False')
2023
+ out.falseLabel = i.falseLabel;
2024
+ return out;
2025
+ }
2026
+ /** Forward-reachable node set from a set of entries, within a scope. */
2027
+ function forwardReach(entries, scope, graph) {
2028
+ const seen = new Set();
2029
+ const stack = [...entries];
2030
+ while (stack.length) {
2031
+ const n = stack.pop();
2032
+ if (seen.has(n) || !scope.has(n))
2033
+ continue;
2034
+ seen.add(n);
2035
+ for (const t of scopeSucc(graph, n, scope))
2036
+ stack.push(t);
2037
+ }
2038
+ return seen;
2039
+ }
2040
+ /**
2041
+ * Emit a `.switch(...)` from a `core.logic.switch` node. Each case's serialized
2042
+ * expression is `<discriminant> === <JSON-literal>`; the discriminant (shared by
2043
+ * all cases) becomes `on`, and each RHS literal a case `value`. The case port is
2044
+ * `case-<caseId>`; the optional default arm is the `default` port.
2045
+ */
2046
+ function emitSwitch(node, ipdom, scope, ctx, stop, refInto) {
2047
+ const r = ipdom.get(node.id) ?? stop;
2048
+ const cases = (node.inputs ?? {}).cases ?? [];
2049
+ let disc;
2050
+ let nonUniform = false; // a case whose expr isn't `<disc> === <literal>`, or a differing discriminant
2051
+ const caseSrcs = [];
2052
+ for (const c of cases) {
2053
+ // SDK-emitted switch cases are `<discriminant> === <JSON-literal>` (all cases
2054
+ // share the discriminant). Accept `==` too (hand-authored flows use it).
2055
+ const m = String(c.expression ?? '').match(/^(.*?)\s*===?\s*(.*)$/);
2056
+ const lhs = m ? m[1].trim() : '';
2057
+ if (m && lhs) {
2058
+ if (disc === undefined)
2059
+ disc = lhs;
2060
+ else if (disc !== lhs)
2061
+ nonUniform = true;
2062
+ }
2063
+ else {
2064
+ nonUniform = true;
2065
+ }
2066
+ let value = '0';
2067
+ let valueLiteral = '';
2068
+ if (m) {
2069
+ valueLiteral = m[2].trim();
2070
+ try {
2071
+ value = literal(JSON.parse(valueLiteral));
2072
+ }
2073
+ catch {
2074
+ // A non-JSON literal (e.g. single-quoted) — strip matching quotes if any.
2075
+ const q = valueLiteral.match(/^'(.*)'$/);
2076
+ value = q ? str(q[1]) : str(valueLiteral);
2077
+ }
2078
+ }
2079
+ else {
2080
+ nonUniform = true;
2081
+ }
2082
+ const armSegs = region(portTarget(ctx.graph, node.id, `case-${c.id}`), r, scope, ipdom, ctx, refInto);
2083
+ const defaultLabel = `Case ${valueLiteral}`;
2084
+ const labelPart = c.label && c.label !== defaultLabel ? `, label: ${str(c.label)}` : '';
2085
+ caseSrcs.push(`{ value: ${value}${labelPart}, body: ${armCb('b', armSegs)} }`);
2086
+ }
2087
+ const on = emitBareExpr(disc ?? '', ctx.imp, ctx.inputNames);
2088
+ const hasDefault = (node.inputs ?? {}).hasDefault === true || portTarget(ctx.graph, node.id, 'default') !== undefined;
2089
+ const defArm = hasDefault
2090
+ ? `, ${armCb('d', region(portTarget(ctx.graph, node.id, 'default'), r, scope, ipdom, ctx, refInto))}`
2091
+ : '';
2092
+ const caseList = `[\n${caseSrcs.map((c) => reindent(c, ' ')).join(',\n')},\n]`;
2093
+ // The builder's `.switch(on, [{value}])` models `on === value` only. A flow whose
2094
+ // cases carry arbitrary per-case boolean expressions (or a non-uniform
2095
+ // discriminant) cannot round-trip through it — flag it rather than emit silently
2096
+ // wrong values.
2097
+ const warn = nonUniform
2098
+ ? `\n/* TODO: this switch's case expressions are not a uniform \`<discriminant> === <value>\`; the recovered on/value are best-effort and need review. */`
2099
+ : '';
2100
+ const ver = verArg(node);
2101
+ return `.switch(${str(node.id)}, ${on}, ${caseList}${defArm}${ver && !defArm ? ', undefined' : ''}${ver})${warn}`;
2102
+ }
2103
+ /** The manifest-less annotation node type — the only annotation `.flow` has. */
2104
+ const STICKY_NOTE_TYPE = 'stickyNote';
2105
+ /**
2106
+ * Recover `.stickyNotes([...])` from the scope's `stickyNote` nodes.
2107
+ *
2108
+ * They are real nodes in `nodes[]`, so they arrive with the graph rather than
2109
+ * with the layout — but they have no definition, no ports and no edges, so the
2110
+ * control-flow walk never reaches them and they have to be picked up explicitly.
2111
+ * Ids are always emitted, never left to the `stickyNote_<n>` default: an
2112
+ * unnamed note would renumber if a note ahead of it were deleted, and read as
2113
+ * delete-plus-create on the next diff.
2114
+ */
2115
+ function stickyNotesSeg(nodes, flow) {
2116
+ const hoisted = (flow.layout?.nodes ?? {});
2117
+ const notes = nodes.filter((n) => n.type === STICKY_NOTE_TYPE);
2118
+ if (!notes.length)
2119
+ return undefined;
2120
+ const entries = notes.map((n) => {
2121
+ const i = (n.inputs ?? {});
2122
+ const ui = { ...(n.ui ?? {}), ...(hoisted[n.id] ?? {}) };
2123
+ const pos = ui.position ?? { x: 0, y: 0 };
2124
+ const fields = [
2125
+ `id: ${str(n.id)}`,
2126
+ `content: ${str(typeof i.content === 'string' ? i.content : '')}`,
2127
+ ];
2128
+ if (typeof i.color === 'string' && i.color !== 'yellow')
2129
+ fields.push(`color: ${str(i.color)}`);
2130
+ fields.push(`x: ${numLit(pos.x)}`, `y: ${numLit(pos.y)}`);
2131
+ if (ui.size && typeof ui.size.width === 'number' && typeof ui.size.height === 'number') {
2132
+ fields.push(`width: ${numLit(ui.size.width)}`, `height: ${numLit(ui.size.height)}`);
2133
+ }
2134
+ return ` { ${fields.join(', ')} },`;
2135
+ });
2136
+ return `.stickyNotes([\n${entries.join('\n')}\n])`;
2137
+ }
2138
+ /**
2139
+ * Recover the designer `.layout()` for one scope.
2140
+ *
2141
+ * Layout persists in TWO legal shapes, and both must read. The designer's own
2142
+ * write path (`extractLayout` in flow-v1) hoists each node's `ui` into
2143
+ * `layout.nodes[<nodeId>]` and STRIPS `ui` from the node; the SDK's serializer
2144
+ * writes `ui` on the node and leaves `layout.nodes` empty. flow-v1's read path
2145
+ * merges them (`{ ...node.ui, ...layoutEntry }`), so the hoisted entry wins —
2146
+ * this follows the same precedence.
2147
+ *
2148
+ * Keys are AUTHOR-level ids, which for decompiled source are the node ids: the
2149
+ * decompiler names every step with its original id, so the two coincide (see
2150
+ * the node-id stability note at the top of this file).
2151
+ *
2152
+ * A scope is the FLAT `nodes[]` array, not the root-level nodes: BPMN containment
2153
+ * is by `parentId`, so a loop body's steps sit in the same array with their own
2154
+ * positions, and `applyAuthorLayout` resolves them by raw id. Filtering them out
2155
+ * would silently drop the layout of everything inside a loop.
2156
+ *
2157
+ * Edge layout is keyed by EDGE ID in the file and by `"<from>-><to>"` in the
2158
+ * builder. That mapping is only well-defined when exactly ONE edge joins the
2159
+ * pair — `applyAuthorLayout` refuses an ambiguous key rather than guess — so a
2160
+ * pair carrying two edges (a decision whose `true` and `false` ports reach the
2161
+ * same node, say) has its route dropped here. Emitting it would generate source
2162
+ * that fails to compile, which is worse than losing a bend.
2163
+ *
2164
+ * Everything is emitted, including positions the serializer would have assigned
2165
+ * anyway. Decompiled source is deliberately more explicit than hand-written
2166
+ * source (the same rule as explicit node versions): re-deriving "is this the
2167
+ * default?" would mean reimplementing the auto-layout column algorithm here and
2168
+ * silently dropping a hand-placed node that happened to land on the grid.
2169
+ */
2170
+ function layoutSeg(flow, nodes, edges) {
2171
+ const hoisted = (flow.layout?.nodes ?? {});
2172
+ const nodeEntries = [];
2173
+ for (const n of nodes) {
2174
+ // A sticky note carries its own geometry in `.stickyNotes()`; a layout entry
2175
+ // for it would be a second, competing source for the same coordinates.
2176
+ if (n.type === STICKY_NOTE_TYPE)
2177
+ continue;
2178
+ const ui = { ...(n.ui ?? {}), ...(hoisted[n.id] ?? {}) };
2179
+ const pos = ui.position;
2180
+ if (!pos || typeof pos.x !== 'number' || typeof pos.y !== 'number')
2181
+ continue;
2182
+ const fields = [`position: { x: ${numLit(pos.x)}, y: ${numLit(pos.y)} }`];
2183
+ if (ui.size && typeof ui.size.width === 'number' && typeof ui.size.height === 'number') {
2184
+ fields.push(`size: { width: ${numLit(ui.size.width)}, height: ${numLit(ui.size.height)} }`);
2185
+ }
2186
+ if (typeof ui.collapsed === 'boolean')
2187
+ fields.push(`collapsed: ${boolLit(ui.collapsed)}`);
2188
+ nodeEntries.push(`${key(n.id)}: { ${fields.join(', ')} }`);
2189
+ }
2190
+ // How many edges join each (source, target) pair — a pair with more than one
2191
+ // has no unambiguous builder key.
2192
+ const pairCount = new Map();
2193
+ for (const e of edges) {
2194
+ const k = `${e.sourceNodeId}->${e.targetNodeId}`;
2195
+ pairCount.set(k, (pairCount.get(k) ?? 0) + 1);
2196
+ }
2197
+ const hoistedEdges = (flow.layout?.edges ?? {});
2198
+ const edgeEntries = [];
2199
+ for (const e of edges) {
2200
+ const ui = { ...(e.ui ?? {}), ...(e.id ? hoistedEdges[e.id] ?? {} : {}) };
2201
+ const parts = [];
2202
+ for (const which of ['waypoints', 'routedWaypoints']) {
2203
+ const pts = ui[which];
2204
+ if (!Array.isArray(pts) || pts.length === 0)
2205
+ continue;
2206
+ const rendered = pts
2207
+ .filter((p) => p && typeof p.x === 'number' && typeof p.y === 'number')
2208
+ .map((p) => `{ x: ${numLit(p.x)}, y: ${numLit(p.y)}${typeof p.id === 'string' ? `, id: ${str(p.id)}` : ''} }`);
2209
+ if (rendered.length)
2210
+ parts.push(`${which}: [${rendered.join(', ')}]`);
2211
+ }
2212
+ if (!parts.length)
2213
+ continue;
2214
+ const pair = `${e.sourceNodeId}->${e.targetNodeId}`;
2215
+ if ((pairCount.get(pair) ?? 0) !== 1)
2216
+ continue; // ambiguous — see the note above
2217
+ edgeEntries.push(`${str(pair)}: { ${parts.join(', ')} }`);
2218
+ }
2219
+ if (!nodeEntries.length && !edgeEntries.length)
2220
+ return undefined;
2221
+ const sections = [];
2222
+ if (nodeEntries.length)
2223
+ sections.push(` nodes: {\n${nodeEntries.map((s) => ` ${s},`).join('\n')}\n },`);
2224
+ if (edgeEntries.length)
2225
+ sections.push(` edges: {\n${edgeEntries.map((s) => ` ${s},`).join('\n')}\n },`);
2226
+ return `.layout({\n${sections.join('\n')}\n})`;
2227
+ }
2228
+ /**
2229
+ * Emit `subflow(child, inputs)` for a `core.subflow` node. The child flow body
2230
+ * lives in `flow.subflows[<nodeId>]` (keyed by the CALLING node id — the child's
2231
+ * own `flow(id)` is not preserved and is irrelevant to the output). We render the
2232
+ * child inline as a nested `flow(...)` builder (no `.build()` — `subflow()`
2233
+ * collects the builder) and pass the caller's per-input expressions.
2234
+ */
2235
+ function emitSubflow(node, ctx) {
2236
+ const child = ctx.flow.subflows?.[node.id];
2237
+ const inputsSrc = emitValMap(node.inputs, ctx.imp, ctx.inputNames) || '{}';
2238
+ if (!child) {
2239
+ if (ctx.o.bestEffort === false)
2240
+ throw new Error(`decompile: subflow "${node.id}" has no body in subflows[].`);
2241
+ return `${ctx.imp.use('mock')}() /* TODO: subflow ${node.id} body missing */`;
2242
+ }
2243
+ const childName = `${node.id}Child`;
2244
+ const childSrc = emitChildFlow(childName, child, ctx);
2245
+ return `${ctx.imp.use('subflow')}(${childSrc}, ${inputsSrc})`;
2246
+ }
2247
+ /**
2248
+ * Render a child (sub)flow body as a nested `flow(name)…` builder expression
2249
+ * (parenthesized, no `.build()`). Reuses the same region machinery on the child's
2250
+ * own scope; the child's trigger is its `<caller>Start` node.
2251
+ */
2252
+ function emitChildFlow(name, child, ctx) {
2253
+ const globals = readFlowGlobals(child.variables, runStrict);
2254
+ const childCtx = {
2255
+ graph: new Graph(child.nodes ?? [], child.edges ?? []),
2256
+ flow: child,
2257
+ imp: ctx.imp,
2258
+ inputNames: new Set(globals.filter((g) => g.direction === 'in').map((g) => g.name ?? g.id)),
2259
+ emitted: new Set(),
2260
+ o: ctx.o,
2261
+ };
2262
+ const parts = [`${ctx.imp.use('flow')}(${str(name)})`];
2263
+ // Variables.
2264
+ const shape = (dir) => `{ ${globals.filter((g) => g.direction === dir).map((g) => `${key(g.name ?? g.id)}: ${declExpr(g, dir === 'in', ctx.imp)}`).join(', ')} }`;
2265
+ if (globals.some((g) => g.direction === 'in'))
2266
+ parts.push(`.input(${shape('in')})`);
2267
+ if (globals.some((g) => g.direction === 'out'))
2268
+ parts.push(`.output(${shape('out')})`);
2269
+ for (const g of globals.filter((g) => g.direction === 'inout')) {
2270
+ const def = g.defaultValue !== undefined ? `, ${literal(g.defaultValue)}` : '';
2271
+ parts.push(`.var(${str(g.name ?? g.id)}, ${declExpr(g, false, ctx.imp)}${def})`);
2272
+ }
2273
+ // Body: the child's trigger is its own manual Start; walk from its successor.
2274
+ const trigger = (child.nodes ?? []).find((n) => isTrigger(n.type) && !(child.edges ?? []).some((e) => e.targetNodeId === n.id));
2275
+ const scope = scopeIds(child.nodes ?? [], undefined);
2276
+ const entry = trigger ? scopeSucc(childCtx.graph, trigger.id, scope)[0] : undefined;
2277
+ const ipdom = entry ? postDominators(entry, scope, childCtx.graph) : new Map();
2278
+ const bodySegs = entry ? region(entry, EXIT, scope, ipdom, childCtx) : [];
2279
+ parts.push(...bodySegs);
2280
+ // A subflow carries its own scoped layout, resolved against its own node ids.
2281
+ const childNotes = stickyNotesSeg(child.nodes ?? [], child);
2282
+ if (childNotes)
2283
+ parts.push(childNotes);
2284
+ const childLayout = layoutSeg(child, child.nodes ?? [], child.edges ?? []);
2285
+ if (childLayout)
2286
+ parts.push(childLayout);
2287
+ return `(\n${renderSegs(parts.map((p, i) => (i === 0 ? p : p)), ' ')}\n)`;
2288
+ }
2289
+ // ─── Entry point ──────────────────────────────────────────────────────────────
2290
+ /** Slugify the flow name/id into a stable-but-cosmetic `flow(id)` argument. */
2291
+ function slugId(flow) {
2292
+ const base = flow.id ?? flow.name ?? 'flow';
2293
+ const slug = base.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
2294
+ return slug || 'flow';
2295
+ }
2296
+ export function decompile(flow, options = {}) {
2297
+ const importSpecifier = options.importSpecifier ?? './flow-sdk.js';
2298
+ runStrict = options.bestEffort === false;
2299
+ runWarn = options.warn;
2300
+ runWarned = new Set();
2301
+ runDefPairs = new Set((flow.definitions ?? [])
2302
+ .filter((d) => typeof d?.nodeType === 'string' && typeof d?.version === 'string')
2303
+ .map((d) => `${d.nodeType}@${d.version}`));
2304
+ runDefs = new Map();
2305
+ runRawManifests = [];
2306
+ for (const d of (flow.definitions ?? [])) {
2307
+ if (typeof d?.nodeType !== 'string')
2308
+ continue;
2309
+ runDefs.set(`${d.nodeType}@${d.version}`, d);
2310
+ if (!runDefs.has(d.nodeType))
2311
+ runDefs.set(d.nodeType, d);
2312
+ }
2313
+ const imp = new Imports();
2314
+ const graph = new Graph(flow.nodes ?? [], flow.edges ?? []);
2315
+ const globals = readFlowGlobals(flow.variables, runStrict);
2316
+ const inputNames = new Set(globals.filter((g) => g.direction === 'in').map((g) => g.name ?? g.id));
2317
+ // Trigger-bound globals name the node they came from; fall back to the graph's
2318
+ // own trigger node, then to the default.
2319
+ currentTriggerId =
2320
+ globals.find((g) => g.direction === 'in' && g.triggerNodeId)?.triggerNodeId ??
2321
+ (flow.nodes ?? []).find((n) => String(n.type ?? '').startsWith('core.trigger.'))?.id ??
2322
+ DEFAULT_TRIGGER_ID;
2323
+ const e = new Emit();
2324
+ e.line(0, `export default ${imp.use('flow')}(${str(slugId(flow))})`);
2325
+ const d = 1;
2326
+ // The file-format version FIRST, mirroring the order the file itself carries
2327
+ // it (`id`, `version`, `name`) — and emitted even when it equals this SDK's
2328
+ // default, because preserving means RE-DECLARING: a later default bump must
2329
+ // not silently upgrade a file a decompile already captured. A version this
2330
+ // build cannot write is reported instead of dropped; emitting the call would
2331
+ // hand `compile` a source it refuses.
2332
+ const schema = preserveSchemaVersion(FLOW_SCHEMA_VERSIONS, flow.version);
2333
+ if (schema.call)
2334
+ e.line(d, schema.call);
2335
+ if (schema.problem) {
2336
+ if (runStrict)
2337
+ throw new Error(`decompile: ${schema.problem}`);
2338
+ warnOnce(schema.problem);
2339
+ e.line(d, `// TODO: ${schema.problem}`);
2340
+ }
2341
+ if (flow.name !== undefined && flow.name !== flow.id)
2342
+ e.line(d, `.name(${str(flow.name)})`);
2343
+ if (typeof flow.description === 'string') {
2344
+ e.line(d, `.description(${str(flow.description)})`);
2345
+ }
2346
+ // Trigger nodes. One root is the default: the isDefaultEntryPoint carrier
2347
+ // when the file marks one (a multi-root flow always does), else `start`.
2348
+ // A node's TYPE is not enough: `uipath.connector.event.<key>.<event>` is a
2349
+ // start trigger when it opens the flow and a mid-flow WAIT step when it has
2350
+ // an incoming edge. Only a root can be a trigger.
2351
+ const triggerNodes = (flow.nodes ?? [])
2352
+ .filter((n) => isTrigger(n.type) && graph.inEdges(n.id).length === 0);
2353
+ const trigger = triggerNodes.find((n) => (n.inputs ?? {}).isDefaultEntryPoint === true)
2354
+ ?? graph.byId.get('start')
2355
+ ?? triggerNodes[0];
2356
+ if (trigger)
2357
+ emitTrigger(e, d, trigger, imp, options, inputNames);
2358
+ // Nodes no trigger reaches. The region walk below starts at the trigger's
2359
+ // successor and follows edges, so a node with no path from any root was
2360
+ // silently absent from the source — it survived only as a `.layout()` key,
2361
+ // which `compile` then refused as naming no step. The builder has no
2362
+ // detached-node construct, so the honest output is: leave it out, say so in
2363
+ // the chain and on stderr, and drop its layout entry so the source compiles.
2364
+ const detached = detachedTopLevelNodes(flow.nodes ?? [], graph, triggerNodes, trigger);
2365
+ const detachedIds = new Set(detached.map((n) => n.id));
2366
+ if (detached.length > 0) {
2367
+ const listing = detached.map((n) => `${str(n.id)} (${n.type})`).join(', ');
2368
+ const message = `${detached.length} node(s) are not reachable from any trigger — no edge leads to them: ${listing}. `
2369
+ + 'The builder has no detached-node construct, so this source omits them and recompiling drops them. '
2370
+ + 'Connect each one with an edge in the .flow first, or edit that node\'s JSON directly.';
2371
+ if (runStrict)
2372
+ throw new Error(`decompile: ${message}`);
2373
+ warnOnce(message);
2374
+ for (const n of detached) {
2375
+ e.line(d, `// TODO: node ${str(n.id)} (${n.type}) is not reachable from any trigger and is NOT in this source; recompiling drops it. Connect it in the .flow or edit its JSON directly.`);
2376
+ }
2377
+ }
2378
+ emitVariables(e, d, flow, imp);
2379
+ // Reconstruct the body: structured control flow recovered from the graph.
2380
+ const ctx = { graph, flow, imp, inputNames, o: options, emitted: new Set() };
2381
+ const scope = scopeIds(flow.nodes ?? [], undefined);
2382
+ const startId = trigger?.id ?? 'start';
2383
+ const first = scopeSucc(graph, startId, scope)[0];
2384
+ const ipdom = first !== undefined ? postDominators(first, scope, graph) : new Map();
2385
+ const bodySegs = first !== undefined ? region(first, EXIT, scope, ipdom, ctx) : [];
2386
+ // Additional roots decompile to .entryPoint(...): scoped inputs from the
2387
+ // globals bound to them, and a prefix region that hands off into the shared
2388
+ // body — the shared nodes are in ctx.emitted by now, so the walk naturally
2389
+ // closes each prefix with a .stepToRef into the join point.
2390
+ const extraEps = [];
2391
+ for (const t of triggerNodes) {
2392
+ if (t === trigger)
2393
+ continue;
2394
+ extraEps.push(entryPointSeg(t, scope, ipdom, ctx));
2395
+ }
2396
+ const header = e.toString();
2397
+ const body = bodySegs.map((s) => reindent(s, ' ')).join('\n');
2398
+ const eps = extraEps.map((s) => reindent(s, ' ')).join('\n');
2399
+ // Designer layout last, so the graph reads first and the coordinates do not
2400
+ // interleave with the steps they position.
2401
+ const notes = stickyNotesSeg(flow.nodes ?? [], flow);
2402
+ const sticky = notes ? reindent(notes, ' ') : '';
2403
+ const lay = layoutSeg(flow, (flow.nodes ?? []).filter((n) => !detachedIds.has(n.id)), (flow.edges ?? []).filter((edge) => !detachedIds.has(edge.sourceNodeId) && !detachedIds.has(edge.targetNodeId)));
2404
+ const layout = lay ? reindent(lay, ' ') : '';
2405
+ const chain = [header, eps, body, sticky, layout, ' .build();'].filter((s) => s.length > 0).join('\n');
2406
+ const hoisted = runRawManifests
2407
+ .map(([name, json]) => `const ${name} = ${json};`)
2408
+ .join('\n\n');
2409
+ return [imp.line(importSpecifier), hoisted, chain].filter((p) => p.length > 0).join('\n\n') + '\n';
2410
+ }
2411
+ /**
2412
+ * Top-level nodes (no `parentId`) that no root trigger reaches over ANY edge —
2413
+ * error, branch, outcome and artifact ports included, so a node that only an
2414
+ * error path leads to is not reported. Sticky notes carry no graph position and
2415
+ * the roots themselves are excluded. Container children are the container's
2416
+ * business (their own walk emits them) and are never candidates.
2417
+ */
2418
+ function detachedTopLevelNodes(nodes, graph, roots, trigger) {
2419
+ const rootIds = new Set(roots.map((n) => n.id));
2420
+ if (trigger)
2421
+ rootIds.add(trigger.id);
2422
+ const seen = new Set();
2423
+ const stack = [...rootIds];
2424
+ while (stack.length) {
2425
+ const id = stack.pop();
2426
+ if (seen.has(id))
2427
+ continue;
2428
+ seen.add(id);
2429
+ for (const edge of graph.outEdges(id))
2430
+ if (!seen.has(edge.targetNodeId))
2431
+ stack.push(edge.targetNodeId);
2432
+ }
2433
+ return nodes.filter((n) => (n.parentId ?? undefined) === undefined
2434
+ && n.type !== STICKY_NOTE_TYPE
2435
+ && !rootIds.has(n.id)
2436
+ && !seen.has(n.id));
2437
+ }
2438
+ /**
2439
+ * The `every` value a scheduled trigger node carries, across both definition
2440
+ * shapes: 1.1 splits preset/custom over `timerPreset`/`timerValue`; 1.2 (the
2441
+ * cron-capable contract) dropped `timerPreset` and rides `timerValue` alone.
2442
+ */
2443
+ function scheduleEvery(t) {
2444
+ const i = t.inputs ?? {};
2445
+ if (typeof i.timerPreset !== 'string')
2446
+ return typeof i.timerValue === 'string' ? i.timerValue : '';
2447
+ return i.timerPreset === 'custom'
2448
+ ? (typeof i.timerValue === 'string' ? i.timerValue : '')
2449
+ : i.timerPreset;
2450
+ }
2451
+ /** One `.entryPoint(id, trigger, { inputs, version }, prefix)` segment for a non-default root. */
2452
+ function entryPointSeg(t, scope, ipdom, ctx) {
2453
+ const factory = t.type === T.triggerScheduled
2454
+ ? `${ctx.imp.use('scheduled')}(${opts([['every', str(scheduleEvery(t))]])})`
2455
+ : t.type === T.triggerForm
2456
+ ? `${ctx.imp.use('formTrigger')}()`
2457
+ : t.type === T.triggerConversation
2458
+ ? `${ctx.imp.use('conversationTrigger')}()`
2459
+ : t.type === T.triggerVoice
2460
+ ? `${ctx.imp.use('voiceTrigger')}()`
2461
+ : t.type === T.triggerManual
2462
+ ? `${ctx.imp.use('manual')}()`
2463
+ : (() => {
2464
+ if (ctx.o.bestEffort === false)
2465
+ throw new Error(`decompile: unsupported entry-point trigger type "${t.type}".`);
2466
+ return `${ctx.imp.use('manual')}() /* TODO: unsupported trigger type ${t.type} */`;
2467
+ })();
2468
+ const scoped = readFlowGlobals(ctx.flow.variables, runStrict)
2469
+ .filter((g) => g.direction === 'in' && g.triggerNodeId === t.id);
2470
+ const optParts = [];
2471
+ if (scoped.length) {
2472
+ optParts.push(`inputs: { ${scoped.map((g) => `${key(g.name ?? g.id)}: ${declExpr(g, true, ctx.imp)}`).join(', ')} }`);
2473
+ }
2474
+ const ver = verClause(t);
2475
+ if (ver)
2476
+ optParts.push(ver);
2477
+ const first = scopeSucc(ctx.graph, t.id, scope)[0];
2478
+ const prefixSegs = first !== undefined ? region(first, EXIT, scope, ipdom, ctx) : [];
2479
+ const optArgText = optParts.length ? `, { ${optParts.join(', ')} }` : (prefixSegs.length ? ', undefined' : '');
2480
+ const prefix = prefixSegs.length ? `, ${armCb('b', prefixSegs)}` : '';
2481
+ return `.entryPoint(${str(t.id)}, ${factory}${optArgText}${prefix})`;
2482
+ }