@uipath/maestro-builder-sdk 6.59.1 → 6.61.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.
- package/README.md +10 -12
- package/dist/api-index.md +5 -4
- package/dist/check-cli.js +8 -7
- package/dist/check.d.ts +11 -0
- package/dist/check.js +192 -783
- package/dist/cli-library.d.ts +2 -1
- package/dist/cli-library.js +7 -2
- package/dist/cli-result.d.ts +14 -0
- package/dist/cli-result.js +18 -1
- package/dist/compile-cli.js +2 -2
- package/dist/core/compile-refusal.d.ts +7 -1
- package/dist/core/compile-refusal.js +9 -1
- package/dist/core/connector-checks.d.ts +8 -30
- package/dist/core/connector-checks.js +11 -60
- package/dist/core/connector-inputs.js +9 -1
- package/dist/core/event-checks.d.ts +15 -2
- package/dist/core/event-checks.js +28 -13
- package/dist/core/hitl-routing.d.ts +10 -1
- package/dist/core/hitl-routing.js +20 -5
- package/dist/core/lookups.d.ts +13 -0
- package/dist/core/lookups.js +26 -0
- package/dist/core/refusals.d.ts +2 -0
- package/dist/core/refusals.js +5 -1
- package/dist/core/script-function.d.ts +12 -0
- package/dist/core/script-function.js +119 -1
- package/dist/flow-node-type.d.ts +35 -21
- package/dist/flow-parallel-hitl.d.ts +105 -0
- package/dist/flow-parallel-hitl.js +214 -0
- package/dist/flow-sdk.d.ts +4 -4
- package/dist/flow-sdk.js +2 -2
- package/dist/flow-terminate-names.d.ts +2 -2
- package/dist/flow-terminate-names.js +2 -2
- package/dist/merge-compile.js +2 -1
- package/dist/nodes/agent/flow/check.js +13 -88
- package/dist/nodes/agent/flow/inline-agent.js +41 -14
- package/dist/nodes/connector/flow/node-type.js +45 -18
- package/dist/nodes/document/flow/ixp.d.ts +1 -1
- package/dist/nodes/document/flow/ixp.js +22 -26
- package/dist/nodes/hitl/flow/check.js +4 -11
- package/dist/nodes/hitl/flow/node-type.js +16 -7
- package/dist/nodes/http/flow/check.js +5 -31
- package/dist/nodes/http/flow/node-type.js +7 -4
- package/dist/nodes/queue/flow/check.d.ts +4 -3
- package/dist/nodes/queue/flow/check.js +7 -6
- package/dist/nodes/queue/flow/node-type.js +1 -1
- package/dist/nodes/script/flow/runtime.d.ts +17 -5
- package/dist/nodes/script/flow/runtime.js +40 -5
- package/dist/nodes/transform/flow/check.d.ts +2 -0
- package/dist/nodes/transform/flow/check.js +3 -6
- package/dist/nodes/transform/flow/node-type.js +6 -6
- package/dist/raw-node.js +8 -8
- package/dist/script-globals.d.ts +35 -0
- package/dist/script-globals.js +1 -0
- package/dist/serialize.d.ts +38 -3
- package/dist/serialize.js +757 -322
- package/package.json +6 -2
package/dist/check.js
CHANGED
|
@@ -2,61 +2,39 @@
|
|
|
2
2
|
* check — static validation of a BuiltFlow before serialization.
|
|
3
3
|
*
|
|
4
4
|
* Produces structured, actionable diagnostics (the kind an agent can act on):
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* references to undeclared variables/steps, empty branch arms, missing
|
|
6
|
+
* terminals, and every refusal `compile` would raise (duplicate step names, an
|
|
7
|
+
* input a connector does not declare, …), which it gets by running the
|
|
8
|
+
* serializer in collect mode (`serializerRefusals`), so each of those rules
|
|
9
|
+
* lives in the serializer alone.
|
|
7
10
|
*/
|
|
8
11
|
import { Expr, SCHEDULE_PRESETS, parallelFork } from './flow-sdk.js';
|
|
9
12
|
import { FlowNode } from './core/node-classes.js';
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import { bundledActionNodeType, bundledContainerDefinition, bundledDefinition, resolveContributionDefinition } from './serialize.js';
|
|
13
|
+
import { definitionHasErrorPort } from './core/error-port.js';
|
|
14
|
+
import { bundledDefinition, resolveContributionDefinition, serialize } from './serialize.js';
|
|
13
15
|
import { checkEventResolution, checkEventSubscription, eventParamsFor, } from './core/event-checks.js';
|
|
14
16
|
import { checkCookedEscapes } from './flow-expr-check.js';
|
|
15
|
-
import { FLOW_EXPR_DIALECT, checkBindingLabels,
|
|
16
|
-
import { commentText } from './core/comment-text.js';
|
|
17
|
+
import { FLOW_EXPR_DIALECT, checkBindingLabels, checkConnectorLookups as coreCheckConnectorLookups, checkConnectorSchema as coreCheckConnectorSchema, } from './core/connector-checks.js';
|
|
17
18
|
import { nearestName } from './core/edit-distance.js';
|
|
18
19
|
import { hitlDerivesOutcomeHandles, hitlExitModel, hitlRoutesPerOutcome, outcomeIdOf } from './core/hitl-routing.js';
|
|
19
20
|
import { hasExits } from './core/step-ports.js';
|
|
20
|
-
import {
|
|
21
|
+
import { actionTails, armReachesJoin } from './flow-parallel-hitl.js';
|
|
21
22
|
import { FLOW_SCHEMA_VERSIONS } from './format-profile.js';
|
|
22
23
|
import { compareSchemaVersions, schemaVersionRefusal } from './schema-version.js';
|
|
23
|
-
import {
|
|
24
|
-
import { planOutputBacking } from './flow-output-backing.js';
|
|
24
|
+
import { planSharedInputs, sharedInputRoots } from './flow-variables.js';
|
|
25
25
|
import { VARS_READ_RE } from './core/vars-read.js';
|
|
26
|
-
import { terminateStepClashMessage, terminateTwiceMessage } from './flow-terminate-names.js';
|
|
27
26
|
import { endContentKey } from './flow-end-content.js';
|
|
28
27
|
import { FLOW_NODE_TYPES } from './flow-node-registry.js';
|
|
29
28
|
import { stepExits } from './flow-step-exits.js';
|
|
30
29
|
import { stepDeclaredVariables } from './flow-step-variables.js';
|
|
31
30
|
import { plainHttpOnErrorMessage } from './nodes/http/flow/family.js';
|
|
32
31
|
import { resolveVersion, resolveVersionCarried } from './core/nodes/versioned.js';
|
|
33
|
-
import {
|
|
34
|
-
import { bodyUsesBreak, loopParallelWarning, loopVersionRefusal, resolveLoopDefinition, loopVersion } from './loop-version.js';
|
|
35
|
-
const newResourceIdLedger = () => ({ explicit: new Map(), taken: new Map(), agentSources: new Map() });
|
|
36
|
-
/** INLINE_AGENT_RESOURCE_NODE_ID for each explicit `nodeId` another node of the file also gets. */
|
|
37
|
-
function resourceNodeIdClashes(ledger) {
|
|
38
|
-
const out = [];
|
|
39
|
-
for (const [id, { step, what }] of ledger.explicit) {
|
|
40
|
-
const other = ledger.taken.get(id);
|
|
41
|
-
if (other === undefined)
|
|
42
|
-
continue;
|
|
43
|
-
out.push({
|
|
44
|
-
level: 'error', code: 'INLINE_AGENT_RESOURCE_NODE_ID', step,
|
|
45
|
-
message: `Inline agent step "${step}" gives its ${what} the \`nodeId\` "${id}", which is already `
|
|
46
|
-
+ `${other}. Node ids are unique in a flow, so compile cannot write both. Give the resource another id, `
|
|
47
|
-
+ 'or omit `nodeId` to derive one.',
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
|
-
return out;
|
|
51
|
-
}
|
|
32
|
+
import { loopParallelWarning, resolveLoopDefinition, loopVersion } from './loop-version.js';
|
|
52
33
|
export function check(built, opts = {}) {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
diags
|
|
57
|
-
for (const { id, message } of connectionConnectorConflicts(uses, opts)) {
|
|
58
|
-
diags.push({ level: 'error', code: 'CONNECTION_CONNECTOR_CONFLICT', step: id, message });
|
|
59
|
-
}
|
|
34
|
+
// A connection two connectors use, and an agent resource's `nodeId` another
|
|
35
|
+
// node of the file has, are the serializer's CONNECTION_CONNECTOR_CONFLICT /
|
|
36
|
+
// INLINE_AGENT_RESOURCE_NODE_ID: it claims ids and resolves connections file-wide.
|
|
37
|
+
const diags = checkFlow(built, [], opts);
|
|
60
38
|
// Once, from the top: the walk reaches child flows itself, so running it per
|
|
61
39
|
// `checkFlow` would report a child's escape twice.
|
|
62
40
|
for (const d of checkCookedEscapes(built)) {
|
|
@@ -66,37 +44,65 @@ export function check(built, opts = {}) {
|
|
|
66
44
|
...(d.suggestion ? { suggestion: d.suggestion } : {}),
|
|
67
45
|
});
|
|
68
46
|
}
|
|
47
|
+
if (opts.serializer !== false)
|
|
48
|
+
diags.push(...serializerRefusals(built, { library: opts.library, bindings: opts.bindings }, diags));
|
|
69
49
|
return diags;
|
|
70
50
|
}
|
|
51
|
+
/** The error-level diagnostics a custom node's own `diagnostics()` reported (see `serializerRefusals`). */
|
|
52
|
+
const OWN_NODE_ERRORS = new WeakSet();
|
|
71
53
|
/**
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
54
|
+
* Every refusal `compile` would raise for `built`: the serializer, run in
|
|
55
|
+
* collect mode (docs/CHECK_COMPILE_CONTRACT_DESIGN.md §3.5), so each rule it
|
|
56
|
+
* enforces is reported from its one implementation. A refusal inside a subflow
|
|
57
|
+
* is worded and placed as `check` words a child's findings: `In subflow "call": …`
|
|
58
|
+
* on step `call › x`.
|
|
59
|
+
*
|
|
60
|
+
* A throw that is no refusal (a malformed spec the serializer has no refusal
|
|
61
|
+
* for) comes back as COMPILE_FAILED, on its step, which is what `compile` says;
|
|
62
|
+
* on a step `check` already refuses it is that refusal's consequence and is not
|
|
63
|
+
* repeated. An `internal SDK error:` is passed on, unless `check` or the
|
|
64
|
+
* serializer already found an error: `compile` runs `check` first and stops at
|
|
65
|
+
* its errors, so it never serializes such a flow, and the author has those
|
|
66
|
+
* errors to fix first.
|
|
78
67
|
*/
|
|
79
|
-
function
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
68
|
+
function serializerRefusals(built, compile, found) {
|
|
69
|
+
const collected = [];
|
|
70
|
+
try {
|
|
71
|
+
serialize(built, { ...compile, collect: (r) => collected.push(r) });
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
// LIBRARY_REQUIRED is dropped below, so it does not count as something reported.
|
|
75
|
+
if (!found.some((d) => d.level === 'error') && !collected.some((r) => r.code !== 'LIBRARY_REQUIRED'))
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
78
|
+
const out = [];
|
|
79
|
+
const seen = new Set();
|
|
80
|
+
for (const r of collected) {
|
|
81
|
+
// "Compile needs a library" is about compile's inputs, not the flow: a check
|
|
82
|
+
// run without one fails open, as its library rules do.
|
|
83
|
+
if (r.code === 'LIBRARY_REQUIRED')
|
|
84
|
+
continue;
|
|
85
|
+
const subflows = r.subflows ?? [];
|
|
86
|
+
const step = r.step !== undefined ? [...subflows, r.step].join(' › ') : undefined;
|
|
87
|
+
// A custom node whose own diagnostics() reported an error has said why it
|
|
88
|
+
// cannot be lowered; its contribute() throwing too is not a second finding.
|
|
89
|
+
if (r.code === 'CONTRIBUTION_INVALID' && found.some((d) => d.step === step && OWN_NODE_ERRORS.has(d)))
|
|
90
|
+
continue;
|
|
91
|
+
if (r.code === 'COMPILE_FAILED' && found.some((d) => d.level === 'error' && d.step === step))
|
|
92
|
+
continue;
|
|
93
|
+
const message = subflows.reduceRight((m, caller) => `In subflow "${caller}": ${m}`, r.message);
|
|
94
|
+
// One finding per step path: the same refusal twice there is one rule broken once.
|
|
95
|
+
const key = `${r.code}\n${step ?? ''}\n${message}`;
|
|
96
|
+
if (seen.has(key))
|
|
97
|
+
continue;
|
|
98
|
+
seen.add(key);
|
|
99
|
+
out.push({
|
|
100
|
+
level: 'error', code: r.code, message,
|
|
101
|
+
...(step !== undefined ? { step } : {}),
|
|
102
|
+
...(r.suggestion !== undefined ? { suggestion: r.suggestion } : {}),
|
|
103
|
+
});
|
|
99
104
|
}
|
|
105
|
+
return out;
|
|
100
106
|
}
|
|
101
107
|
/**
|
|
102
108
|
* `ancestors` is the chain of child flows currently being checked, innermost
|
|
@@ -104,7 +110,7 @@ function checkStepIds(stepNames, trigger, diags) {
|
|
|
104
110
|
* to notice a flow that (transitively) calls itself before the serializer
|
|
105
111
|
* recurses forever.
|
|
106
112
|
*/
|
|
107
|
-
function checkFlow(built, ancestors, opts = {}
|
|
113
|
+
function checkFlow(built, ancestors, opts = {}) {
|
|
108
114
|
const diags = [];
|
|
109
115
|
if (!built.id)
|
|
110
116
|
diags.push({ level: 'error', code: 'NO_ID', message: 'flow() requires an id.' });
|
|
@@ -115,11 +121,9 @@ function checkFlow(built, ancestors, opts = {}, ledger = newResourceIdLedger())
|
|
|
115
121
|
if (maybeCustomTrigger !== undefined && !('kind' in maybeCustomTrigger) && maybeCustomTrigger instanceof FlowNode) {
|
|
116
122
|
// A custom FlowTrigger subclass reports through its own diagnostics() hook;
|
|
117
123
|
// the built-in trigger rules key on `kind` and don't apply.
|
|
118
|
-
const
|
|
119
|
-
for (const d of own) {
|
|
124
|
+
for (const d of maybeCustomTrigger.diagnostics()) {
|
|
120
125
|
diags.push({ level: d.severity, code: d.code, message: d.message, step: 'trigger' });
|
|
121
126
|
}
|
|
122
|
-
checkContribution(maybeCustomTrigger, built.triggerOptions?.version, 'trigger', own.some((d) => d.severity === 'error'), diags);
|
|
123
127
|
}
|
|
124
128
|
else if (built.trigger !== undefined)
|
|
125
129
|
checkTrigger(built.trigger, diags, opts);
|
|
@@ -315,27 +319,11 @@ function checkFlow(built, ancestors, opts = {}, ledger = newResourceIdLedger())
|
|
|
315
319
|
? { kind: 'action', name: s0.name, spec: s0.spec, ...(s0.options ? { options: s0.options } : {}) }
|
|
316
320
|
: s0;
|
|
317
321
|
// `return` carries a synthetic `name` that names no node and is not
|
|
318
|
-
// referenceable
|
|
319
|
-
//
|
|
322
|
+
// referenceable, so it is not registered. The port-edge kinds carry no
|
|
323
|
+
// name at all.
|
|
320
324
|
if ('name' in s && s.kind !== 'return') {
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
// would be renamed `<name>2`, so paths that all stop at ONE Terminate
|
|
324
|
-
// come back with an extra node (#1239). Same code as any step clash (a
|
|
325
|
-
// Terminate's name is a step name), with the remedy spelled out.
|
|
326
|
-
// A step and a Terminate sharing a name (either order) get the same
|
|
327
|
-
// words serialize throws for them: rename one.
|
|
328
|
-
const twoTerminates = s.kind === 'terminate' && terminateNames.has(s.name);
|
|
329
|
-
const oneTerminate = (s.kind === 'terminate') !== terminateNames.has(s.name);
|
|
330
|
-
diags.push({
|
|
331
|
-
level: 'error', code: 'DUP_STEP', step: s.name,
|
|
332
|
-
message: twoTerminates
|
|
333
|
-
? terminateTwiceMessage(s.name)
|
|
334
|
-
: oneTerminate
|
|
335
|
-
? terminateStepClashMessage(s.name)
|
|
336
|
-
: `Duplicate step name "${s.name}".`,
|
|
337
|
-
});
|
|
338
|
-
}
|
|
325
|
+
// A name already taken in this scope is the serializer's DUP_STEP, with
|
|
326
|
+
// the remedy for a step and a Terminate, or two Terminates (#1239).
|
|
339
327
|
stepNames.add(s.name);
|
|
340
328
|
declared.add(s.name); // a step's output is referenceable by name
|
|
341
329
|
// …and so is every variable the step declares by running: a quick
|
|
@@ -345,7 +333,7 @@ function checkFlow(built, ancestors, opts = {}, ledger = newResourceIdLedger())
|
|
|
345
333
|
declared.add(v);
|
|
346
334
|
}
|
|
347
335
|
if ((s.kind === 'stepToList' || s.kind === 'stepToRef') && s.port === 'error')
|
|
348
|
-
checkOnError(s, prevPortSource, diags, prevPortSource && checkedSteps.get(prevPortSource.name), opts
|
|
336
|
+
checkOnError(s, prevPortSource, diags, prevPortSource && checkedSteps.get(prevPortSource.name), opts);
|
|
349
337
|
if ((s.kind === 'stepToList' || s.kind === 'stepToRef') && s.port.startsWith('branch-')) {
|
|
350
338
|
const want = s.port.slice('branch-'.length);
|
|
351
339
|
const prevSpec = prevAction?.spec;
|
|
@@ -361,38 +349,9 @@ function checkFlow(built, ancestors, opts = {}, ledger = newResourceIdLedger())
|
|
|
361
349
|
});
|
|
362
350
|
}
|
|
363
351
|
}
|
|
364
|
-
//
|
|
365
|
-
//
|
|
366
|
-
//
|
|
367
|
-
// The product draws one `outcome-<id>` handle per outcome on those two node
|
|
368
|
-
// types, at every definition version, and `completed` only when there are
|
|
369
|
-
// none (flow-workbench `build-handle-customization.ts`). An edge on it is
|
|
370
|
-
// stale: Studio Web and the converter remap it to the PRIMARY outcome
|
|
371
|
-
// (#3512), so every other outcome loses its path — and `flow validate`
|
|
372
|
-
// accepts it without `handleCustomization`, so nothing later catches it.
|
|
373
|
-
// `serialize` refuses to emit one; this says why, in the author's terms.
|
|
374
|
-
if ((s.kind === 'stepToList' || s.kind === 'stepToRef') && s.port === 'completed') {
|
|
375
|
-
const prevSpec = prevAction?.spec;
|
|
376
|
-
const isHitl = !!prevSpec && 'kind' in prevSpec && prevSpec.kind === 'hitl';
|
|
377
|
-
const model = isHitl ? hitlExitModel(prevSpec.inputs, prevAction?.options?.version) : undefined;
|
|
378
|
-
if (model && model.shape !== 'placeholder') {
|
|
379
|
-
const ports = model.exits.map((e) => `"${e.port}"`).join(', ');
|
|
380
|
-
const routed = model.shape === 'per-outcome';
|
|
381
|
-
diags.push({
|
|
382
|
-
level: 'error', code: 'HITL_COMPLETED_PORT_GONE', step: prevAction?.name,
|
|
383
|
-
message: `Port "completed" does not exist on human task "${prevAction?.name}". It has outcomes, so `
|
|
384
|
-
+ `the product draws one handle per outcome (${ports}) and no 'completed' — an edge there is `
|
|
385
|
-
+ `remapped to the primary outcome and the others lose their path. `
|
|
386
|
-
+ (routed
|
|
387
|
-
? `The FIRST outcome ("${model.exits[0]?.port}") is what continues the main path.`
|
|
388
|
-
: `.step() after this task already continues every outcome to the next step.`),
|
|
389
|
-
suggestion: routed
|
|
390
|
-
? `route the outcomes (.stepToList('${model.exits[1]?.port ?? model.exits[0]?.port}', …)), or continue `
|
|
391
|
-
+ `every outcome to the next step with outcomePorts: false`
|
|
392
|
-
: `drop the 'completed' route — .step() continues every outcome — or route each outcome with .stepSwitch`,
|
|
393
|
-
});
|
|
394
|
-
}
|
|
395
|
-
}
|
|
352
|
+
// `completed` on a base or quick-form human task that has outcomes is the
|
|
353
|
+
// serializer's HITL_COMPLETED_PORT_GONE: the product draws one
|
|
354
|
+
// `outcome-<id>` handle per outcome and no `completed` (#3512).
|
|
396
355
|
if ((s.kind === 'stepToList' || s.kind === 'stepToRef') && s.port.startsWith('outcome-')) {
|
|
397
356
|
const want = s.port.slice('outcome-'.length);
|
|
398
357
|
const prevSpec = prevAction?.spec;
|
|
@@ -407,41 +366,12 @@ function checkFlow(built, ancestors, opts = {}, ledger = newResourceIdLedger())
|
|
|
407
366
|
else {
|
|
408
367
|
const model = hitlExitModel(prevSpec.inputs, prevAction?.options?.version);
|
|
409
368
|
const ids = (prevSpec.inputs.outcomes ?? []).map((o, i) => outcomeIdOf(o, i));
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
: `the '${prevSpec.inputs?.variant}' variant reads its outcomes from its app`}, so its single `
|
|
417
|
-
+ `exit is 'completed'.`,
|
|
418
|
-
suggestion: model.legacyCompleted
|
|
419
|
-
? 'drop `legacyCompleted` (and set outcomePorts: true to route each outcome)'
|
|
420
|
-
: `route on out('${prevAction?.name}', 'Action') downstream`,
|
|
421
|
-
});
|
|
422
|
-
}
|
|
423
|
-
else if (!ids.includes(want)) {
|
|
424
|
-
diags.push({
|
|
425
|
-
level: 'error', code: 'HITL_OUTCOME_PORT_UNKNOWN', step: prevAction?.name,
|
|
426
|
-
message: `Port "${s.port}" routes the outcome id ${JSON.stringify(want)}, but human task `
|
|
427
|
-
+ `"${prevAction?.name}" declares ${ids.map((i) => `outcome-${i}`).join(', ')}. Ids are the `
|
|
428
|
-
+ `outcome names slugified (lowercase, non-alphanumerics to '-').`,
|
|
429
|
-
});
|
|
430
|
-
}
|
|
431
|
-
else if (model.shape === 'every-outcome') {
|
|
432
|
-
// The port is real, but it is ALREADY wired: a task that does not
|
|
433
|
-
// route per outcome continues every outcome to the next step, so a
|
|
434
|
-
// second route would put two edges on one port (#741).
|
|
435
|
-
diags.push({
|
|
436
|
-
level: 'error', code: 'HITL_OUTCOME_PORTS_OFF', step: prevAction?.name,
|
|
437
|
-
message: `Port "${s.port}" is already wired: human task "${prevAction?.name}" does not route per `
|
|
438
|
-
+ `outcome — ${fanOutWhy(model.why, prevAction?.options?.version)} — so .step() continues every `
|
|
439
|
-
+ `outcome to the next step, "${s.port}" included. A second route would put two edges on one port.`,
|
|
440
|
-
suggestion: `outcomePorts: true (then the first outcome continues and the others are routed), `
|
|
441
|
-
+ `or route every outcome with .stepSwitch`,
|
|
442
|
-
});
|
|
443
|
-
}
|
|
444
|
-
else if (prevAction) {
|
|
369
|
+
// A task with no per-outcome exits, an id the task does not declare,
|
|
370
|
+
// and a port a task that does not route per outcome already continues
|
|
371
|
+
// on are the serializer's HITL_OUTCOME_PORTS_NONE /
|
|
372
|
+
// HITL_OUTCOME_PORT_UNKNOWN / HITL_OUTCOME_PORTS_OFF (#741). A routed
|
|
373
|
+
// port is recorded for the unrouted-outcome rule.
|
|
374
|
+
if (model.shape === 'per-outcome' && ids.includes(want) && prevAction) {
|
|
445
375
|
let routed = hitlOutcomeRouted.get(prevAction.name);
|
|
446
376
|
if (!routed)
|
|
447
377
|
hitlOutcomeRouted.set(prevAction.name, routed = new Set());
|
|
@@ -454,7 +384,7 @@ function checkFlow(built, ancestors, opts = {}, ledger = newResourceIdLedger())
|
|
|
454
384
|
prevPortSource = s;
|
|
455
385
|
const registered = registeredKind(s.spec);
|
|
456
386
|
if (registered) {
|
|
457
|
-
const { placed, checked } = checkRegisteredStep(registered, s, exprs, errorSource, opts, diags, typeOf, triggerId,
|
|
387
|
+
const { placed, checked } = checkRegisteredStep(registered, s, exprs, errorSource, opts, diags, typeOf, triggerId, carriedLookup([built, ...ancestors]));
|
|
458
388
|
checkedSteps.set(s.name, checked);
|
|
459
389
|
if (placed)
|
|
460
390
|
registeredSteps.set(s.name, checked);
|
|
@@ -468,11 +398,12 @@ function checkFlow(built, ancestors, opts = {}, ledger = newResourceIdLedger())
|
|
|
468
398
|
// don't apply, and it reports through its own diagnostics() hook.
|
|
469
399
|
const maybeCustom = s.spec;
|
|
470
400
|
if (!('kind' in maybeCustom) && maybeCustom instanceof FlowNode) {
|
|
471
|
-
const
|
|
472
|
-
|
|
473
|
-
|
|
401
|
+
for (const d of maybeCustom.diagnostics()) {
|
|
402
|
+
const diag = { level: d.severity, code: d.code, message: d.message, step: s.name };
|
|
403
|
+
if (d.severity === 'error')
|
|
404
|
+
OWN_NODE_ERRORS.add(diag);
|
|
405
|
+
diags.push(diag);
|
|
474
406
|
}
|
|
475
|
-
checkContribution(maybeCustom, s.options?.version, s.name, own.some((d) => d.severity === 'error'), diags);
|
|
476
407
|
}
|
|
477
408
|
// `.stepSwitch` routes its exits as ARMS, so the `.stepToList` bookkeeping
|
|
478
409
|
// would report every one of them unrouted. STEP_SWITCH_EXIT_UNROUTED is
|
|
@@ -527,10 +458,10 @@ function checkFlow(built, ancestors, opts = {}, ledger = newResourceIdLedger())
|
|
|
527
458
|
level, code, message, step: stepName,
|
|
528
459
|
...(suggestion ? { suggestion } : {}),
|
|
529
460
|
});
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
461
|
+
// What compile refuses (the event itself, its filters, its binding
|
|
462
|
+
// labels) is the serializer's, reported from its run.
|
|
463
|
+
checkEventSubscription(s.spec.subscription, `waitForEvent "${stepName}"`, push('error'), push('warning'), eventParamsFor(s.spec.subscription, opts), false);
|
|
464
|
+
checkEventResolution(s.spec.subscription, `waitForEvent "${stepName}"`, 'event', opts, push('error'), push('warning'), eventParamsFor(s.spec.subscription, opts), false);
|
|
534
465
|
}
|
|
535
466
|
if (s.spec.kind === 'subflow') {
|
|
536
467
|
// Only a real child goes in the map: a missing one is reported by
|
|
@@ -689,12 +620,6 @@ function checkFlow(built, ancestors, opts = {}, ledger = newResourceIdLedger())
|
|
|
689
620
|
// clashes, and a read of it is a read of a merge — nothing.
|
|
690
621
|
const fork = parallelFork(s.options);
|
|
691
622
|
if (fork) {
|
|
692
|
-
if (stepNames.has(fork.name)) {
|
|
693
|
-
diags.push({
|
|
694
|
-
level: 'error', code: 'DUP_STEP', step: s.name,
|
|
695
|
-
message: `Duplicate step name "${fork.name}": the fork of parallel "${s.name}" and another step share it.`,
|
|
696
|
-
});
|
|
697
|
-
}
|
|
698
623
|
stepNames.add(fork.name);
|
|
699
624
|
declared.add(fork.name);
|
|
700
625
|
parallelNames.add(fork.name);
|
|
@@ -711,15 +636,11 @@ function checkFlow(built, ancestors, opts = {}, ledger = newResourceIdLedger())
|
|
|
711
636
|
checkLoopListEnvelope(s, registeredSteps, opts, diags);
|
|
712
637
|
if (s.options?.completionCondition)
|
|
713
638
|
addExpr(s.options.completionCondition.js, s.name);
|
|
714
|
-
//
|
|
715
|
-
//
|
|
716
|
-
//
|
|
717
|
-
// carried — as serialize resolves it
|
|
718
|
-
// and `parallel: true` are judged here and not first at compile (#1311).
|
|
639
|
+
// An option or a `.break()` the pinned version's definition cannot express
|
|
640
|
+
// is the serializer's LOOP_VERSION_UNSUPPORTED (#1186). The legacy
|
|
641
|
+
// `parallel` warning is judged against the same definition — bundled, else
|
|
642
|
+
// carried — as serialize resolves it (#1311).
|
|
719
643
|
const loopDef = resolveLoopDefinition(loopVersion(s.options), carriedLookup([built, ...ancestors]));
|
|
720
|
-
const versionRefusal = loopVersionRefusal(s.name, s.options, bodyUsesBreak(s.body), loopDef);
|
|
721
|
-
if (versionRefusal)
|
|
722
|
-
diags.push({ level: 'error', code: 'LOOP_VERSION_UNSUPPORTED', step: s.name, message: versionRefusal });
|
|
723
644
|
const parallelWarning = loopParallelWarning(s.name, s.options, loopDef);
|
|
724
645
|
if (parallelWarning)
|
|
725
646
|
diags.push({ level: 'warning', code: 'LOOP_PARALLEL_LEGACY', step: s.name, message: parallelWarning });
|
|
@@ -753,24 +674,11 @@ function checkFlow(built, ancestors, opts = {}, ledger = newResourceIdLedger())
|
|
|
753
674
|
diags.push({ level: 'warning', code: 'EMPTY_LOOP', message: `Do-while "${s.name}" has an empty body.`, step: s.name });
|
|
754
675
|
walk(s.body, true);
|
|
755
676
|
}
|
|
756
|
-
else if (s.kind === 'break') {
|
|
757
|
-
if (!inLoop) {
|
|
758
|
-
diags.push({
|
|
759
|
-
level: 'error', code: 'BREAK_OUTSIDE_LOOP',
|
|
760
|
-
message: '.break() is only valid inside a .loop() or .doWhile() body.',
|
|
761
|
-
});
|
|
762
|
-
}
|
|
763
|
-
}
|
|
764
677
|
else if (s.kind === 'continue') {
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
message: '.continue() is only valid inside a .loop() or .doWhile() body: it goes on to the container\'s next iteration.',
|
|
769
|
-
});
|
|
770
|
-
}
|
|
771
|
-
else if (inLoop === 'arm') {
|
|
678
|
+
// Outside any container it is the serializer's CONTINUE_OUTSIDE_LOOP (and
|
|
679
|
+
// `.break()`'s BREAK_OUTSIDE_LOOP).
|
|
680
|
+
if (inLoop === 'arm')
|
|
772
681
|
diags.push(loopExitInArm());
|
|
773
|
-
}
|
|
774
682
|
}
|
|
775
683
|
else if (s.kind === 'return') {
|
|
776
684
|
// A named End (`.return({…}, { name })`) is a node merge can place a
|
|
@@ -784,16 +692,12 @@ function checkFlow(built, ancestors, opts = {}, ledger = newResourceIdLedger())
|
|
|
784
692
|
// AFTER the dispatch chain, not inside it: `s` was normalised to an action
|
|
785
693
|
// above, so an `else if` here would be unreachable — which it silently was
|
|
786
694
|
// on the first cut. The action rules have run by now; what is left is the
|
|
787
|
-
// arms and the
|
|
695
|
+
// arms and the things only this step can get wrong. A step with no exits
|
|
696
|
+
// to route, and an arm for an exit it does not declare, are the
|
|
697
|
+
// serializer's STEP_SWITCH_NO_EXITS / STEP_SWITCH_EXIT_UNKNOWN.
|
|
788
698
|
if (s0.kind === 'stepSwitch') {
|
|
789
699
|
const exits = stepExits(s0.spec, s0.options?.version);
|
|
790
700
|
if (!hasExits(exits)) {
|
|
791
|
-
diags.push({
|
|
792
|
-
level: 'error', code: 'STEP_SWITCH_NO_EXITS', step: s0.name,
|
|
793
|
-
message: `.stepSwitch("${s0.name}") routes this step's declared exits, but ${exits.reason}, `
|
|
794
|
-
+ `so there are none to route.`,
|
|
795
|
-
...(exits.suggestion ? { suggestion: exits.suggestion } : {}),
|
|
796
|
-
});
|
|
797
701
|
for (const c of s0.cases)
|
|
798
702
|
walk(c.body, inLoop);
|
|
799
703
|
}
|
|
@@ -801,23 +705,14 @@ function checkFlow(built, ancestors, opts = {}, ledger = newResourceIdLedger())
|
|
|
801
705
|
const seenExit = new Set();
|
|
802
706
|
for (const c of s0.cases) {
|
|
803
707
|
const exit = exits.find((e) => e.value === c.value);
|
|
804
|
-
if (
|
|
805
|
-
const nearest = nearestName(c.value, exits.map((e) => e.value));
|
|
806
|
-
diags.push({
|
|
807
|
-
level: 'error', code: 'STEP_SWITCH_EXIT_UNKNOWN', step: s0.name,
|
|
808
|
-
message: `.stepSwitch("${s0.name}") has an arm for ${JSON.stringify(c.value)}, which is not `
|
|
809
|
-
+ `an exit it declares — those are ${exits.map((e) => JSON.stringify(e.value)).join(', ')}.`,
|
|
810
|
-
...(nearest !== undefined ? { suggestion: `value: '${nearest}'` } : {}),
|
|
811
|
-
});
|
|
812
|
-
}
|
|
813
|
-
else if (seenExit.has(exit.port)) {
|
|
708
|
+
if (exit !== undefined && seenExit.has(exit.port)) {
|
|
814
709
|
diags.push({
|
|
815
710
|
level: 'error', code: 'STEP_SWITCH_EXIT_DUP', step: s0.name,
|
|
816
711
|
message: `.stepSwitch("${s0.name}") has two arms for ${JSON.stringify(c.value)}. A port takes `
|
|
817
712
|
+ `exactly one outgoing edge, so put everything that exit needs in one arm.`,
|
|
818
713
|
});
|
|
819
714
|
}
|
|
820
|
-
else {
|
|
715
|
+
else if (exit !== undefined) {
|
|
821
716
|
seenExit.add(exit.port);
|
|
822
717
|
}
|
|
823
718
|
if (c.body.length === 0) {
|
|
@@ -893,32 +788,14 @@ function checkFlow(built, ancestors, opts = {}, ledger = newResourceIdLedger())
|
|
|
893
788
|
reportUnreachableHitlOutcomes(hitlChoiceNoPorts, exprs, diags);
|
|
894
789
|
checkRegisteredFamilies(registeredSteps, unresolvedSteps, stepNames, diags);
|
|
895
790
|
checkPackageNodes(registeredSteps, diags);
|
|
896
|
-
//
|
|
897
|
-
|
|
898
|
-
if (!ledger.taken.has(n))
|
|
899
|
-
ledger.taken.set(n, `the id of step "${n}"`);
|
|
900
|
-
if (!ledger.taken.has(triggerId))
|
|
901
|
-
ledger.taken.set(triggerId, 'the id of the trigger');
|
|
902
|
-
for (const ep of built.entryPoints ?? [])
|
|
903
|
-
if (!ledger.taken.has(ep.id))
|
|
904
|
-
ledger.taken.set(ep.id, `the id of entry point "${ep.id}"`);
|
|
905
|
-
// Entry points: unique roots with valid triggers, colliding with nothing.
|
|
906
|
-
const epIds = new Set();
|
|
791
|
+
// Entry points: valid triggers. An id that collides with a step, the trigger
|
|
792
|
+
// or another entry point is the serializer's ENTRY_POINT_DUP_ID.
|
|
907
793
|
for (const ep of built.entryPoints ?? []) {
|
|
908
|
-
if (epIds.has(ep.id) || ep.id === triggerId || ep.id === 'start') {
|
|
909
|
-
diags.push({
|
|
910
|
-
level: 'error', code: 'ENTRY_POINT_DUP_ID', step: ep.id,
|
|
911
|
-
message: `.entryPoint("${ep.id}"): the id collides with the default trigger or another entry point.`,
|
|
912
|
-
});
|
|
913
|
-
}
|
|
914
|
-
epIds.add(ep.id);
|
|
915
794
|
const maybeCustomEp = ep.trigger;
|
|
916
795
|
if (!('kind' in maybeCustomEp) && maybeCustomEp instanceof FlowNode) {
|
|
917
|
-
const
|
|
918
|
-
for (const d of own) {
|
|
796
|
+
for (const d of maybeCustomEp.diagnostics()) {
|
|
919
797
|
diags.push({ level: d.severity, code: d.code, message: d.message, step: ep.id });
|
|
920
798
|
}
|
|
921
|
-
checkContribution(maybeCustomEp, ep.options?.version, ep.id, own.some((d) => d.severity === 'error'), diags);
|
|
922
799
|
}
|
|
923
800
|
else {
|
|
924
801
|
checkTrigger(ep.trigger, diags, opts, ep.id);
|
|
@@ -952,15 +829,9 @@ function checkFlow(built, ancestors, opts = {}, ledger = newResourceIdLedger())
|
|
|
952
829
|
}
|
|
953
830
|
walk(ep.steps);
|
|
954
831
|
}
|
|
955
|
-
//
|
|
956
|
-
//
|
|
957
|
-
|
|
958
|
-
if (built.entryPoints?.length && !built.steps.some((st) => 'name' in st && st.kind !== 'return')) {
|
|
959
|
-
diags.push({
|
|
960
|
-
level: 'error', code: 'ENTRY_POINT_NO_SHARED_BODY',
|
|
961
|
-
message: 'Entry points join the shared body, but this flow has no shared step. Add one, or end each prefix terminally.',
|
|
962
|
-
});
|
|
963
|
-
}
|
|
832
|
+
// Two steps whose names give one node id, or a step named like the trigger,
|
|
833
|
+
// are the serializer's STEP_ID_COLLISION (#1249); entry points with no shared
|
|
834
|
+
// step to join are its ENTRY_POINT_NO_SHARED_BODY.
|
|
964
835
|
const conversationRoots = [
|
|
965
836
|
...(built.trigger?.kind === 'conversation' ? [triggerId] : []),
|
|
966
837
|
...(built.entryPoints ?? []).filter((ep) => ep.trigger.kind === 'conversation').map((ep) => ep.id),
|
|
@@ -972,9 +843,7 @@ function checkFlow(built, ancestors, opts = {}, ledger = newResourceIdLedger())
|
|
|
972
843
|
+ `${conversationRoots.map((id) => `"${id}"`).join(', ')}. Keep one and give the others another trigger.`,
|
|
973
844
|
});
|
|
974
845
|
}
|
|
975
|
-
|
|
976
|
-
// start is the SDK's to name, so it reserves nothing.
|
|
977
|
-
checkStepRefs(built, diags, ancestors.length === 0 ? [triggerId, ...(built.entryPoints ?? []).map((ep) => ep.id)] : []);
|
|
846
|
+
checkStepRefs(built, diags);
|
|
978
847
|
checkArmScopedReads(built, diags);
|
|
979
848
|
// A fire-and-forget dispatch publishes no output: any read of it would wait
|
|
980
849
|
// on a value the run will never produce. Error reads stay legal — dispatch
|
|
@@ -1321,29 +1190,25 @@ function checkFlow(built, ancestors, opts = {}, ledger = newResourceIdLedger())
|
|
|
1321
1190
|
for (const [stepName, child] of subflowChildren) {
|
|
1322
1191
|
if (ancestors.includes(child) || child === built)
|
|
1323
1192
|
continue;
|
|
1324
|
-
for (const d of checkFlow(child, [...ancestors, built], opts
|
|
1325
|
-
|
|
1193
|
+
for (const d of checkFlow(child, [...ancestors, built], opts)) {
|
|
1194
|
+
const placed = {
|
|
1326
1195
|
...d,
|
|
1327
1196
|
code: d.code,
|
|
1328
1197
|
message: `In subflow "${stepName}": ${d.message}`,
|
|
1329
1198
|
step: d.step ? `${stepName} › ${d.step}` : stepName,
|
|
1330
|
-
}
|
|
1199
|
+
};
|
|
1200
|
+
if (OWN_NODE_ERRORS.has(d))
|
|
1201
|
+
OWN_NODE_ERRORS.add(placed);
|
|
1202
|
+
diags.push(placed);
|
|
1331
1203
|
}
|
|
1332
1204
|
}
|
|
1333
1205
|
return diags;
|
|
1334
1206
|
}
|
|
1335
|
-
/** What `registry prepare` takes for an event subscription: its connector key and curated event name. */
|
|
1336
|
-
function eventTarget(sub) {
|
|
1337
|
-
const { connector, event } = (sub ?? {});
|
|
1338
|
-
return typeof connector === 'string' && connector !== ''
|
|
1339
|
-
? { key: connector, ...(typeof event === 'string' && event !== '' ? { action: event } : {}) }
|
|
1340
|
-
: undefined;
|
|
1341
|
-
}
|
|
1342
1207
|
/**
|
|
1343
1208
|
* The connector rules live in `core/connector-checks.ts`, shared with BPMN —
|
|
1344
1209
|
* they were always a function of the connector corpus and the step's own
|
|
1345
1210
|
* `{ key, action, version, object, inputs, connection, folder }`, never of
|
|
1346
|
-
* anything flow-shaped. These
|
|
1211
|
+
* anything flow-shaped. These adapters add what IS flow-shaped: the
|
|
1347
1212
|
* `Connector step "…"` vocabulary, the `step` field on the diagnostic, and the
|
|
1348
1213
|
* `$vars`/`Expr` dialect its inputs are written in.
|
|
1349
1214
|
*/
|
|
@@ -1355,22 +1220,13 @@ function connectorSite(step, diags) {
|
|
|
1355
1220
|
}
|
|
1356
1221
|
function checkConnectorLookups(step, spec, opts, diags) {
|
|
1357
1222
|
const { where, report } = connectorSite(step, diags);
|
|
1358
|
-
|
|
1223
|
+
// An unresolved lookup is the serializer's LOOKUP_UNRESOLVED.
|
|
1224
|
+
coreCheckConnectorLookups(where, spec, opts, FLOW_EXPR_DIALECT, report, false);
|
|
1359
1225
|
}
|
|
1360
1226
|
function checkConnectorSchema(step, spec, opts, diags) {
|
|
1361
1227
|
const { where, report } = connectorSite(step, diags);
|
|
1362
1228
|
coreCheckConnectorSchema(where, spec, opts, FLOW_EXPR_DIALECT, report);
|
|
1363
1229
|
}
|
|
1364
|
-
/**
|
|
1365
|
-
* The core contract (fields with a library, names with a bindings.json), plus
|
|
1366
|
-
* BINDING_MISSING, which needs neither: an absent label is wrong whatever the
|
|
1367
|
-
* file says.
|
|
1368
|
-
*/
|
|
1369
|
-
function checkConnectorContract(step, spec, opts, diags) {
|
|
1370
|
-
const { where, report } = connectorSite(step, diags);
|
|
1371
|
-
coreCheckConnectorContract(where, spec, opts, report, step);
|
|
1372
|
-
checkBindingPresence(`connector "${step}"`, spec, (code, message) => report({ level: 'error', code, message }), { key: spec.key, action: spec.action });
|
|
1373
|
-
}
|
|
1374
1230
|
/**
|
|
1375
1231
|
* Reject a wrapper hop when the runtime shape of a list has been measured: the
|
|
1376
1232
|
* step the loop reads says what its list output looks like (`listShape`, a
|
|
@@ -1607,14 +1463,8 @@ function checkSubflow(step, child, inputs, parent, ancestors, diags) {
|
|
|
1607
1463
|
+ `trigger of its own — the schedule would be dropped. Remove the .trigger() from "${child.id}" `
|
|
1608
1464
|
+ `(\`.trigger(manual(), { label })\` is allowed, to name the child's start node).`);
|
|
1609
1465
|
}
|
|
1610
|
-
// `{ shared }` merges one input across a flow's ROOTS (#924)
|
|
1611
|
-
// one root, its caller
|
|
1612
|
-
for (const inp of child.inputs) {
|
|
1613
|
-
if (inp.shared === undefined)
|
|
1614
|
-
continue;
|
|
1615
|
-
at('SHARED_INPUT_IN_SUBFLOW', `Subflow "${step}" runs "${child.id}", whose input "${inp.name}" has { shared: "${inp.shared}" }. A subflow has `
|
|
1616
|
-
+ `one root — its caller — so there is nothing to merge. Read the input with input('${inp.name}') inside the child.`);
|
|
1617
|
-
}
|
|
1466
|
+
// `{ shared }` merges one input across a flow's ROOTS (#924); a subflow has
|
|
1467
|
+
// one root, its caller: the serializer's SHARED_INPUT_IN_SUBFLOW.
|
|
1618
1468
|
const declared = new Map(child.inputs.map((i) => [i.name, String(i.type)]));
|
|
1619
1469
|
const given = inputs ?? {};
|
|
1620
1470
|
for (const key of Object.keys(given)) {
|
|
@@ -1849,34 +1699,20 @@ const INTERVAL_WORDS = [
|
|
|
1849
1699
|
*/
|
|
1850
1700
|
/**
|
|
1851
1701
|
* Every declared name must become exactly one global, because `flow validate`
|
|
1852
|
-
* rejects a repeated id (`[variables.globals] Duplicate variable ID
|
|
1853
|
-
*
|
|
1854
|
-
*
|
|
1855
|
-
*
|
|
1856
|
-
*
|
|
1857
|
-
*
|
|
1702
|
+
* rejects a repeated id (`[variables.globals] Duplicate variable ID`,
|
|
1703
|
+
* flow-builder-sdk#890). The plans that decide it (`planFlowGlobals`,
|
|
1704
|
+
* `planSharedInputs`, `planOutputBacking`) are the serializer's, and so are
|
|
1705
|
+
* their refusals (VARIABLE_NAME_COLLISION, OUTPUT_VAR_MISMATCH, SHARED_INPUT_*,
|
|
1706
|
+
* OUTPUT_BACKING_COLLISION). What is left here is the shared-input plan's
|
|
1707
|
+
* warnings, which compile does not raise.
|
|
1858
1708
|
*/
|
|
1859
1709
|
function checkGlobalNames(built, diags) {
|
|
1860
|
-
// `{ shared }` inputs (#924) may add `.var()`s; the plan is the serializer's
|
|
1861
|
-
// own (`emittableGlobals`), so check and the emitted file agree on them.
|
|
1862
1710
|
const shared = planSharedInputs({ roots: sharedInputRoots(built), outputs: built.outputs ?? [], vars: built.vars ?? [] });
|
|
1863
1711
|
for (const p of shared.problems) {
|
|
1712
|
+
if (p.level !== 'warning')
|
|
1713
|
+
continue;
|
|
1864
1714
|
diags.push({ level: p.level, code: p.code, message: p.message, ...(p.suggestion ? { suggestion: p.suggestion } : {}) });
|
|
1865
1715
|
}
|
|
1866
|
-
const vars = [...(built.vars ?? []), ...shared.autoVars];
|
|
1867
|
-
const plan = planFlowGlobals({ inputs: built.inputs ?? [], outputs: built.outputs ?? [], vars });
|
|
1868
|
-
for (const p of plan.problems) {
|
|
1869
|
-
diags.push({ level: 'error', code: p.code, message: p.message, ...(p.suggestion ? { suggestion: p.suggestion } : {}) });
|
|
1870
|
-
}
|
|
1871
|
-
if (plan.problems.length)
|
|
1872
|
-
return;
|
|
1873
|
-
// An output the flow sets from inside is emitted with an `inout` Variable
|
|
1874
|
-
// `<output>Value` behind it (#1092); that id must be free. Same plan as
|
|
1875
|
-
// `emittableGlobals`, so a name check passes is one serialize can emit.
|
|
1876
|
-
const sharedOutputs = new Set(shared.targets.filter((t) => t.declaredAs === 'output').map((t) => t.name));
|
|
1877
|
-
for (const p of planOutputBacking(built, plan.outputs, plan.vars, sharedOutputs).problems) {
|
|
1878
|
-
diags.push({ level: 'error', code: p.code, message: p.message, ...(p.suggestion ? { suggestion: p.suggestion } : {}) });
|
|
1879
|
-
}
|
|
1880
1716
|
}
|
|
1881
1717
|
/**
|
|
1882
1718
|
* `{ updates }` writes a declared flow variable when the node completes. The
|
|
@@ -1932,8 +1768,6 @@ function checkNodeUpdates(built, diags) {
|
|
|
1932
1768
|
visit(built.steps);
|
|
1933
1769
|
}
|
|
1934
1770
|
function checkTrigger(spec, diags, opts = {}, entryPointId) {
|
|
1935
|
-
const where = entryPointId === undefined ? undefined : `onEvent() entry point "${entryPointId}"`;
|
|
1936
|
-
const call = entryPointId === undefined ? '.trigger()' : `.entryPoint("${entryPointId}")`;
|
|
1937
1771
|
const push = (level) => (code, message, suggestion) => {
|
|
1938
1772
|
diags.push({ level, code, message, step: entryPointId ?? 'trigger', ...(suggestion ? { suggestion } : {}) });
|
|
1939
1773
|
};
|
|
@@ -1942,10 +1776,9 @@ function checkTrigger(spec, diags, opts = {}, entryPointId) {
|
|
|
1942
1776
|
// object without its factory) or a typo'd kind reaches here intact.
|
|
1943
1777
|
if (spec?.kind === 'event') {
|
|
1944
1778
|
const sub = spec.subscription;
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
checkBindingPresence(where ?? 'event trigger', sub, at, eventTarget(sub));
|
|
1779
|
+
// As for waitForEvent: what compile refuses is the serializer's.
|
|
1780
|
+
checkEventSubscription(sub, 'trigger', at, push('warning'), eventParamsFor(sub, opts), false);
|
|
1781
|
+
checkEventResolution(sub, 'trigger', 'trigger', opts, at, push('warning'), eventParamsFor(sub, opts), false);
|
|
1949
1782
|
return;
|
|
1950
1783
|
}
|
|
1951
1784
|
// manual() is the explicit spelling of the default — nothing to validate.
|
|
@@ -1959,14 +1792,9 @@ function checkTrigger(spec, diags, opts = {}, entryPointId) {
|
|
|
1959
1792
|
// platform started is the trigger's own output, declared by the definition.
|
|
1960
1793
|
if (spec?.kind === 'conversation' || spec?.kind === 'voice')
|
|
1961
1794
|
return;
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
+ `Wrap it in a factory: scheduled({ every: 'R/PT1H' }) for a schedule, `
|
|
1965
|
-
+ `onEvent({ connector, event, where, connection, folder }) to start on a connector event, formTrigger() to start from a `
|
|
1966
|
-
+ `submitted form, or conversationTrigger() to start on a new conversation. Omit .trigger() entirely `
|
|
1967
|
-
+ `for the manual trigger (the default).`, `scheduled({ every: 'R/PT1H' })`);
|
|
1795
|
+
// Anything but a schedule here is not a trigger: the serializer's TRIGGER_UNKNOWN_KIND.
|
|
1796
|
+
if (spec?.kind !== 'scheduled')
|
|
1968
1797
|
return;
|
|
1969
|
-
}
|
|
1970
1798
|
const inputs = spec.inputs;
|
|
1971
1799
|
const raw = inputs?.every;
|
|
1972
1800
|
if (raw === undefined || raw === null || (typeof raw === 'string' && raw.trim() === '')) {
|
|
@@ -2262,14 +2090,9 @@ function checkParallel(s, diags) {
|
|
|
2262
2090
|
+ `terminate its own arm, or move the earlier step(s) before the \`.parallel(…)\` call.`, `.parallel('${s.name}', [(a) => a.terminate('${term.name}'), …])`);
|
|
2263
2091
|
}
|
|
2264
2092
|
});
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
if (fanIn)
|
|
2269
|
-
diags.push(fanInDiag(s.name, i, steps, fanIn));
|
|
2270
|
-
for (const h of hitlEndHazards(steps))
|
|
2271
|
-
diags.push(endDiag(s.name, h));
|
|
2272
|
-
});
|
|
2093
|
+
// A human task whose alternative outcomes would each enter the join, or whose
|
|
2094
|
+
// End outcome would starve it, is the serializer's HITL_FAN_OUT_INTO_JOIN /
|
|
2095
|
+
// HITL_END_OUTCOME_IN_PARALLEL (worded from `flow-parallel-hitl.ts`).
|
|
2273
2096
|
// An arm that ends the flow reaches an End node instead of the Merge, so it
|
|
2274
2097
|
// contributes NO incoming edge. Verified on the emitted graph: with two arms
|
|
2275
2098
|
// and one returning, the Merge has a single incoming edge and is no longer a
|
|
@@ -2293,202 +2116,6 @@ function checkParallel(s, diags) {
|
|
|
2293
2116
|
});
|
|
2294
2117
|
}
|
|
2295
2118
|
}
|
|
2296
|
-
/**
|
|
2297
|
-
* Whether a `.parallel` arm hands its join an edge, as serialize wires it: it
|
|
2298
|
-
* has a step, and `armTails` (serialize's `emit` tail for tail) leaves it at
|
|
2299
|
-
* least one tail. So an arm that ends by `.return`, `.terminate`, `.break`,
|
|
2300
|
-
* `.continue` or a `.stepToRef` on the default port does not, and neither does
|
|
2301
|
-
* one that ends in a `.branch` / `.switch` / `.stepSwitch` every arm of which
|
|
2302
|
-
* ends, or in a nested `.parallel` none of whose arms reaches its own join.
|
|
2303
|
-
* When no arm hands on a tail, serialize emits no Merge at all (#1267), so the
|
|
2304
|
-
* join rules (PARALLEL_ARM_RETURNS, REF_INTO_PARALLEL_ARM,
|
|
2305
|
-
* REF_OUT_OF_PARALLEL_ARM) have no join to protect.
|
|
2306
|
-
*/
|
|
2307
|
-
function armReachesJoin(arm) {
|
|
2308
|
-
return arm.length > 0 && armTails(arm, [{ from: '' }]).length > 0;
|
|
2309
|
-
}
|
|
2310
|
-
/**
|
|
2311
|
-
* The edges a step list hands on, mirroring serialize's `emit` tail for tail.
|
|
2312
|
-
*
|
|
2313
|
-
* `.branch` / `.switch` / `.stepSwitch` hand on every arm's tails (an empty arm
|
|
2314
|
-
* hands on the port it started from), a `.parallel` its Merge when an arm
|
|
2315
|
-
* reaches it and nothing otherwise, a fan-out human task one per continuing
|
|
2316
|
-
* outcome, a routed one its primary outcome; `.return` / `.terminate` / `.break`
|
|
2317
|
-
* and a `stepToRef('output', …)` hand on none, `.stepToList` leaves the path as
|
|
2318
|
-
* it was, and every other step hands on its own single edge.
|
|
2319
|
-
*/
|
|
2320
|
-
function armTails(steps, incoming) {
|
|
2321
|
-
let tails = incoming;
|
|
2322
|
-
for (const st of steps) {
|
|
2323
|
-
if (st.kind === 'stepToList')
|
|
2324
|
-
continue;
|
|
2325
|
-
if (st.kind === 'stepToRef') {
|
|
2326
|
-
if (st.port === 'output')
|
|
2327
|
-
tails = [];
|
|
2328
|
-
continue;
|
|
2329
|
-
}
|
|
2330
|
-
if (st.kind === 'return' || st.kind === 'terminate' || st.kind === 'break' || st.kind === 'continue')
|
|
2331
|
-
return [];
|
|
2332
|
-
if (st.kind === 'action') {
|
|
2333
|
-
tails = actionTails(st.name, st.spec, st.options?.version);
|
|
2334
|
-
}
|
|
2335
|
-
else if (st.kind === 'stepSwitch') {
|
|
2336
|
-
const spec = st.spec;
|
|
2337
|
-
const outcomes = spec.kind === 'hitl' && hitlDerivesOutcomeHandles(spec.inputs?.variant);
|
|
2338
|
-
tails = st.cases.flatMap((c) => armTails(c.body, [{ from: st.name, ...(outcomes ? { outcome: String(c.value) } : {}) }])
|
|
2339
|
-
.map((t) => (outcomes && !t.humanSwitch ? { ...t, humanSwitch: { step: st.name, outcome: String(c.value) } } : t)));
|
|
2340
|
-
}
|
|
2341
|
-
else if (st.kind === 'branch') {
|
|
2342
|
-
tails = [...armTails(st.then, [{ from: st.name }]), ...armTails(st.otherwise, [{ from: st.name }])];
|
|
2343
|
-
}
|
|
2344
|
-
else if (st.kind === 'switch') {
|
|
2345
|
-
tails = [
|
|
2346
|
-
...st.cases.flatMap((c) => armTails(c.body, [{ from: st.name }])),
|
|
2347
|
-
...(st.default !== undefined ? armTails(st.default, [{ from: st.name }]) : []),
|
|
2348
|
-
];
|
|
2349
|
-
}
|
|
2350
|
-
else if (st.kind === 'parallel') {
|
|
2351
|
-
// serialize joins a nested `.parallel` at its Merge, the one edge it hands
|
|
2352
|
-
// on, and emits no Merge, so no edge, when no arm hands on a tail (#1267).
|
|
2353
|
-
// An empty arm hands on the path it started from, which the Merge joins.
|
|
2354
|
-
const arms = Array.isArray(st.arms) ? st.arms : [];
|
|
2355
|
-
tails = arms.some((arm) => armTails(arm, tails).length > 0) ? [{ from: st.name }] : [];
|
|
2356
|
-
}
|
|
2357
|
-
else {
|
|
2358
|
-
tails = [{ from: st.name ?? st.kind }];
|
|
2359
|
-
}
|
|
2360
|
-
}
|
|
2361
|
-
return tails;
|
|
2362
|
-
}
|
|
2363
|
-
function actionTails(name, spec, version) {
|
|
2364
|
-
if (spec.kind !== 'hitl' || !hitlDerivesOutcomeHandles(spec.inputs?.variant))
|
|
2365
|
-
return [{ from: name }];
|
|
2366
|
-
const model = hitlExitModel(spec.inputs, version);
|
|
2367
|
-
if (model.shape === 'placeholder')
|
|
2368
|
-
return [{ from: name }];
|
|
2369
|
-
if (model.shape === 'per-outcome')
|
|
2370
|
-
return model.exits.slice(0, 1).map((e) => ({ from: name, outcome: e.value }));
|
|
2371
|
-
return model.exits.filter((e) => !e.endsProcess).map((e) => ({ from: name, outcome: e.value }));
|
|
2372
|
-
}
|
|
2373
|
-
/**
|
|
2374
|
-
* The tails of a `.parallel` arm when they would starve its join: two or more
|
|
2375
|
-
* alternative edges — only one fires — at least one of which is a human task's
|
|
2376
|
-
* outcome edge, or the end of a human-task `.stepSwitch` arm. The join waits
|
|
2377
|
-
* for a token on every incoming flow.
|
|
2378
|
-
*
|
|
2379
|
-
* However the task got there, it is the same graph: a fan-out task at the arm's
|
|
2380
|
-
* end (its outcomes are the alternatives), a `.stepSwitch` with an empty arm
|
|
2381
|
-
* beside a body arm, a complete `.stepSwitch` with nothing after it (one edge
|
|
2382
|
-
* per arm's last step), a task on one side of a `.branch`. serialize refuses the
|
|
2383
|
-
* same condition, edge for edge. Rejoining arms with NO human task in them are
|
|
2384
|
-
* the older, general case, and not refused here.
|
|
2385
|
-
*/
|
|
2386
|
-
function hitlFanInto(steps) {
|
|
2387
|
-
const tails = armTails(steps, [{ from: '(fork)' }]);
|
|
2388
|
-
return tails.length >= 2 && tails.some((t) => t.outcome !== undefined || t.humanSwitch !== undefined) ? tails : undefined;
|
|
2389
|
-
}
|
|
2390
|
-
function fanInDiag(join, arm, steps, tails) {
|
|
2391
|
-
const tagged = tails.find((t) => t.outcome !== undefined || t.humanSwitch !== undefined);
|
|
2392
|
-
const task = tagged.outcome !== undefined ? tagged.from : tagged.humanSwitch.step;
|
|
2393
|
-
if (tails.every((t) => t.from === task && t.outcome !== undefined)) {
|
|
2394
|
-
return {
|
|
2395
|
-
level: 'error', code: 'HITL_FAN_OUT_INTO_JOIN', step: task,
|
|
2396
|
-
message: `Human task "${task}" ends its path inside parallel "${join}", so its outcomes `
|
|
2397
|
-
+ `${tails.map((t) => t.outcome).join(', ')} each get their own edge into the join. Only one outcome ever `
|
|
2398
|
-
+ `fires, and the join waits for a token on every incoming edge, so the run never gets past "${join}" — with `
|
|
2399
|
-
+ `no incident, and \`flow validate\` answers Valid. Add a step after "${task}" inside the arm, so every `
|
|
2400
|
-
+ `outcome continues to that step and only its one edge reaches the join (the decision is `
|
|
2401
|
-
+ `out('${task}', 'Action')), or move the task out of the \`.parallel(…)\`.`,
|
|
2402
|
-
suggestion: `a.step('${task}', hitl(…)).step('record', …)`,
|
|
2403
|
-
};
|
|
2404
|
-
}
|
|
2405
|
-
const edges = tails.map((t) => (t.outcome !== undefined ? `outcome ${t.outcome} of "${t.from}"`
|
|
2406
|
-
: t.humanSwitch ? `"${t.from}" (on outcome ${t.humanSwitch.outcome} of "${t.humanSwitch.step}")` : `"${t.from}"`));
|
|
2407
|
-
const last = [...steps].reverse().find((x) => x.kind !== 'stepToList' && x.kind !== 'stepToRef');
|
|
2408
|
-
return {
|
|
2409
|
-
level: 'error', code: 'HITL_FAN_OUT_INTO_JOIN', step: task,
|
|
2410
|
-
message: `Parallel "${join}" arm ${arm + 1} ends on ${tails.length} alternative paths, and each gets its own edge `
|
|
2411
|
-
+ `into the join: ${edges.join(', ')}. Only one of them runs, and the join waits for a token on every incoming `
|
|
2412
|
-
+ `edge, so the run never gets past "${join}" — with no incident, and \`flow validate\` answers Valid. Add a `
|
|
2413
|
-
+ `step at the end of the arm${last?.name ? `, after "${last.name}"` : ''}, so the paths meet on that step and `
|
|
2414
|
-
+ `only its one edge reaches the join, or move human task "${task}" out of the \`.parallel(…)\`.`,
|
|
2415
|
-
suggestion: `….step('record', …) // last step of arm ${commentText(arm + 1)}`,
|
|
2416
|
-
};
|
|
2417
|
-
}
|
|
2418
|
-
/**
|
|
2419
|
-
* Human tasks in one `.parallel` arm that continue every outcome while some of
|
|
2420
|
-
* them are `action: 'End'`. The End outcome's path stops at its End node, so the
|
|
2421
|
-
* join waits for the continuing path that did not run.
|
|
2422
|
-
*
|
|
2423
|
-
* Recurses into `.branch` / `.switch` / `.stepSwitch` bodies, which reach the
|
|
2424
|
-
* join too. Not into a loop body (the loop node is what reaches the join) or a
|
|
2425
|
-
* nested `.parallel` (its own join is checked on its own).
|
|
2426
|
-
*/
|
|
2427
|
-
function hitlEndHazards(steps) {
|
|
2428
|
-
const found = [];
|
|
2429
|
-
for (const st of steps) {
|
|
2430
|
-
if (st.kind === 'action' && st.spec.kind === 'hitl') {
|
|
2431
|
-
const model = hitlExitModel(st.spec.inputs, st.options?.version);
|
|
2432
|
-
if (model.shape !== 'every-outcome')
|
|
2433
|
-
continue;
|
|
2434
|
-
const ending = model.exits.filter((e) => e.endsProcess).map((e) => e.value);
|
|
2435
|
-
if (ending.length > 0 && ending.length < model.exits.length)
|
|
2436
|
-
found.push({ step: st.name, outcomes: ending, via: 'action-end' });
|
|
2437
|
-
}
|
|
2438
|
-
else if (st.kind === 'branch') {
|
|
2439
|
-
found.push(...hitlEndHazards(st.then), ...hitlEndHazards(st.otherwise));
|
|
2440
|
-
}
|
|
2441
|
-
else if (st.kind === 'switch') {
|
|
2442
|
-
for (const c of st.cases)
|
|
2443
|
-
found.push(...hitlEndHazards(c.body));
|
|
2444
|
-
if (st.default)
|
|
2445
|
-
found.push(...hitlEndHazards(st.default));
|
|
2446
|
-
}
|
|
2447
|
-
else if (st.kind === 'stepSwitch') {
|
|
2448
|
-
// An outcome with no arm gets an End (serialize's `endExit`), and so does
|
|
2449
|
-
// an arm that ends in `.return()`. Beside an arm that continues, that End
|
|
2450
|
-
// is where this arm's token stops on that outcome.
|
|
2451
|
-
const spec = st.spec;
|
|
2452
|
-
const exits = spec.kind === 'hitl' && hitlDerivesOutcomeHandles(spec.inputs?.variant)
|
|
2453
|
-
? stepExits(st.spec, st.options?.version) : undefined;
|
|
2454
|
-
if (exits && hasExits(exits)) {
|
|
2455
|
-
const stopped = exits.filter((e) => {
|
|
2456
|
-
const c = st.cases.find((x) => x.value === e.value);
|
|
2457
|
-
return !c || (armTails(c.body, [{ from: st.name }]).length === 0 && stopsAtEnd(c.body));
|
|
2458
|
-
}).map((e) => e.value);
|
|
2459
|
-
if (stopped.length > 0 && armTails([st], [{ from: '(fork)' }]).length > 0) {
|
|
2460
|
-
found.push({ step: st.name, outcomes: stopped, via: 'step-switch' });
|
|
2461
|
-
}
|
|
2462
|
-
}
|
|
2463
|
-
for (const c of st.cases)
|
|
2464
|
-
found.push(...hitlEndHazards(c.body));
|
|
2465
|
-
}
|
|
2466
|
-
}
|
|
2467
|
-
return found;
|
|
2468
|
-
}
|
|
2469
|
-
function endDiag(join, h) {
|
|
2470
|
-
const list = h.outcomes.join(', ');
|
|
2471
|
-
if (h.via === 'step-switch') {
|
|
2472
|
-
return {
|
|
2473
|
-
level: 'error', code: 'HITL_END_OUTCOME_IN_PARALLEL', step: h.step,
|
|
2474
|
-
message: `Human task "${h.step}" is a .stepSwitch inside parallel "${join}", and ${h.outcomes.length === 1
|
|
2475
|
-
? `outcome ${list} has` : `outcomes ${list} have`} no arm, or an arm that ends in .return(). That path `
|
|
2476
|
-
+ `stops at an End node, while the join waits for a token from this arm on the path that continues, so the `
|
|
2477
|
-
+ `run never gets past "${join}" — with no incident, and \`flow validate\` answers Valid. Give every outcome `
|
|
2478
|
-
+ `an arm that continues (and move a .return() after the \`.parallel(…)\`), or move the task out of the `
|
|
2479
|
-
+ `\`.parallel(…)\`.`,
|
|
2480
|
-
suggestion: `{ value: '${h.outcomes[0]}', body: (b) => b.step(…) }`,
|
|
2481
|
-
};
|
|
2482
|
-
}
|
|
2483
|
-
return {
|
|
2484
|
-
level: 'error', code: 'HITL_END_OUTCOME_IN_PARALLEL', step: h.step,
|
|
2485
|
-
message: `Human task "${h.step}" is inside parallel "${join}", and ${h.outcomes.length === 1
|
|
2486
|
-
? `outcome ${list} is` : `outcomes ${list} are`} action: 'End'. `
|
|
2487
|
-
+ `That path stops at its own End node, while the join waits for a token from this arm on its `
|
|
2488
|
-
+ `continuing path, so the run never gets past "${join}". Drop action: 'End' there and act on `
|
|
2489
|
-
+ `out('${h.step}', 'Action') after the join, or move the task out of the \`.parallel(…)\`.`,
|
|
2490
|
-
};
|
|
2491
|
-
}
|
|
2492
2119
|
/**
|
|
2493
2120
|
* Diagnose a `transform` step.
|
|
2494
2121
|
*
|
|
@@ -2504,19 +2131,6 @@ function endDiag(join, h) {
|
|
|
2504
2131
|
* reference to hide — which is what the checks below enforce for the cases
|
|
2505
2132
|
* where types have been stripped at run time.
|
|
2506
2133
|
*/
|
|
2507
|
-
/**
|
|
2508
|
-
* Why a base or quick-form task continues every outcome to the next step, as a
|
|
2509
|
-
* clause for a diagnostic. `pinned` is the step's `{ version }`.
|
|
2510
|
-
*/
|
|
2511
|
-
function fanOutWhy(why, pinned) {
|
|
2512
|
-
switch (why) {
|
|
2513
|
-
case 'outcomePorts: false': return 'outcomePorts: false says so';
|
|
2514
|
-
case 'quick-form': return 'a quick-form task does not unless it sets outcomePorts: true';
|
|
2515
|
-
case 'pinned': return `{ version: '${pinned}' } pins it without outcomePorts: true`;
|
|
2516
|
-
case 'one outcome': return 'it has a single outcome';
|
|
2517
|
-
case 'every outcome ends': return "every outcome is action: 'End'";
|
|
2518
|
-
}
|
|
2519
|
-
}
|
|
2520
2134
|
/**
|
|
2521
2135
|
* HITL_OUTCOMES_UNREACHABLE — a human task offers the reviewer a CHOICE that
|
|
2522
2136
|
* nothing downstream can act on.
|
|
@@ -2703,7 +2317,7 @@ function registeredKind(spec) {
|
|
|
2703
2317
|
* which serialize enforces (#942) and the type's tests pin; an author cannot
|
|
2704
2318
|
* cause the gap, so there is no author diagnostic to report.
|
|
2705
2319
|
*/
|
|
2706
|
-
function checkRegisteredStep(entry, s, exprs, errorSource, opts, diags, typeOf, triggerId,
|
|
2320
|
+
function checkRegisteredStep(entry, s, exprs, errorSource, opts, diags, typeOf, triggerId, carried) {
|
|
2707
2321
|
const spec = s.spec;
|
|
2708
2322
|
/** The step's node-local rules and expressions, as `impl` defines them. */
|
|
2709
2323
|
const runRules = (impl, version = impl.version, carriedDef) => {
|
|
@@ -2718,30 +2332,14 @@ function checkRegisteredStep(entry, s, exprs, errorSource, opts, diags, typeOf,
|
|
|
2718
2332
|
return name ? { name, type: typeOf.get(name) } : undefined;
|
|
2719
2333
|
},
|
|
2720
2334
|
options: opts,
|
|
2721
|
-
bindingLabels: (where, labels,
|
|
2335
|
+
bindingLabels: (where, labels, _use, target) => checkBindingLabels(where, labels, opts, (code, message) => diags.push({ level: 'error', code, step: s.name, message }), target),
|
|
2336
|
+
resources: { derivedNodeId: () => { }, claimNodeId: () => undefined, claimFolder: () => undefined },
|
|
2337
|
+
// The operation, its field contract and its binding labels are the
|
|
2338
|
+
// serializer's (CONNECTOR_*, BINDING_*), reported from its run.
|
|
2722
2339
|
connector: (c) => {
|
|
2723
2340
|
const spec = c;
|
|
2724
2341
|
checkConnectorLookups(s.name, spec, opts, diags);
|
|
2725
2342
|
checkConnectorSchema(s.name, spec, opts, diags);
|
|
2726
|
-
checkConnectorContract(s.name, spec, opts, diags);
|
|
2727
|
-
},
|
|
2728
|
-
resources: {
|
|
2729
|
-
derivedNodeId: (id, what) => { if (!ledger.taken.has(id))
|
|
2730
|
-
ledger.taken.set(id, what); },
|
|
2731
|
-
claimNodeId: (id, what) => {
|
|
2732
|
-
const other = ledger.explicit.get(id);
|
|
2733
|
-
if (other === undefined)
|
|
2734
|
-
ledger.explicit.set(id, { step: s.name, what });
|
|
2735
|
-
return other;
|
|
2736
|
-
},
|
|
2737
|
-
claimFolder: (source) => {
|
|
2738
|
-
const key = source.toLowerCase();
|
|
2739
|
-
const other = ledger.agentSources.get(key);
|
|
2740
|
-
if (other !== undefined && other !== s.name)
|
|
2741
|
-
return other;
|
|
2742
|
-
ledger.agentSources.set(key, s.name);
|
|
2743
|
-
return undefined;
|
|
2744
|
-
},
|
|
2745
2343
|
},
|
|
2746
2344
|
};
|
|
2747
2345
|
impl.check?.(spec, ctx);
|
|
@@ -2771,7 +2369,7 @@ function checkRegisteredStep(entry, s, exprs, errorSource, opts, diags, typeOf,
|
|
|
2771
2369
|
// A select() that picks an unregistered version is the node type's bug, not the author's pin.
|
|
2772
2370
|
if (isInternalSdkError(e))
|
|
2773
2371
|
throw e;
|
|
2774
|
-
|
|
2372
|
+
// The pin itself is the serializer's NODE_VERSION_UNREGISTERED (`serializerRefusals`).
|
|
2775
2373
|
// Still report the step's other mistakes in this same pass, with the version it
|
|
2776
2374
|
// would get unpinned (select(), else the default) — as decompile lifts an
|
|
2777
2375
|
// unregistered version — so fixing the pin is not followed by a second round.
|
|
@@ -2899,12 +2497,6 @@ function collectActionExprs(s, out, errorSource) {
|
|
|
2899
2497
|
* registered node type — whose node type has no error port. One spelling for
|
|
2900
2498
|
* both, so porting a family onto the registry changes no message.
|
|
2901
2499
|
*/
|
|
2902
|
-
function typedNoErrorPortMessage(stepName, kind, def) {
|
|
2903
|
-
const nodeType = def ? ` (${def.nodeType}@${def.version})` : '';
|
|
2904
|
-
return `Step "${stepName}" is a ${kind} step${nodeType}, and that node type has no error port — its definition does not `
|
|
2905
|
-
+ `set supportsErrorHandling, so the edge would leave a handle the node does not have. Handle the failure `
|
|
2906
|
-
+ `downstream instead (a delay cannot fail; a wait resumes or does not).`;
|
|
2907
|
-
}
|
|
2908
2500
|
/** The five fields the platform's error envelope declares — no more, no less. */
|
|
2909
2501
|
// The five the definitions declare, plus the two every probed runtime envelope
|
|
2910
2502
|
// carried anyway (`response`, `element`) — see ErrorEnvelopeField in core/expr.ts.
|
|
@@ -2914,56 +2506,27 @@ const ERROR_ENVELOPE_FIELDS = ['code', 'message', 'detail', 'category', 'status'
|
|
|
2914
2506
|
* mistakes at build time (no previous step, two handlers on one node, an empty
|
|
2915
2507
|
* handler); these are the ones only a checker can see.
|
|
2916
2508
|
*
|
|
2917
|
-
* Whether the step has an error port at all
|
|
2918
|
-
* definition, never
|
|
2919
|
-
*
|
|
2920
|
-
*
|
|
2921
|
-
*
|
|
2922
|
-
*
|
|
2923
|
-
*
|
|
2924
|
-
*
|
|
2925
|
-
* `publishedFunction` was refused here although `publishedFunctionDef` sets
|
|
2926
|
-
* the flag), or for a connector the library's definition, which check reads
|
|
2927
|
-
* through `connectorDefinition` and, without a library, assumes grants the
|
|
2928
|
-
* port, as every operation the library serves does.
|
|
2929
|
-
*
|
|
2930
|
-
* `publishedFunction`'s flag is measured, not assumed: on 2026-10-08 four
|
|
2931
|
-
* `flow debug` jobs on alpha/codereval called `acme-echo` with a non-string
|
|
2932
|
-
* `message` (null, object, number, array). In each, the function job failed
|
|
2933
|
-
* (`Python.FUNCTION_EXECUTION_ERROR`, category User, status 502), the node's
|
|
2934
|
-
* error boundary fired, and the handler's `err('message')` read
|
|
2935
|
-
* "Failure in the Orchestrator Job".
|
|
2936
|
-
*
|
|
2937
|
-
* A kind in none of these (`waitEvent`) has no error port: an event wait
|
|
2938
|
-
* resumes or does not.
|
|
2509
|
+
* Whether the step has an error port at all is the serializer's
|
|
2510
|
+
* ONERROR_NO_ERROR_PORT: it asks the definition it emits, never a list (a
|
|
2511
|
+
* bundled family's bundled definition, #1011; a per-tenant one's `errorPort`,
|
|
2512
|
+
* #1040; a custom node's own definition, #943). `publishedFunction`'s port is
|
|
2513
|
+
* measured, not assumed: on 2026-10-08 four `flow debug` jobs on
|
|
2514
|
+
* alpha/codereval called `acme-echo` with a non-string `message`, and in each
|
|
2515
|
+
* the node's error boundary fired and the handler read "Failure in the
|
|
2516
|
+
* Orchestrator Job".
|
|
2939
2517
|
*/
|
|
2940
2518
|
function checkOnError(_handler, step, diags,
|
|
2941
2519
|
/** The step's registry record, when its kind is registered and its version resolved. */
|
|
2942
|
-
placed, opts
|
|
2520
|
+
placed, opts) {
|
|
2943
2521
|
// The builder refuses a handler with no eligible step before it, so a missing
|
|
2944
2522
|
// source here means a hand-built `BuiltFlow`; there is nothing to say about it.
|
|
2945
|
-
|
|
2523
|
+
// A step whose definition has no error port is the serializer's
|
|
2524
|
+
// ONERROR_NO_ERROR_PORT, asked of the definition it emits (#943, #1011). What
|
|
2525
|
+
// is left here is a node type's own refusal of a handler on a port it has.
|
|
2526
|
+
if (!step || step.kind === 'loop' || step.kind === 'doWhile')
|
|
2946
2527
|
return;
|
|
2947
|
-
// A
|
|
2948
|
-
//
|
|
2949
|
-
// to the container's port, measured: the container's envelope carries the
|
|
2950
|
-
// body's message in `detail` and the failing body step's canvas label (its id when it has no `label`) in `element`, and
|
|
2951
|
-
// the instance completes. `core.logic.dowhile@1.0` declares no `error` handle,
|
|
2952
|
-
// but the flag alone grants it: flow validate adds the handle from the flag.
|
|
2953
|
-
if (step.kind === 'loop' || step.kind === 'doWhile') {
|
|
2954
|
-
const def = bundledContainerDefinition(step.kind, step.options?.version);
|
|
2955
|
-
if (def === undefined || definitionHasErrorPort(def))
|
|
2956
|
-
return;
|
|
2957
|
-
diags.push({
|
|
2958
|
-
level: 'error',
|
|
2959
|
-
code: 'ONERROR_NO_ERROR_PORT',
|
|
2960
|
-
message: errorPortRefusal(step.name, def),
|
|
2961
|
-
step: step.name,
|
|
2962
|
-
});
|
|
2963
|
-
return;
|
|
2964
|
-
}
|
|
2965
|
-
// A custom FlowAction or rawNode has no `kind`: ask its DEFINITION, with the
|
|
2966
|
-
// product's own rule (definitionHasErrorPort), not the kind list (#943).
|
|
2528
|
+
// A custom node backed by a typed family's bundled definition gets that
|
|
2529
|
+
// family's refusals: the rules are about the node type, not the SDK kind.
|
|
2967
2530
|
const maybeCustom = step.spec;
|
|
2968
2531
|
if (!('kind' in maybeCustom) && maybeCustom instanceof FlowNode) {
|
|
2969
2532
|
let def;
|
|
@@ -2972,98 +2535,31 @@ placed, opts, carried) {
|
|
|
2972
2535
|
def = resolveContributionDefinition(maybeCustom.contribute(requestedVersion !== undefined ? { requestedVersion } : {}).definition, requestedVersion, `step "${step.name}"`);
|
|
2973
2536
|
}
|
|
2974
2537
|
catch {
|
|
2975
|
-
//
|
|
2538
|
+
// A contribution compile cannot lower is the serializer's CONTRIBUTION_INVALID.
|
|
2976
2539
|
return;
|
|
2977
2540
|
}
|
|
2978
|
-
|
|
2979
|
-
// family's refusals: the rules are about the node type, not the SDK kind.
|
|
2980
|
-
if (def?.nodeType === 'core.action.http') {
|
|
2541
|
+
if (def?.nodeType === 'core.action.http')
|
|
2981
2542
|
diags.push(plainHttpOnErrorRefusal(step.name));
|
|
2982
|
-
return;
|
|
2983
|
-
}
|
|
2984
|
-
if (definitionHasErrorPort(def))
|
|
2985
|
-
return;
|
|
2986
|
-
diags.push({
|
|
2987
|
-
level: 'error',
|
|
2988
|
-
code: 'ONERROR_NO_ERROR_PORT',
|
|
2989
|
-
message: errorPortRefusal(step.name, def),
|
|
2990
|
-
step: step.name,
|
|
2991
|
-
});
|
|
2992
|
-
return;
|
|
2993
|
-
}
|
|
2994
|
-
const spec = step.spec;
|
|
2995
|
-
// A registered node type: a bundled one asks its definition at the version it
|
|
2996
|
-
// will be emitted at; a per-tenant one answers for itself (its errorPort).
|
|
2997
|
-
if (registeredKind(spec)) {
|
|
2998
|
-
// An unregistered pin still has a record, with the version it would get
|
|
2999
|
-
// unpinned; NODE_VERSION_UNREGISTERED reports the pin itself.
|
|
3000
|
-
if (!placed)
|
|
3001
|
-
return;
|
|
3002
|
-
const { impl } = placed;
|
|
3003
|
-
if (impl.definition !== 'bundled' && placed.carried !== undefined) {
|
|
3004
|
-
// At a carried version the carried definition answers, as serialize asks
|
|
3005
|
-
// it: the type's declaration covers only the versions it registers.
|
|
3006
|
-
if (!definitionHasErrorPort(placed.carried)) {
|
|
3007
|
-
diags.push({ level: 'error', code: 'ONERROR_NO_ERROR_PORT', message: typedNoErrorPortMessage(step.name, impl.kind, placed.carried), step: step.name });
|
|
3008
|
-
return;
|
|
3009
|
-
}
|
|
3010
|
-
}
|
|
3011
|
-
else if (impl.definition !== 'bundled') {
|
|
3012
|
-
// The definition serialize will be supplied, when check's options can name
|
|
3013
|
-
// it (a connector's, from the library); the type answers without it otherwise.
|
|
3014
|
-
const def = impl.checkDefinition?.(placed.spec, opts);
|
|
3015
|
-
const port = typeof impl.errorPort === 'function' ? impl.errorPort(spec, def) : impl.errorPort === true;
|
|
3016
|
-
if (!port) {
|
|
3017
|
-
const named = def !== undefined && typeof def.nodeType === 'string' && typeof def.version === 'string';
|
|
3018
|
-
diags.push({
|
|
3019
|
-
level: 'error', code: 'ONERROR_NO_ERROR_PORT', step: step.name,
|
|
3020
|
-
message: named
|
|
3021
|
-
? declaredErrorPortRefusal(step.name, def.nodeType, def.version)
|
|
3022
|
-
: declaredErrorPortRefusal(step.name, impl.typeName, impl.version),
|
|
3023
|
-
});
|
|
3024
|
-
return;
|
|
3025
|
-
}
|
|
3026
|
-
}
|
|
3027
|
-
else {
|
|
3028
|
-
const def = placed.carried ?? bundledDefinition(impl.typeName, impl.version);
|
|
3029
|
-
if (def !== undefined && !definitionHasErrorPort(def)) {
|
|
3030
|
-
diags.push({ level: 'error', code: 'ONERROR_NO_ERROR_PORT', message: typedNoErrorPortMessage(step.name, impl.kind, def), step: step.name });
|
|
3031
|
-
return;
|
|
3032
|
-
}
|
|
3033
|
-
}
|
|
3034
|
-
// The port exists; the node type may still refuse the handler (plain HTTP's rule, once ported).
|
|
3035
|
-
impl.checkErrorHandler?.(placed.spec, placed.ctx);
|
|
3036
2543
|
return;
|
|
3037
2544
|
}
|
|
3038
|
-
// A
|
|
3039
|
-
//
|
|
3040
|
-
//
|
|
3041
|
-
|
|
3042
|
-
// (which has one); `hitl({ exposeError: true })` selects uipath.human-in-the-loop@1.2
|
|
3043
|
-
// with no `{ version }` at all.
|
|
3044
|
-
// A version the SDK does not bundle (an inline agent's registry version) has
|
|
3045
|
-
// no definition here to ask; version resolution reports that, not this check.
|
|
3046
|
-
const bundledType = bundledActionNodeType(spec);
|
|
3047
|
-
// A version the flow CARRIES (`.definitions()`, #1128) answers the same way
|
|
3048
|
-
// a bundled one does: it is the definition serialize will emit.
|
|
3049
|
-
const pinned = step.options?.version;
|
|
3050
|
-
const bundledDef = bundledType
|
|
3051
|
-
? bundledDefinition(bundledType, pinned) ?? (pinned !== undefined ? carried?.(bundledType, pinned) : undefined)
|
|
3052
|
-
: undefined;
|
|
3053
|
-
// Every per-tenant family is registered and answers above (its errorPort, or
|
|
3054
|
-
// a carried definition); what is left is bundled, or has no definition here.
|
|
3055
|
-
const noPort = bundledDef !== undefined
|
|
3056
|
-
? !definitionHasErrorPort(bundledDef)
|
|
3057
|
-
: !bundledType;
|
|
3058
|
-
if (noPort) {
|
|
3059
|
-
diags.push({
|
|
3060
|
-
level: 'error',
|
|
3061
|
-
code: 'ONERROR_NO_ERROR_PORT',
|
|
3062
|
-
message: typedNoErrorPortMessage(step.name, spec.kind, bundledDef),
|
|
3063
|
-
step: step.name,
|
|
3064
|
-
});
|
|
2545
|
+
// A registered node type (an unregistered pin still has a record, with the
|
|
2546
|
+
// version it would get unpinned) may refuse the handler on a port it has
|
|
2547
|
+
// (plain HTTP's rule, once ported).
|
|
2548
|
+
if (!registeredKind(step.spec) || !placed)
|
|
3065
2549
|
return;
|
|
3066
|
-
}
|
|
2550
|
+
const { impl } = placed;
|
|
2551
|
+
const hasPort = impl.definition !== 'bundled' && placed.carried !== undefined
|
|
2552
|
+
? definitionHasErrorPort(placed.carried)
|
|
2553
|
+
: impl.definition !== 'bundled'
|
|
2554
|
+
? (typeof impl.errorPort === 'function'
|
|
2555
|
+
? impl.errorPort(step.spec, impl.checkDefinition?.(placed.spec, opts))
|
|
2556
|
+
: impl.errorPort === true)
|
|
2557
|
+
: (() => {
|
|
2558
|
+
const def = placed.carried ?? bundledDefinition(impl.typeName, impl.version);
|
|
2559
|
+
return def === undefined || definitionHasErrorPort(def);
|
|
2560
|
+
})();
|
|
2561
|
+
if (hasPort)
|
|
2562
|
+
impl.checkErrorHandler?.(placed.spec, placed.ctx);
|
|
3067
2563
|
}
|
|
3068
2564
|
/** `.onError` after a custom node built on the plain `core.action.http` definition (the http family's refusal). */
|
|
3069
2565
|
function plainHttpOnErrorRefusal(stepName) {
|
|
@@ -3225,7 +2721,7 @@ function checkArmScopedReads(built, diags) {
|
|
|
3225
2721
|
* Backward targets are ALLOWED. Flow JSON can express a backward edge, so the SDK
|
|
3226
2722
|
* can author one; refusing it would fence off something the wire format permits.
|
|
3227
2723
|
*/
|
|
3228
|
-
function checkStepRefs(built, diags
|
|
2724
|
+
function checkStepRefs(built, diags) {
|
|
3229
2725
|
const order = new Map();
|
|
3230
2726
|
/** Step name → the `.stepToList` whose port path it lives in — a path only that port reaches. */
|
|
3231
2727
|
const portListOf = new Map();
|
|
@@ -3337,56 +2833,10 @@ function checkStepRefs(built, diags, reservedIds = []) {
|
|
|
3337
2833
|
// End, so its Ends count toward the duplicate-name rule below (#1231).
|
|
3338
2834
|
for (const ep of built.entryPoints ?? [])
|
|
3339
2835
|
index(ep.steps, undefined, '', undefined);
|
|
3340
|
-
// Two `.return`s asking for
|
|
3341
|
-
//
|
|
3342
|
-
//
|
|
3343
|
-
//
|
|
3344
|
-
for (const [name, count] of endNames) {
|
|
3345
|
-
if (count < 2)
|
|
3346
|
-
continue;
|
|
3347
|
-
diags.push({
|
|
3348
|
-
level: 'error', code: 'DUP_END_NAME', step: name,
|
|
3349
|
-
message: `${count} different \`.return({}, { name: '${name}' })\` calls ask for the same End (their values, label, `
|
|
3350
|
-
+ `version or updates differ, or they sit in different scopes), so they would be ${count} End nodes with one id. `
|
|
3351
|
-
+ `To end several paths at one End, keep one \`.return({}, { name: '${name}' })\` and end the other paths with `
|
|
3352
|
-
+ `\`.stepToRef('${name}')\`; to have separate Ends, give each its own name.`,
|
|
3353
|
-
});
|
|
3354
|
-
}
|
|
3355
|
-
// A named End's node id is its name with every character outside
|
|
3356
|
-
// `[A-Za-z0-9_]` made `_`, as a step's is, and compile never renames it
|
|
3357
|
-
// (#1253): it is an id the author chose, or a decompiled file's own. So an End
|
|
3358
|
-
// whose name gives the id of another End, a step, the trigger or an entry
|
|
3359
|
-
// point is refused here, and `serialize` throws for it under `--no-check`.
|
|
3360
|
-
// Before, it was the End that moved (`.step('x').return({}, { name: 'x' })`
|
|
3361
|
-
// compiled to End `x2`), or, emitted first, the step.
|
|
3362
|
-
const safeId = (n) => n.replace(/[^A-Za-z0-9_]/g, '_') || 'node';
|
|
3363
|
-
const stepById = new Map();
|
|
3364
|
-
for (const n of stepNamesSeen)
|
|
3365
|
-
if (!stepById.has(safeId(n)))
|
|
3366
|
-
stepById.set(safeId(n), n);
|
|
3367
|
-
const endById = new Map();
|
|
3368
|
-
for (const name of endNames.keys()) {
|
|
3369
|
-
const id = safeId(name);
|
|
3370
|
-
const other = endById.get(id);
|
|
3371
|
-
if (other !== undefined) {
|
|
3372
|
-
diags.push({
|
|
3373
|
-
level: 'error', code: 'DUP_END_NAME', step: name,
|
|
3374
|
-
message: `Ends "${other}" and "${name}" would both get node id "${id}". Give them names that differ in `
|
|
3375
|
-
+ 'letters, digits or "_".',
|
|
3376
|
-
});
|
|
3377
|
-
continue;
|
|
3378
|
-
}
|
|
3379
|
-
endById.set(id, name);
|
|
3380
|
-
const step = stepById.get(id);
|
|
3381
|
-
if (step !== undefined || reservedIds.includes(id)) {
|
|
3382
|
-
diags.push({
|
|
3383
|
-
level: 'error', code: 'STEP_ID_COLLISION', step: name,
|
|
3384
|
-
message: `End "${name}" (\`.return({}, { name: '${name}' })\`) would get node id "${id}", which `
|
|
3385
|
-
+ `${step !== undefined ? `step "${step}"` : 'the trigger or an entry point'} has. Compile will not rename `
|
|
3386
|
-
+ 'either one, so give the End a name no step or trigger uses.',
|
|
3387
|
-
});
|
|
3388
|
-
}
|
|
3389
|
-
}
|
|
2836
|
+
// Two `.return`s asking for one End name with different content, and an End
|
|
2837
|
+
// whose name gives the node id of another End, a step, the trigger or an
|
|
2838
|
+
// entry point, are the serializer's DUP_END_NAME / STEP_ID_COLLISION: compile
|
|
2839
|
+
// never renames an End's id (#1231, #1253).
|
|
3390
2840
|
/** Every ref, with where it sits. */
|
|
3391
2841
|
const found = [];
|
|
3392
2842
|
const collect = (steps, from, scope, arm, enclosing = [], paths = []) => {
|
|
@@ -3444,13 +2894,9 @@ function checkStepRefs(built, diags, reservedIds = []) {
|
|
|
3444
2894
|
const say = (level, code, message, suggestion) => {
|
|
3445
2895
|
diags.push({ level, code, message, step: from ?? 'stepToRef', ...(suggestion ? { suggestion } : {}) });
|
|
3446
2896
|
};
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
say('error', 'REF_UNKNOWN_TARGET', `.stepToRef('${target}') ${where} names a step this flow does not have`
|
|
3450
|
-
+ `${near ? ` (did you mean "${near}"?)` : ''}. It takes the same name you passed to .step() or `
|
|
3451
|
-
+ `.terminate(), or the \`name\` of a \`.return({}, { name })\` End.`, near ? `.stepToRef('${near}')` : undefined);
|
|
2897
|
+
// A name the flow does not have is the serializer's REF_UNKNOWN_TARGET.
|
|
2898
|
+
if (!order.has(target))
|
|
3452
2899
|
continue;
|
|
3453
|
-
}
|
|
3454
2900
|
const ends = endNames.get(target) ?? 0;
|
|
3455
2901
|
// Two Ends sharing the name are DUP_END_NAME above, reported once whether or
|
|
3456
2902
|
// not a ref names them; this is the step-and-End clash.
|
|
@@ -3620,11 +3066,8 @@ function checkSchemaVersion(built, ancestors, diags) {
|
|
|
3620
3066
|
const declared = built.schemaVersion;
|
|
3621
3067
|
if (declared === undefined)
|
|
3622
3068
|
return;
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
diags.push({ level: 'error', code: 'SCHEMA_VERSION_UNSUPPORTED', message: refusal });
|
|
3626
|
-
return;
|
|
3627
|
-
}
|
|
3069
|
+
// A version this build cannot write is the serializer's SCHEMA_VERSION_UNSUPPORTED
|
|
3070
|
+
// on the root flow. On a subflow it has no effect, whatever it is.
|
|
3628
3071
|
if (ancestors.length > 0) {
|
|
3629
3072
|
diags.push({
|
|
3630
3073
|
level: 'warning',
|
|
@@ -3635,6 +3078,8 @@ function checkSchemaVersion(built, ancestors, diags) {
|
|
|
3635
3078
|
});
|
|
3636
3079
|
return;
|
|
3637
3080
|
}
|
|
3081
|
+
if (schemaVersionRefusal(FLOW_SCHEMA_VERSIONS, declared))
|
|
3082
|
+
return;
|
|
3638
3083
|
const floor = FLOW_SCHEMA_VERSIONS.default;
|
|
3639
3084
|
const side = compareSchemaVersions(declared, floor);
|
|
3640
3085
|
if (side > 0) {
|
|
@@ -3658,42 +3103,6 @@ function checkSchemaVersion(built, ancestors, diags) {
|
|
|
3658
3103
|
});
|
|
3659
3104
|
}
|
|
3660
3105
|
}
|
|
3661
|
-
/**
|
|
3662
|
-
* Run a custom node's contribution lowering DRY, so `check` reports what
|
|
3663
|
-
* `compile` would throw. For instance, a `rawNode` carrying an in-solution
|
|
3664
|
-
* sibling's manifest with no `values` row for `<bindings.folderPath>` fails here
|
|
3665
|
-
* with the same code compile throws.
|
|
3666
|
-
*
|
|
3667
|
-
* It calls `contribute()`, resolves the definition the way serialize does, and
|
|
3668
|
-
* plans the `bindings[]` entries serialize would build
|
|
3669
|
-
* ({@link planContributionBindings}). A name the plan cannot close is
|
|
3670
|
-
* `CONTRIBUTION_BINDING_UNRESOLVED` — the same code and sentence serialize
|
|
3671
|
-
* throws. A contribution that throws is `CONTRIBUTION_INVALID`, unless
|
|
3672
|
-
* `ownError` says the node's own `diagnostics()` already reported an error (a
|
|
3673
|
-
* raw connector node throws for the reason `RAW_NODE_CONNECTOR` already gives).
|
|
3674
|
-
*/
|
|
3675
|
-
function checkContribution(node, requestedVersion, step, ownError, diags) {
|
|
3676
|
-
let def;
|
|
3677
|
-
let supplied;
|
|
3678
|
-
try {
|
|
3679
|
-
const contribution = node.contribute(requestedVersion !== undefined ? { requestedVersion } : {});
|
|
3680
|
-
def = resolveContributionDefinition(contribution.definition, requestedVersion, `step "${step}"`);
|
|
3681
|
-
supplied = contribution.bindings;
|
|
3682
|
-
}
|
|
3683
|
-
catch (e) {
|
|
3684
|
-
if (ownError)
|
|
3685
|
-
return;
|
|
3686
|
-
diags.push({
|
|
3687
|
-
level: 'error', code: 'CONTRIBUTION_INVALID', step,
|
|
3688
|
-
message: `compile cannot lower step "${step}": ${e instanceof Error ? e.message : String(e)}`,
|
|
3689
|
-
});
|
|
3690
|
-
return;
|
|
3691
|
-
}
|
|
3692
|
-
const plan = planContributionBindings(def, supplied);
|
|
3693
|
-
if (plan.problems.length === 0)
|
|
3694
|
-
return;
|
|
3695
|
-
diags.push({ level: 'error', code: 'CONTRIBUTION_BINDING_UNRESOLVED', step, message: plan.problems.join(' ') });
|
|
3696
|
-
}
|
|
3697
3106
|
/**
|
|
3698
3107
|
* The one step every arm of a `.parallel` ends by referring to, when the arms
|
|
3699
3108
|
* join there instead of on a Merge: each arm ends in `.stepToRef(<that step>)`
|