@uipath/maestro-builder-sdk 6.17.0 → 6.17.2

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.
package/dist/check.js CHANGED
@@ -8,6 +8,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
10
  import { planContributionBindings } from './core/contribution-bindings.js';
11
+ import { definitionHasErrorPort, errorPortRefusal } from './core/error-port.js';
11
12
  import { resolveContributionDefinition } from './serialize.js';
12
13
  import { QUEUE_ITEM_RECORD_SCHEMA } from './core/queue-record.js';
13
14
  import { AGENTIC_PROCESS_FORMS } from './core/agentic-process-forms.js';
@@ -257,7 +258,7 @@ function checkFlow(built, ancestors, opts = {}) {
257
258
  stepNames.add(s.name);
258
259
  declared.add(s.name); // a step's output is referenceable by name
259
260
  }
260
- if (s.kind === 'stepToList' && s.port === 'error')
261
+ if ((s.kind === 'stepToList' || s.kind === 'stepToRef') && s.port === 'error')
261
262
  checkOnError(s, prevPortSource, diags);
262
263
  if ((s.kind === 'stepToList' || s.kind === 'stepToRef') && s.port.startsWith('branch-')) {
263
264
  const want = s.port.slice('branch-'.length);
@@ -776,10 +777,10 @@ function checkFlow(built, ancestors, opts = {}) {
776
777
  }
777
778
  // An exit with no arm compiles to an End node, so the run FINISHES
778
779
  // there instead of reaching a port with no edge and stalling. Better
779
- // than stalling, still probably not what the author meant — and the
780
- // End carries none of the flow's output mappings, so the run answers
781
- // with nothing (flow-builder-sdk#742). An `action: 'End'` outcome
782
- // already ends the run and needs no arm.
780
+ // than stalling, still probably not what the author meant: the End
781
+ // maps each output from its variable, so the run answers whatever
782
+ // those hold at that point, usually nothing (flow-builder-sdk#742).
783
+ // An `action: 'End'` outcome already ends the run and needs no arm.
783
784
  const unrouted = exits.filter((e) => !seenExit.has(e.port) && e.endsProcess !== true);
784
785
  if (unrouted.length > 0) {
785
786
  const outs = built.outputs.map((o) => o.name);
@@ -789,8 +790,9 @@ function checkFlow(built, ancestors, opts = {}) {
789
790
  + `${unrouted.map((e) => JSON.stringify(e.value)).join(', ')}. Each compiles to an End node, `
790
791
  + `so the run finishes there`
791
792
  + (outs.length > 0
792
- ? ` without writing the flow's declared output${outs.length === 1 ? '' : 's'} `
793
- + `(${outs.join(', ')}).`
793
+ ? `, and nothing on that path sets the flow's declared output${outs.length === 1 ? '' : 's'} `
794
+ + `(${outs.join(', ')}): the End answers whatever ${outs.length === 1 ? 'its variable holds' : 'their variables hold'}, `
795
+ + `usually nothing.`
794
796
  : '.'),
795
797
  suggestion: `{ value: '${unrouted[0].value}', body: (b) => … }`,
796
798
  });
@@ -1294,9 +1296,7 @@ function checkFlow(built, ancestors, opts = {}) {
1294
1296
  }
1295
1297
  }
1296
1298
  // The End compile appends when the root path runs out of steps (#941).
1297
- const fallOff = checkFallOffEnd(built);
1298
- if (fallOff)
1299
- diags.push(fallOff);
1299
+ diags.push(...checkFallOffEnd(built));
1300
1300
  // Check every child flow with the same rules, in its OWN scope — a child's
1301
1301
  // steps never see the parent's variables, so its undeclared references,
1302
1302
  // duplicate step names and type mismatches are its own. Diagnostics come back
@@ -1384,32 +1384,29 @@ function checkLoopListEnvelope(loop, connectors, opts, diags) {
1384
1384
  });
1385
1385
  }
1386
1386
  /**
1387
- * NO_RETURN: a path that runs off the end of the flow, where a declared output
1388
- * is never written (#941).
1387
+ * NO_RETURN: a path that runs off the end of the flow, or off the end of a
1388
+ * `.stepToList` / `.onError` body, where a declared output is never written
1389
+ * (#941, #742).
1389
1390
  *
1390
1391
  * It asks serialize's question, not "is there a `.return()` anywhere?".
1391
1392
  * serialize appends an End exactly when the root walk has tails left
1392
1393
  * (`if (finalTails.length)`), and `armTails` mirrors that walk tail for tail.
1393
1394
  * So a `.terminate()` path appends no End, and a branch where one arm returns
1394
- * and the other falls off DOES append one.
1395
+ * and the other falls off DOES append one. A `.stepToList` body is a side exit:
1396
+ * it never rejoins the main path, and when it runs out of steps `emitPortList`
1397
+ * gives it an End of its own. `writtenTails` collects those bodies' tails.
1395
1398
  *
1396
- * The appended End maps every output from its own variable
1399
+ * Every such End maps every output from its own variable
1397
1400
  * (`result <- $vars.result`), the same as a `.return()` that names no value.
1398
1401
  * That loses nothing when the flow declares no outputs, or when each output
1399
1402
  * was set on the way: by a `default`, a `{ shared }` input that every root
1400
- * feeds, or `{ updates }` on a step of that path. The warning names the outputs
1401
- * that are not, and the steps the path falls off from.
1402
- *
1403
- * Not covered: the private End a `.stepToList` body gets (#742). `armTails`
1404
- * skips those bodies, and that End is a different emission site with no
1405
- * output mappings at all.
1403
+ * feeds, or `{ updates }` on a step of that path. One warning per End: it
1404
+ * names the outputs that are not set, and the steps the path runs out from.
1406
1405
  */
1407
1406
  function checkFallOffEnd(built) {
1408
1407
  if (built.outputs.length === 0)
1409
- return undefined;
1408
+ return [];
1410
1409
  const from = built.triggerId ?? 'start';
1411
- if (armTails(built.steps, [{ from }]).length === 0)
1412
- return undefined;
1413
1410
  const preset = new Set(built.outputs.filter((o) => o.default !== undefined).map((o) => o.name));
1414
1411
  // A shared input sets its variable only on runs that enter through a root
1415
1412
  // feeding it, so it counts as preset only when every root does.
@@ -1418,21 +1415,42 @@ function checkFallOffEnd(built) {
1418
1415
  if (t.declaredAs === 'output' && roots.every((r) => t.feeds.some((f) => f.root === r.id)))
1419
1416
  preset.add(t.name);
1420
1417
  }
1421
- const tails = writtenTails(built.steps, [{ from, written: preset }], []);
1422
- const missing = built.outputs.map((o) => o.name).filter((n) => tails.some((t) => !t.written.has(n)));
1423
- if (missing.length === 0)
1424
- return undefined;
1425
- const steps = [...new Set(tails.filter((t) => missing.some((n) => !t.written.has(n))).map((t) => t.from))];
1426
- const one = missing.length === 1;
1427
- return {
1428
- level: 'warning',
1429
- code: 'NO_RETURN',
1430
- message: `Path ending at ${steps.map((x) => `"${x}"`).join(', ')} falls into the End compile appends. `
1431
- + `${one ? 'Output' : 'Outputs'} ${missing.map((n) => `"${n}"`).join(', ')} may be unset there. `
1432
- + `End the path with .return({ … }) or set ${one ? 'it' : 'them'} with { updates } (a loop body may run zero times).`,
1433
- step: steps[0],
1434
- suggestion: `.return({ ${missing.map((n) => `${n}: …`).join(', ')} })`,
1418
+ const sideEnds = [];
1419
+ const rootTails = writtenTails(built.steps, [{ from, written: preset }], { breaks: [], sideEnds });
1420
+ const diags = [];
1421
+ const unset = (tails) => {
1422
+ const missing = built.outputs.map((o) => o.name).filter((n) => tails.some((t) => !t.written.has(n)));
1423
+ if (missing.length === 0)
1424
+ return undefined;
1425
+ return { missing, steps: [...new Set(tails.filter((t) => missing.some((n) => !t.written.has(n))).map((t) => t.from))] };
1435
1426
  };
1427
+ // `lead(at)` is the sentence saying where the path ends and which End it falls into.
1428
+ const warn = (lead, u, fix) => {
1429
+ const one = u.missing.length === 1;
1430
+ return {
1431
+ level: 'warning',
1432
+ code: 'NO_RETURN',
1433
+ message: `${lead(u.steps.map((x) => `"${x}"`).join(', '))} ${one ? 'Output' : 'Outputs'} `
1434
+ + `${u.missing.map((n) => `"${n}"`).join(', ')} may be unset there. ${fix(one ? 'it' : 'them')}`,
1435
+ step: u.steps[0],
1436
+ suggestion: `.return({ ${u.missing.map((n) => `${n}: …`).join(', ')} })`,
1437
+ };
1438
+ };
1439
+ // `armTails` and `writtenTails` hand on the same tails; the root End exists
1440
+ // exactly when there are any.
1441
+ const root = rootTails.length ? unset(rootTails) : undefined;
1442
+ if (root) {
1443
+ diags.push(warn((at) => `Path ending at ${at} falls into the End compile appends.`, root, (it) => `End the path with .return({ … }) or set ${it} with { updates } (a loop body may run zero times).`));
1444
+ }
1445
+ for (const side of sideEnds) {
1446
+ const u = unset(side.tails);
1447
+ if (!u)
1448
+ continue;
1449
+ const call = side.port === 'error' ? '.onError(…)' : `.stepToList('${side.port}', …)`;
1450
+ diags.push(warn((at) => `The ${call} path from "${side.source}" ends at ${at} and does not rejoin the main path, `
1451
+ + `so compile ends the run there on an End of its own.`, u, (it) => `End the path with .return({ … }), set ${it} with { updates }, or hand it back with .stepToRef('<step>').`));
1452
+ }
1453
+ return diags;
1436
1454
  }
1437
1455
  /**
1438
1456
  * `armTails`, step for step, carrying what each path has surely written. A
@@ -1446,12 +1464,31 @@ function checkFallOffEnd(built) {
1446
1464
  * runs at least once, and is left by its end or by a `.break()`, so what all of
1447
1465
  * those wrote counts. Every `.parallel` arm runs, so what each surely writes
1448
1466
  * counts.
1467
+ *
1468
+ * A `.stepToList` body leaves the main path as it was (as in `armTails`), but
1469
+ * it is walked too: it starts with what was written before its port fired, and
1470
+ * if it hands back tails, serialize ends it on an End of its own, so those
1471
+ * tails go to `sideEnds`. On the `error` port the step failed, so its own
1472
+ * `{ updates }` are not counted (they usually read the output it never made).
1473
+ * Bodies nested anywhere, a plain loop's included, are walked with the same
1474
+ * `sideEnds`.
1449
1475
  */
1450
- function writtenTails(steps, incoming, breaks) {
1476
+ function writtenTails(steps, incoming, sinks) {
1477
+ const { breaks, sideEnds } = sinks;
1451
1478
  let tails = incoming;
1479
+ // What was written before the last step on this path, i.e. without its own `{ updates }`.
1480
+ let beforeLast = surelyWritten(tails);
1452
1481
  for (const st of steps) {
1453
- if (st.kind === 'stepToList')
1482
+ if (st.kind === 'stepToList') {
1483
+ const source = tails[0]?.from;
1484
+ if (source === undefined)
1485
+ continue;
1486
+ const entry = st.port === 'error' ? beforeLast : surelyWritten(tails);
1487
+ const ends = writtenTails(st.body, [{ from: source, written: entry }], sinks);
1488
+ if (ends.length)
1489
+ sideEnds.push({ port: st.port, source, tails: ends });
1454
1490
  continue;
1491
+ }
1455
1492
  if (st.kind === 'stepToRef') {
1456
1493
  if (st.port === 'output')
1457
1494
  tails = [];
@@ -1464,8 +1501,9 @@ function writtenTails(steps, incoming, breaks) {
1464
1501
  if (st.kind === 'return' || st.kind === 'terminate')
1465
1502
  return [];
1466
1503
  const before = surelyWritten(tails);
1504
+ beforeLast = before;
1467
1505
  const written = new Set([...before, ...Object.keys(st.options?.updates ?? {})]);
1468
- const arm = (body) => writtenTails(body, [{ from: st.name, written }], breaks);
1506
+ const arm = (body) => writtenTails(body, [{ from: st.name, written }], sinks);
1469
1507
  if (st.kind === 'action') {
1470
1508
  tails = actionTails(st.name, st.spec, st.options?.version)
1471
1509
  .map((t) => ({ from: t.from, written }));
@@ -1481,7 +1519,7 @@ function writtenTails(steps, incoming, breaks) {
1481
1519
  }
1482
1520
  else if (st.kind === 'doWhile') {
1483
1521
  const exits = [];
1484
- exits.push(...writtenTails(st.body, [{ from: st.name, written }], exits));
1522
+ exits.push(...writtenTails(st.body, [{ from: st.name, written }], { breaks: exits, sideEnds }));
1485
1523
  surelyWritten(exits).forEach((n) => written.add(n));
1486
1524
  tails = [{ from: st.name, written }];
1487
1525
  }
@@ -1491,6 +1529,9 @@ function writtenTails(steps, incoming, breaks) {
1491
1529
  tails = [{ from: st.name, written }];
1492
1530
  }
1493
1531
  else {
1532
+ // A plain loop's body writes nothing for sure, but its side exits still end runs.
1533
+ if (st.kind === 'loop')
1534
+ writtenTails(st.body, [{ from: st.name, written }], { breaks: [], sideEnds });
1494
1535
  tails = [{ from: st.name, written }];
1495
1536
  }
1496
1537
  }
@@ -5341,7 +5382,7 @@ function checkOnError(_handler, step, diags) {
5341
5382
  // A `.loop()` CONTAINER is fine: `core.logic.loop` declares the error handle at
5342
5383
  // both bundled versions, and a measured run routed a body failure to it — the
5343
5384
  // container's envelope carried the body's message in `detail` and the failing
5344
- // body step's id in `element`.
5385
+ // body step's label in `element`.
5345
5386
  if (step.kind === 'loop')
5346
5387
  return;
5347
5388
  // `.doWhile()` is not. `core.logic.dowhile@1.0` declares an error VARIABLE and
@@ -5350,12 +5391,38 @@ function checkOnError(_handler, step, diags) {
5350
5391
  // declare that handle". The builder refuses this too; this catches a
5351
5392
  // hand-built flow and says the same thing.
5352
5393
  if (step.kind === 'doWhile') {
5394
+ diags.push(doWhileOnErrorRefusal(step.name));
5395
+ return;
5396
+ }
5397
+ // A custom FlowAction or rawNode has no `kind`: ask its DEFINITION, with the
5398
+ // product's own rule (definitionHasErrorPort), not the kind list (#943).
5399
+ const maybeCustom = step.spec;
5400
+ if (!('kind' in maybeCustom) && maybeCustom instanceof FlowNode) {
5401
+ let def;
5402
+ try {
5403
+ const requestedVersion = step.options?.version;
5404
+ def = resolveContributionDefinition(maybeCustom.contribute(requestedVersion !== undefined ? { requestedVersion } : {}).definition, requestedVersion, `step "${step.name}"`);
5405
+ }
5406
+ catch {
5407
+ // checkContribution already reports a contribution compile cannot lower.
5408
+ return;
5409
+ }
5410
+ // A custom node backed by a typed family's bundled definition gets that
5411
+ // family's refusals: the rules are about the node type, not the SDK kind.
5412
+ if (def?.nodeType === 'core.logic.dowhile') {
5413
+ diags.push(doWhileOnErrorRefusal(step.name));
5414
+ return;
5415
+ }
5416
+ if (def?.nodeType === 'core.action.http') {
5417
+ diags.push(plainHttpOnErrorRefusal(step.name));
5418
+ return;
5419
+ }
5420
+ if (definitionHasErrorPort(def))
5421
+ return;
5353
5422
  diags.push({
5354
5423
  level: 'error',
5355
5424
  code: 'ONERROR_NO_ERROR_PORT',
5356
- message: `Step "${step.name}" is a .doWhile() container. It declares an error variable but no error HANDLE `
5357
- + `(core.logic.dowhile@1.0: input, success, start, continue, break), so the edge would leave a handle the `
5358
- + `node does not have. Handle the failure inside the body, on the step that can fail.`,
5425
+ message: errorPortRefusal(step.name, def),
5359
5426
  step: step.name,
5360
5427
  });
5361
5428
  return;
@@ -5386,19 +5453,33 @@ function checkOnError(_handler, step, diags) {
5386
5453
  // So this stays an error, and says the second part too: an author who reads
5387
5454
  // the old message ("use the managed node if statuses must route") could
5388
5455
  // reasonably conclude a transport-failure handler is fine as it is.
5389
- if (spec.kind === 'http' && !spec.inputs?.managed) {
5390
- diags.push({
5391
- level: 'error',
5392
- code: 'HTTP_ONERROR_V1',
5393
- message: `Step "${step.name}" handles errors on a plain http() request. A 4xx/5xx does NOT fail that node — it `
5394
- + `completes with out('${step.name}', 'statusCode') set — so the handler runs only on a transport failure, `
5395
- + `and when it does the node publishes no failure envelope at all (measured: "<step>.error" is the boolean `
5396
- + `true and "<step>.output" is null, in 1.0.0 and in the 1.3 flow migrate produces), so err() has nothing `
5397
- + `to read. The managed node is the one whose failures route AND carry the envelope.`,
5398
- step: step.name,
5399
- suggestion: `http({ url: …, managed: true })`,
5400
- });
5401
- }
5456
+ if (spec.kind === 'http' && !spec.inputs?.managed)
5457
+ diags.push(plainHttpOnErrorRefusal(step.name));
5458
+ }
5459
+ /** `.onError` after a `core.logic.dowhile` container: it declares no error handle. */
5460
+ function doWhileOnErrorRefusal(stepName) {
5461
+ return {
5462
+ level: 'error',
5463
+ code: 'ONERROR_NO_ERROR_PORT',
5464
+ message: `Step "${stepName}" is a .doWhile() container. It declares an error variable but no error HANDLE `
5465
+ + `(core.logic.dowhile@1.0: input, success, start, continue, break), so the edge would leave a handle the `
5466
+ + `node does not have. Handle the failure inside the body, on the step that can fail.`,
5467
+ step: stepName,
5468
+ };
5469
+ }
5470
+ /** `.onError` after a plain `core.action.http` node (see the two measured facts in {@link checkOnError}). */
5471
+ function plainHttpOnErrorRefusal(stepName) {
5472
+ return {
5473
+ level: 'error',
5474
+ code: 'HTTP_ONERROR_V1',
5475
+ message: `Step "${stepName}" handles errors on a plain http() request. A 4xx/5xx does NOT fail that node — it `
5476
+ + `completes with out('${stepName}', 'statusCode') set — so the handler runs only on a transport failure, `
5477
+ + `and when it does the node publishes no failure envelope at all (measured: "<step>.error" is the boolean `
5478
+ + `true and "<step>.output" is null, in 1.0.0 and in the 1.3 flow migrate produces), so err() has nothing `
5479
+ + `to read. The managed node is the one whose failures route AND carry the envelope.`,
5480
+ step: stepName,
5481
+ suggestion: `http({ url: …, managed: true })`,
5482
+ };
5402
5483
  }
5403
5484
  /**
5404
5485
  * Reading a value that only ONE arm produces (flow-builder-sdk#200).
@@ -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
+ }
@@ -22,9 +22,9 @@ import { writeJsonFile } from './core/json-file.js';
22
22
  import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync } from 'node:fs';
23
23
  import { basename, dirname, join, resolve } from 'node:path';
24
24
  import { resolveFlowFile, loadBuiltFlow } from './load.js';
25
- import { serializeWithSidecars, connectorInputErrors, compileOwnsResource } from './serialize.js';
25
+ import { serializeWithSidecars, compileOwnsResource } from './serialize.js';
26
26
  import { check } from './check.js';
27
- import { byoaCheckOpts, eventResolveErrorFor, openCliLibrary } from './cli-library.js';
27
+ import { flowCheckOpts, openCliLibrary } from './cli-library.js';
28
28
  import { Bindings } from './bindings.js';
29
29
  import { flagValue, runWhenInvokedDirectly } from './cli-run.js';
30
30
  import { connectionResources, inlineAgentResources, mergeBindingsV2, mergeInlineAgentResources, readBindingsV2, readInlineAgentFiles, } from './bindings-v2.js';
@@ -70,37 +70,8 @@ export async function run(argv) {
70
70
  // One list per run; every exit path reports it (reportResult overwrites keys).
71
71
  const diagnostics = [];
72
72
  if (!noCheck) {
73
- const diags = check(built, library
74
- ? {
75
- // The refusal serialize() would throw for an event it cannot resolve,
76
- // printed as `✗ [EVENT_UNKNOWN]` before stopping instead of hiding
77
- // behind "1 error(s); not emitting".
78
- eventResolveError: eventResolveErrorFor(library),
79
- ...byoaCheckOpts(library),
80
- eventParams: (connector, event) => {
81
- try {
82
- return library.eventParameterFields(connector, event);
83
- }
84
- catch {
85
- return undefined; // unresolvable: serialize() reports it properly
86
- }
87
- },
88
- lookups: (connector, action, version, object) => library.lookupSpecs(connector, action, version, object),
89
- resolutions: library.lookupResolutions,
90
- // Same validator serialize() runs below, so the check pass names the
91
- // refusal (and stops) instead of printing `0 error(s)` first.
92
- connectorInputs: (spec, inputs) => {
93
- try {
94
- const rc = library.resolve(spec.key, spec.action, spec.version, spec.object);
95
- return connectorInputErrors(rc, inputs, library.lookupResolutions ?? {}, library.lookupSpecs(spec.key, spec.action, spec.version, spec.object) ?? {});
96
- }
97
- catch {
98
- return undefined;
99
- }
100
- },
101
- bindings,
102
- }
103
- : { bindings });
73
+ // Same options as `flow check`, so the two cannot disagree.
74
+ const diags = check(built, flowCheckOpts(library, bindings));
104
75
  const errors = diags.filter((d) => d.level === 'error');
105
76
  for (const d of diags)
106
77
  console.error(` ${d.level === 'error' ? '✗' : '⚠'} [${d.code}] ${d.message}${d.suggestion ? ` (did you mean "${d.suggestion}"?)` : ''}`);
@@ -58,7 +58,7 @@ interface HttpInputsBase {
58
58
  * an error port only makes sense here (`check` says so: HTTP_ONERROR_V1).
59
59
  * The handler can answer the flow (`.return`), stop the run
60
60
  * (`.terminate`), or compensate and continue at a step the success path
61
- * also reaches (`h.rejoin('<step>')` — see `references/error-handling.md`);
61
+ * also reaches (`h.stepToRef('<step>')` — see `references/error-handling.md`);
62
62
  * - both nodes expose JSON responses as parsed values in the live product;
63
63
  * use `returns` to declare fields before reading them. The node choice does
64
64
  * not decide whether application/json is parsed.
@@ -1,6 +1,7 @@
1
- import { Library } from './library.js';
1
+ import { Library, type ResolvedConnector } from './library.js';
2
2
  import type { BindingsView } from './binding-messages.js';
3
3
  import { type ConnectorCheckOpts } from './connector-checks.js';
4
+ import type { LookupResolutions, LookupSpec } from './lookups.js';
4
5
  import type { EventCheckOpts } from './event-checks.js';
5
6
  /**
6
7
  * The library for this invocation, or `undefined` when there is none.
@@ -15,17 +16,25 @@ export declare function openCliLibrary(argv: readonly string[]): Library | undef
15
16
  * The connector half of a family's check options, from a library the CLI already
16
17
  * opened.
17
18
  *
18
- * Every callback fails OPEN: an entry the library cannot resolve, or no library
19
- * at all, means "unknown", and an unknown fact fires no rule. That is what lets
20
- * `check` stay the fast inner-loop rung — a process with no connectors needs no
21
- * library, and one whose connector is absent from the library is `compile`'s
22
- * business to name, not this pass's to guess at.
19
+ * Every fact callback fails OPEN: no library at all, or an entry the library
20
+ * cannot resolve, means "unknown", and an unknown fact fires no rule. That is
21
+ * what lets `check` stay the fast inner-loop rung — a process with no
22
+ * connectors needs no library. The one exception is the resolution itself:
23
+ * `connectorResolveError` reports the refusal `compile` would throw for an
24
+ * action, the way `eventResolveError` does for an event, so `check` never
25
+ * passes a connector step `compile` refuses (#948).
26
+ *
27
+ * `inputErrors` is the family's field-contract validator. The default speaks
28
+ * the `=`-expression dialect BPMN uses; Flow passes its own, which renders its
29
+ * expressions (see `flowCheckOpts` in `../cli-library.ts`).
23
30
  *
24
31
  * The `resolutions` are the overlay's: without them every lookup already
25
32
  * recorded by `registry prepare --resolve` would be reported as still owed,
26
33
  * telling the author to re-run a prepare they already ran.
27
34
  */
28
- export declare function connectorCheckOpts(library: Library | undefined, bindings?: BindingsView): ConnectorCheckOpts & EventCheckOpts;
35
+ export declare function connectorCheckOpts(library: Library | undefined, bindings?: BindingsView, inputErrors?: ConnectorInputValidator): ConnectorCheckOpts & EventCheckOpts;
36
+ /** A family's field-contract validator: `compile`'s refusal for these inputs, or `undefined`. */
37
+ export type ConnectorInputValidator = (rc: ResolvedConnector, inputs: Record<string, unknown>, resolutions: LookupResolutions, lookups: Readonly<Record<string, LookupSpec>>) => string | undefined;
29
38
  /**
30
39
  * `check`'s EVENT_UNKNOWN verdict: the message `library.resolveEvent` throws
31
40
  * for a subscription, or `undefined` when it resolves.
@@ -16,7 +16,7 @@
16
16
  */
17
17
  import { existsSync } from 'node:fs';
18
18
  import { join } from 'node:path';
19
- import { Library } from './library.js';
19
+ import { ConnectorResolveError, Library } from './library.js';
20
20
  import { resolveLibraryDir } from './library-cache.js';
21
21
  import { flagValue } from '../cli-run.js';
22
22
  import { defaultOverlayDir } from '../workdir.js';
@@ -41,19 +41,28 @@ export function openCliLibrary(argv) {
41
41
  * The connector half of a family's check options, from a library the CLI already
42
42
  * opened.
43
43
  *
44
- * Every callback fails OPEN: an entry the library cannot resolve, or no library
45
- * at all, means "unknown", and an unknown fact fires no rule. That is what lets
46
- * `check` stay the fast inner-loop rung — a process with no connectors needs no
47
- * library, and one whose connector is absent from the library is `compile`'s
48
- * business to name, not this pass's to guess at.
44
+ * Every fact callback fails OPEN: no library at all, or an entry the library
45
+ * cannot resolve, means "unknown", and an unknown fact fires no rule. That is
46
+ * what lets `check` stay the fast inner-loop rung — a process with no
47
+ * connectors needs no library. The one exception is the resolution itself:
48
+ * `connectorResolveError` reports the refusal `compile` would throw for an
49
+ * action, the way `eventResolveError` does for an event, so `check` never
50
+ * passes a connector step `compile` refuses (#948).
51
+ *
52
+ * `inputErrors` is the family's field-contract validator. The default speaks
53
+ * the `=`-expression dialect BPMN uses; Flow passes its own, which renders its
54
+ * expressions (see `flowCheckOpts` in `../cli-library.ts`).
49
55
  *
50
56
  * The `resolutions` are the overlay's: without them every lookup already
51
57
  * recorded by `registry prepare --resolve` would be reported as still owed,
52
58
  * telling the author to re-run a prepare they already ran.
53
59
  */
54
- export function connectorCheckOpts(library, bindings) {
60
+ export function connectorCheckOpts(library, bindings, inputErrors = (rc, inputs, resolutions, lookups) => connectorInputErrors(rc, inputs, resolutions, lookups, { dialect: EQUALS_EXPR_DIALECT })) {
55
61
  if (!library)
56
62
  return bindings ? { bindings } : {};
63
+ // One memoized resolve per connector step, shared by `connectorResolveError`
64
+ // and `connectorInputs`, so the second hook does not resolve again.
65
+ const resolve = connectorResolver(library);
57
66
  return {
58
67
  eventResolveError: eventResolveErrorFor(library),
59
68
  ...byoaCheckOpts(library),
@@ -85,26 +94,51 @@ export function connectorCheckOpts(library, bindings) {
85
94
  return undefined;
86
95
  }
87
96
  },
97
+ // The refusal compile throws for an operation it cannot resolve, with its
98
+ // code and text: CONNECTOR_NOT_IN_LIBRARY, CONNECTOR_OBJECT_REQUIRED,
99
+ // CONNECTOR_OBJECT_NOT_GENERIC.
100
+ connectorResolveError: (spec) => {
101
+ const r = resolve(spec);
102
+ return 'error' in r ? r.error : undefined;
103
+ },
88
104
  // The field contract compile enforces, from the same validator, so the inner
89
105
  // loop and the emit rung cannot disagree about a missing required field, an
90
- // unknown key or a mistyped value. An operation the library cannot resolve is
91
- // compile's business to name — here it just means "unknown".
92
- //
93
- // The `=`-expression dialect, because this bag serves the families that spell
94
- // an expression that way. Flow binds its own `connectorInputErrors` in
95
- // `serialize.ts`, which carries `renderDeep` with it, and overrides this.
106
+ // unknown key or a mistyped value. An unresolvable operation is
107
+ // `connectorResolveError`'s to report; here it just means "unknown".
96
108
  connectorInputs: (spec, inputs) => {
97
- try {
98
- const rc = library.resolve(spec.key, spec.action, spec.version, spec.object);
99
- return connectorInputErrors(rc, inputs, library.lookupResolutions ?? {}, library.lookupSpecs(spec.key, spec.action, spec.version, spec.object) ?? {}, { dialect: EQUALS_EXPR_DIALECT });
100
- }
101
- catch {
109
+ const r = resolve(spec);
110
+ if (!('rc' in r))
102
111
  return undefined;
103
- }
112
+ return inputErrors(r.rc, inputs, library.lookupResolutions ?? {}, library.lookupSpecs(spec.key, spec.action, spec.version, spec.object) ?? {});
104
113
  },
105
114
  ...(bindings ? { bindings } : {}),
106
115
  };
107
116
  }
117
+ /**
118
+ * `library.resolve` for one connector step, memoized per step for the life of
119
+ * one check's options. A {@link ConnectorResolveError} is the refusal compile
120
+ * throws and becomes `error`; anything else thrown is not a verdict about the
121
+ * author's source and stays "unknown", as before.
122
+ */
123
+ function connectorResolver(library) {
124
+ const memo = new Map();
125
+ return (spec) => {
126
+ const id = JSON.stringify([spec.key, spec.action, spec.version ?? null, spec.object ?? null]);
127
+ let r = memo.get(id);
128
+ if (!r) {
129
+ try {
130
+ r = { rc: library.resolve(spec.key, spec.action, spec.version, spec.object) };
131
+ }
132
+ catch (error) {
133
+ r = error instanceof ConnectorResolveError
134
+ ? { error: { code: error.code, message: error.message } }
135
+ : { unknown: true };
136
+ }
137
+ memo.set(id, r);
138
+ }
139
+ return r;
140
+ };
141
+ }
108
142
  /**
109
143
  * `check`'s EVENT_UNKNOWN verdict: the message `library.resolveEvent` throws
110
144
  * for a subscription, or `undefined` when it resolves.
@@ -113,6 +113,21 @@ export interface ConnectorCheckOpts {
113
113
  resolutions?: LookupResolutions;
114
114
  /** Whether a connector step's schema is materialized locally. */
115
115
  schemaReadiness?: (connector: string, action: string, version?: string, object?: string) => SchemaReadiness | undefined;
116
+ /**
117
+ * Why `compile` could not resolve this operation at all (`library.resolve`
118
+ * throws), or `undefined` when it resolves. Checked before
119
+ * {@link ConnectorCheckOpts.connectorInputs}, which answers `undefined` for an
120
+ * unresolvable operation and so cannot tell "not in the library" from "no facts".
121
+ */
122
+ connectorResolveError?: (spec: {
123
+ key: string;
124
+ action: string;
125
+ version?: string;
126
+ object?: string;
127
+ }) => {
128
+ code: string;
129
+ message: string;
130
+ } | undefined;
116
131
  /** What `compile` would refuse these inputs with, or `undefined` when it would accept them. */
117
132
  connectorInputs?: (spec: {
118
133
  key: string;
@@ -157,9 +172,10 @@ export declare function checkConnectorLookups(where: string, spec: ConnectorSpec
157
172
  */
158
173
  export declare function checkConnectorSchema(where: string, spec: ConnectorSpec, opts: ConnectorCheckOpts, dialect: ExprDialect, report: ReportConnector): void;
159
174
  /**
160
- * The field contract and the binding names `compile` enforces, reported here first.
175
+ * The operation, its field contract and the binding names `compile` enforces,
176
+ * reported here first.
161
177
  *
162
- * Both rules run only with the facts in hand (a library for the fields, a
178
+ * Each rule runs only with the facts in hand (a library for the operation and its fields, a
163
179
  * bindings.json for the names); without them `check` stays a pure function of the
164
180
  * built model and says nothing, as before.
165
181
  */