@uipath/maestro-builder-sdk 6.63.0 → 6.64.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.
@@ -10,12 +10,17 @@
10
10
  * --library defaults to $FLOW_SDK_LIBRARY_JSON; --bindings defaults to
11
11
  * .flow-sdk/bindings.json (or a legacy ./bindings.json) when present. Connector actions require a library.
12
12
  *
13
- * `--no-check` skips the SDK's static `check()` pass and emits whatever
14
- * `.build()` produced. It exists for loops whose verification is somewhere else
15
- * entirely — e.g. authoring against the product CLI (`uip maestro flow
16
- * validate` / `debug`), where a pre-emit static pass would answer questions the
17
- * product's own gates are supposed to answer. `.build()` still throws on
18
- * structural problems; only the diagnostic pass is skipped.
13
+ * `compile` only serializes (docs/CHECK_COMPILE_CONTRACT_DESIGN.md D1, D2). It
14
+ * does not run `check()`'s lint rules: run `flow check` first for those. It
15
+ * stops only when it cannot write a correct `.flow`, a refusal the serializer
16
+ * throws. A draft node (a connector with no connection, an empty required
17
+ * input) the canvas saved is written, with a warning: decompile marks its step
18
+ * `{ draft: true }` and carries its definition in `.definitions([...])`. Any
19
+ * other draft, such as one an agent just wrote (even of an operation the canvas
20
+ * has a draft of), is refused.
21
+ *
22
+ * `--no-check` is a deprecated no-op: it is accepted for one minor version, with
23
+ * a warning, and then removed.
19
24
  */
20
25
  import { diagnosticLine, reportDiagnostics, reportResult, thrownDiagnostic } from './cli-result.js';
21
26
  import { writeJsonFile } from './core/json-file.js';
@@ -24,8 +29,7 @@ import { basename, dirname, join, relative, resolve } from 'node:path';
24
29
  import { resolveFlowFile, loadBuiltFlow } from './load.js';
25
30
  import { serializeWithSidecars } from './serialize.js';
26
31
  import { compileOwnsResource } from './core/agent-tool-resources.js';
27
- import { check } from './check.js';
28
- import { flowCheckOpts, openCliLibrary } from './cli-library.js';
32
+ import { openCliLibrary } from './cli-library.js';
29
33
  import { Bindings } from './bindings.js';
30
34
  import { flagValue, runWhenInvokedDirectly } from './cli-run.js';
31
35
  import { connectionResources, inlineAgentResources, mergeBindingsV2, refreshInlineAgentRows } from './bindings-v2.js';
@@ -35,8 +39,8 @@ import { defaultBindingsFile } from './workdir.js';
35
39
  import { isWithin } from './path-within.js';
36
40
  import { flowSources, resourceSourcesOfOtherFlows } from './project-resource-users.js';
37
41
  import { fnv1aX4Uuid } from './core/stable-id.js';
38
- import { BindingRefusal } from './core/binding-messages.js';
39
- import { ByoaRefusal } from './core/byoa.js';
42
+ import { CompileRefusal } from './core/compile-refusal.js';
43
+ import { cliSpelling } from './core/cli-spelling.js';
40
44
  /** An agent folder name: a uuid, any case. */
41
45
  const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
42
46
  export async function run(argv) {
@@ -45,7 +49,9 @@ export async function run(argv) {
45
49
  return 2;
46
50
  }
47
51
  const flags = new Set(['-o', '--library', '--bindings', '--connectors-local']);
48
- const noCheck = argv.includes('--no-check');
52
+ if (argv.includes('--no-check')) {
53
+ console.error(`compile: --no-check is deprecated and does nothing: compile no longer runs check. Run \`${cliSpelling()} flow check\` before \`compile\`; the flag will be removed.`);
54
+ }
49
55
  const pos = argv.find((a, i) => !flags.has(a) && !flags.has(argv[i - 1]));
50
56
  if (!pos) {
51
57
  console.error('compile: missing <flow.ts | BaseName>');
@@ -66,37 +72,20 @@ export async function run(argv) {
66
72
  // (e.g. HTTP-only) then compile fine even if the connector cache isn't built.
67
73
  // A connector action with no library resolves to a clear error in serialize().
68
74
  //
69
- // Loaded BEFORE check() on purpose: one of check's rules (whether a connector
70
- // event operation declares any event parameters) can only be answered from the
71
- // library, and answering it from an assumption instead is what made
72
- // EVENT_NO_SCOPE an error for HTTP Webhook, whose only correct `where` is empty.
73
75
  const library = openCliLibrary(argv);
74
76
  const bindFile = flagValue(argv, '--bindings') ?? defaultBindingsFile();
75
77
  const bindings = new Bindings(bindFile);
76
78
  // One list per run; every exit path reports it (reportResult overwrites keys).
77
79
  const diagnostics = [];
78
- if (!noCheck) {
79
- // Same options as `flow check`, so the two cannot disagree.
80
- const diags = check(built, flowCheckOpts(library, bindings));
81
- const errors = diags.filter((d) => d.level === 'error');
82
- for (const d of diags)
83
- console.error(` ${diagnosticLine(d)}`);
84
- diagnostics.push(...diags);
85
- if (errors.length) {
86
- reportDiagnostics(diagnostics);
87
- console.error(`compile: ${errors.length} error(s); not emitting.`);
88
- return 1;
89
- }
90
- }
91
80
  // The serializer's own observations — a connection or folder value that can
92
- // never be a tenant resource key — printed in the same `⚠ [CODE]` shape as
93
- // check()'s. Warnings are never fatal: the .flow is still written, exit stays 0.
94
- // A binding LABEL the serializer refuses (its own name as the resourceKey, or a
95
- // label a populated bindings.json does not declare) is an error, not a
96
- // warning, and so is a BYOA-only event on a connection `prepare` never checked
97
- // (EVENT_BYOA_UNVERIFIED): the check pass above reports each first, and this is
98
- // the same refusal in the same `✗ [CODE]` shape when that pass was skipped
99
- // (`--no-check`, or an emit-only project). Nothing is written.
81
+ // never be a tenant resource key, a draft node written from its carried
82
+ // definition — printed in the same `⚠ [CODE]` shape as check()'s. Warnings are
83
+ // never fatal: the .flow is still written, exit stays 0. A binding LABEL the
84
+ // serializer refuses (its own name as the resourceKey, or a label a populated
85
+ // bindings.json does not declare) is an error, not a warning, and so is a
86
+ // BYOA-only event on a connection `prepare` never checked
87
+ // (EVENT_BYOA_UNVERIFIED): printed in the same `✗ [CODE]` shape `check` uses
88
+ // for it. Nothing is written.
100
89
  const out = flagValue(argv, '-o') ?? `${built.name || basename(file).replace(/\.flow\.ts$|\.ts$/, '')}.flow`;
101
90
  const outDir = dirname(resolve(out));
102
91
  // ONE FOLDER, ONE FLOW (#1184). An agent's `source` is the name of the folder
@@ -147,28 +136,37 @@ export async function run(argv) {
147
136
  const warnings = [];
148
137
  let emitted;
149
138
  try {
150
- emitted = serializeWithSidecars(built, { library, bindings, warn: (w) => warnings.push(w), agentSource });
139
+ emitted = serializeWithSidecars(built, {
140
+ library, bindings, warn: (w) => warnings.push(w), agentSource,
141
+ carriedDrafts: (d) => warnings.push(draftWarning(d)),
142
+ });
151
143
  }
152
144
  catch (error) {
153
145
  for (const w of warnings)
154
- console.error(` ⚠ [${w.code}] ${w.message}`);
146
+ console.error(` ${diagnosticLine(serializeWarningDiagnostic(w))}`);
155
147
  diagnostics.push(...warnings.map(serializeWarningDiagnostic));
156
- if (!(error instanceof BindingRefusal || error instanceof ByoaRefusal)) {
157
- // Any other throw still ends the run through the caller (exit 1, `--json`
158
- // `error`); the list is reported first.
148
+ if (!(error instanceof CompileRefusal)) {
149
+ // Any other throw (an internal SDK error) still ends the run through the
150
+ // caller (exit 1, `--json` `error`); the list is reported first.
159
151
  diagnostics.push(thrownDiagnostic(error));
160
152
  reportDiagnostics(diagnostics);
161
153
  throw error;
162
154
  }
163
- console.error(` ✗ [${error.code}] ${error.message}`);
164
- diagnostics.push({ level: 'error', code: error.code, message: error.message });
155
+ // A coded refusal: the serializer cannot write a correct `.flow`. Printed in
156
+ // `check`'s `✗ [CODE]` shape, which `check` reports for it too.
157
+ const refused = {
158
+ level: 'error', code: error.code, message: error.message,
159
+ ...(error.suggestion !== undefined ? { suggestion: error.suggestion } : {}),
160
+ };
161
+ console.error(` ${diagnosticLine(refused)}`);
162
+ diagnostics.push(refused);
165
163
  reportDiagnostics(diagnostics);
166
164
  console.error('compile: 1 error(s); not emitting.');
167
165
  return 1;
168
166
  }
169
167
  const { flow: flowJson, sidecars } = emitted;
170
168
  for (const w of warnings)
171
- console.error(` ⚠ [${w.code}] ${w.message}`);
169
+ console.error(` ${diagnosticLine(serializeWarningDiagnostic(w))}`);
172
170
  diagnostics.push(...warnings.map(serializeWarningDiagnostic));
173
171
  // An inline agent is TWO artifacts: the node, and the agent's own configuration
174
172
  // at `<source>/agent.json` — which is where flow-v1's serializer points the
@@ -325,7 +323,19 @@ export async function run(argv) {
325
323
  return 0;
326
324
  }
327
325
  function serializeWarningDiagnostic(w) {
328
- return { level: 'warning', code: w.code, message: w.message };
326
+ return { level: 'warning', code: w.code, message: w.message, ...(w.step !== undefined ? { step: w.step } : {}) };
327
+ }
328
+ /**
329
+ * A draft node written from its carried definition, as a warning: the node is
330
+ * in the `.flow` as the canvas saved it, unfinished. For instance
331
+ * `⚠ [BINDING_MISSING] … written as a draft from its carried definition`.
332
+ */
333
+ function draftWarning(d) {
334
+ return {
335
+ code: String(d.code),
336
+ message: `${d.message} Written as a draft, with the definition the source carries; finish the node before the flow runs.`,
337
+ ...(d.step !== undefined ? { step: d.step } : {}),
338
+ };
329
339
  }
330
340
  runWhenInvokedDirectly(import.meta.url, 'compile', run);
331
341
  /** A JSON file's contents, or `undefined` when it is missing or unreadable. */
@@ -159,7 +159,7 @@ if (!existsSync(BASELINE)) {
159
159
  // ── Step 1: capture the baseline = compile of the PRISTINE decompiled code,
160
160
  // BEFORE any edit. Run this once right after decompiling, then edit. ──
161
161
  mkdirSync(dirname(BASELINE), { recursive: true });
162
- run('compile', [TS, '-o', BASELINE, '--no-check']);
162
+ run('compile', [TS, '-o', BASELINE]);
163
163
  console.log('\\n>>> Baseline captured. Now EDIT ' + ${JSON.stringify(basename(tsPath))} + ', then re-run this script to compile + merge.');
164
164
  } else {
165
165
  // ── Step 2: compile your edits, then merge them back into the original. ──
@@ -182,7 +182,7 @@ if (!existsSync(BASELINE)) {
182
182
  process.exit(2);
183
183
  }
184
184
  mkdirSync(dirname(EDITED), { recursive: true });
185
- run('compile', [TS, '-o', EDITED, '--no-check']);
185
+ run('compile', [TS, '-o', EDITED]);
186
186
 
187
187
  // ── DIFF GATE (seam) ──────────────────────────────────────────────────────
188
188
  // A future step can diff ORIGINAL vs the about-to-be-written MERGED here and
package/dist/decompile.js CHANGED
@@ -298,6 +298,11 @@ function optsArgWith(node, ctx, extra) {
298
298
  const entries = updates.map((u) => `${key(u.variableId)}: ${emitVal(u.expression, ctx.imp, ctx.inputNames)}`);
299
299
  parts.push(`updates: { ${entries.join(', ')} }`);
300
300
  }
301
+ // A draft the canvas saved: `compile` writes it back only when the step says
302
+ // so AND the source carries its definition (carriedPairs), so a draft an agent
303
+ // adds of the same operation is still refused (StepOptions.draft).
304
+ if (isCanvasDraft(node))
305
+ parts.push('draft: true');
301
306
  return parts.length ? `, { ${parts.join(', ')} }` : '';
302
307
  }
303
308
  // ─── Node type constants (mirror of serialize.ts NODE_TYPE) ───────────────────
@@ -957,8 +962,9 @@ function emitStepSpec(node, imp, inputNames, o, graph) {
957
962
  /**
958
963
  * A connector-bound node the file carries UNBOUND: Studio Web saves a draft whose
959
964
  * connection was never picked as `connectionId: null`. The source cannot invent
960
- * labels for it, and `compile` refuses a node without both (BINDING_MISSING, the
961
- * rule `flow validate` applies too: flow-builder-sdk#890). So the source says so
965
+ * labels for it, and `check` refuses a node without both (BINDING_MISSING, the
966
+ * rule `flow validate` applies too: flow-builder-sdk#890), and so does `compile`
967
+ * unless it is a carried canvas draft (`{ draft: true }`). So the source says so
962
968
  * at the node and the run warns once, instead of handing back a file whose first
963
969
  * compile fails with no context. Returns the inline marker, or '' when bound.
964
970
  */
@@ -968,10 +974,12 @@ function unboundMarker(node, labels, key, action) {
968
974
  return '';
969
975
  const names = missing.join(' and ');
970
976
  warnOnce(`node ${tsLiteral(node.id)} (${node.type}) has no ${missing.join(' or ')} in the .flow (an unbound draft). `
971
- + `The source leaves ${missing.length > 1 ? 'them' : 'it'} out, and compile refuses the node (BINDING_MISSING) `
972
- + `until it names both labels: run \`${prepareCommand(key, action ?? '<action>')}\`, then pass `
977
+ + `The source leaves ${missing.length > 1 ? 'them' : 'it'} out${isCanvasDraft(node)
978
+ ? ' and marks the step `{ draft: true }`: compile writes it as a draft, and check refuses it'
979
+ : ', and compile and check refuse it'} (BINDING_MISSING) until it names both labels: `
980
+ + `run \`${prepareCommand(key, action ?? '<action>')}\`, then pass `
973
981
  + `\`connection\` and \`folder\`.`);
974
- return ` /* TODO: bind ${commentText(names)}; compile refuses this node until it has both labels (BINDING_MISSING) */`;
982
+ return ` /* TODO: bind ${commentText(names)}; check refuses this node until it has both labels (BINDING_MISSING) */`;
975
983
  }
976
984
  function emitEventSubscription(node, imp, inputNames) {
977
985
  const prefix = node.type.startsWith('uipath.connector.event.') ? 'uipath.connector.event.'
@@ -1071,11 +1079,27 @@ function unsupportedSpec(node, imp, o) {
1071
1079
  return `${imp.use('mock')}() /* TODO: unsupported node type ${commentText(node.type)} */`;
1072
1080
  }
1073
1081
  /**
1074
- * Every pinned `<type>@<version>` in the file (root and subflows) that this SDK
1075
- * cannot resolve, for a type it ships at some version, and that the file's
1076
- * `definitions[]` holds — mapped to the const name decompile hoists the
1077
- * definition under. For instance a quick-form node at 1.6 maps
1078
- * `uipath.human-in-the-loop.quick-form@1.6` → `quickFormV1_6Definition`.
1082
+ * Whether `node` is a draft the canvas saved whose definition the file holds
1083
+ * (its node type's `isDraft`). Decompile carries that definition and marks the
1084
+ * step `{ draft: true }`. For instance a Slack send with `detail.connectionId: null`.
1085
+ */
1086
+ function isCanvasDraft(node) {
1087
+ const key = `${node.type}@${node.typeVersion}`;
1088
+ return !!node.typeVersion && runDefs.has(key) && registeredImpl(node)?.isDraft?.(node, runDefs.get(key)) === true;
1089
+ }
1090
+ /**
1091
+ * Every `<type>@<version>` in the file (root and subflows) whose definition the
1092
+ * source must carry, and that the file's `definitions[]` holds — mapped to the
1093
+ * const name decompile hoists the definition under. Two kinds of node need it:
1094
+ *
1095
+ * - a pin this SDK cannot resolve, for a type it ships at some version. For
1096
+ * instance a quick-form node at 1.6 maps
1097
+ * `uipath.human-in-the-loop.quick-form@1.6` → `quickFormV1_6Definition`.
1098
+ * - a DRAFT the canvas saved (its node type's `isDraft`): `compile`
1099
+ * writes a draft node only with the definition the source carries for it
1100
+ * (docs/CHECK_COMPILE_CONTRACT_DESIGN.md D2), so an untouched canvas draft
1101
+ * compiles back. The step is also marked `{ draft: true }`, so a draft an
1102
+ * agent writes, even of the same operation, is refused.
1079
1103
  */
1080
1104
  function carriedPairs(flow) {
1081
1105
  const out = new Map();
@@ -1086,7 +1110,8 @@ function carriedPairs(flow) {
1086
1110
  if (!n.typeVersion || out.has(key) || !runDefs.has(key))
1087
1111
  continue;
1088
1112
  const shipped = shippedVersions(n.type);
1089
- if (shipped.length === 0 || shipped.includes(n.typeVersion))
1113
+ const unresolvable = shipped.length > 0 && !shipped.includes(n.typeVersion);
1114
+ if (!unresolvable && !isCanvasDraft(n))
1090
1115
  continue;
1091
1116
  const base = `${camel(String(n.type).split('.').pop() ?? 'node')}V${String(n.typeVersion).replace(/[^A-Za-z0-9]+/g, '_')}Definition`;
1092
1117
  let name = base;
@@ -484,6 +484,15 @@ export interface FlowRawNodeAdvice {
484
484
  }
485
485
  /** One Flow node type at one version. */
486
486
  export interface FlowNodeType<Spec> extends NodeType<Spec, FlowNodeContribution, FlowContexts> {
487
+ /**
488
+ * Whether a serialized node of this type is a DRAFT the canvas saved: one an
489
+ * ordinary compile refuses only for a draft code (a connector with no
490
+ * connection, an empty required input). Decompile then copies the node's
491
+ * definition from the file into `.definitions([...])`, and `compile` writes
492
+ * the draft back with it (`SerializeOpts.carriedDrafts`). `definition` is the
493
+ * file's own `definitions[]` entry for the node, if any. Absent: never a draft.
494
+ */
495
+ isDraft?(node: unknown, definition: unknown): boolean;
487
496
  /**
488
497
  * The node-level `model` every node of this type carries, whichever path emits
489
498
  * it — this type's `serialize`, or a `rawNode` of its definition. `'omit'`: none
@@ -444,6 +444,27 @@ export interface NodeOptions {
444
444
  */
445
445
  label?: string;
446
446
  }
447
+ /**
448
+ * The options of `.step()`: the {@link NodeOptions}, plus `draft` for a node the
449
+ * canvas saved unfinished.
450
+ */
451
+ export interface StepOptions extends NodeOptions {
452
+ /**
453
+ * This node is a DRAFT the canvas saved: a connector with no connection, or
454
+ * with an empty required input. Decompile sets it on such a node, and also
455
+ * carries the node's definition in `.definitions([...])`. `compile` then
456
+ * writes the node with that carried definition and the inputs it has, and
457
+ * prints a warning (`BINDING_MISSING`, `CONNECTOR_INPUT_MISSING`) instead of
458
+ * refusing. Without the flag, or without a carried definition for the node's
459
+ * exact type and version, the draft is refused as usual. `check` reports the
460
+ * draft either way.
461
+ *
462
+ * Do not set it on a node you write: finish the node instead. For instance
463
+ * decompile prints `.step('notify', connector(SLACK, SEND, { ... }), { draft: true })`
464
+ * for a Slack send saved with no connection.
465
+ */
466
+ draft?: boolean;
467
+ }
447
468
  /**
448
469
  * The options of `.parallel()`: the join's {@link NodeOptions}, plus the fork
449
470
  * node when the flow has one.
@@ -558,7 +579,7 @@ export type Step = {
558
579
  kind: 'action';
559
580
  name: string;
560
581
  spec: FlowActionSpec;
561
- options?: NodeOptions;
582
+ options?: StepOptions;
562
583
  } | {
563
584
  kind: 'branch';
564
585
  name: string;
@@ -920,10 +941,10 @@ declare class StepList {
920
941
  * so it must be unique within the flow.
921
942
  * @param spec - What the node does, from an action factory.
922
943
  * @param options - Node options; `version` selects the exact definition
923
- * version (see {@link NodeOptions}).
944
+ * version (see {@link StepOptions}).
924
945
  * @returns This builder, so calls chain.
925
946
  */
926
- step(name: string, spec: FlowActionSpec | FlowAction, options?: NodeOptions): this;
947
+ step(name: string, spec: FlowActionSpec | FlowAction, options?: StepOptions): this;
927
948
  /**
928
949
  * Add an action node and route EVERY exit it declares, one arm per exit.
929
950
  *
package/dist/flow-sdk.js CHANGED
@@ -388,7 +388,7 @@ class StepList {
388
388
  * so it must be unique within the flow.
389
389
  * @param spec - What the node does, from an action factory.
390
390
  * @param options - Node options; `version` selects the exact definition
391
- * version (see {@link NodeOptions}).
391
+ * version (see {@link StepOptions}).
392
392
  * @returns This builder, so calls chain.
393
393
  */
394
394
  step(name, spec, options) {
@@ -340,15 +340,15 @@ export function checkInlineAgent(inputs, ctx) {
340
340
  //
341
341
  // An artifact that compiles and can never validate is the worst of the two
342
342
  // failures, so this is said here — before the emit — rather than left to a rung.
343
- // It is an ERROR because there is no spelling that works today, and `--no-check`
344
- // remains the escape for a tenant whose manifest does declare the handle.
343
+ // It is an ERROR because there is no spelling that works today. `compile` does
344
+ // not run check, so a tenant whose manifest does declare the handle still compiles it.
345
345
  if (inputs?.memory !== undefined) {
346
346
  at('AGENT_MEMORY_NOT_AUTHORABLE', `Inline agent step "${step}" attaches \`memory\`, which the platform cannot resolve: the tenant's `
347
347
  + `\`uipath.agent.autonomous\` manifest declares no \`memory\` handle (only escalation, context, tool, `
348
348
  + `success, error) and the registry serves no \`uipath.agent.resource.memory.*\` node type. The flow `
349
349
  + `compiles and then fails \`uip maestro flow validate\` as RetryWillNotFix. Drop \`memory\`; for `
350
350
  + `retrieval over a knowledge source use a context index instead (\`tools\`/context grounding). `
351
- + `Pass --no-check only on a tenant whose manifest does declare the handle.`);
351
+ + `On a tenant whose manifest does declare the handle, ignore this finding: \`compile\` does not run check.`);
352
352
  }
353
353
  // A context `query` is what the RUNTIME searches with, and it is not an
354
354
  // expression slot. A plain string is the model-invoked `dynamic` variant: the
@@ -468,7 +468,7 @@ export function checkInlineAgent(inputs, ctx) {
468
468
  + `of the prompt) with one line per field, in the platform's own wording: "${rewrite}" Declare a field \`'object'\` or `
469
469
  + `\`'array'\` if it should carry structure. \`uip maestro flow validate\` accepts this prompt, so `
470
470
  + `nothing later catches it. If this is a false positive (the prompt only mentions JSON as data), `
471
- + `compile with --no-check.`);
471
+ + `ignore this finding: \`compile\` does not run check.`);
472
472
  }
473
473
  }
474
474
  // The prompts-vs-variables agreement, in both directions. Both prompts are
@@ -270,6 +270,40 @@ function liftConnector(node, ctx) {
270
270
  missing,
271
271
  };
272
272
  }
273
+ /**
274
+ * Whether a connector operation node is a draft the canvas saved, by what the
275
+ * file itself shows: no connection or folder (`BINDING_MISSING`), or a field its
276
+ * own definition marks `required` that the node gives no value
277
+ * (`CONNECTOR_INPUT_MISSING`). For instance a Slack send saved before a
278
+ * connection was picked has `detail.connectionId: null`. An operation with no
279
+ * object picked is not one: `compile` can only stand in for it, so it is never
280
+ * written either way.
281
+ */
282
+ function isConnectorDraft(node, definition) {
283
+ const detail = (node.inputs?.detail ?? {});
284
+ const label = (v) => (typeof v === 'string' ? v : undefined);
285
+ if (missingBindingLabels({ connection: label(detail.connectionId), folder: label(detail.connectionFolderKey) }).length > 0)
286
+ return true;
287
+ const fields = definition?.inputDefinition?.fields;
288
+ if (!Array.isArray(fields))
289
+ return false;
290
+ const slots = ['bodyParameters', 'queryParameters', 'pathParameters']
291
+ .map((slot) => detail[slot])
292
+ .filter((m) => !!m && typeof m === 'object' && !Array.isArray(m));
293
+ const parts = Array.isArray(detail.multipartParameters) ? detail.multipartParameters : [];
294
+ const given = (name) => parts.some((p) => p?.name === name) || slots.some((m) => {
295
+ if (Object.prototype.hasOwnProperty.call(m, name))
296
+ return true;
297
+ let at = m;
298
+ for (const seg of name.split('.')) {
299
+ if (!at || typeof at !== 'object' || !Object.prototype.hasOwnProperty.call(at, seg))
300
+ return false;
301
+ at = at[seg];
302
+ }
303
+ return true;
304
+ });
305
+ return fields.some((f) => f?.required === true && typeof f.name === 'string' && !given(f.name));
306
+ }
273
307
  function printConnector(l, ctx) {
274
308
  // A present input whose value is `undefined` (the inputs check accepts one, and
275
309
  // serialize keeps it in memory) prints as `undefined`, as it did before the port.
@@ -284,7 +318,7 @@ function printConnector(l, ctx) {
284
318
  ['object', optional(l.object)],
285
319
  ],
286
320
  ...(l.missing.length > 0
287
- ? { comment: `TODO: bind ${l.missing.join(' and ')}; compile refuses this node until it has both labels (BINDING_MISSING)` }
321
+ ? { comment: `TODO: bind ${l.missing.join(' and ')}; check refuses this node until it has both labels (BINDING_MISSING)` }
288
322
  : {}),
289
323
  };
290
324
  }
@@ -302,6 +336,7 @@ const connector10 = {
302
336
  },
303
337
  serialize: (spec, ctx) => serializeConnector(spec, ctx),
304
338
  deserialize: (node, ctx) => liftConnector(node, ctx),
339
+ isDraft: (node, definition) => isConnectorDraft(node, definition),
305
340
  print: (spec, ctx) => printConnector(spec, ctx),
306
341
  check: (spec, ctx) => ctx.connector(spec),
307
342
  // Every Expr in the inputs, nested ones included, resolves against the flow's names.
@@ -29,6 +29,8 @@ export type DraftCode = BindingRefusalCode | typeof CONNECTOR_INPUT_MISSING | 'C
29
29
  export interface DraftFinding {
30
30
  /** The emitted node's id. */
31
31
  nodeId: string;
32
+ /** The step whose node it is. Set by a carried-drafts compile ({@link SerializeOpts.carriedDrafts}). */
33
+ step?: string;
32
34
  /** A {@link DraftCode}, or a node-type package's own code reported through `ctx.draft`. */
33
35
  code: DraftCode | string;
34
36
  /** The sentence the strict compile refuses with. */
@@ -71,6 +73,28 @@ export interface SerializeOpts {
71
73
  * ordinary compile), behaviour is unchanged.
72
74
  */
73
75
  draft?: (finding: DraftFinding) => void;
76
+ /**
77
+ * Carried drafts, for `compile` (docs/CHECK_COMPILE_CONTRACT_DESIGN.md D2).
78
+ *
79
+ * A step marked `{ draft: true }` ({@link StepOptions.draft}) that the
80
+ * ordinary compile refuses only for draft findings ({@link DraftCode}: a
81
+ * connector with no connection, an empty required input) is written anyway
82
+ * when the flow CARRIES the definition of the node it emits
83
+ * (`.definitions([...])`). Decompile sets both for a canvas draft node. The
84
+ * node is written with that definition and the inputs it has, and each
85
+ * finding is reported here. A draft with no flag, or no carried definition,
86
+ * or one the node type can only stand in for (a generic operation with no
87
+ * object), is refused exactly as without this option. So is any other
88
+ * refusal. A flow with no draft compiles byte for byte as without it.
89
+ *
90
+ * For instance, a decompiled canvas flow whose Slack send has no `messageToSend`
91
+ * compiles with a `CONNECTOR_INPUT_MISSING` finding here, and a second Slack
92
+ * send written by hand with no `messageToSend` (no flag) is refused, though
93
+ * the flow carries the definition of that operation.
94
+ *
95
+ * Ignored when {@link draft} is given (merge's compile tolerates every draft).
96
+ */
97
+ carriedDrafts?: (finding: DraftFinding) => void;
74
98
  /**
75
99
  * Collect mode, for `check` (docs/CHECK_COMPILE_CONTRACT_DESIGN.md §3.5).
76
100
  *
package/dist/serialize.js CHANGED
@@ -2542,6 +2542,76 @@ function serializeFlow(built, opts) {
2542
2542
  }
2543
2543
  return [{ node: id, port }];
2544
2544
  }
2545
+ /**
2546
+ * Run a node type's `serialize`, and in a carried-drafts compile
2547
+ * ({@link SerializeOpts.carriedDrafts}) try a refused `{ draft: true }` step
2548
+ * once more as a draft. The ordinary emission runs first, so a flow with no draft compiles
2549
+ * exactly as it would without the option. When it throws a refusal and the flow
2550
+ * carries any definition, what that emission bound is undone and the node is
2551
+ * emitted again with `drafting` on. That second contribution is returned with
2552
+ * the first refusal as `strict` when it reported a draft finding; the caller
2553
+ * writes it only with a carried definition, and throws `strict` otherwise. A
2554
+ * second emission that throws, or reports no finding, ends in `strict` too, so
2555
+ * a refusal that is not a draft reads as it always has.
2556
+ */
2557
+ function serializeCarriedDraft(impl, spec, ctx, scope, id, bound, stepOptions, findings, enableDrafting, warnings) {
2558
+ if (!opts.carriedDrafts || opts.draft || opts.collect || carriedDefs.size === 0 || stepOptions?.draft !== true) {
2559
+ return { contribution: impl.serialize(spec, ctx) };
2560
+ }
2561
+ const mark = {
2562
+ resources: new Set(resourceBindings.keys()), sidecars: sidecars.length,
2563
+ agentSchemas: scope.agentSchemas.length, unbound: unboundDrafts.has(id),
2564
+ };
2565
+ // Each emission's warnings are held, so a node emitted twice reports only
2566
+ // the warnings of the emission that is kept (or of the refused one).
2567
+ const flush = (held) => { for (const w of held)
2568
+ opts.warn?.(w); };
2569
+ const first = [];
2570
+ warnings.held = first;
2571
+ try {
2572
+ const contribution = impl.serialize(spec, ctx);
2573
+ warnings.held = undefined;
2574
+ flush(first);
2575
+ return { contribution };
2576
+ }
2577
+ catch (error) {
2578
+ warnings.held = undefined;
2579
+ if (!(error instanceof CompileRefusal)) {
2580
+ flush(first);
2581
+ throw error;
2582
+ }
2583
+ for (const key of [...resourceBindings.keys()])
2584
+ if (!mark.resources.has(key))
2585
+ resourceBindings.delete(key);
2586
+ for (const f of sidecars.splice(mark.sidecars)) {
2587
+ if (!sidecars.some((kept) => kept.path === f.path))
2588
+ explicitSidecars.delete(f.path);
2589
+ }
2590
+ scope.agentSchemas.length = mark.agentSchemas;
2591
+ if (!mark.unbound)
2592
+ unboundDrafts.delete(id);
2593
+ bound.length = 0;
2594
+ enableDrafting();
2595
+ const second = [];
2596
+ warnings.held = second;
2597
+ let contribution;
2598
+ try {
2599
+ contribution = impl.serialize(spec, ctx);
2600
+ }
2601
+ catch {
2602
+ flush(first);
2603
+ throw error;
2604
+ }
2605
+ finally {
2606
+ warnings.held = undefined;
2607
+ }
2608
+ if (findings.length === 0) {
2609
+ flush(first);
2610
+ throw error;
2611
+ }
2612
+ return { contribution, strict: error, strictWarnings: first, draftWarnings: second };
2613
+ }
2614
+ }
2545
2615
  /**
2546
2616
  * Lower a step whose kind is a registered node type (flow-node-registry.ts):
2547
2617
  * the node type serializes its spec through a context, and this owns the rest —
@@ -2572,6 +2642,19 @@ function serializeFlow(built, opts) {
2572
2642
  rendered.add(copy);
2573
2643
  return copy;
2574
2644
  };
2645
+ // Merge's compile drafts every node; `compile` (carriedDrafts) drafts a node
2646
+ // only on a second try, after the ordinary emission refused it.
2647
+ let drafting = opts.draft !== undefined;
2648
+ const carriedFindings = [];
2649
+ // Where this step's warnings go: `opts.warn`, or held while a carried-drafts
2650
+ // compile does not yet know which emission it keeps (serializeCarriedDraft).
2651
+ const warnings = {};
2652
+ const bindingOpts = opts.warn
2653
+ ? { ...opts, warn: (w) => { if (warnings.held)
2654
+ warnings.held.push(w);
2655
+ else
2656
+ opts.warn(w); } }
2657
+ : opts;
2575
2658
  const ctx = {
2576
2659
  step: step.name,
2577
2660
  nodeId: id,
@@ -2598,7 +2681,7 @@ function serializeFlow(built, opts) {
2598
2681
  ? resolved.carried
2599
2682
  : bundledDefinition(typeName, v) ?? carriedFor(typeName, v)),
2600
2683
  bindings: resourceBindings,
2601
- resolveBinding: ((name, attribute, at, target) => resolveBinding(opts, name, attribute, at, target)),
2684
+ resolveBinding: ((name, attribute, at, target) => resolveBinding(bindingOpts, name, attribute, at, target)),
2602
2685
  bindConnection: (c) => {
2603
2686
  const { entries } = connectionResourceBindings({
2604
2687
  def: { model: c.model },
@@ -2625,15 +2708,26 @@ function serializeFlow(built, opts) {
2625
2708
  const out = renderDeep(value, scope.rename, rendered);
2626
2709
  return out !== null && typeof out === 'object' ? fileCopy(out) : out;
2627
2710
  },
2628
- drafting: opts.draft !== undefined,
2711
+ get drafting() { return drafting; },
2629
2712
  draft: (code, message) => {
2630
2713
  if (code === 'BINDING_MISSING')
2631
2714
  unboundDrafts.add(id);
2632
- opts.draft?.({ nodeId: id, code: code, message });
2715
+ if (opts.draft)
2716
+ opts.draft({ nodeId: id, code: code, message });
2717
+ else
2718
+ carriedFindings.push({ nodeId: id, step: step.name, code: code, message });
2633
2719
  },
2634
2720
  };
2635
2721
  const bound = [];
2636
- const contribution = impl.serialize(spec, ctx);
2722
+ const { contribution, strict, strictWarnings, draftWarnings } = serializeCarriedDraft(impl, spec, ctx, scope, id, bound, step.options, carriedFindings, () => { drafting = true; }, warnings);
2723
+ // A carried draft refused after all reports the warnings of the refused emission.
2724
+ const refuse = () => {
2725
+ for (const w of strictWarnings ?? [])
2726
+ opts.warn?.(w);
2727
+ throw strict;
2728
+ };
2729
+ if (strict !== undefined && contribution.standIn !== undefined)
2730
+ refuse();
2637
2731
  if (contribution.standIn !== undefined) {
2638
2732
  // A draft stand-in: the node as the draft has it, with no definition to
2639
2733
  // place it against (see FlowNodeContribution.standIn).
@@ -2677,6 +2771,22 @@ function serializeFlow(built, opts) {
2677
2771
  const origin = entry.family.package;
2678
2772
  def = { ...def, source: { package: origin.name, ...(origin.version !== undefined ? { version: origin.version } : {}) } };
2679
2773
  }
2774
+ // A draft `compile` writes: only a step marked `{ draft: true }` (checked in
2775
+ // serializeCarriedDraft) with the definition the flow carries for the node it
2776
+ // emits (decompile's copy of the canvas file's entry), else refused as the
2777
+ // ordinary emission refused it (SerializeOpts.carriedDrafts).
2778
+ if (strict !== undefined) {
2779
+ const carried = typeof def?.nodeType === 'string' && typeof def?.version === 'string'
2780
+ ? carriedFor(def.nodeType, def.version)
2781
+ : undefined;
2782
+ if (carried === undefined)
2783
+ refuse();
2784
+ def = carried;
2785
+ for (const w of draftWarnings ?? [])
2786
+ opts.warn?.(w);
2787
+ for (const f of carriedFindings)
2788
+ opts.carriedDrafts?.(f);
2789
+ }
2680
2790
  // A type that owns a family of names (`typeNamePrefix`) is placed under the
2681
2791
  // name its definition carries — `uipath.core.function.<key>`, not the prefix.
2682
2792
  // The bare prefix is allowed: an empty `key` builds it, check refuses that