@uipath/maestro-builder-sdk 6.61.1 → 6.62.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.
@@ -12,6 +12,9 @@
12
12
  *
13
13
  * For instance, a canvas Slack node with no connection yet that the author did
14
14
  * not touch is `kept`; the same node with a new message text is `refused`.
15
+ * A node the SDK cannot lower is compared by the stand-ins the two collect-mode
16
+ * compiles emit for it ({@link isStandIn}), and kept only when the original's
17
+ * own round trip refuses it too ({@link classifyLowering}).
15
18
  *
16
19
  * Pure: no file access, so a portable entry (`check.ts`, `merge.ts`) can use
17
20
  * it. Reading the original and compiling its baseline is merge-compile.ts's.
@@ -70,6 +73,13 @@ export declare function byId(nodes: AnyNode[] | undefined): Map<string, AnyNode>
70
73
  * decide "did the user edit this node?" against the baseline.
71
74
  */
72
75
  export declare function nodeUnchanged(a: AnyNode | undefined, b: AnyNode | undefined): boolean;
76
+ /**
77
+ * Whether `n` is the opaque node a collect-mode compile emits in place of a
78
+ * step it could not lower (serialize.ts `standInNode`): the node type, the
79
+ * step's `{ version }`, and `inputs: { standIn: true, spec }`, the spec as
80
+ * authored. It is never written: merge restores the original node or refuses.
81
+ */
82
+ export declare function isStandIn(n: AnyNode | undefined): boolean;
73
83
  /**
74
84
  * The keys of the subflows `mergeFlow` (merge.ts) takes from `edited` rather than
75
85
  * restoring from `original`: every subflow that is new, that the original does
@@ -96,9 +106,15 @@ export declare function isKept(e: AnyNode, origNodes: Map<string, AnyNode>, base
96
106
  * Why a finding is an error: `edited`, its node is one the author changed or
97
107
  * added; `subflow`, it sits in a subflow the edit changed, which merge takes
98
108
  * whole; `unplaced`, it names no node and the original's own round trip does
99
- * not report it, so the edit introduced it.
109
+ * not report it, so the edit introduced it. Two more are for a lowering
110
+ * refusal on a node merge would keep ({@link classifyLowering}):
111
+ * `introduced`, the original's own round trip does not report it on that node,
112
+ * so an edit elsewhere broke it; `unrestorable`, restoring the node from the
113
+ * original would not undo it; `uncompared`, the node is the original's, but
114
+ * there is no collect-mode compile of the original to compare its stand-in
115
+ * with (no `--baseline-from`), so merge cannot tell whether the edit touched it.
100
116
  */
101
- export type TakenReason = 'edited' | 'subflow' | 'unplaced';
117
+ export type TakenReason = 'edited' | 'subflow' | 'unplaced' | 'introduced' | 'unrestorable' | 'uncompared';
102
118
  /**
103
119
  * A finding on a node merge takes from the edited flow, with the reason it
104
120
  * takes it: `edited`, the author changed or added the node; `subflow`, the
@@ -135,6 +151,47 @@ export declare function classifyDrafts<T extends {
135
151
  refused: Refused<T>[];
136
152
  unplaced: T[];
137
153
  };
154
+ /** A refusal a compile reported on the node `nodeId` (`''` when it names none). */
155
+ export interface NodeRefusal {
156
+ nodeId: string;
157
+ code: string;
158
+ message: string;
159
+ }
160
+ /**
161
+ * Sort the lowering refusals of a collect-mode compile of the edited flow
162
+ * (`SerializeOpts.collect`, design D3) by what merge does with their node, as
163
+ * {@link classifyDrafts} does for drafts, then hold a kept one to the rest of
164
+ * what "pre-existing" means (docs/CHECK_COMPILE_CONTRACT_DESIGN.md §3.3). It
165
+ * stays `kept`, and merge restores the original node, only when:
166
+ *
167
+ * - the ORIGINAL's own round trip (`baselineRefusals`, the baseline compiled
168
+ * in collect mode) reports the same code on the same node; else it is
169
+ * refused as `introduced`: an edit elsewhere broke a node the author did
170
+ * not touch (a deleted node it reads), and restoring it would keep the
171
+ * break;
172
+ * - restoring the node undoes the refusal; else it is refused as
173
+ * `unrestorable`. That holds for a root node the compile stood in for
174
+ * ({@link isStandIn}: its own emission failed, and its edges are wired as
175
+ * any node's), and for any node of an untouched subflow, which merge
176
+ * restores whole. A refusal on a root node that did lower (an edge out of
177
+ * it, a join it feeds) is about edges merge takes from the edited flow.
178
+ * The {@link NEVER_KEPT_CODES} are `unrestorable` too.
179
+ *
180
+ * `baselineRefusals` is `undefined` when the baseline was not compiled in
181
+ * collect mode (a precompiled `--baseline` file, or none). Then a stand-in has
182
+ * nothing to equal (a precompiled baseline holds the node lowered, or the file
183
+ * would not exist), and nothing says how the original's round trip treats the
184
+ * node, so every refusal on a node the original has is `uncompared`, never
185
+ * `edited`: the author may not have touched it.
186
+ *
187
+ * For instance a canvas Jira node whose input the SDK cannot read, untouched,
188
+ * is `kept`; the same node with a changed summary is `refused` as `edited`.
189
+ */
190
+ export declare function classifyLowering<T extends NodeRefusal>(original: any, edited: any, baseline: any, refusals: readonly T[], baselineRefusals: readonly NodeRefusal[] | undefined): {
191
+ kept: Kept<T>[];
192
+ refused: Refused<T>[];
193
+ unplaced: T[];
194
+ };
138
195
  /**
139
196
  * A `check()` diagnostic, placed on the node its step emitted (`''` when it
140
197
  * names no node); `subflow` is the calling step when the finding is inside a
@@ -15,9 +15,25 @@ export function nodeUnchanged(a, b) {
15
15
  return false;
16
16
  if (a.type !== b.type)
17
17
  return false;
18
+ // A step that fails to lower has no compiled node on either side, only the
19
+ // collect-mode stand-in (D5): equal stand-ins mean the edit did not touch the
20
+ // spec. Its `typeVersion` is the author's `{ version }` pin, so it counts.
21
+ if (isStandIn(a) || isStandIn(b)) {
22
+ return isStandIn(a) && isStandIn(b) && a.typeVersion === b.typeVersion
23
+ && JSON.stringify(a.inputs) === JSON.stringify(b.inputs);
24
+ }
18
25
  return JSON.stringify(a.inputs ?? {}) === JSON.stringify(b.inputs ?? {})
19
26
  && JSON.stringify(outputSources(a)) === JSON.stringify(outputSources(b));
20
27
  }
28
+ /**
29
+ * Whether `n` is the opaque node a collect-mode compile emits in place of a
30
+ * step it could not lower (serialize.ts `standInNode`): the node type, the
31
+ * step's `{ version }`, and `inputs: { standIn: true, spec }`, the spec as
32
+ * authored. It is never written: merge restores the original node or refuses.
33
+ */
34
+ export function isStandIn(n) {
35
+ return n?.inputs?.standIn === true;
36
+ }
21
37
  /** Each output's `source` by output name; outputs without one are left out. */
22
38
  function outputSources(n) {
23
39
  const sources = {};
@@ -160,6 +176,72 @@ export function classifyDrafts(original, edited, baseline, findings) {
160
176
  }
161
177
  return { kept, refused, unplaced };
162
178
  }
179
+ /**
180
+ * Lowering codes merge never keeps as saved, even on an untouched node:
181
+ * `COMPILE_FAILED` is a throw the SDK has no refusal for (a crash, not a
182
+ * finding to step over), and `LIBRARY_REQUIRED` is about compile's inputs,
183
+ * not about the node.
184
+ */
185
+ const NEVER_KEPT_CODES = new Set(['COMPILE_FAILED', 'LIBRARY_REQUIRED']);
186
+ /**
187
+ * Sort the lowering refusals of a collect-mode compile of the edited flow
188
+ * (`SerializeOpts.collect`, design D3) by what merge does with their node, as
189
+ * {@link classifyDrafts} does for drafts, then hold a kept one to the rest of
190
+ * what "pre-existing" means (docs/CHECK_COMPILE_CONTRACT_DESIGN.md §3.3). It
191
+ * stays `kept`, and merge restores the original node, only when:
192
+ *
193
+ * - the ORIGINAL's own round trip (`baselineRefusals`, the baseline compiled
194
+ * in collect mode) reports the same code on the same node; else it is
195
+ * refused as `introduced`: an edit elsewhere broke a node the author did
196
+ * not touch (a deleted node it reads), and restoring it would keep the
197
+ * break;
198
+ * - restoring the node undoes the refusal; else it is refused as
199
+ * `unrestorable`. That holds for a root node the compile stood in for
200
+ * ({@link isStandIn}: its own emission failed, and its edges are wired as
201
+ * any node's), and for any node of an untouched subflow, which merge
202
+ * restores whole. A refusal on a root node that did lower (an edge out of
203
+ * it, a join it feeds) is about edges merge takes from the edited flow.
204
+ * The {@link NEVER_KEPT_CODES} are `unrestorable` too.
205
+ *
206
+ * `baselineRefusals` is `undefined` when the baseline was not compiled in
207
+ * collect mode (a precompiled `--baseline` file, or none). Then a stand-in has
208
+ * nothing to equal (a precompiled baseline holds the node lowered, or the file
209
+ * would not exist), and nothing says how the original's round trip treats the
210
+ * node, so every refusal on a node the original has is `uncompared`, never
211
+ * `edited`: the author may not have touched it.
212
+ *
213
+ * For instance a canvas Jira node whose input the SDK cannot read, untouched,
214
+ * is `kept`; the same node with a changed summary is `refused` as `edited`.
215
+ */
216
+ export function classifyLowering(original, edited, baseline, refusals, baselineRefusals) {
217
+ const sorted = classifyDrafts(original, edited, baseline, refusals);
218
+ if (baselineRefusals === undefined) {
219
+ const originalIds = new Set([
220
+ ...(original?.nodes ?? []).map((n) => n.id),
221
+ ...Object.values(original?.subflows ?? {}).flatMap((sf) => (sf?.nodes ?? []).map((n) => n.id)),
222
+ ]);
223
+ const refused = [...sorted.kept, ...sorted.refused].map((f) => ({
224
+ ...f,
225
+ reason: originalIds.has(f.nodeId) ? 'uncompared' : f.reason ?? 'edited',
226
+ }));
227
+ return { kept: [], refused, unplaced: sorted.unplaced };
228
+ }
229
+ const onNode = (r) => `${r.nodeId}\n${r.code}`;
230
+ const inBaseline = new Set(baselineRefusals.filter((r) => r.nodeId !== '').map(onNode));
231
+ const editedNodes = byId(edited?.nodes);
232
+ const kept = [];
233
+ const refused = [...sorted.refused];
234
+ for (const f of sorted.kept) {
235
+ const restorable = !NEVER_KEPT_CODES.has(f.code) && (f.subflow !== undefined || isStandIn(editedNodes.get(f.nodeId)));
236
+ if (!restorable)
237
+ refused.push({ ...f, reason: 'unrestorable' });
238
+ else if (!inBaseline.has(onNode(f)))
239
+ refused.push({ ...f, reason: 'introduced' });
240
+ else
241
+ kept.push(f);
242
+ }
243
+ return { kept, refused, unplaced: sorted.unplaced };
244
+ }
163
245
  /**
164
246
  * Place each `check()` diagnostic on the node its step emitted in `editedFlow`, so
165
247
  * {@link classifyDrafts} can sort it: a root step on its node (by label, then
package/dist/merge-cli.js CHANGED
@@ -9,8 +9,14 @@
9
9
  * `--baseline-from <original.flow>` makes merge build the baseline itself
10
10
  * (decompile + compile, nothing written), and an edited `.flow.ts` makes merge
11
11
  * compile it: both compiles tolerate DRAFT nodes, and merge refuses a draft
12
- * only where it would take the node from the edited flow. See merge-compile.ts
13
- * and edit-scope.ts.
12
+ * only where it would take the node from the edited flow. Both also run in
13
+ * collect mode, so a node the SDK cannot lower (#1402) is stood in for: merge
14
+ * restores it from the original when the edit did not touch it and the
15
+ * original's own round trip refuses it too (`LOWERING_NODE_KEPT`, printed as a
16
+ * probable SDK defect), and refuses the write when the edit changed it. See
17
+ * merge-compile.ts and edit-scope.ts. Neither warning depends on `--strict`:
18
+ * both nodes are written as the canvas saved them; `--strict` still refuses a
19
+ * stand-in whose wiring changed the round trip.
14
20
  *
15
21
  * `edited.flow` is compile(decompile(original) after your edits). `--baseline`
16
22
  * is compile(decompile(original)) BEFORE any edit — supplying it enables the
@@ -57,7 +63,7 @@ import { Bindings } from './bindings.js';
57
63
  import { defaultBindingsFile } from './workdir.js';
58
64
  import { loadNodePackages } from './node-packages.js';
59
65
  import { baselineFromOriginal, checkEdited, compileSourceDraftTolerant } from './merge-compile.js';
60
- import { classifyDrafts, findingCounts, splitUnplaced } from './edit-scope.js';
66
+ import { classifyDrafts, classifyLowering, findingCounts, splitUnplaced } from './edit-scope.js';
61
67
  const USAGE = 'usage: flow-merge <original.flow> <edited.flow | Edited.flow.ts> [-o merged.flow] '
62
68
  + '[--baseline <baseline.flow> | --baseline-from <original.flow>] [--library <dir>] [--bindings <file>] [--strict]';
63
69
  /**
@@ -185,6 +191,7 @@ export async function run(argv) {
185
191
  const compileOpts = { library, bindings };
186
192
  const original = JSON.parse(readFileSync(originalPath, 'utf8'));
187
193
  let drafts = [];
194
+ let lowering = [];
188
195
  let diagnostics = [];
189
196
  let edited;
190
197
  let editedSidecars;
@@ -198,6 +205,7 @@ export async function run(argv) {
198
205
  edited = compiled.flow;
199
206
  editedSidecars = compiled.sidecars;
200
207
  drafts = compiled.drafts;
208
+ lowering = compiled.lowering.map(onNode);
201
209
  diagnostics = checkEdited(compiled.built, edited, flowCheckOpts(library, bindings));
202
210
  }
203
211
  else {
@@ -229,6 +237,9 @@ export async function run(argv) {
229
237
  // check()'s errors follow the same rule; its warnings are printed as compile prints them.
230
238
  const { kept, refused, unplaced } = classifyDrafts(original, edited, baseline, drafts);
231
239
  const checked = classifyDrafts(original, edited, baseline, diagnostics.filter((d) => d.level === 'error'));
240
+ // A node the SDK cannot lower follows the draft rule, held to the baseline:
241
+ // kept only when the original's own round trip refuses it the same way.
242
+ const lowered = classifyLowering(original, edited, baseline, lowering, baselineCompile?.lowering.map(onNode));
232
243
  for (const d of diagnostics)
233
244
  if (d.level === 'warning')
234
245
  console.error(` ⚠ [${d.code}] ${d.message}`);
@@ -237,6 +248,14 @@ export async function run(argv) {
237
248
  ? ` ⚠ [DRAFT_NODE_KEPT] "${d.nodeId}" is unfinished in the original (${d.code}); it is inside subflow "${d.subflow}", which the edit did not touch, so the original subflow is kept as is.`
238
249
  : ` ⚠ [DRAFT_NODE_KEPT] "${d.nodeId}" is unfinished in the original (${d.code}); it is untouched, so the original node is kept as is.`);
239
250
  }
251
+ for (const d of lowered.kept) {
252
+ const where = d.subflow !== undefined
253
+ ? `is inside subflow "${d.subflow}", which the edit did not touch, so the original subflow is kept as is`
254
+ : 'is untouched, so the original node is kept as is';
255
+ console.error(` ⚠ [LOWERING_NODE_KEPT] "${d.nodeId}" ${where}, but the SDK cannot compile it (${d.code}). `
256
+ + 'This is probably an SDK defect: the SDK cannot read what the designer wrote. Report it: https://github.com/UiPath/builder-sdk/issues');
257
+ console.error(` ${d.message}`);
258
+ }
240
259
  // A finding merge cannot place on a node — an unnamed End's `.return`, a
241
260
  // flow-level finding — is printed when the ORIGINAL's own round trip reports
242
261
  // it too (the designer's file had it; check has already said it), and
@@ -245,16 +264,18 @@ export async function run(argv) {
245
264
  // one is refused, as before.
246
265
  const baselineFindings = baselineCompile === undefined ? undefined : findingCounts([
247
266
  ...baselineCompile.drafts,
267
+ ...baselineCompile.lowering,
248
268
  ...checkEdited(baselineCompile.built, baselineCompile.flow, flowCheckOpts(library, bindings)).filter((d) => d.level === 'error'),
249
269
  ]);
250
270
  const unplacedDrafts = splitUnplaced(unplaced, baselineFindings);
251
271
  const unplacedChecks = splitUnplaced(checked.unplaced, baselineFindings);
252
- for (const d of [...unplacedDrafts.known, ...unplacedChecks.known]) {
272
+ const unplacedLowering = splitUnplaced(lowered.unplaced, baselineFindings);
273
+ for (const d of [...unplacedDrafts.known, ...unplacedChecks.known, ...unplacedLowering.known]) {
253
274
  console.error(` ⚠ [CHECK_FINDING_UNPLACED] ${d.code} names no node, and the original's own round trip reports it too, so it does not stop the write: ${d.message}`);
254
275
  }
255
276
  const errors = [
256
- ...refused, ...checked.refused,
257
- ...[...unplacedDrafts.introduced, ...unplacedChecks.introduced].map((d) => ({ ...d, reason: 'unplaced' })),
277
+ ...refused, ...checked.refused, ...lowered.refused,
278
+ ...[...unplacedDrafts.introduced, ...unplacedChecks.introduced, ...unplacedLowering.introduced].map((d) => ({ ...d, reason: 'unplaced' })),
258
279
  ];
259
280
  if (errors.length) {
260
281
  for (const d of errors) {
@@ -262,10 +283,17 @@ export async function run(argv) {
262
283
  ? 'the edited flow (this finding names no node, and the original\'s own round trip does not report it, so the edit introduced it)'
263
284
  : d.reason === 'subflow'
264
285
  ? `"${d.nodeId}" is inside subflow "${d.subflow}", which the edit changed, so merge takes that subflow whole; finish it`
265
- : `"${d.nodeId}" is changed or new in the edited flow`;
286
+ : d.reason === 'introduced'
287
+ ? `"${d.nodeId}" is untouched, but the original's own round trip does not refuse it, so an edit elsewhere broke it`
288
+ : d.reason === 'unrestorable'
289
+ ? unrestorable(d.nodeId, d.code)
290
+ : d.reason === 'uncompared'
291
+ ? `"${d.nodeId}" is the original's, but the SDK cannot compile it, and ${baseline ? 'a precompiled --baseline has no record of' : 'with no baseline merge cannot tell'} `
292
+ + 'whether the edit touched it; pass --baseline-from <original.flow> (instead of --baseline) so merge compiles the original itself and can compare the node'
293
+ : `"${d.nodeId}" is changed or new in the edited flow`;
266
294
  console.error(` ✗ [${d.code}] ${where}: ${d.message}`);
267
295
  }
268
- console.error(`flow-merge: ${errors.length} error(s) in nodes merge takes from the edited flow; finish them or revert the edit. Nothing written.`);
296
+ console.error(`flow-merge: ${errors.length} error(s) in nodes merge takes from the edited flow; each line says what to do (finish the node, revert the edit, or pass what merge needs). Nothing written.`);
269
297
  return 1;
270
298
  }
271
299
  // Each compile writes `<source>/` beside its own output, so a baseline in the
@@ -345,4 +373,23 @@ export async function run(argv) {
345
373
  });
346
374
  return 0;
347
375
  }
376
+ /**
377
+ * The error for a refusal on an untouched node that restoring it from the
378
+ * original would not undo, with the step to take: a crash is an SDK defect;
379
+ * a missing input to compile (the node library) is the caller's to pass; any
380
+ * other is about the edges merge takes from the edited flow.
381
+ */
382
+ function unrestorable(nodeId, code) {
383
+ const report = 'https://github.com/UiPath/builder-sdk/issues';
384
+ if (code === 'COMPILE_FAILED')
385
+ return `"${nodeId}" is untouched, but the SDK failed while compiling it; this is an SDK defect, report it: ${report}`;
386
+ if (code === 'LIBRARY_REQUIRED')
387
+ return `"${nodeId}" is untouched, but compiling it needs the node library; pass --library <dir> and merge again`;
388
+ return `"${nodeId}" is untouched, but restoring it from the original would not undo this: revert the edit's changes to the edges `
389
+ + `into or out of it; if there are none, this is an SDK defect, report it: ${report}`;
390
+ }
391
+ /** A collect-mode refusal as edit-scope.ts sorts it: on its node, or on none (`''`). */
392
+ function onNode(r) {
393
+ return { nodeId: r.nodeId ?? '', code: r.code, message: r.message };
394
+ }
348
395
  runWhenInvokedDirectly(import.meta.url, 'flow-merge', run);
@@ -1,34 +1,44 @@
1
- import { type DraftFinding, type SerializeOpts } from './serialize.js';
1
+ import { type CollectedRefusal, type DraftFinding, type SerializeOpts } from './serialize.js';
2
2
  import { type CheckOpts } from './check.js';
3
3
  import { type PlacedDiagnostic } from './edit-scope.js';
4
4
  import type { BuiltFlow } from './flow-sdk.js';
5
5
  /**
6
- * A compile's flow, the draft refusals it reported instead of throwing, and the
7
- * agent sidecars (`<source>/agent.json`, resource files) it produced, none written.
6
+ * A compile's flow, the draft refusals it reported instead of throwing, the
7
+ * lowering refusals collect mode reported on the steps it stood in for (or on
8
+ * no step), and the agent sidecars (`<source>/agent.json`, resource files) it
9
+ * produced, none written.
8
10
  */
9
11
  export interface DraftTolerantCompile {
10
12
  flow: any;
11
13
  drafts: DraftFinding[];
14
+ lowering: CollectedRefusal[];
12
15
  sidecars: Array<{
13
16
  path: string;
14
17
  json: unknown;
15
18
  }>;
16
19
  }
17
- /** Compile `built` with draft tolerance. Every other refusal still throws. */
18
- export declare function compileDraftTolerant(built: BuiltFlow, opts?: Omit<SerializeOpts, 'draft'>): DraftTolerantCompile;
20
+ /**
21
+ * Compile `built` with draft tolerance, in collect mode: a step whose
22
+ * emission is refused is reported in `lowering` and stood in for, so the
23
+ * rest of the flow is still compiled. A scope-level refusal (two steps with
24
+ * one name, a variable plan) leaves no flow to merge, so it is thrown, as an
25
+ * ordinary compile throws it.
26
+ */
27
+ export declare function compileDraftTolerant(built: BuiltFlow, opts?: Omit<SerializeOpts, 'draft' | 'collect'>): DraftTolerantCompile;
19
28
  /**
20
29
  * Load a `.flow.ts` source and compile it with draft tolerance.
21
30
  * The caller has already called `ensureTypeScriptRuntime()`.
22
31
  */
23
- export declare function compileSourceDraftTolerant(file: string, opts?: Omit<SerializeOpts, 'draft'>): Promise<DraftTolerantCompile & {
32
+ export declare function compileSourceDraftTolerant(file: string, opts?: Omit<SerializeOpts, 'draft' | 'collect'>): Promise<DraftTolerantCompile & {
24
33
  built: BuiltFlow;
25
34
  }>;
26
35
  /**
27
36
  * The baseline of `originalPath`: decompile it (reading agent sidecars beside
28
- * it, as `flow decompile` does), then compile the source with draft tolerance.
37
+ * it, as `flow decompile` does), then compile the source with draft tolerance,
38
+ * in collect mode ({@link compileDraftTolerant}).
29
39
  * The source goes to a temporary directory that is removed afterwards.
30
40
  */
31
- export declare function baselineFromOriginal(originalPath: string, opts?: Omit<SerializeOpts, 'draft'>, warn?: (message: string) => void): Promise<DraftTolerantCompile & {
41
+ export declare function baselineFromOriginal(originalPath: string, opts?: Omit<SerializeOpts, 'draft' | 'collect'>, warn?: (message: string) => void): Promise<DraftTolerantCompile & {
32
42
  built: BuiltFlow;
33
43
  }>;
34
44
  /**
@@ -1,11 +1,13 @@
1
1
  /**
2
2
  * merge-compile — the compiles `flow merge --baseline-from` runs itself (#1134).
3
3
  *
4
- * Both compiles tolerate draft nodes (`SerializeOpts.draft`). A draft on a node
5
- * merge keeps from the original is a warning; a draft or `check()` error on a
6
- * node merge takes from the edited flow is an error. Nothing draft-tolerant is
7
- * written to disk. Which nodes count as edited, and how each finding is sorted,
8
- * is edit-scope.ts's.
4
+ * Both compiles tolerate draft nodes (`SerializeOpts.draft`) and run in collect
5
+ * mode (`SerializeOpts.collect`), so a node that fails to lower is reported and
6
+ * stood in for instead of stopping the compile. A draft or lowering refusal on
7
+ * a node merge keeps from the original is a warning; one on a node merge takes
8
+ * from the edited flow, like a `check()` error there, is an error. Nothing
9
+ * these compiles produce is written to disk. Which nodes count as edited, and
10
+ * how each finding is sorted, is edit-scope.ts's.
9
11
  */
10
12
  import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
11
13
  import { tmpdir } from 'node:os';
@@ -13,15 +15,28 @@ import { dirname, join, resolve } from 'node:path';
13
15
  import { decompile } from './decompile.js';
14
16
  import { loadBuiltFlow } from './load.js';
15
17
  import { serialize } from './serialize.js';
18
+ import { CompileRefusal } from './core/compile-refusal.js';
16
19
  import { PACKAGE_NAME } from './package-name.js';
17
20
  import { check } from './check.js';
18
21
  import { placeDiagnostics } from './edit-scope.js';
19
- /** Compile `built` with draft tolerance. Every other refusal still throws. */
22
+ /**
23
+ * Compile `built` with draft tolerance, in collect mode: a step whose
24
+ * emission is refused is reported in `lowering` and stood in for, so the
25
+ * rest of the flow is still compiled. A scope-level refusal (two steps with
26
+ * one name, a variable plan) leaves no flow to merge, so it is thrown, as an
27
+ * ordinary compile throws it.
28
+ */
20
29
  export function compileDraftTolerant(built, opts = {}) {
21
30
  const drafts = [];
31
+ const lowering = [];
22
32
  const sidecars = [];
23
- const flow = serialize(built, { ...opts, draft: (f) => drafts.push(f), sidecars });
24
- return { flow, drafts, sidecars };
33
+ const flow = serialize(built, { ...opts, draft: (f) => drafts.push(f), collect: (r) => lowering.push(r), sidecars });
34
+ if (flow === undefined) {
35
+ // Collection stopped: the refusal that stopped it is the last one reported.
36
+ const stop = lowering[lowering.length - 1];
37
+ throw new CompileRefusal(stop.code, stop.message, stop.suggestion);
38
+ }
39
+ return { flow, drafts, lowering, sidecars };
25
40
  }
26
41
  /**
27
42
  * Load a `.flow.ts` source and compile it with draft tolerance.
@@ -33,7 +48,8 @@ export async function compileSourceDraftTolerant(file, opts = {}) {
33
48
  }
34
49
  /**
35
50
  * The baseline of `originalPath`: decompile it (reading agent sidecars beside
36
- * it, as `flow decompile` does), then compile the source with draft tolerance.
51
+ * it, as `flow decompile` does), then compile the source with draft tolerance,
52
+ * in collect mode ({@link compileDraftTolerant}).
37
53
  * The source goes to a temporary directory that is removed afterwards.
38
54
  */
39
55
  export async function baselineFromOriginal(originalPath, opts = {}, warn) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uipath/maestro-builder-sdk",
3
- "version": "6.61.1",
3
+ "version": "6.62.0",
4
4
  "description": "Build UiPath Flow, Case, and BPMN artifacts by writing TypeScript.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://docs.uipath.com/maestro",
@@ -100,5 +100,5 @@
100
100
  "@types/node": "^22.7.0",
101
101
  "esbuild": "^0.28.1"
102
102
  },
103
- "gitref": "b4aa73a5f403c78c1b6bae2c136f6b281ba8fbac"
103
+ "gitref": "2a6094aa49f4cbf18cb309ba93c33573f3e1f226"
104
104
  }