@uipath/maestro-builder-sdk 6.19.0 → 6.20.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/dist/api-index.md +107 -107
  2. package/dist/api-members.md +358 -356
  3. package/dist/check.js +181 -45
  4. package/dist/cli/commands/flow.js +3 -0
  5. package/dist/compile-cli.js +51 -0
  6. package/dist/core/actions.d.ts +13 -5
  7. package/dist/core/actions.js +1 -1
  8. package/dist/core/binding-identity.d.ts +22 -0
  9. package/dist/core/binding-identity.js +28 -0
  10. package/dist/core/context-index.d.ts +34 -0
  11. package/dist/core/context-index.js +38 -0
  12. package/dist/core/definitionless-node-types.d.ts +16 -0
  13. package/dist/core/definitionless-node-types.js +16 -0
  14. package/dist/core/form-field-type.d.ts +33 -0
  15. package/dist/core/form-field-type.js +30 -0
  16. package/dist/core/hitl-routing.d.ts +8 -0
  17. package/dist/core/hitl-routing.js +17 -1
  18. package/dist/core/process-resource-sort-order.d.ts +40 -0
  19. package/dist/core/process-resource-sort-order.js +44 -0
  20. package/dist/core/wire-expr.d.ts +27 -0
  21. package/dist/core/wire-expr.js +34 -0
  22. package/dist/decompile-cli.js +11 -1
  23. package/dist/decompile.d.ts +8 -3
  24. package/dist/decompile.js +680 -104
  25. package/dist/flow-expr-check.js +1 -1
  26. package/dist/flow-sdk.d.ts +33 -1
  27. package/dist/flow-sdk.js +21 -2
  28. package/dist/flow-step-tree.js +2 -1
  29. package/dist/index.d.ts +2 -1
  30. package/dist/index.js +1 -0
  31. package/dist/merge-cli.js +25 -8
  32. package/dist/merge-roundtrip.d.ts +81 -0
  33. package/dist/merge-roundtrip.js +404 -0
  34. package/dist/merge.d.ts +35 -6
  35. package/dist/merge.js +124 -24
  36. package/dist/node-placement.d.ts +93 -0
  37. package/dist/node-placement.js +163 -0
  38. package/dist/nodes/transform/flow/node-type.js +2 -1
  39. package/dist/registry/uip/pipeline-front-door.js +5 -3
  40. package/dist/serialize.js +112 -61
  41. package/package.json +2 -2
@@ -0,0 +1,404 @@
1
+ /**
2
+ * merge-roundtrip — what the no-edit round trip changed that the merged file
3
+ * still carries, and what the merged file gets wrong from mixing its sources.
4
+ *
5
+ * `merge` is given `baseline = compile(decompile(original))`, the round trip of
6
+ * the original with NO edit. Wherever baseline and original differ in wire
7
+ * identity, decompile or compile lost or invented something. The merged file
8
+ * inherits that change unless the author's edit undid it, so each difference is
9
+ * reported only when the merged file still differs from the original at that
10
+ * spot. For example:
11
+ *
12
+ * original: logFailure1@success -> forEachItem1@continue
13
+ * baseline: logFailure1@success -> callApi1ErrorEnd@input (validate: Success)
14
+ *
15
+ * is reported when the merged file has the `callApi1ErrorEnd` edge, and not
16
+ * when the author re-wired `logFailure1@success` back to `forEachItem1@continue`
17
+ * in the edited source.
18
+ *
19
+ * BPMN has the same invariant as a gate: `bpmn/lift-verify.ts` builds both
20
+ * sources and writes the nested one only when the graphs agree. Here the two
21
+ * graphs are already built, so this compares them directly:
22
+ *
23
+ * - node ids (per scope: the root and each subflow), and the type of a node the
24
+ * merge takes from the edited side;
25
+ * - edges as `(source, sourcePort, target, targetPort)`; edge ids are not
26
+ * identity;
27
+ * - switch case ids and human-task outcome ids, which name the handles the
28
+ * edges leave from;
29
+ * - binding rows by what they bind (`core/binding-identity.ts`), never by the
30
+ * row `id`.
31
+ *
32
+ * Two checks run on the MERGED file whatever the baseline says, because merge
33
+ * mixes sources (untouched nodes from the original, edges from the edited file):
34
+ *
35
+ * - every root edge leaves from a handle its source node declares — the static
36
+ * handles of the node's definition, its `repeat` handles expanded over the
37
+ * node's inputs (`case-{item.id}` over `inputs.cases`), and for a human task
38
+ * the `outcome-<id>` handles the product derives from its outcomes. This is
39
+ * the rule `flow validate` applies ("Edge references undeclared source
40
+ * handle");
41
+ * - no two binding rows bind the same resource property.
42
+ *
43
+ * Two classes are reported but never refused, because the merged file does the
44
+ * same thing as the original:
45
+ *
46
+ * - an End the compile appends to a tail the original left open. The canvas
47
+ * does not require a trailing End, the SDK always writes one, and a path that
48
+ * runs out of nodes ends either way;
49
+ * - a binding row the round trip drops. Merge keeps every original row, so the
50
+ * merged file still has it.
51
+ */
52
+ import { bindingIdentity } from './core/binding-identity.js';
53
+ import { OUTCOME_HANDLE_NODE_TYPES } from './core/hitl-routing.js';
54
+ /** A file's definitions, keyed `nodeType@version`. */
55
+ function defsOf(f) {
56
+ const out = new Map();
57
+ for (const d of f.definitions ?? [])
58
+ out.set(`${String(d.nodeType)}@${String(d.version)}`, d);
59
+ return out;
60
+ }
61
+ const defOf = (defs, n) => defs.get(`${n.type}@${String(n.typeVersion)}`);
62
+ /** The root and every subflow, keyed `''` for the root and `subflows.<key>` otherwise. */
63
+ function scopesOf(f) {
64
+ const out = new Map([['', { nodes: f.nodes ?? [], edges: f.edges ?? [] }]]);
65
+ for (const [key, sf] of Object.entries(f.subflows ?? {})) {
66
+ const s = (sf ?? {});
67
+ out.set(`subflows.${key}`, { nodes: s.nodes ?? [], edges: s.edges ?? [] });
68
+ }
69
+ return out;
70
+ }
71
+ const where = (scope) => (scope ? ` in ${scope}` : '');
72
+ const edgeText = (e) => `${e.sourceNodeId}@${e.sourcePort} -> ${e.targetNodeId}@${e.targetPort}`;
73
+ const edgeKey = (e) => JSON.stringify([e.sourceNodeId, e.sourcePort, e.targetNodeId, e.targetPort]);
74
+ /** `inputs.schema` may be stored as a JSON string. */
75
+ function schemaOf(n) {
76
+ let schema = n.inputs?.schema;
77
+ if (typeof schema === 'string') {
78
+ try {
79
+ schema = JSON.parse(schema);
80
+ }
81
+ catch {
82
+ return undefined;
83
+ }
84
+ }
85
+ return schema && typeof schema === 'object' ? schema : undefined;
86
+ }
87
+ function idsOf(list) {
88
+ return Array.isArray(list) ? list.map((x) => x?.id).filter((id) => typeof id === 'string') : [];
89
+ }
90
+ function bindingText(b) {
91
+ const sub = b.resourceSubType ? `/${String(b.resourceSubType)}` : '';
92
+ return `${String(b.resource)}${sub} "${String(b.resourceKey ?? '')}" ${String(b.propertyAttribute ?? b.name)}`;
93
+ }
94
+ /**
95
+ * Every wire-identity difference between the original and its no-edit round
96
+ * trip that the merged file still carries, phrased as what the merge writes.
97
+ *
98
+ * A difference the author's edit undid is not reported: an edge the round trip
99
+ * dropped that the merged file has again, an edge or node the round trip added
100
+ * that the merged file lacks, a node whose type or handles the round trip
101
+ * changed that the merged file no longer has (or has with the original's).
102
+ *
103
+ * @param original - The file being merged into.
104
+ * @param baseline - compile(decompile(original)) before any edit.
105
+ * @param merged - The merged file, before it is written.
106
+ * @returns One finding per difference, in a stable order.
107
+ */
108
+ export function roundTripFindings(original, baseline, merged) {
109
+ const out = [];
110
+ const origDefs = defsOf(original);
111
+ const baseDefs = defsOf(baseline);
112
+ const mergedDefs = defsOf(merged);
113
+ const origScopes = scopesOf(original);
114
+ const baseScopes = scopesOf(baseline);
115
+ const mergedScopes = scopesOf(merged);
116
+ const scopeKeys = [...new Set([...origScopes.keys(), ...baseScopes.keys()])];
117
+ for (const scope of scopeKeys) {
118
+ const o = origScopes.get(scope) ?? { nodes: [], edges: [] };
119
+ const b = baseScopes.get(scope) ?? { nodes: [], edges: [] };
120
+ const m = mergedScopes.get(scope);
121
+ if (!origScopes.has(scope)) {
122
+ if (m)
123
+ out.push({ kind: 'node-added', refuses: true, message: `the round trip adds ${scope}, which the original does not have; merge will write it.` });
124
+ continue;
125
+ }
126
+ if (!baseScopes.has(scope)) {
127
+ if (!m)
128
+ out.push({ kind: 'node-removed', refuses: true, message: `the round trip drops ${scope}; merge will not write it.` });
129
+ continue;
130
+ }
131
+ const oNodes = new Map(o.nodes.map((n) => [n.id, n]));
132
+ const bNodes = new Map(b.nodes.map((n) => [n.id, n]));
133
+ const mNodes = new Map((m?.nodes ?? []).map((n) => [n.id, n]));
134
+ const mKeys = new Set((m?.edges ?? []).map(edgeKey));
135
+ const inMerged = (e) => mKeys.has(edgeKey(e));
136
+ // ── Edges, first, because whether an added End is neutral depends on them. ──
137
+ const oKeys = new Set(o.edges.map(edgeKey));
138
+ const bKeys = new Set(b.edges.map(edgeKey));
139
+ let removed = o.edges.filter((e) => !bKeys.has(edgeKey(e)));
140
+ let added = b.edges.filter((e) => !oKeys.has(edgeKey(e)));
141
+ // Same source, same target, different handle(s): the handle was renamed
142
+ // (`case-default-1` -> `case-case_1`, `completed` -> one edge per outcome).
143
+ // Reported while the merged file carries a renamed edge.
144
+ const byLink = (e) => JSON.stringify([e.sourceNodeId, e.targetNodeId, e.targetPort]);
145
+ for (const link of [...new Set(removed.map(byLink))]) {
146
+ const r = removed.filter((e) => byLink(e) === link);
147
+ const a = added.filter((e) => byLink(e) === link);
148
+ if (a.length === 0)
149
+ continue;
150
+ removed = removed.filter((e) => byLink(e) !== link);
151
+ added = added.filter((e) => byLink(e) !== link);
152
+ const live = a.filter(inMerged);
153
+ if (live.length === 0)
154
+ continue;
155
+ const [s, t] = [r[0].sourceNodeId, `${r[0].targetNodeId}@${r[0].targetPort}`];
156
+ out.push({
157
+ kind: 'edge-handle-renamed', refuses: true,
158
+ message: `the round trip moves the edge ${s} -> ${t}${where(scope)} from handle `
159
+ + `${r.map((e) => `"${e.sourcePort}"`).join(', ')} to ${live.map((e) => `"${e.sourcePort}"`).join(', ')}; merge will write the new handle(s).`,
160
+ });
161
+ }
162
+ // Same source handle, different target: the edge was rewired. Reported
163
+ // while the merged file carries a rewired edge.
164
+ const byPort = (e) => JSON.stringify([e.sourceNodeId, e.sourcePort]);
165
+ for (const port of [...new Set(removed.map(byPort))]) {
166
+ const r = removed.filter((e) => byPort(e) === port);
167
+ const a = added.filter((e) => byPort(e) === port);
168
+ if (a.length === 0)
169
+ continue;
170
+ removed = removed.filter((e) => byPort(e) !== port);
171
+ added = added.filter((e) => byPort(e) !== port);
172
+ const live = a.filter(inMerged);
173
+ if (live.length === 0)
174
+ continue;
175
+ out.push({
176
+ kind: 'edge-rewired', refuses: true,
177
+ message: `the round trip rewires ${r[0].sourceNodeId}@${r[0].sourcePort}${where(scope)} from `
178
+ + `${r.map((e) => `${e.targetNodeId}@${e.targetPort}`).join(', ')} to ${live.map((e) => `${e.targetNodeId}@${e.targetPort}`).join(', ')}; merge will write the new target.`,
179
+ });
180
+ }
181
+ // A dropped edge is reported while both its ends are in the merged file and
182
+ // the edge is not: an edge into a node the author deleted is gone by the edit.
183
+ for (const e of removed) {
184
+ if (inMerged(e) || !mNodes.has(e.sourceNodeId) || !mNodes.has(e.targetNodeId))
185
+ continue;
186
+ out.push({ kind: 'edge-removed', refuses: true, message: `the round trip drops the edge ${edgeText(e)}${where(scope)}; merge will not write it.` });
187
+ }
188
+ // An added TERMINAL node (its definition declares no source handle — an
189
+ // End) is neutral when every edge into it leaves a handle the original left
190
+ // open (no edge from that source handle at all). Read off the manifest, not
191
+ // a type name, so any terminal type counts.
192
+ const openInOriginal = (e) => !o.edges.some((x) => x.sourceNodeId === e.sourceNodeId && x.sourcePort === e.sourcePort);
193
+ const neutralEnd = new Set();
194
+ for (const n of b.nodes) {
195
+ if (oNodes.has(n.id) || declaredSourceHandles(n, defOf(baseDefs, n))?.size !== 0)
196
+ continue;
197
+ const into = b.edges.filter((e) => e.targetNodeId === n.id);
198
+ if (into.length > 0 && into.every((e) => added.includes(e) && openInOriginal(e)))
199
+ neutralEnd.add(n.id);
200
+ }
201
+ for (const e of added) {
202
+ if (neutralEnd.has(e.targetNodeId) || !inMerged(e))
203
+ continue;
204
+ out.push({ kind: 'edge-added', refuses: true, message: `the round trip adds the edge ${edgeText(e)}${where(scope)}; merge will write it.` });
205
+ }
206
+ // ── Nodes ──
207
+ for (const n of o.nodes) {
208
+ if (!bNodes.has(n.id) && !mNodes.has(n.id)) {
209
+ out.push({ kind: 'node-removed', refuses: true, message: `the round trip drops node "${n.id}" (${n.type})${where(scope)}; merge will not write it.` });
210
+ }
211
+ }
212
+ for (const n of b.nodes) {
213
+ if (oNodes.has(n.id) || !mNodes.has(n.id))
214
+ continue;
215
+ if (neutralEnd.has(n.id)) {
216
+ const tails = b.edges.filter((e) => e.targetNodeId === n.id).map((e) => `${e.sourceNodeId}@${e.sourcePort}`);
217
+ out.push({
218
+ kind: 'implicit-end', refuses: false,
219
+ message: `the round trip appends End "${n.id}" (${n.type})${where(scope)} after ${tails.join(', ')}, which the original leaves open; `
220
+ + `merge will write it (same behaviour: a path that runs out of nodes ends; not refused under --strict).`,
221
+ });
222
+ }
223
+ else {
224
+ out.push({ kind: 'node-added', refuses: true, message: `the round trip adds node "${n.id}" (${n.type})${where(scope)}; merge will write it.` });
225
+ }
226
+ }
227
+ for (const n of o.nodes) {
228
+ const bn = bNodes.get(n.id);
229
+ const mn = mNodes.get(n.id);
230
+ if (!bn || !mn)
231
+ continue;
232
+ if (bn.type !== n.type && mn.type === bn.type) {
233
+ out.push({
234
+ kind: 'node-type', refuses: true,
235
+ message: `the round trip changes node "${n.id}"${where(scope)} from ${n.type} to ${bn.type}; merge will write ${bn.type}.`,
236
+ });
237
+ }
238
+ // Handle ids live on the node; the edges that leave them come from the
239
+ // edited side. A merged node with the round-trip ids is reported as is; a
240
+ // merged node with the original's ids (merge kept the original node) only
241
+ // when a merged edge leaves it on a round-trip id.
242
+ if (n.type !== bn.type || mn.type !== n.type)
243
+ continue;
244
+ const oh = derivedHandles(n, defOf(origDefs, n));
245
+ const bh = derivedHandles(bn, defOf(baseDefs, bn) ?? defOf(origDefs, n));
246
+ if (!oh || !bh || JSON.stringify(oh) === JSON.stringify(bh))
247
+ continue;
248
+ const mh = JSON.stringify(derivedHandles(mn, defOf(mergedDefs, mn) ?? defOf(origDefs, n)));
249
+ const roundTripOnly = new Set(bh.filter((h) => !oh.includes(h)));
250
+ let consequence;
251
+ if (mh === JSON.stringify(bh))
252
+ consequence = 'merge will write the round-trip ids';
253
+ else if (mh === JSON.stringify(oh) && (m?.edges ?? []).some((e) => e.sourceNodeId === n.id && roundTripOnly.has(e.sourcePort))) {
254
+ consequence = 'merge keeps the original node, but the edges it writes name the round-trip ids';
255
+ }
256
+ if (!consequence)
257
+ continue;
258
+ out.push({
259
+ kind: 'handle-ids', refuses: true,
260
+ message: `the round trip renames node "${n.id}" (${n.type})${where(scope)} handles ${oh.join(', ') || '(none)'} to ${bh.join(', ') || '(none)'}; ${consequence}.`,
261
+ });
262
+ }
263
+ }
264
+ // ── Bindings, by what they bind ──
265
+ const oRows = new Map((original.bindings ?? []).map((r) => [bindingIdentity(r), r]));
266
+ const bRows = new Map((baseline.bindings ?? []).map((r) => [bindingIdentity(r), r]));
267
+ const mRows = new Set((merged.bindings ?? []).map((r) => bindingIdentity(r)));
268
+ for (const [k, r] of oRows) {
269
+ if (!bRows.has(k)) {
270
+ out.push({ kind: 'binding-removed', refuses: false, message: `the round trip drops the binding row for ${bindingText(r)}; merge keeps the original's row (not refused under --strict).` });
271
+ }
272
+ }
273
+ for (const [k, r] of bRows) {
274
+ if (!oRows.has(k) && mRows.has(k)) {
275
+ out.push({ kind: 'binding-added', refuses: true, message: `the round trip adds a binding row for ${bindingText(r)}; merge will write it.` });
276
+ }
277
+ }
278
+ return out;
279
+ }
280
+ /** The source handles a node's definition declares, or `undefined` when they cannot be known. */
281
+ function declaredSourceHandles(node, def) {
282
+ const groups = def?.handleConfiguration;
283
+ if (!Array.isArray(groups))
284
+ return undefined;
285
+ const out = new Set();
286
+ for (const g of groups) {
287
+ for (const h of (g?.handles ?? [])) {
288
+ if (h?.type !== 'source' || typeof h.id !== 'string')
289
+ continue;
290
+ const template = /\{item\.([A-Za-z0-9_]+)\}/.exec(h.id);
291
+ if (!template) {
292
+ out.add(h.id);
293
+ continue;
294
+ }
295
+ if (typeof h.repeat !== 'string' || !h.repeat.startsWith('inputs.'))
296
+ return undefined; // a template this check cannot expand
297
+ let items = node.inputs;
298
+ for (const seg of h.repeat.slice('inputs.'.length).split('.'))
299
+ items = items?.[seg];
300
+ for (const item of Array.isArray(items) ? items : []) {
301
+ const v = item?.[template[1]];
302
+ if (typeof v === 'string' || typeof v === 'number')
303
+ out.add(h.id.replace(template[0], String(v)));
304
+ }
305
+ }
306
+ }
307
+ return out;
308
+ }
309
+ /**
310
+ * The handles a node derives from its own inputs, in order: a definition's
311
+ * `repeat` handles expanded (`case-{item.id}` over `inputs.cases` gives
312
+ * `case-default-1`), and a human task's `outcome-<id>` per outcome. These are
313
+ * the handle ids a round trip can rename; static handles cannot change.
314
+ * `undefined` when the node has no definition and is not a human task.
315
+ */
316
+ function derivedHandles(node, def) {
317
+ const out = [];
318
+ const groups = def?.handleConfiguration;
319
+ for (const g of Array.isArray(groups) ? groups : []) {
320
+ for (const h of (g?.handles ?? [])) {
321
+ if (h?.type !== 'source' || typeof h.id !== 'string' || !h.id.includes('{item.'))
322
+ continue;
323
+ const single = { ...def, handleConfiguration: [{ handles: [h] }] };
324
+ out.push(...(declaredSourceHandles(node, single) ?? []));
325
+ }
326
+ }
327
+ if (OUTCOME_HANDLE_NODE_TYPES.has(node.type)) {
328
+ for (const id of idsOf(schemaOf(node)?.outcomes))
329
+ if (!out.includes(`outcome-${id}`))
330
+ out.push(`outcome-${id}`);
331
+ }
332
+ else if (!Array.isArray(groups)) {
333
+ return undefined;
334
+ }
335
+ return out;
336
+ }
337
+ /**
338
+ * Inconsistencies in the merged file that come from mixing its sources: a root
339
+ * edge leaving a handle its source node does not declare, and two binding rows
340
+ * binding the same resource property.
341
+ *
342
+ * A node with no definition in `merged.definitions` (a sticky note, a type
343
+ * neither input carried) is not judged. Neither is anything the original
344
+ * already had — an edge it carries verbatim, or as many same-identity binding
345
+ * rows as it already holds — because the merge did not cause it.
346
+ *
347
+ * @param merged - The merged file.
348
+ * @param original - The file merged into.
349
+ * @returns One finding per inconsistency.
350
+ */
351
+ export function mergedFileFindings(merged, original = {}) {
352
+ const out = [];
353
+ const originalEdges = new Set((original.edges ?? []).map(edgeKey));
354
+ const originalRows = new Map();
355
+ for (const r of original.bindings ?? [])
356
+ originalRows.set(bindingIdentity(r), (originalRows.get(bindingIdentity(r)) ?? 0) + 1);
357
+ const nodes = new Map((merged.nodes ?? []).map((n) => [n.id, n]));
358
+ const defs = new Map();
359
+ for (const d of merged.definitions ?? [])
360
+ defs.set(`${String(d.nodeType)}@${String(d.version)}`, d);
361
+ for (const e of merged.edges ?? []) {
362
+ const n = nodes.get(e.sourceNodeId);
363
+ if (!n || originalEdges.has(edgeKey(e)))
364
+ continue;
365
+ let declared = declaredSourceHandles(n, defs.get(`${n.type}@${String(n.typeVersion)}`));
366
+ if (OUTCOME_HANDLE_NODE_TYPES.has(n.type)) {
367
+ // The product derives a human task's `outcome-<id>` handles from its
368
+ // outcomes; the manifest lists only `completed`. (A `completed` edge on a
369
+ // task WITH outcomes is a stale placeholder the product remaps, which
370
+ // `validate` accepts and canvas files carry — not judged here.)
371
+ declared = new Set([...(declared ?? ['completed']), ...idsOf(schemaOf(n)?.outcomes).map((id) => `outcome-${id}`)]);
372
+ }
373
+ if (!declared || declared.has(e.sourcePort))
374
+ continue;
375
+ out.push({
376
+ kind: 'undeclared-handle', refuses: true,
377
+ message: `the merged edge ${edgeText(e)} leaves handle "${e.sourcePort}", which node "${n.id}" (${n.type}) does not declare `
378
+ + `(it declares ${[...declared].map((h) => `"${h}"`).join(', ') || 'none'}); \`validate\` will report "undeclared source handle".`,
379
+ });
380
+ }
381
+ const seen = new Map();
382
+ for (const r of merged.bindings ?? []) {
383
+ const k = bindingIdentity(r);
384
+ seen.set(k, [...(seen.get(k) ?? []), String(r.id ?? '(no id)')]);
385
+ }
386
+ for (const [k, ids] of seen) {
387
+ if (ids.length < 2 || ids.length <= (originalRows.get(k) ?? 0))
388
+ continue;
389
+ const r = (merged.bindings ?? []).find((x) => bindingIdentity(x) === k);
390
+ out.push({ kind: 'duplicate-binding', refuses: true, message: `the merged file has ${ids.length} binding rows for ${bindingText(r)} (ids ${ids.join(', ')}).` });
391
+ }
392
+ return out;
393
+ }
394
+ /** Thrown by `mergeFlow` under `strict` when a finding refuses the merge. */
395
+ export class MergeRefusedError extends Error {
396
+ /** Every refusing finding, in report order. */
397
+ findings;
398
+ constructor(findings) {
399
+ super(`refused (--strict): the merged file would differ from the original where the author made no edit:\n`
400
+ + findings.map((f) => ` - ${f.message}`).join('\n'));
401
+ this.name = 'MergeRefusedError';
402
+ this.findings = findings;
403
+ }
404
+ }
package/dist/merge.d.ts CHANGED
@@ -28,7 +28,7 @@
28
28
  * preserved from the original; `subflows` and edges (with their ids) come from
29
29
  * `edited`; `definitions` and `bindings` are unioned
30
30
  * (original wins on conflict, since the original's are the tenant-resolved
31
- * authority). Node ids are stable by construction (decompile names every step by
31
+ * authority). A binding row's identity is what it binds, not its row id. Node ids are stable by construction (decompile names every step by
32
32
  * its node id), which is what lets every re-attach key on id.
33
33
  *
34
34
  * ## Definitions are keyed by `nodeType@version`, not by `nodeType`
@@ -63,7 +63,18 @@
63
63
  * A node whose pair is in NEITHER file cannot be given a definition here, and a
64
64
  * merge that stayed silent about it wrote a file it already knew would fail
65
65
  * `validate`. Those are reported through {@link MergeOptions.warn}.
66
+ *
67
+ * ## The baseline is also compared with the original
68
+ *
69
+ * `baseline` is the no-edit round trip, so wherever it differs from the original
70
+ * in wire identity (a node, an edge, a switch case id, a human-task outcome id,
71
+ * a binding row), the merged file inherits that change unless the author's edit
72
+ * undid it. Each difference the merged file still carries is reported through
73
+ * `warn`, and `strict` refuses the merge over it; the merged file is also
74
+ * checked for an edge that leaves a handle its kept source node does not
75
+ * declare. See merge-roundtrip.ts.
66
76
  */
77
+ export { MergeRefusedError, type MergeFinding } from './merge-roundtrip.js';
67
78
  interface AnyNode {
68
79
  id: string;
69
80
  type: string;
@@ -115,12 +126,30 @@ export interface MergeOptions {
115
126
  */
116
127
  baseline?: AnyFlow;
117
128
  /**
118
- * Called once per finding, mirroring `DecompileOptions.warn`. Today the only
119
- * finding is a node whose `(type, typeVersion)` pair has no definition in
120
- * either input — the one case where the merge cannot produce a self-consistent
121
- * file and `validate` will refuse the result.
129
+ * Called once per finding, mirroring `DecompileOptions.warn`. The findings are:
130
+ *
131
+ * - a node whose `(type, typeVersion)` pair has no definition in either input,
132
+ * so `validate` will refuse the result;
133
+ * - with a `baseline`, each wire-identity difference between the baseline
134
+ * (the no-edit round trip) and the original — a node or edge added, dropped
135
+ * or rewired, a switch case id or human-task outcome id renamed, a binding
136
+ * row added or dropped — that the merged file still carries, i.e. the
137
+ * author's edit did not undo it;
138
+ * - an edited node whose `typeVersion` differs from the original node with
139
+ * the same id, because the edit took the SDK's version;
140
+ * - a merged edge whose `sourcePort` is not a handle its source node declares,
141
+ * and two merged binding rows for the same resource property.
122
142
  */
123
143
  warn?: (message: string) => void;
144
+ /**
145
+ * Refuse instead of warn: when the merged file differs from the original
146
+ * where the author made no edit, throw {@link MergeRefusedError} listing every
147
+ * such finding, and return nothing. The other findings still go to `warn`.
148
+ * Two findings never refuse, because the merged file does what the original
149
+ * does: an End the compile appends to a tail the original leaves open, and a
150
+ * binding row the round trip drops (merge keeps the original's). Meant to be
151
+ * used with `baseline`; without one, only the merged-file checks can refuse.
152
+ */
153
+ strict?: boolean;
124
154
  }
125
155
  export declare function mergeFlow(original: AnyFlow, edited: AnyFlow, options?: MergeOptions): AnyFlow;
126
- export {};