@uipath/maestro-builder-sdk 6.16.14 → 6.17.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 (74) hide show
  1. package/README.md +2 -2
  2. package/dist/api-index.md +115 -114
  3. package/dist/api-members.md +295 -291
  4. package/dist/bindings-v2.d.ts +39 -1
  5. package/dist/bindings-v2.js +289 -9
  6. package/dist/bpmn/compile-cli.js +11 -2
  7. package/dist/bpmn/decompile.js +2 -1
  8. package/dist/bpmn/registry-fallback.js +9 -7
  9. package/dist/case/compile-cli.js +16 -6
  10. package/dist/case/decompile-cli.js +2 -1
  11. package/dist/case/decompile.js +3 -2
  12. package/dist/case/merge-cli.js +3 -2
  13. package/dist/check-cli.js +2 -30
  14. package/dist/check.js +278 -55
  15. package/dist/cli-library.d.ts +18 -4
  16. package/dist/cli-library.js +24 -6
  17. package/dist/cli-result.d.ts +9 -0
  18. package/dist/cli-result.js +12 -0
  19. package/dist/compile-cli.js +82 -86
  20. package/dist/core/actions.d.ts +71 -17
  21. package/dist/core/agentic-process-forms.d.ts +51 -0
  22. package/dist/core/agentic-process-forms.js +49 -0
  23. package/dist/core/binding-messages.d.ts +5 -0
  24. package/dist/core/cli-library.d.ts +16 -7
  25. package/dist/core/cli-library.js +53 -19
  26. package/dist/core/comment-text.d.ts +29 -0
  27. package/dist/core/comment-text.js +40 -0
  28. package/dist/core/connector-checks.d.ts +18 -2
  29. package/dist/core/connector-checks.js +15 -5
  30. package/dist/core/contribution-bindings.d.ts +73 -0
  31. package/dist/core/contribution-bindings.js +105 -0
  32. package/dist/core/error-port.d.ts +25 -0
  33. package/dist/core/error-port.js +36 -0
  34. package/dist/core/expr-check.js +7 -2
  35. package/dist/core/expr.d.ts +11 -5
  36. package/dist/core/expr.js +9 -3
  37. package/dist/core/json-file.d.ts +1 -0
  38. package/dist/core/json-file.js +12 -0
  39. package/dist/core/library.d.ts +19 -0
  40. package/dist/core/library.js +22 -6
  41. package/dist/core/script-return.js +4 -1
  42. package/dist/core/typed-raw-node.js +1 -0
  43. package/dist/core/typescript-module.d.ts +2 -0
  44. package/dist/core/typescript-module.js +18 -0
  45. package/dist/core-definitions.json +210 -0
  46. package/dist/decompile-cli.js +2 -1
  47. package/dist/decompile.js +64 -24
  48. package/dist/emit.js +4 -3
  49. package/dist/flow-sdk.d.ts +18 -12
  50. package/dist/flow-sdk.js +19 -8
  51. package/dist/flow-step-tree.d.ts +2 -3
  52. package/dist/flow-step-tree.js +2 -3
  53. package/dist/load.js +1 -1
  54. package/dist/merge-cli.js +3 -2
  55. package/dist/merge.d.ts +2 -1
  56. package/dist/merge.js +0 -0
  57. package/dist/path-within.d.ts +33 -0
  58. package/dist/path-within.js +41 -0
  59. package/dist/registry/archive/extract.js +2 -5
  60. package/dist/registry/curated-overlay.js +3 -2
  61. package/dist/registry/generators/descriptors.js +20 -18
  62. package/dist/registry/generators/triggers.js +4 -4
  63. package/dist/registry/prepare/messages.js +2 -1
  64. package/dist/registry/prepare/source-labels.js +60 -1
  65. package/dist/registry/prepare-cli.js +14 -4
  66. package/dist/registry/pull-cli.js +3 -2
  67. package/dist/registry/uip/client.d.ts +28 -3
  68. package/dist/registry/uip/client.js +95 -34
  69. package/dist/registry/uip/host.d.ts +41 -0
  70. package/dist/registry/uip/host.js +312 -0
  71. package/dist/serialize.d.ts +7 -0
  72. package/dist/serialize.js +414 -277
  73. package/dist/workdir.js +1 -1
  74. package/package.json +3 -3
package/dist/check.js CHANGED
@@ -7,16 +7,21 @@
7
7
  */
8
8
  import { Expr, SCHEDULE_PRESETS, DELAY_PRESETS, VARIANT_OPERATION, parseIxpProjectType, flatNameOf, flatNameSegmentsOf, FLAT_SEP } from './flow-sdk.js';
9
9
  import { FlowNode } from './core/node-classes.js';
10
+ import { planContributionBindings } from './core/contribution-bindings.js';
11
+ import { definitionHasErrorPort, errorPortRefusal } from './core/error-port.js';
12
+ import { resolveContributionDefinition } from './serialize.js';
10
13
  import { QUEUE_ITEM_RECORD_SCHEMA } from './core/queue-record.js';
14
+ import { AGENTIC_PROCESS_FORMS } from './core/agentic-process-forms.js';
11
15
  import { checkEventResolution, checkEventSubscription, eventParamsFor, } from './core/event-checks.js';
12
16
  import { checkCookedEscapes } from './flow-expr-check.js';
13
17
  import { FLOW_EXPR_DIALECT, checkBindingLabels, checkBindingPresence, checkConnectorContract as coreCheckConnectorContract, checkConnectorLookups as coreCheckConnectorLookups, checkConnectorSchema as coreCheckConnectorSchema, collectConnectionUses, connectionConnectorConflicts, } from './core/connector-checks.js';
14
18
  import { prepareCommand } from './core/cli-spelling.js';
19
+ import { commentText } from './core/comment-text.js';
15
20
  import { editDistance } from './core/edit-distance.js';
16
21
  import { isAnswerField } from './core/hitl-answers.js';
17
22
  import { hitlDerivesOutcomeHandles, hitlExitModel, hitlRoutesPerOutcome } from './core/hitl-routing.js';
18
23
  import { declaredExits, hasExits } from './core/step-ports.js';
19
- import { childStepLists, stopsAtEnd } from './flow-step-tree.js';
24
+ import { stopsAtEnd } from './flow-step-tree.js';
20
25
  import { FLOW_SCHEMA_VERSIONS } from './format-profile.js';
21
26
  import { compareSchemaVersions, schemaVersionRefusal } from './schema-version.js';
22
27
  import { planFlowGlobals, planSharedInputs, sharedInputRoots } from './flow-variables.js';
@@ -54,9 +59,11 @@ function checkFlow(built, ancestors, opts = {}) {
54
59
  if (maybeCustomTrigger !== undefined && !('kind' in maybeCustomTrigger) && maybeCustomTrigger instanceof FlowNode) {
55
60
  // A custom FlowTrigger subclass reports through its own diagnostics() hook;
56
61
  // the built-in trigger rules key on `kind` and don't apply.
57
- for (const d of maybeCustomTrigger.diagnostics()) {
62
+ const own = maybeCustomTrigger.diagnostics();
63
+ for (const d of own) {
58
64
  diags.push({ level: d.severity, code: d.code, message: d.message, step: 'trigger' });
59
65
  }
66
+ checkContribution(maybeCustomTrigger, built.triggerOptions?.version, 'trigger', own.some((d) => d.severity === 'error'), diags);
60
67
  }
61
68
  else if (built.trigger !== undefined)
62
69
  checkTrigger(built.trigger, diags, opts);
@@ -251,7 +258,7 @@ function checkFlow(built, ancestors, opts = {}) {
251
258
  stepNames.add(s.name);
252
259
  declared.add(s.name); // a step's output is referenceable by name
253
260
  }
254
- if (s.kind === 'stepToList' && s.port === 'error')
261
+ if ((s.kind === 'stepToList' || s.kind === 'stepToRef') && s.port === 'error')
255
262
  checkOnError(s, prevPortSource, diags);
256
263
  if ((s.kind === 'stepToList' || s.kind === 'stepToRef') && s.port.startsWith('branch-')) {
257
264
  const want = s.port.slice('branch-'.length);
@@ -366,9 +373,11 @@ function checkFlow(built, ancestors, opts = {}) {
366
373
  // don't apply, and it reports through its own diagnostics() hook.
367
374
  const maybeCustom = s.spec;
368
375
  if (!('kind' in maybeCustom) && maybeCustom instanceof FlowNode) {
369
- for (const d of maybeCustom.diagnostics()) {
376
+ const own = maybeCustom.diagnostics();
377
+ for (const d of own) {
370
378
  diags.push({ level: d.severity, code: d.code, message: d.message, step: s.name });
371
379
  }
380
+ checkContribution(maybeCustom, s.options?.version, s.name, own.some((d) => d.severity === 'error'), diags);
372
381
  }
373
382
  if (s.spec.kind === 'transform')
374
383
  checkTransform(s.name, s.spec.inputs, diags, typeOf, triggerId);
@@ -864,9 +873,11 @@ function checkFlow(built, ancestors, opts = {}) {
864
873
  epIds.add(ep.id);
865
874
  const maybeCustomEp = ep.trigger;
866
875
  if (!('kind' in maybeCustomEp) && maybeCustomEp instanceof FlowNode) {
867
- for (const d of maybeCustomEp.diagnostics()) {
876
+ const own = maybeCustomEp.diagnostics();
877
+ for (const d of own) {
868
878
  diags.push({ level: d.severity, code: d.code, message: d.message, step: ep.id });
869
879
  }
880
+ checkContribution(maybeCustomEp, ep.options?.version, ep.id, own.some((d) => d.severity === 'error'), diags);
870
881
  }
871
882
  else {
872
883
  checkTrigger(ep.trigger, diags, opts, ep.id);
@@ -1283,13 +1294,10 @@ function checkFlow(built, ancestors, opts = {}) {
1283
1294
  }
1284
1295
  }
1285
1296
  }
1286
- // Walk for a `.return()` rather than stringifying the step tree: a subflow
1287
- // step carries a whole child flow, so the tree can be cyclic (a flow that
1288
- // runs itself — SUBFLOW_CYCLE) and always contains the CHILD's returns, which
1289
- // say nothing about whether THIS flow terminates.
1290
- const hasTerminal = hasReturn(built.steps);
1291
- if (!hasTerminal)
1292
- diags.push({ level: 'warning', code: 'NO_RETURN', message: 'Flow has no return(); a default End node will be appended.' });
1297
+ // The End compile appends when the root path runs out of steps (#941).
1298
+ const fallOff = checkFallOffEnd(built);
1299
+ if (fallOff)
1300
+ diags.push(fallOff);
1293
1301
  // Check every child flow with the same rules, in its OWN scope — a child's
1294
1302
  // steps never see the parent's variables, so its undeclared references,
1295
1303
  // duplicate step names and type mismatches are its own. Diagnostics come back
@@ -1376,9 +1384,124 @@ function checkLoopListEnvelope(loop, connectors, opts, diags) {
1376
1384
  suggestion,
1377
1385
  });
1378
1386
  }
1379
- /** True if any step on THIS flow's own tree is a `.return()`. */
1380
- function hasReturn(steps) {
1381
- return steps.some((s) => s.kind === 'return' || childStepLists(s).some(hasReturn));
1387
+ /**
1388
+ * NO_RETURN: a path that runs off the end of the flow, where a declared output
1389
+ * is never written (#941).
1390
+ *
1391
+ * It asks serialize's question, not "is there a `.return()` anywhere?".
1392
+ * serialize appends an End exactly when the root walk has tails left
1393
+ * (`if (finalTails.length)`), and `armTails` mirrors that walk tail for tail.
1394
+ * So a `.terminate()` path appends no End, and a branch where one arm returns
1395
+ * and the other falls off DOES append one.
1396
+ *
1397
+ * The appended End maps every output from its own variable
1398
+ * (`result <- $vars.result`), the same as a `.return()` that names no value.
1399
+ * That loses nothing when the flow declares no outputs, or when each output
1400
+ * was set on the way: by a `default`, a `{ shared }` input that every root
1401
+ * feeds, or `{ updates }` on a step of that path. The warning names the outputs
1402
+ * that are not, and the steps the path falls off from.
1403
+ *
1404
+ * Not covered: the private End a `.stepToList` body gets (#742). `armTails`
1405
+ * skips those bodies, and that End is a different emission site with no
1406
+ * output mappings at all.
1407
+ */
1408
+ function checkFallOffEnd(built) {
1409
+ if (built.outputs.length === 0)
1410
+ return undefined;
1411
+ const from = built.triggerId ?? 'start';
1412
+ if (armTails(built.steps, [{ from }]).length === 0)
1413
+ return undefined;
1414
+ const preset = new Set(built.outputs.filter((o) => o.default !== undefined).map((o) => o.name));
1415
+ // A shared input sets its variable only on runs that enter through a root
1416
+ // feeding it, so it counts as preset only when every root does.
1417
+ const roots = sharedInputRoots(built);
1418
+ for (const t of planSharedInputs({ roots, outputs: built.outputs, vars: built.vars }).targets) {
1419
+ if (t.declaredAs === 'output' && roots.every((r) => t.feeds.some((f) => f.root === r.id)))
1420
+ preset.add(t.name);
1421
+ }
1422
+ const tails = writtenTails(built.steps, [{ from, written: preset }], []);
1423
+ const missing = built.outputs.map((o) => o.name).filter((n) => tails.some((t) => !t.written.has(n)));
1424
+ if (missing.length === 0)
1425
+ return undefined;
1426
+ const steps = [...new Set(tails.filter((t) => missing.some((n) => !t.written.has(n))).map((t) => t.from))];
1427
+ const one = missing.length === 1;
1428
+ return {
1429
+ level: 'warning',
1430
+ code: 'NO_RETURN',
1431
+ message: `Path ending at ${steps.map((x) => `"${x}"`).join(', ')} falls into the End compile appends. `
1432
+ + `${one ? 'Output' : 'Outputs'} ${missing.map((n) => `"${n}"`).join(', ')} may be unset there. `
1433
+ + `End the path with .return({ … }) or set ${one ? 'it' : 'them'} with { updates } (a loop body may run zero times).`,
1434
+ step: steps[0],
1435
+ suggestion: `.return({ ${missing.map((n) => `${n}: …`).join(', ')} })`,
1436
+ };
1437
+ }
1438
+ /**
1439
+ * `armTails`, step for step, carrying what each path has surely written. A
1440
+ * `.return()` value is NOT a write here: it lives on that `.return()`'s End,
1441
+ * which is not the End this is about. Where paths meet, only what all of them
1442
+ * wrote counts.
1443
+ *
1444
+ * A `.break()` leaves the enclosing loop with what its path wrote so far, so
1445
+ * its tail goes to `breaks`, and the loop counts it as one of the ways out. A
1446
+ * loop body may run zero times, so it writes nothing for sure. A do-while body
1447
+ * runs at least once, and is left by its end or by a `.break()`, so what all of
1448
+ * those wrote counts. Every `.parallel` arm runs, so what each surely writes
1449
+ * counts.
1450
+ */
1451
+ function writtenTails(steps, incoming, breaks) {
1452
+ let tails = incoming;
1453
+ for (const st of steps) {
1454
+ if (st.kind === 'stepToList')
1455
+ continue;
1456
+ if (st.kind === 'stepToRef') {
1457
+ if (st.port === 'output')
1458
+ tails = [];
1459
+ continue;
1460
+ }
1461
+ if (st.kind === 'break') {
1462
+ breaks.push(...tails);
1463
+ return [];
1464
+ }
1465
+ if (st.kind === 'return' || st.kind === 'terminate')
1466
+ return [];
1467
+ const before = surelyWritten(tails);
1468
+ const written = new Set([...before, ...Object.keys(st.options?.updates ?? {})]);
1469
+ const arm = (body) => writtenTails(body, [{ from: st.name, written }], breaks);
1470
+ if (st.kind === 'action') {
1471
+ tails = actionTails(st.name, st.spec, st.options?.version)
1472
+ .map((t) => ({ from: t.from, written }));
1473
+ }
1474
+ else if (st.kind === 'stepSwitch') {
1475
+ tails = st.cases.flatMap((c) => arm(c.body));
1476
+ }
1477
+ else if (st.kind === 'branch') {
1478
+ tails = [...arm(st.then), ...arm(st.otherwise)];
1479
+ }
1480
+ else if (st.kind === 'switch') {
1481
+ tails = [...st.cases.flatMap((c) => arm(c.body)), ...(st.default !== undefined ? arm(st.default) : [])];
1482
+ }
1483
+ else if (st.kind === 'doWhile') {
1484
+ const exits = [];
1485
+ exits.push(...writtenTails(st.body, [{ from: st.name, written }], exits));
1486
+ surelyWritten(exits).forEach((n) => written.add(n));
1487
+ tails = [{ from: st.name, written }];
1488
+ }
1489
+ else if (st.kind === 'parallel') {
1490
+ for (const body of st.arms)
1491
+ surelyWritten(arm(body)).forEach((n) => written.add(n));
1492
+ tails = [{ from: st.name, written }];
1493
+ }
1494
+ else {
1495
+ tails = [{ from: st.name, written }];
1496
+ }
1497
+ }
1498
+ return tails;
1499
+ }
1500
+ /** What every one of `tails` has written; nothing when there are none. */
1501
+ function surelyWritten(tails) {
1502
+ if (tails.length === 0)
1503
+ return new Set();
1504
+ return new Set([...tails[0].written].filter((n) => tails.every((t) => t.written.has(n))));
1382
1505
  }
1383
1506
  /**
1384
1507
  * Closest of `candidates` to `name`: same word ignoring case, one contained in
@@ -1982,10 +2105,11 @@ function renamedFieldMessage(label, step, oldField) {
1982
2105
  * - a `key` that is not a GUID. The commonest form of this is passing the
1983
2106
  * process's NAME where its release key belongs, which produces a node type
1984
2107
  * the platform cannot resolve to anything.
1985
- * - a `folderPath` that looks like a name, or a `name` that looks like a path —
1986
- * the two are swappable by shape and the resulting `resourceKey` is wrong in a
1987
- * way that only surfaces as a 400 from Orchestrator ("Folder does not exist").
1988
- * That is v1's own 2026-06-26 failure mode on this task.
2108
+ * - a `name` that looks like a path (contains "/"). The two are swappable, and
2109
+ * the resulting `resourceKey` is wrong in a way that only surfaces as a 400
2110
+ * from Orchestrator ("Folder does not exist"). That is v1's own 2026-06-26
2111
+ * failure mode on this task. A single-segment `folderPath` is NOT decidable:
2112
+ * root folders and personal workspaces are one segment, so it is not flagged.
1989
2113
  */
1990
2114
  function checkRpaWorkflow(step, inputs, diags) {
1991
2115
  const at = (code, message, suggestion) => {
@@ -2009,33 +2133,33 @@ function checkRpaWorkflow(step, inputs, diags) {
2009
2133
  at('RPA_NO_NAME', `RPA step "${step}" has no \`name\`. The runtime starts the job by name + folder, not by the key in `
2010
2134
  + `the node type, so without it the process cannot be resolved and the run fails before dispatch.`, `name: 'RPA Workflow'`);
2011
2135
  }
2012
- if (blank(inputs?.folderPath)) {
2136
+ const inSolution = inputs?.location === 'in-solution';
2137
+ if (!inSolution && blank(inputs?.folderPath)) {
2013
2138
  at('RPA_NO_FOLDER', `RPA step "${step}" has no \`folderPath\`. The runtime starts the job by name + folder, so the `
2014
2139
  + `Orchestrator folder holding the process is required.`, `folderPath: 'Shared/uipath-maestro-flow/ProjectEuler RPA'`);
2015
2140
  }
2141
+ if (inSolution && blank(inputs?.projectId)) {
2142
+ at('RPA_LOCAL_NO_PROJECT_ID', `In-solution RPA step "${step}" has no \`projectId\`. Read \`resource.projectKey\` from `
2143
+ + `\`resources/solution_folder/process/process/<project>.json\`.`, `projectId: '<resource.projectKey>'`);
2144
+ }
2145
+ else if (inSolution && !GUID_RE.test(String(inputs.projectId).trim())) {
2146
+ at('RPA_LOCAL_PROJECT_ID_NOT_GUID', `In-solution RPA step "${step}" has projectId ${JSON.stringify(inputs.projectId)}, which is not `
2147
+ + `the sibling project's GUID. Read \`resource.projectKey\` from the generated resource file.`, `projectId: '<resource.projectKey>'`);
2148
+ }
2149
+ if (inSolution && !blank(inputs?.folderPath)) {
2150
+ at('RPA_LOCAL_HAS_FOLDER', `In-solution RPA step "${step}" also sets \`folderPath\` ${JSON.stringify(inputs.folderPath)}. An `
2151
+ + `in-solution sibling has no folder until the solution is deployed — the platform's own manifest `
2152
+ + `writes \`""\` — so the value would be dropped. Remove it, or drop \`location: 'in-solution'\` to `
2153
+ + `call the published copy.`);
2154
+ }
2016
2155
  // The two are swappable by shape, and swapping them is invisible until
2017
2156
  // Orchestrator answers "Folder does not exist or the user does not have access".
2018
2157
  const name = typeof inputs?.name === 'string' ? inputs.name.trim() : '';
2019
- const folder = typeof inputs?.folderPath === 'string' ? inputs.folderPath.trim() : '';
2020
2158
  if (name.includes('/')) {
2021
2159
  at('RPA_NAME_IS_PATH', `RPA step "${step}"'s \`name\` is ${JSON.stringify(inputs.name)}, which is a folder path. \`name\` is `
2022
2160
  + `the process's own name in Orchestrator; the path it lives in goes in \`folderPath\`. Together they `
2023
2161
  + `form the resourceKey "<folderPath>.<name>", so swapping them points the job at nothing.`);
2024
2162
  }
2025
- if (folder && !folder.includes('/') && name && !name.includes('/')) {
2026
- // A single-segment folder is legal ("Shared"), so this is only worth saying
2027
- // when the folder ALSO fails to look like one of the tenant's roots.
2028
- if (!/^(Shared|Personal|My Workspace)$/i.test(folder)) {
2029
- diags.push({
2030
- level: 'warning',
2031
- code: 'RPA_FOLDER_NOT_PATH',
2032
- message: `RPA step "${step}"'s \`folderPath\` is ${JSON.stringify(inputs.folderPath)} — a single segment with no `
2033
- + `"/". Orchestrator folder paths are usually nested (e.g. "Shared/<team>/<solution>"). If this is `
2034
- + `the process's name rather than its folder, the two arguments are swapped.`,
2035
- step,
2036
- });
2037
- }
2038
- }
2039
2163
  }
2040
2164
  /**
2041
2165
  * Check an `apiWorkflow(...)` call.
@@ -2636,10 +2760,33 @@ function checkAgenticProcess(step, inputs, diags) {
2636
2760
  + `the key in the node type, so without it the process cannot be resolved and the run fails before `
2637
2761
  + `dispatch.`, `name: 'ProcurementProcess'`);
2638
2762
  }
2639
- if (blank(inputs?.folderPath)) {
2763
+ const inSolution = inputs?.location === 'in-solution';
2764
+ if (!inSolution && blank(inputs?.folderPath)) {
2640
2765
  at('AGENTIC_NO_FOLDER', `Agentic process step "${step}" has no \`folderPath\`. The runtime starts the job by name + folder, so `
2641
2766
  + `the Orchestrator folder holding the process is required.`, `folderPath: 'Shared/uipath-agents/ProcurementProcess'`);
2642
2767
  }
2768
+ const form = inputs?.form ?? 'bpmn';
2769
+ if (inSolution && form !== 'flow') {
2770
+ at('AGENTIC_LOCAL_FORM_UNVERIFIED', `In-solution agentic process step "${step}" has \`form: ${JSON.stringify(form)}\`. Only \`form: 'flow'\` `
2771
+ + `has been run against an in-solution sibling; the ${JSON.stringify(form)} shape is unproven. Call the `
2772
+ + `published process instead (drop \`location\` and \`projectId\`, set \`folderPath\`), or use `
2773
+ + `\`form: 'flow'\` if the sibling is a Flow.`, `location: 'published', folderPath: 'Shared/<folder>'`);
2774
+ }
2775
+ if (inSolution && blank(inputs?.projectId)) {
2776
+ const folder = (AGENTIC_PROCESS_FORMS[inputs?.form ?? 'bpmn'] ?? AGENTIC_PROCESS_FORMS.bpmn).solutionFolder;
2777
+ at('AGENTIC_LOCAL_NO_PROJECT_ID', `In-solution agentic process step "${step}" has no \`projectId\`. Read \`resource.projectKey\` from `
2778
+ + `\`resources/solution_folder/process/${folder}/<project>.json\`.`, `projectId: '<resource.projectKey>'`);
2779
+ }
2780
+ else if (inSolution && !GUID_RE.test(String(inputs.projectId).trim())) {
2781
+ at('AGENTIC_LOCAL_PROJECT_ID_NOT_GUID', `In-solution agentic process step "${step}" has projectId ${JSON.stringify(inputs.projectId)}, which is not `
2782
+ + `the sibling project's GUID. Read \`resource.projectKey\` from the generated resource file.`, `projectId: '<resource.projectKey>'`);
2783
+ }
2784
+ if (inSolution && !blank(inputs?.folderPath)) {
2785
+ at('AGENTIC_LOCAL_HAS_FOLDER', `In-solution agentic process step "${step}" also sets \`folderPath\` ${JSON.stringify(inputs.folderPath)}. An `
2786
+ + `in-solution sibling has no folder until the solution is deployed — the platform's own manifest `
2787
+ + `writes \`""\` — so the value would be dropped. Remove it, or drop \`location: 'in-solution'\` to `
2788
+ + `call the published copy.`);
2789
+ }
2643
2790
  const name = typeof inputs?.name === 'string' ? inputs.name.trim() : '';
2644
2791
  if (name.includes('/')) {
2645
2792
  at('AGENTIC_NAME_IS_PATH', `Agentic process step "${step}"'s \`name\` is ${JSON.stringify(inputs.name)}, which is a folder path. `
@@ -3642,7 +3789,7 @@ function checkQueueItem(step, inputs, diags) {
3642
3789
  + `Orchestrator rejects a queue item whose property values are not simple: `
3643
3790
  + `"Property values must be simple (like numbers or text) and not complex or composite `
3644
3791
  + `data types (like objects or arrays)". Flatten it, or JSON-encode it into one string `
3645
- + `field.`, `item: { ${k}: JSON.stringify(...) } // or ${k}Id, ${k}Name, …`);
3792
+ + `field.`, `item: { ${k}: JSON.stringify(...) } // or ${commentText(k)}Id, ${commentText(k)}Name, …`);
3646
3793
  }
3647
3794
  }
3648
3795
  }
@@ -3933,7 +4080,7 @@ function fanInDiag(join, arm, steps, tails) {
3933
4080
  + `edge, so the run never gets past "${join}" — with no incident, and \`flow validate\` answers Valid. Add a `
3934
4081
  + `step at the end of the arm${last?.name ? `, after "${last.name}"` : ''}, so the paths meet on that step and `
3935
4082
  + `only its one edge reaches the join, or move human task "${task}" out of the \`.parallel(…)\`.`,
3936
- suggestion: `….step('record', …) // last step of arm ${arm + 1}`,
4083
+ suggestion: `….step('record', …) // last step of arm ${commentText(arm + 1)}`,
3937
4084
  };
3938
4085
  }
3939
4086
  /**
@@ -5195,7 +5342,7 @@ function checkOnError(_handler, step, diags) {
5195
5342
  // A `.loop()` CONTAINER is fine: `core.logic.loop` declares the error handle at
5196
5343
  // both bundled versions, and a measured run routed a body failure to it — the
5197
5344
  // container's envelope carried the body's message in `detail` and the failing
5198
- // body step's id in `element`.
5345
+ // body step's label in `element`.
5199
5346
  if (step.kind === 'loop')
5200
5347
  return;
5201
5348
  // `.doWhile()` is not. `core.logic.dowhile@1.0` declares an error VARIABLE and
@@ -5204,12 +5351,38 @@ function checkOnError(_handler, step, diags) {
5204
5351
  // declare that handle". The builder refuses this too; this catches a
5205
5352
  // hand-built flow and says the same thing.
5206
5353
  if (step.kind === 'doWhile') {
5354
+ diags.push(doWhileOnErrorRefusal(step.name));
5355
+ return;
5356
+ }
5357
+ // A custom FlowAction or rawNode has no `kind`: ask its DEFINITION, with the
5358
+ // product's own rule (definitionHasErrorPort), not the kind list (#943).
5359
+ const maybeCustom = step.spec;
5360
+ if (!('kind' in maybeCustom) && maybeCustom instanceof FlowNode) {
5361
+ let def;
5362
+ try {
5363
+ const requestedVersion = step.options?.version;
5364
+ def = resolveContributionDefinition(maybeCustom.contribute(requestedVersion !== undefined ? { requestedVersion } : {}).definition, requestedVersion, `step "${step.name}"`);
5365
+ }
5366
+ catch {
5367
+ // checkContribution already reports a contribution compile cannot lower.
5368
+ return;
5369
+ }
5370
+ // A custom node backed by a typed family's bundled definition gets that
5371
+ // family's refusals: the rules are about the node type, not the SDK kind.
5372
+ if (def?.nodeType === 'core.logic.dowhile') {
5373
+ diags.push(doWhileOnErrorRefusal(step.name));
5374
+ return;
5375
+ }
5376
+ if (def?.nodeType === 'core.action.http') {
5377
+ diags.push(plainHttpOnErrorRefusal(step.name));
5378
+ return;
5379
+ }
5380
+ if (definitionHasErrorPort(def))
5381
+ return;
5207
5382
  diags.push({
5208
5383
  level: 'error',
5209
5384
  code: 'ONERROR_NO_ERROR_PORT',
5210
- message: `Step "${step.name}" is a .doWhile() container. It declares an error variable but no error HANDLE `
5211
- + `(core.logic.dowhile@1.0: input, success, start, continue, break), so the edge would leave a handle the `
5212
- + `node does not have. Handle the failure inside the body, on the step that can fail.`,
5385
+ message: errorPortRefusal(step.name, def),
5213
5386
  step: step.name,
5214
5387
  });
5215
5388
  return;
@@ -5240,19 +5413,33 @@ function checkOnError(_handler, step, diags) {
5240
5413
  // So this stays an error, and says the second part too: an author who reads
5241
5414
  // the old message ("use the managed node if statuses must route") could
5242
5415
  // reasonably conclude a transport-failure handler is fine as it is.
5243
- if (spec.kind === 'http' && !spec.inputs?.managed) {
5244
- diags.push({
5245
- level: 'error',
5246
- code: 'HTTP_ONERROR_V1',
5247
- message: `Step "${step.name}" handles errors on a plain http() request. A 4xx/5xx does NOT fail that node — it `
5248
- + `completes with out('${step.name}', 'statusCode') set — so the handler runs only on a transport failure, `
5249
- + `and when it does the node publishes no failure envelope at all (measured: "<step>.error" is the boolean `
5250
- + `true and "<step>.output" is null, in 1.0.0 and in the 1.3 flow migrate produces), so err() has nothing `
5251
- + `to read. The managed node is the one whose failures route AND carry the envelope.`,
5252
- step: step.name,
5253
- suggestion: `http({ url: …, managed: true })`,
5254
- });
5255
- }
5416
+ if (spec.kind === 'http' && !spec.inputs?.managed)
5417
+ diags.push(plainHttpOnErrorRefusal(step.name));
5418
+ }
5419
+ /** `.onError` after a `core.logic.dowhile` container: it declares no error handle. */
5420
+ function doWhileOnErrorRefusal(stepName) {
5421
+ return {
5422
+ level: 'error',
5423
+ code: 'ONERROR_NO_ERROR_PORT',
5424
+ message: `Step "${stepName}" is a .doWhile() container. It declares an error variable but no error HANDLE `
5425
+ + `(core.logic.dowhile@1.0: input, success, start, continue, break), so the edge would leave a handle the `
5426
+ + `node does not have. Handle the failure inside the body, on the step that can fail.`,
5427
+ step: stepName,
5428
+ };
5429
+ }
5430
+ /** `.onError` after a plain `core.action.http` node (see the two measured facts in {@link checkOnError}). */
5431
+ function plainHttpOnErrorRefusal(stepName) {
5432
+ return {
5433
+ level: 'error',
5434
+ code: 'HTTP_ONERROR_V1',
5435
+ message: `Step "${stepName}" handles errors on a plain http() request. A 4xx/5xx does NOT fail that node — it `
5436
+ + `completes with out('${stepName}', 'statusCode') set — so the handler runs only on a transport failure, `
5437
+ + `and when it does the node publishes no failure envelope at all (measured: "<step>.error" is the boolean `
5438
+ + `true and "<step>.output" is null, in 1.0.0 and in the 1.3 flow migrate produces), so err() has nothing `
5439
+ + `to read. The managed node is the one whose failures route AND carry the envelope.`,
5440
+ step: stepName,
5441
+ suggestion: `http({ url: …, managed: true })`,
5442
+ };
5256
5443
  }
5257
5444
  /**
5258
5445
  * Reading a value that only ONE arm produces (flow-builder-sdk#200).
@@ -5623,3 +5810,39 @@ function checkSchemaVersion(built, ancestors, diags) {
5623
5810
  });
5624
5811
  }
5625
5812
  }
5813
+ /**
5814
+ * Run a custom node's contribution lowering DRY, so `check` reports what
5815
+ * `compile` would throw. For instance, a `rawNode` carrying an in-solution
5816
+ * sibling's manifest with no `values` row for `<bindings.folderPath>` fails here
5817
+ * with the same code compile throws.
5818
+ *
5819
+ * It calls `contribute()`, resolves the definition the way serialize does, and
5820
+ * plans the `bindings[]` entries serialize would build
5821
+ * ({@link planContributionBindings}). A name the plan cannot close is
5822
+ * `CONTRIBUTION_BINDING_UNRESOLVED` — the same code and sentence serialize
5823
+ * throws. A contribution that throws is `CONTRIBUTION_INVALID`, unless
5824
+ * `ownError` says the node's own `diagnostics()` already reported an error (a
5825
+ * raw connector node throws for the reason `RAW_NODE_CONNECTOR` already gives).
5826
+ */
5827
+ function checkContribution(node, requestedVersion, step, ownError, diags) {
5828
+ let def;
5829
+ let supplied;
5830
+ try {
5831
+ const contribution = node.contribute(requestedVersion !== undefined ? { requestedVersion } : {});
5832
+ def = resolveContributionDefinition(contribution.definition, requestedVersion, `step "${step}"`);
5833
+ supplied = contribution.bindings;
5834
+ }
5835
+ catch (e) {
5836
+ if (ownError)
5837
+ return;
5838
+ diags.push({
5839
+ level: 'error', code: 'CONTRIBUTION_INVALID', step,
5840
+ message: `compile cannot lower step "${step}": ${e instanceof Error ? e.message : String(e)}`,
5841
+ });
5842
+ return;
5843
+ }
5844
+ const plan = planContributionBindings(def, supplied);
5845
+ if (plan.problems.length === 0)
5846
+ return;
5847
+ diags.push({ level: 'error', code: 'CONTRIBUTION_BINDING_UNRESOLVED', step, message: plan.problems.join(' ') });
5848
+ }
@@ -1,9 +1,23 @@
1
1
  /**
2
- * cli-library — compatibility re-export.
2
+ * cli-library — Flow's check options, plus the historical re-exports.
3
3
  *
4
4
  * How a CLI opens the connector library and its `connectors-local/` overlay, and
5
- * the event verdicts `check` shares with `compile`, all moved to
6
- * `core/cli-library.ts` when BPMN's `compile` and `check` needed the same things.
7
- * This shim preserves the historical `./cli-library.js` import path.
5
+ * the connector and event verdicts every family's `check` shares with its
6
+ * `compile`, live in `core/cli-library.ts`. This module keeps the historical
7
+ * `./cli-library.js` import path and adds the one thing that is Flow's own:
8
+ * {@link flowCheckOpts}, the options both `flow check` and `flow compile`'s check
9
+ * pass run on.
8
10
  */
11
+ import type { CheckOpts } from './check.js';
12
+ import type { BindingsView } from './core/binding-messages.js';
13
+ import type { Library } from './core/library.js';
9
14
  export { openCliLibrary, connectorCheckOpts, eventResolveErrorFor, byoaCheckOpts } from './core/cli-library.js';
15
+ /**
16
+ * Flow's check options from a library the CLI already opened: the shared
17
+ * connector and event hooks, Flow's own field validator (it renders Flow
18
+ * expressions, as `serialize` does), and the measured list envelopes.
19
+ *
20
+ * One builder for `flow check` and `flow compile`'s check pass, so the two
21
+ * cannot disagree.
22
+ */
23
+ export declare function flowCheckOpts(library: Library | undefined, bindings?: BindingsView): CheckOpts;
@@ -1,9 +1,27 @@
1
+ import { connectorCheckOpts } from './core/cli-library.js';
2
+ import { connectorInputErrors } from './serialize.js';
3
+ export { openCliLibrary, connectorCheckOpts, eventResolveErrorFor, byoaCheckOpts } from './core/cli-library.js';
1
4
  /**
2
- * cli-library — compatibility re-export.
5
+ * Flow's check options from a library the CLI already opened: the shared
6
+ * connector and event hooks, Flow's own field validator (it renders Flow
7
+ * expressions, as `serialize` does), and the measured list envelopes.
3
8
  *
4
- * How a CLI opens the connector library and its `connectors-local/` overlay, and
5
- * the event verdicts `check` shares with `compile`, all moved to
6
- * `core/cli-library.ts` when BPMN's `compile` and `check` needed the same things.
7
- * This shim preserves the historical `./cli-library.js` import path.
9
+ * One builder for `flow check` and `flow compile`'s check pass, so the two
10
+ * cannot disagree.
8
11
  */
9
- export { openCliLibrary, connectorCheckOpts, eventResolveErrorFor, byoaCheckOpts } from './core/cli-library.js';
12
+ export function flowCheckOpts(library, bindings) {
13
+ const shared = connectorCheckOpts(library, bindings, connectorInputErrors);
14
+ if (!library)
15
+ return shared;
16
+ return {
17
+ ...shared,
18
+ listEnvelope: (connector, operation, version, object) => {
19
+ try {
20
+ return library.resolve(connector, operation, version, object).listEnvelope;
21
+ }
22
+ catch {
23
+ return undefined; // an unresolvable operation is connectorResolveError's to report
24
+ }
25
+ },
26
+ };
27
+ }
@@ -34,3 +34,12 @@ export declare function reportDiagnostics(diags: ReadonlyArray<{
34
34
  message: string;
35
35
  suggestion?: string;
36
36
  }>): void;
37
+ /**
38
+ * The error diagnostic for a throw that ends a compile: the error's own string
39
+ * `code` when it carries one, else `COMPILE_FAILED`.
40
+ */
41
+ export declare function thrownDiagnostic(error: unknown): {
42
+ level: 'error';
43
+ code: string;
44
+ message: string;
45
+ };
@@ -47,3 +47,15 @@ export function reportDiagnostics(diags) {
47
47
  diagnostics: diags.map((d) => ({ level: d.level, code: d.code, message: d.message, ...(d.suggestion ? { suggestion: d.suggestion } : {}) })),
48
48
  });
49
49
  }
50
+ /**
51
+ * The error diagnostic for a throw that ends a compile: the error's own string
52
+ * `code` when it carries one, else `COMPILE_FAILED`.
53
+ */
54
+ export function thrownDiagnostic(error) {
55
+ const code = error?.code;
56
+ return {
57
+ level: 'error',
58
+ code: typeof code === 'string' && code.length > 0 ? code : 'COMPILE_FAILED',
59
+ message: error instanceof Error ? error.message : String(error),
60
+ };
61
+ }