@uipath/maestro-builder-sdk 6.18.10 → 6.20.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/dist/api-index.md +185 -184
- package/dist/api-members.md +506 -473
- package/dist/check.js +358 -695
- package/dist/cli/commands/flow.js +3 -0
- package/dist/compile-cli.js +51 -0
- package/dist/core/actions.d.ts +132 -765
- package/dist/core/actions.js +1 -307
- package/dist/core/binding-identity.d.ts +22 -0
- package/dist/core/binding-identity.js +28 -0
- package/dist/core/context-index.d.ts +34 -0
- package/dist/core/context-index.js +38 -0
- package/dist/core/definitionless-node-types.d.ts +16 -0
- package/dist/core/definitionless-node-types.js +16 -0
- package/dist/core/error-port.d.ts +6 -0
- package/dist/core/error-port.js +9 -0
- package/dist/core/form-field-type.d.ts +33 -0
- package/dist/core/form-field-type.js +30 -0
- package/dist/core/hitl-routing.d.ts +8 -0
- package/dist/core/hitl-routing.js +17 -1
- package/dist/core/node-classes.d.ts +4 -63
- package/dist/core/node-classes.js +0 -112
- package/dist/core/nodes/family.d.ts +36 -0
- package/dist/core/nodes/family.js +1 -0
- package/dist/core/nodes/node-type.d.ts +136 -0
- package/dist/core/nodes/node-type.js +1 -0
- package/dist/core/nodes/registry.d.ts +57 -0
- package/dist/core/nodes/registry.js +122 -0
- package/dist/core/nodes/versioned.d.ts +50 -0
- package/dist/core/nodes/versioned.js +46 -0
- package/dist/core/process-resource-sort-order.d.ts +40 -0
- package/dist/core/process-resource-sort-order.js +44 -0
- package/dist/core/typed-raw-node.d.ts +13 -62
- package/dist/core/typed-raw-node.js +40 -154
- package/dist/core/wire-expr.d.ts +27 -0
- package/dist/core/wire-expr.js +34 -0
- package/dist/decompile-cli.js +11 -1
- package/dist/decompile.d.ts +8 -3
- package/dist/decompile.js +665 -354
- package/dist/flow-definitions.d.ts +10 -0
- package/dist/flow-definitions.js +35 -0
- package/dist/flow-expr-check.js +1 -1
- package/dist/flow-node-registry.d.ts +13 -0
- package/dist/flow-node-registry.js +19 -0
- package/dist/flow-node-type.d.ts +192 -0
- package/dist/flow-node-type.js +1 -0
- package/dist/flow-sdk.d.ts +40 -1
- package/dist/flow-sdk.js +29 -2
- package/dist/flow-step-tree.js +2 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -0
- package/dist/merge-cli.js +25 -8
- package/dist/merge-roundtrip.d.ts +81 -0
- package/dist/merge-roundtrip.js +404 -0
- package/dist/merge.d.ts +35 -6
- package/dist/merge.js +124 -24
- package/dist/node-placement.d.ts +93 -0
- package/dist/node-placement.js +163 -0
- package/dist/nodes/data-fabric/flow/check.d.ts +13 -0
- package/dist/nodes/data-fabric/flow/check.js +164 -0
- package/dist/nodes/data-fabric/flow/factories.d.ts +113 -0
- package/dist/nodes/data-fabric/flow/factories.js +134 -0
- package/dist/nodes/data-fabric/flow/family.d.ts +7 -0
- package/dist/nodes/data-fabric/flow/family.js +9 -0
- package/dist/nodes/data-fabric/flow/index.d.ts +8 -0
- package/dist/nodes/data-fabric/flow/index.js +8 -0
- package/dist/nodes/data-fabric/flow/raw.d.ts +32 -0
- package/dist/nodes/data-fabric/flow/raw.js +100 -0
- package/dist/nodes/data-fabric/flow/shared.d.ts +77 -0
- package/dist/nodes/data-fabric/flow/shared.js +174 -0
- package/dist/nodes/data-fabric/flow/spec.d.ts +238 -0
- package/dist/nodes/data-fabric/flow/spec.js +1 -0
- package/dist/nodes/data-fabric/flow/verbs.d.ts +29 -0
- package/dist/nodes/data-fabric/flow/verbs.js +239 -0
- package/dist/nodes/delay/flow/check.d.ts +16 -0
- package/dist/nodes/delay/flow/check.js +86 -0
- package/dist/nodes/delay/flow/factories.d.ts +28 -0
- package/dist/nodes/delay/flow/factories.js +38 -0
- package/dist/nodes/delay/flow/family.d.ts +3 -0
- package/dist/nodes/delay/flow/family.js +8 -0
- package/dist/nodes/delay/flow/index.d.ts +3 -0
- package/dist/nodes/delay/flow/index.js +3 -0
- package/dist/nodes/delay/flow/node-type.d.ts +13 -0
- package/dist/nodes/delay/flow/node-type.js +71 -0
- package/dist/nodes/delay/flow/spec.d.ts +43 -0
- package/dist/nodes/delay/flow/spec.js +16 -0
- package/dist/nodes/document/flow/factories.d.ts +27 -0
- package/dist/nodes/document/flow/factories.js +30 -0
- package/dist/nodes/document/flow/family.d.ts +3 -0
- package/dist/nodes/document/flow/family.js +7 -0
- package/dist/nodes/document/flow/index.d.ts +3 -0
- package/dist/nodes/document/flow/index.js +3 -0
- package/dist/nodes/document/flow/node-type.d.ts +14 -0
- package/dist/nodes/document/flow/node-type.js +150 -0
- package/dist/nodes/document/flow/spec.d.ts +65 -0
- package/dist/nodes/document/flow/spec.js +1 -0
- package/dist/nodes/mock/flow/factories.d.ts +51 -0
- package/dist/nodes/mock/flow/factories.js +47 -0
- package/dist/nodes/mock/flow/family.d.ts +3 -0
- package/dist/nodes/mock/flow/family.js +7 -0
- package/dist/nodes/mock/flow/index.d.ts +2 -0
- package/dist/nodes/mock/flow/index.js +2 -0
- package/dist/nodes/mock/flow/node-type.d.ts +11 -0
- package/dist/nodes/mock/flow/node-type.js +51 -0
- package/dist/nodes/script/flow/check.d.ts +19 -0
- package/dist/nodes/script/flow/check.js +49 -0
- package/dist/nodes/script/flow/factories.d.ts +14 -0
- package/dist/nodes/script/flow/factories.js +15 -0
- package/dist/nodes/script/flow/family.d.ts +3 -0
- package/dist/nodes/script/flow/family.js +8 -0
- package/dist/nodes/script/flow/index.d.ts +3 -0
- package/dist/nodes/script/flow/index.js +3 -0
- package/dist/nodes/script/flow/node-type.d.ts +7 -0
- package/dist/nodes/script/flow/node-type.js +125 -0
- package/dist/nodes/script/flow/spec.d.ts +52 -0
- package/dist/nodes/script/flow/spec.js +1 -0
- package/dist/nodes/transform/flow/check.d.ts +6 -0
- package/dist/nodes/transform/flow/check.js +229 -0
- package/dist/nodes/transform/flow/factories.d.ts +52 -0
- package/dist/nodes/transform/flow/factories.js +53 -0
- package/dist/nodes/transform/flow/family.d.ts +3 -0
- package/dist/nodes/transform/flow/family.js +8 -0
- package/dist/nodes/transform/flow/index.d.ts +3 -0
- package/dist/nodes/transform/flow/index.js +3 -0
- package/dist/nodes/transform/flow/node-type.d.ts +7 -0
- package/dist/nodes/transform/flow/node-type.js +245 -0
- package/dist/nodes/transform/flow/spec.d.ts +116 -0
- package/dist/nodes/transform/flow/spec.js +10 -0
- package/dist/raw-node-advice.d.ts +16 -0
- package/dist/raw-node-advice.js +36 -0
- package/dist/raw-node.d.ts +71 -0
- package/dist/raw-node.js +116 -0
- package/dist/registry/uip/pipeline-front-door.js +5 -3
- package/dist/serialize.js +246 -549
- package/package.json +2 -2
package/dist/check.js
CHANGED
|
@@ -5,11 +5,12 @@
|
|
|
5
5
|
* duplicate step names, references to undeclared variables/steps, empty branch
|
|
6
6
|
* arms, and missing terminals.
|
|
7
7
|
*/
|
|
8
|
-
import { Expr, SCHEDULE_PRESETS,
|
|
8
|
+
import { Expr, SCHEDULE_PRESETS, 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
11
|
import { definitionHasErrorPort, errorPortRefusal } from './core/error-port.js';
|
|
12
12
|
import { bundledActionNodeType, bundledDefinition, resolveContributionDefinition } from './serialize.js';
|
|
13
|
+
import { sdkFormFieldType } from './core/form-field-type.js';
|
|
13
14
|
import { QUEUE_ITEM_RECORD_SCHEMA } from './core/queue-record.js';
|
|
14
15
|
import { AGENTIC_PROCESS_FORMS } from './core/agentic-process-forms.js';
|
|
15
16
|
import { checkEventResolution, checkEventSubscription, eventParamsFor, } from './core/event-checks.js';
|
|
@@ -19,13 +20,16 @@ import { prepareCommand } from './core/cli-spelling.js';
|
|
|
19
20
|
import { commentText } from './core/comment-text.js';
|
|
20
21
|
import { editDistance } from './core/edit-distance.js';
|
|
21
22
|
import { isAnswerField } from './core/hitl-answers.js';
|
|
22
|
-
import { hitlDerivesOutcomeHandles, hitlExitModel, hitlRoutesPerOutcome } from './core/hitl-routing.js';
|
|
23
|
+
import { hitlDerivesOutcomeHandles, hitlExitModel, hitlRoutesPerOutcome, outcomeIdOf } from './core/hitl-routing.js';
|
|
23
24
|
import { declaredExits, hasExits } from './core/step-ports.js';
|
|
24
25
|
import { stopsAtEnd } from './flow-step-tree.js';
|
|
25
26
|
import { FLOW_SCHEMA_VERSIONS } from './format-profile.js';
|
|
26
27
|
import { compareSchemaVersions, schemaVersionRefusal } from './schema-version.js';
|
|
27
28
|
import { planFlowGlobals, planSharedInputs, sharedInputRoots } from './flow-variables.js';
|
|
28
29
|
import { ownEntry } from './core/own-entry.js';
|
|
30
|
+
import { FLOW_NODE_TYPES } from './flow-node-registry.js';
|
|
31
|
+
import { resolveVersion } from './core/nodes/versioned.js';
|
|
32
|
+
import { declaredErrorPortRefusal } from './core/error-port.js';
|
|
29
33
|
export function check(built, opts = {}) {
|
|
30
34
|
let diags = [];
|
|
31
35
|
const uses = collectConnectionUses(() => { diags = checkFlow(built, [], opts); });
|
|
@@ -115,6 +119,12 @@ function checkFlow(built, ancestors, opts = {}) {
|
|
|
115
119
|
]);
|
|
116
120
|
const outputType = new Map(built.outputs.map((x) => [x.name, String(x.type)]));
|
|
117
121
|
const stepNames = new Set();
|
|
122
|
+
// Steps whose kind is a registered node type (flow-node-registry.ts), by name:
|
|
123
|
+
// their node-local rules, expressions and output contract come from the node
|
|
124
|
+
// type, and their family's rules run over all of them after the walk.
|
|
125
|
+
const registeredSteps = new Map();
|
|
126
|
+
/** Steps of a registered kind whose version did not resolve (NODE_VERSION_UNREGISTERED), so a family rule can stay silent about them. */
|
|
127
|
+
const unresolvedSteps = new Set();
|
|
118
128
|
/**
|
|
119
129
|
* Every expression the flow carries, with the step that failed when the
|
|
120
130
|
* expression sits inside that step's error handler. The failed step's own
|
|
@@ -133,8 +143,6 @@ function checkFlow(built, ancestors, opts = {}) {
|
|
|
133
143
|
// `core.logic.merge` definition declares no outputs at all (see
|
|
134
144
|
// PARALLEL_HAS_NO_OUTPUT below).
|
|
135
145
|
const parallelNames = new Set();
|
|
136
|
-
const delayNames = new Set();
|
|
137
|
-
const dataFabricReads = new Set();
|
|
138
146
|
// Per-outcome routing bookkeeping: which hitl steps opted into outcome ports
|
|
139
147
|
// (and their declared outcome ids), and which ids actually got a route.
|
|
140
148
|
const hitlOutcomeSteps = new Map();
|
|
@@ -151,22 +159,6 @@ function checkFlow(built, ancestors, opts = {}) {
|
|
|
151
159
|
// waits until the expressions are collected (see HITL_OUTCOMES_UNREACHABLE
|
|
152
160
|
// below).
|
|
153
161
|
const hitlChoiceNoPorts = new Map();
|
|
154
|
-
// Data Fabric writes that target a record `{ fromRead: '<step>' }` — update and
|
|
155
|
-
// delete share the selector, so they share the check.
|
|
156
|
-
const dataFabricRefs = [];
|
|
157
|
-
// Delete step names. `core.datafabric.delete` is the one verb of the family
|
|
158
|
-
// whose definition declares NO outputDefinition, so `$vars.<delete>.output` is
|
|
159
|
-
// undefined at run time (see DATAFABRIC_DELETE_HAS_NO_OUTPUT below).
|
|
160
|
-
const dataFabricDeletes = new Set();
|
|
161
|
-
// Reads that return a LIST. A multi-record read is refused as a write target
|
|
162
|
-
// (see DATAFABRIC_FROMREAD_MULTIPLE below) — the serializer emits a plain
|
|
163
|
-
// no-op task for the write, and nothing at any rung reports it.
|
|
164
|
-
const dataFabricMultiReads = new Set();
|
|
165
|
-
// Placeholder (`mock()`) step names. The platform's `core.logic.mock` declares
|
|
166
|
-
// one output whose `source` is the literal `"null"` — it publishes nothing —
|
|
167
|
-
// so `$vars.<mock>.output` is null at run time, always (see
|
|
168
|
-
// MOCK_HAS_NO_OUTPUT below).
|
|
169
|
-
const mockNames = new Set();
|
|
170
162
|
// Terminate step names. A terminate declares no action output in any registry
|
|
171
163
|
// version, so `$vars.<terminate>` is undefined at run time. The terminal
|
|
172
164
|
// flow-output mappings the serializer emits are validation metadata, not a
|
|
@@ -188,6 +180,8 @@ function checkFlow(built, ancestors, opts = {}) {
|
|
|
188
180
|
// OBJECT of the child's declared outputs, so a read of it is checked against
|
|
189
181
|
// that child rather than waved through (see SUBFLOW_* below).
|
|
190
182
|
const subflowChildren = new Map();
|
|
183
|
+
// `inLoop`: inside a loop/do-while body; `'arm'` when, counting from the
|
|
184
|
+
// innermost container, the path also sits in a `.parallel` arm.
|
|
191
185
|
const walk = (steps, inLoop = false, errorSource) => {
|
|
192
186
|
// Nothing may follow a terminate. `.return()` merely ends its own path, so a
|
|
193
187
|
// later step there is at worst dead weight — but a terminate ends the RUN,
|
|
@@ -206,6 +200,22 @@ function checkFlow(built, ancestors, opts = {}) {
|
|
|
206
200
|
step: term.name,
|
|
207
201
|
});
|
|
208
202
|
}
|
|
203
|
+
// Nothing may follow a `.break()` or `.continue()` on the same path: the
|
|
204
|
+
// path leaves through the container's handle, so a later step would get no
|
|
205
|
+
// incoming edge.
|
|
206
|
+
const exitAt = steps.findIndex((st) => st.kind === 'break' || st.kind === 'continue');
|
|
207
|
+
if (exitAt !== -1 && exitAt < steps.length - 1) {
|
|
208
|
+
const after = steps[exitAt + 1];
|
|
209
|
+
const kind = steps[exitAt].kind;
|
|
210
|
+
diags.push({
|
|
211
|
+
level: 'error',
|
|
212
|
+
code: 'LOOP_EXIT_UNREACHABLE_AFTER',
|
|
213
|
+
message: `${steps.length - exitAt - 1} step(s) follow .${kind}(), starting with `
|
|
214
|
+
+ `"${'name' in after ? after.name : after.kind}". .${kind}() leaves this path through the container's `
|
|
215
|
+
+ `${kind} handle, so none of them can ever run. Move them before the .${kind}().`,
|
|
216
|
+
...('name' in after ? { step: String(after.name) } : {}),
|
|
217
|
+
});
|
|
218
|
+
}
|
|
209
219
|
// Nothing may follow a human task whose EVERY outcome ends the run, except a
|
|
210
220
|
// `.return()`. Each outcome gets its own End node — the flow runtime never
|
|
211
221
|
// reads `action`, so only the graph can end the run there — which leaves
|
|
@@ -260,7 +270,7 @@ function checkFlow(built, ancestors, opts = {}) {
|
|
|
260
270
|
declared.add(s.name); // a step's output is referenceable by name
|
|
261
271
|
}
|
|
262
272
|
if ((s.kind === 'stepToList' || s.kind === 'stepToRef') && s.port === 'error')
|
|
263
|
-
checkOnError(s, prevPortSource, diags);
|
|
273
|
+
checkOnError(s, prevPortSource, diags, prevPortSource && registeredSteps.get(prevPortSource.name));
|
|
264
274
|
if ((s.kind === 'stepToList' || s.kind === 'stepToRef') && s.port.startsWith('branch-')) {
|
|
265
275
|
const want = s.port.slice('branch-'.length);
|
|
266
276
|
const prevSpec = prevAction?.spec;
|
|
@@ -320,7 +330,7 @@ function checkFlow(built, ancestors, opts = {}) {
|
|
|
320
330
|
}
|
|
321
331
|
else {
|
|
322
332
|
const model = hitlExitModel(prevSpec.inputs, prevAction?.options?.version);
|
|
323
|
-
const ids = (prevSpec.inputs.outcomes ?? []).map((o, i) =>
|
|
333
|
+
const ids = (prevSpec.inputs.outcomes ?? []).map((o, i) => outcomeIdOf(o, i));
|
|
324
334
|
if (model.shape === 'placeholder') {
|
|
325
335
|
diags.push({
|
|
326
336
|
level: 'error', code: 'HITL_OUTCOME_PORTS_OFF', step: prevAction?.name,
|
|
@@ -363,7 +373,17 @@ function checkFlow(built, ancestors, opts = {}) {
|
|
|
363
373
|
if (s.kind === 'action') {
|
|
364
374
|
prevAction = s;
|
|
365
375
|
prevPortSource = s;
|
|
366
|
-
|
|
376
|
+
const registered = registeredKind(s.spec);
|
|
377
|
+
if (registered) {
|
|
378
|
+
const placed = checkRegisteredStep(registered, s, exprs, errorSource, opts, diags, typeOf, triggerId);
|
|
379
|
+
if (placed)
|
|
380
|
+
registeredSteps.set(s.name, placed);
|
|
381
|
+
else
|
|
382
|
+
unresolvedSteps.add(s.name);
|
|
383
|
+
}
|
|
384
|
+
else {
|
|
385
|
+
collectActionExprs(s, exprs, errorSource);
|
|
386
|
+
}
|
|
367
387
|
if (s.spec.kind === 'connector') {
|
|
368
388
|
connectorSteps.set(s.name, s.spec);
|
|
369
389
|
checkConnectorLookups(s.name, s.spec, opts, diags);
|
|
@@ -380,8 +400,6 @@ function checkFlow(built, ancestors, opts = {}) {
|
|
|
380
400
|
}
|
|
381
401
|
checkContribution(maybeCustom, s.options?.version, s.name, own.some((d) => d.severity === 'error'), diags);
|
|
382
402
|
}
|
|
383
|
-
if (s.spec.kind === 'transform')
|
|
384
|
-
checkTransform(s.name, s.spec.inputs, diags, typeOf, triggerId);
|
|
385
403
|
if (s.spec.kind === 'http') {
|
|
386
404
|
checkHttp(s.name, s.spec.inputs, diags);
|
|
387
405
|
// Only a complete pair: an empty half is HTTP_MANAGED_BINDING_INCOMPLETE, which
|
|
@@ -419,21 +437,13 @@ function checkFlow(built, ancestors, opts = {}) {
|
|
|
419
437
|
}
|
|
420
438
|
}
|
|
421
439
|
}
|
|
422
|
-
if (s.spec.kind === 'script')
|
|
423
|
-
checkScript(s.name, s.spec.inputs, diags);
|
|
424
|
-
if (s.spec.kind === 'delay') {
|
|
425
|
-
checkDelay(s.name, s.spec.inputs, diags);
|
|
426
|
-
delayNames.add(s.name);
|
|
427
|
-
}
|
|
428
|
-
if (s.spec.kind === 'mock')
|
|
429
|
-
mockNames.add(s.name);
|
|
430
440
|
// `.stepSwitch` routes its exits as ARMS, so the `.stepToList` bookkeeping
|
|
431
441
|
// would report every one of them unrouted. STEP_SWITCH_EXIT_UNROUTED is
|
|
432
442
|
// that step's version of the same question.
|
|
433
443
|
if (s0.kind !== 'stepSwitch'
|
|
434
444
|
&& s.spec.kind === 'hitl' && hitlRoutesPerOutcome(s.spec.inputs, s.options?.version)) {
|
|
435
445
|
hitlOutcomeSteps.set(s.name, (s.spec.inputs.outcomes ?? []).map((o, i) => ({
|
|
436
|
-
id:
|
|
446
|
+
id: outcomeIdOf(o, i),
|
|
437
447
|
endsProcess: typeof o !== 'string' && o?.action === 'End',
|
|
438
448
|
})));
|
|
439
449
|
}
|
|
@@ -451,6 +461,7 @@ function checkFlow(built, ancestors, opts = {}) {
|
|
|
451
461
|
&& !hitlRoutesPerOutcome(s.spec.inputs, s.options?.version)) {
|
|
452
462
|
hitlChoiceNoPorts.set(s.name, {
|
|
453
463
|
names: outcomes.map((o) => String((typeof o === 'string' ? o : o?.name) ?? '').trim()),
|
|
464
|
+
ids: outcomes.map((o, i) => outcomeIdOf(o, i)),
|
|
454
465
|
ends: outcomes.filter((o) => typeof o !== 'string' && o?.action === 'End')
|
|
455
466
|
.map((o) => String(o.name ?? '').trim()),
|
|
456
467
|
variant: s.spec.inputs?.variant,
|
|
@@ -497,71 +508,6 @@ function checkFlow(built, ancestors, opts = {}) {
|
|
|
497
508
|
outputReaders.set(stepName, (field, rest, at, d) => rule(stepName, field, rest, inputs, at, d));
|
|
498
509
|
}
|
|
499
510
|
}
|
|
500
|
-
if (s.spec.kind === 'documentClassify' || s.spec.kind === 'dynamicExtract') {
|
|
501
|
-
const fr = s.spec.inputs.fileRef;
|
|
502
|
-
const empty = fr === undefined || fr === null || (typeof fr === 'string' && fr.trim() === '');
|
|
503
|
-
if (empty) {
|
|
504
|
-
diags.push({
|
|
505
|
-
level: 'error', code: 'DOC_NO_FILEREF', step: s.name,
|
|
506
|
-
message: `"${s.name}" has no fileRef — nothing to ${s.spec.kind === 'documentClassify' ? 'classify' : 'extract from'}. Reference a file input or an upstream attachment.`,
|
|
507
|
-
});
|
|
508
|
-
}
|
|
509
|
-
if (s.spec.kind === 'dynamicExtract' && s.spec.inputs.schema !== undefined
|
|
510
|
-
&& (typeof s.spec.inputs.schema !== 'object' || Array.isArray(s.spec.inputs.schema))) {
|
|
511
|
-
diags.push({
|
|
512
|
-
level: 'error', code: 'EXTRACT_SCHEMA_SHAPE', step: s.name,
|
|
513
|
-
message: `"${s.name}"'s schema must be an object (the inline extraction schema document).`,
|
|
514
|
-
});
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
if (s.spec.kind === 'dataFabricRead' || s.spec.kind === 'dataFabricCreate'
|
|
518
|
-
|| s.spec.kind === 'dataFabricUpdate' || s.spec.kind === 'dataFabricDelete') {
|
|
519
|
-
if (!String(s.spec.inputs.entity ?? '').trim()) {
|
|
520
|
-
diags.push({
|
|
521
|
-
level: 'error', code: 'DATAFABRIC_NO_ENTITY', step: s.name,
|
|
522
|
-
message: `"${s.name}" names no Data Fabric entity. Set { entity: '<entity name>' }.`,
|
|
523
|
-
});
|
|
524
|
-
}
|
|
525
|
-
if (s.spec.kind === 'dataFabricRead') {
|
|
526
|
-
dataFabricReads.add(s.name);
|
|
527
|
-
if (s.spec.inputs.resultMode === 'multiple')
|
|
528
|
-
dataFabricMultiReads.add(s.name);
|
|
529
|
-
checkDataFabricQuery(s.name, s.spec.inputs, diags);
|
|
530
|
-
}
|
|
531
|
-
if (s.spec.kind === 'dataFabricCreate') {
|
|
532
|
-
// Not the same failure as an empty update: a BLANK value is omitted
|
|
533
|
-
// from the insert so the column default applies, so an all-blank
|
|
534
|
-
// `fieldValues` serializes to an empty body and runs green having
|
|
535
|
-
// inserted nothing. The node's schema rejects that — and the fix is
|
|
536
|
-
// a real value, never a placeholder to satisfy the rule.
|
|
537
|
-
const written = Object.values(s.spec.inputs.values ?? {})
|
|
538
|
-
.filter((v) => (v instanceof Expr ? true : String(v ?? '').trim() !== ''));
|
|
539
|
-
if (written.length === 0) {
|
|
540
|
-
diags.push({
|
|
541
|
-
level: 'error', code: 'DATAFABRIC_EMPTY_VALUES', step: s.name,
|
|
542
|
-
message: `"${s.name}" sets no column to a non-blank value, so it would insert nothing and run green. `
|
|
543
|
-
+ 'The node\'s schema rejects that — give a real column a real value rather than a placeholder.',
|
|
544
|
-
});
|
|
545
|
-
}
|
|
546
|
-
checkDataFabricColumns(s.name, Object.keys(s.spec.inputs.values ?? {}), 'values', diags);
|
|
547
|
-
}
|
|
548
|
-
if (s.spec.kind === 'dataFabricUpdate') {
|
|
549
|
-
if (Object.keys(s.spec.inputs.set ?? {}).length === 0) {
|
|
550
|
-
diags.push({
|
|
551
|
-
level: 'error', code: 'DATAFABRIC_EMPTY_SET', step: s.name,
|
|
552
|
-
message: `"${s.name}" writes no columns — { set } is empty, so the update would no-op green.`,
|
|
553
|
-
});
|
|
554
|
-
}
|
|
555
|
-
checkDataFabricColumns(s.name, Object.keys(s.spec.inputs.set ?? {}), 'set', diags);
|
|
556
|
-
}
|
|
557
|
-
checkDataFabricScope(s.name, s.spec.inputs, diags);
|
|
558
|
-
if (s.spec.kind === 'dataFabricUpdate' || s.spec.kind === 'dataFabricDelete') {
|
|
559
|
-
const factory = s.spec.kind === 'dataFabricUpdate' ? 'dataFabricUpdate' : 'dataFabricDelete';
|
|
560
|
-
dataFabricRefs.push({ step: s.name, factory, record: s.spec.inputs.record });
|
|
561
|
-
if (s.spec.kind === 'dataFabricDelete')
|
|
562
|
-
dataFabricDeletes.add(s.name);
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
511
|
if (s.spec.kind === 'ixpExtract')
|
|
566
512
|
checkIxpExtract(s.name, s.spec.inputs, diags);
|
|
567
513
|
if (s.spec.kind === 'waitEvent') {
|
|
@@ -637,6 +583,23 @@ function checkFlow(built, ancestors, opts = {}) {
|
|
|
637
583
|
// Two arms with the same value: the second can never be reached, since
|
|
638
584
|
// the runtime takes the first matching case.
|
|
639
585
|
const seenValues = new Set();
|
|
586
|
+
// Two cases on one id share one `case-<id>` port (#1130: an explicit
|
|
587
|
+
// `id` can collide with a positional `case_<n>`).
|
|
588
|
+
const seenIds = new Set();
|
|
589
|
+
s.cases.forEach((c, i) => {
|
|
590
|
+
const caseId = c.id ?? `case_${i + 1}`;
|
|
591
|
+
if (typeof caseId !== 'string' || caseId === '' || seenIds.has(caseId)) {
|
|
592
|
+
diags.push({
|
|
593
|
+
level: 'error',
|
|
594
|
+
code: 'SWITCH_CASE_ID',
|
|
595
|
+
message: typeof caseId !== 'string' || caseId === ''
|
|
596
|
+
? `Switch "${s.name}" case ${JSON.stringify(c.value)} has an id that is not a non-empty string. Leave \`id\` out for the positional case_<n>.`
|
|
597
|
+
: `Switch "${s.name}" has two cases with the id "${caseId}", so both would leave from one 'case-${caseId}' port. Give one a different \`id\`.`,
|
|
598
|
+
step: s.name,
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
seenIds.add(String(caseId));
|
|
602
|
+
});
|
|
640
603
|
for (const c of s.cases) {
|
|
641
604
|
const key = `${typeof c.value}:${String(c.value)}`;
|
|
642
605
|
if (seenValues.has(key))
|
|
@@ -675,7 +638,7 @@ function checkFlow(built, ancestors, opts = {}) {
|
|
|
675
638
|
parallelNames.add(s.name);
|
|
676
639
|
checkParallel(s, diags);
|
|
677
640
|
for (const arm of s.arms)
|
|
678
|
-
walk(arm, inLoop);
|
|
641
|
+
walk(arm, inLoop ? 'arm' : false);
|
|
679
642
|
}
|
|
680
643
|
else if (s.kind === 'loop') {
|
|
681
644
|
// A container is a legal ERROR-port source (its definition declares the
|
|
@@ -723,6 +686,17 @@ function checkFlow(built, ancestors, opts = {}) {
|
|
|
723
686
|
});
|
|
724
687
|
}
|
|
725
688
|
}
|
|
689
|
+
else if (s.kind === 'continue') {
|
|
690
|
+
if (!inLoop) {
|
|
691
|
+
diags.push({
|
|
692
|
+
level: 'error', code: 'CONTINUE_OUTSIDE_LOOP',
|
|
693
|
+
message: '.continue() is only valid inside a .loop() or .doWhile() body: it goes on to the container\'s next iteration.',
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
else if (inLoop === 'arm') {
|
|
697
|
+
diags.push(loopExitInArm());
|
|
698
|
+
}
|
|
699
|
+
}
|
|
726
700
|
else if (s.kind === 'return') {
|
|
727
701
|
for (const [name, e] of Object.entries(s.values)) {
|
|
728
702
|
addExpr(e.js, 'return');
|
|
@@ -839,30 +813,7 @@ function checkFlow(built, ancestors, opts = {}) {
|
|
|
839
813
|
}
|
|
840
814
|
}
|
|
841
815
|
reportUnreachableHitlOutcomes(hitlChoiceNoPorts, exprs, diags);
|
|
842
|
-
|
|
843
|
-
const fromRead = record?.fromRead;
|
|
844
|
-
if (typeof fromRead !== 'string')
|
|
845
|
-
continue;
|
|
846
|
-
if (!dataFabricReads.has(fromRead)) {
|
|
847
|
-
diags.push({
|
|
848
|
-
level: 'error', code: 'DATAFABRIC_FROMREAD_UNKNOWN', step,
|
|
849
|
-
message: `"${step}" targets record: { fromRead: ${JSON.stringify(fromRead)} }, but no dataFabricRead step has that name.`,
|
|
850
|
-
suggestion: `${factory}({ …, record: { byId: '<record id>' } })`,
|
|
851
|
-
});
|
|
852
|
-
continue;
|
|
853
|
-
}
|
|
854
|
-
// A LIST is not a write target: the serializer refuses it and emits a plain
|
|
855
|
-
// no-op task, so the flow validates, runs green, and writes nothing.
|
|
856
|
-
if (dataFabricMultiReads.has(fromRead)) {
|
|
857
|
-
diags.push({
|
|
858
|
-
level: 'error', code: 'DATAFABRIC_FROMREAD_MULTIPLE', step,
|
|
859
|
-
message: `"${step}" targets record: { fromRead: ${JSON.stringify(fromRead)} }, but "${fromRead}" is a `
|
|
860
|
-
+ "resultMode: 'multiple' query. A multi-record read is refused as a write target — the step compiles to "
|
|
861
|
-
+ 'a no-op and the run goes green having written nothing.',
|
|
862
|
-
suggestion: `loop the results and target each record with ${factory}({ …, record: { byId: … } })`,
|
|
863
|
-
});
|
|
864
|
-
}
|
|
865
|
-
}
|
|
816
|
+
checkRegisteredFamilies(registeredSteps, unresolvedSteps, stepNames, diags);
|
|
866
817
|
// Entry points: unique roots with valid triggers, colliding with nothing.
|
|
867
818
|
const epIds = new Set();
|
|
868
819
|
for (const ep of built.entryPoints ?? []) {
|
|
@@ -1144,6 +1095,16 @@ function checkFlow(built, ancestors, opts = {}) {
|
|
|
1144
1095
|
});
|
|
1145
1096
|
continue;
|
|
1146
1097
|
}
|
|
1098
|
+
// A registered node type declares what its output is; `none` reports here,
|
|
1099
|
+
// at the same point as the per-family no-output rules below.
|
|
1100
|
+
const placed = registeredSteps.get(name);
|
|
1101
|
+
if (placed) {
|
|
1102
|
+
const contract = placed.impl.output(placed.spec);
|
|
1103
|
+
if (contract.kind === 'none') {
|
|
1104
|
+
diags.push(catalogued(placed.entry, step, contract.code, contract.message({ node: name, ref: `$vars.${name}${field ? '.' + field : ''}`, reader: step })));
|
|
1105
|
+
continue;
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1147
1108
|
// A switch only routes; it has no output of its own, so any read of it is
|
|
1148
1109
|
// undefined at run time. Point the author at the arm's step instead.
|
|
1149
1110
|
if (switchNames.has(name)) {
|
|
@@ -1186,62 +1147,6 @@ function checkFlow(built, ancestors, opts = {}) {
|
|
|
1186
1147
|
});
|
|
1187
1148
|
continue;
|
|
1188
1149
|
}
|
|
1189
|
-
// A Data Fabric delete is the one verb of that family whose definition
|
|
1190
|
-
// declares no outputDefinition at all — the record is gone, so there is
|
|
1191
|
-
// nothing to publish. Read the record on the step that FOUND it instead,
|
|
1192
|
-
// which on the `{ fromRead }` form is already in the flow.
|
|
1193
|
-
if (dataFabricDeletes.has(name)) {
|
|
1194
|
-
diags.push({
|
|
1195
|
-
level: 'error',
|
|
1196
|
-
code: 'DATAFABRIC_DELETE_HAS_NO_OUTPUT',
|
|
1197
|
-
message: `Delete "${name}" produces no output, but "$vars.${name}${field ? '.' + field : ''}" reads one in "${step}". `
|
|
1198
|
-
+ 'A Data Fabric delete declares no outputs — take the value off the dataFabricRead step that found the record, before the delete.',
|
|
1199
|
-
step,
|
|
1200
|
-
});
|
|
1201
|
-
continue;
|
|
1202
|
-
}
|
|
1203
|
-
if (delayNames.has(name)) {
|
|
1204
|
-
diags.push({
|
|
1205
|
-
level: 'error',
|
|
1206
|
-
code: 'DELAY_HAS_NO_OUTPUT',
|
|
1207
|
-
message: `Delay "${name}" produces no output, but "$vars.${name}${field ? '.' + field : ''}" reads one in "${step}". `
|
|
1208
|
-
+ 'A delay only waits — read the step that computed the value instead.',
|
|
1209
|
-
step,
|
|
1210
|
-
});
|
|
1211
|
-
continue;
|
|
1212
|
-
}
|
|
1213
|
-
// A `mock()` is a PLACEHOLDER: `core.logic.mock`'s definition declares one
|
|
1214
|
-
// output whose `source` is the literal `"null"`, so the value is null at run
|
|
1215
|
-
// time whatever the flow does. Unlike the four rules above, the node DOES
|
|
1216
|
-
// declare an output slot — which is exactly why the read is silent: it
|
|
1217
|
-
// resolves in the designer, it resolves in `variables.nodes`, and it hands
|
|
1218
|
-
// the downstream step a null.
|
|
1219
|
-
//
|
|
1220
|
-
// WHY THIS RULE LIVES HERE, in the authoring checker rather than in a
|
|
1221
|
-
// pass over the compiled artifact (the measurement,
|
|
1222
|
-
// convention #191). Reading a placeholder's output is the ARCHIVE's
|
|
1223
|
-
// dominant idiom, not a rarity: 55 of the 62 archived mock-carrying
|
|
1224
|
-
// artifacts do it — 110 read sites, 87 of them inside a script's body and
|
|
1225
|
-
// 19 in a decision's expression — and **39 of those 55 come from runs that
|
|
1226
|
-
// scored 1.0**. That is an author writing the downstream code for the real
|
|
1227
|
-
// node the placeholder stands in for, and a `.flow`-level rule refusing it
|
|
1228
|
-
// would refuse the deployed corpus (lesson 47). An SDK author is in the
|
|
1229
|
-
// other position: they wrote `mock()` here, in this file, and the SDK can
|
|
1230
|
-
// say at compile time that the read is dead.
|
|
1231
|
-
// Measured 2026-08-02 over 4,033 distinct archived artifacts —
|
|
1232
|
-
// runs/flow-sdk-cleanup-2026-08-02/step0/mock-output-read-sites.txt.
|
|
1233
|
-
if (mockNames.has(name)) {
|
|
1234
|
-
diags.push({
|
|
1235
|
-
level: 'error',
|
|
1236
|
-
code: 'MOCK_HAS_NO_OUTPUT',
|
|
1237
|
-
message: `Placeholder "${name}" publishes nothing, but "$vars.${name}${field ? '.' + field : ''}" reads it in "${step}". `
|
|
1238
|
-
+ 'A mock() is a marker for a node that does not exist yet — its output is null at run time, always. '
|
|
1239
|
-
+ 'Keep the placeholder while the real node does not exist, and have the reader use a value you supply instead (a flow input, as example/ClaimScaffold.flow.ts does). '
|
|
1240
|
-
+ 'Replace the placeholder only with the real node, once `registry search` finds it; a script returning fixed values is not the real node.',
|
|
1241
|
-
step,
|
|
1242
|
-
});
|
|
1243
|
-
continue;
|
|
1244
|
-
}
|
|
1245
1150
|
// A subflow hands back an OBJECT keyed by the child flow's declared
|
|
1246
1151
|
// outputs — even when the child declares exactly one. So the reference
|
|
1247
1152
|
// needs the output's NAME (`out('call', 'reversed')`), and the name has to
|
|
@@ -1499,6 +1404,13 @@ function writtenTails(steps, incoming, sinks) {
|
|
|
1499
1404
|
breaks.push(...tails);
|
|
1500
1405
|
return [];
|
|
1501
1406
|
}
|
|
1407
|
+
// A `.continue()` path goes to the container's next pass, not to the steps
|
|
1408
|
+
// after it: for a do-while that is the condition test, one of the ways out,
|
|
1409
|
+
// exactly like reaching the end of the body — so it joins the exits too.
|
|
1410
|
+
if (st.kind === 'continue') {
|
|
1411
|
+
breaks.push(...tails);
|
|
1412
|
+
return [];
|
|
1413
|
+
}
|
|
1502
1414
|
if (st.kind === 'return' || st.kind === 'terminate')
|
|
1503
1415
|
return [];
|
|
1504
1416
|
const before = surelyWritten(tails);
|
|
@@ -1985,26 +1897,6 @@ function checkTrigger(spec, diags, opts = {}, entryPointId) {
|
|
|
1985
1897
|
+ `comes round. Use a non-zero interval.`, `every: 'R/PT1H'`);
|
|
1986
1898
|
}
|
|
1987
1899
|
}
|
|
1988
|
-
/**
|
|
1989
|
-
* An EXACT ISO-8601 duration: weeks/days plus a time part, at least one
|
|
1990
|
-
* component. Copied in shape from the delay definition's own
|
|
1991
|
-
* `timerValue.pattern`, minus the years and months — see DELAY_CALENDAR below.
|
|
1992
|
-
*/
|
|
1993
|
-
const DURATION_RE = /^P(?!$)(\d+W)?(\d+D)?(T(?=\d)(\d+H)?(\d+M)?(\d+S)?)?$/;
|
|
1994
|
-
/** Grammatical ISO-8601 duration, including the calendar-relative parts. */
|
|
1995
|
-
const ANY_DURATION_RE = /^P(?!$)(\d+Y)?(\d+M)?(\d+W)?(\d+D)?(T(?=\d)(\d+H)?(\d+M)?(\d+S)?)?$/;
|
|
1996
|
-
/** Natural-language guesses an author might reach for, mapped to a duration. */
|
|
1997
|
-
const DURATION_WORDS = [
|
|
1998
|
-
['second', 'PT30S'], ['minute', 'PT15M'], ['hour', 'PT1H'],
|
|
1999
|
-
['day', 'P1D'], ['daily', 'P1D'], ['week', 'P1W'], ['weekly', 'P1W'],
|
|
2000
|
-
];
|
|
2001
|
-
/**
|
|
2002
|
-
* Diagnose a `delay` step's wait.
|
|
2003
|
-
*
|
|
2004
|
-
* Rung 1 is the only place an author hears about a bad duration in time to fix
|
|
2005
|
-
* it cheaply. Anything downstream speaks in terms of `timerPreset`/`timerValue`,
|
|
2006
|
-
* the two fields the serializer writes, rather than the one field the author typed.
|
|
2007
|
-
*/
|
|
2008
1900
|
/** A release key is a GUID — the shape 45/45 deployed rpa node types carry. */
|
|
2009
1901
|
/**
|
|
2010
1902
|
* The DECLARED name a single reference resolves to, or undefined.
|
|
@@ -2805,8 +2697,22 @@ function checkAgenticProcess(step, inputs, diags) {
|
|
|
2805
2697
|
}
|
|
2806
2698
|
const inSolution = inputs?.location === 'in-solution';
|
|
2807
2699
|
if (!inSolution && blank(inputs?.folderPath)) {
|
|
2808
|
-
|
|
2809
|
-
|
|
2700
|
+
// `registry list --local` shows an in-solution Flow sibling with folderPath: "".
|
|
2701
|
+
// An explicit `form` other than 'flow' keeps the published hint: switching it would change the node type.
|
|
2702
|
+
const meantInSolution = inputs?.form === 'flow'
|
|
2703
|
+
|| (typeof inputs?.folderPath === 'string' && inputs?.form === undefined);
|
|
2704
|
+
if (meantInSolution) {
|
|
2705
|
+
const formPart = inputs?.form === 'flow' ? '' : `, form: 'flow'`;
|
|
2706
|
+
at('AGENTIC_NO_FOLDER', `Agentic process step "${step}" has an empty \`folderPath\`. If the process is an unpublished sibling `
|
|
2707
|
+
+ `Flow in this solution, call it in-solution: set \`location: 'in-solution'\`${formPart} and \`projectId\` `
|
|
2708
|
+
+ `(the sibling's \`resource.projectKey\` in \`resources/solution_folder/process/flow/<project>.json\`), and `
|
|
2709
|
+
+ `drop \`folderPath\`. If it is published, set \`folderPath\` to the Orchestrator folder that holds it — `
|
|
2710
|
+
+ `the runtime starts the job by name + folder.`, `location: 'in-solution'${formPart}, projectId: '<resource.projectKey>'`);
|
|
2711
|
+
}
|
|
2712
|
+
else {
|
|
2713
|
+
at('AGENTIC_NO_FOLDER', `Agentic process step "${step}" has no \`folderPath\`. The runtime starts the job by name + folder, so `
|
|
2714
|
+
+ `the Orchestrator folder holding the process is required.`, `folderPath: 'Shared/uipath-agents/ProcurementProcess'`);
|
|
2715
|
+
}
|
|
2810
2716
|
}
|
|
2811
2717
|
const form = inputs?.form ?? 'bpmn';
|
|
2812
2718
|
if (inSolution && form !== 'flow') {
|
|
@@ -3278,13 +3184,31 @@ function checkInlineAgent(step, inputs, diags, opts = {}) {
|
|
|
3278
3184
|
+ `run-dependent search needs.`);
|
|
3279
3185
|
}
|
|
3280
3186
|
}
|
|
3187
|
+
// The index id is half of the resource node's TYPE
|
|
3188
|
+
// (`…context.index.<slug>.<id>`), and the platform resolves the index by it.
|
|
3189
|
+
// A blank, padded or non-GUID id builds a type no tenant serves, and nothing
|
|
3190
|
+
// downstream says so: the flow compiles, merges and validates, and the agent
|
|
3191
|
+
// runs ungrounded. So it is an error, not a warning. The id is tested as
|
|
3192
|
+
// written, not trimmed, because serialize emits it as written.
|
|
3193
|
+
for (const cx of contexts) {
|
|
3194
|
+
const id = typeof cx?.id === 'string' ? cx.id : '';
|
|
3195
|
+
if (!GUID_RE.test(id)) {
|
|
3196
|
+
at('AGENT_CONTEXT_NO_ID', `Inline agent step "${step}" grounds on context index "${cx?.name ?? ''}" with `
|
|
3197
|
+
+ (id.trim() === '' ? 'no `id`'
|
|
3198
|
+
: GUID_RE.test(id.trim()) ? `\`id\` ${JSON.stringify(cx?.id)}, which has whitespace around the GUID`
|
|
3199
|
+
: `\`id\` ${JSON.stringify(cx?.id)}, which is not a GUID`) + '. '
|
|
3200
|
+
+ `The id is the index's key: it is the tail of the resource node's type and how the runtime finds `
|
|
3201
|
+
+ `the index, so without a real one the agent runs ungrounded and \`validate\` does not notice. `
|
|
3202
|
+
+ `\`uip solution resources list --kind Index --source remote --search "<name>"\` returns it as \`Key\`.`);
|
|
3203
|
+
}
|
|
3204
|
+
}
|
|
3281
3205
|
// An index outside the deployment folder needs its folder named. The resource
|
|
3282
3206
|
// file carries no `indexId`, so `indexName` + `folderPath` is the whole locator,
|
|
3283
3207
|
// and `@uipath/tool-agent`'s `createIndexBinding` leaves the folder off the
|
|
3284
3208
|
// binding when `folderPath` is empty. A WARNING, not an error: an index in the
|
|
3285
3209
|
// deployment folder resolves without it.
|
|
3286
3210
|
for (const cx of contexts) {
|
|
3287
|
-
if (cx
|
|
3211
|
+
if (cx != null && cx.folderPath === undefined) {
|
|
3288
3212
|
warn('AGENT_CONTEXT_NO_FOLDER_PATH', `Inline agent step "${step}" grounds on context index "${cx.name ?? ''}" without a \`folderPath\`. `
|
|
3289
3213
|
+ `The emitted resource carries no index id, so the folder is half of how the runtime finds the `
|
|
3290
3214
|
+ `index, and the binding is written without one. Give the index's folder — `
|
|
@@ -3611,8 +3535,8 @@ function checkInlineAgent(step, inputs, diags, opts = {}) {
|
|
|
3611
3535
|
for (const f of Array.isArray(e.fields) ? e.fields : []) {
|
|
3612
3536
|
if (!FORM_FIELD_TYPES.includes(String(f?.type))) {
|
|
3613
3537
|
at('INLINE_AGENT_ESCALATION_FIELD_TYPE_UNKNOWN', `Inline agent step "${step}"'s quick-form escalation gives the field "${String(f?.id ?? '?')}" the type `
|
|
3614
|
-
+ `${JSON.stringify(f?.type)}, which the form does not recognize — it would
|
|
3615
|
-
|
|
3538
|
+
+ `${JSON.stringify(f?.type)}, ${formFieldTypeWhy(f?.type, `which the form does not recognize — it would `
|
|
3539
|
+
+ `be published as a plain string`)}. Valid: ${FORM_FIELD_TYPES.join(', ')}.`, formFieldTypeHint(f?.type));
|
|
3616
3540
|
}
|
|
3617
3541
|
}
|
|
3618
3542
|
continue;
|
|
@@ -3870,64 +3794,6 @@ function checkQueueItem(step, inputs, diags) {
|
|
|
3870
3794
|
}
|
|
3871
3795
|
/** The three priorities `core.action.queue.create[-and-wait]`'s form offers. */
|
|
3872
3796
|
const QUEUE_PRIORITIES = ['Low', 'Normal', 'High'];
|
|
3873
|
-
function checkDelay(step, inputs, diags) {
|
|
3874
|
-
const at = (code, message, suggestion) => {
|
|
3875
|
-
diags.push({ level: 'error', code, message, step, ...(suggestion ? { suggestion } : {}) });
|
|
3876
|
-
};
|
|
3877
|
-
const untilRaw = inputs?.until;
|
|
3878
|
-
if (typeof untilRaw === 'string' && untilRaw.trim() !== '') {
|
|
3879
|
-
// The absolute-date arm: an ISO-8601 date-time, e.g. 2026-09-01T09:00:00Z.
|
|
3880
|
-
const until = untilRaw.trim();
|
|
3881
|
-
if (!/^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}(:\d{2}(\.\d+)?)?(Z|[+-]\d{2}:\d{2})?)?$/.test(until)) {
|
|
3882
|
-
at('DELAY_UNTIL_NOT_ISO', `Delay "${step}"'s until ${JSON.stringify(untilRaw)} is not an ISO-8601 date-time. `
|
|
3883
|
-
+ `Use e.g. '2026-09-01T09:00:00Z' (a timezone-less value is read in the tenant's zone).`, `until: '2026-09-01T09:00:00Z'`);
|
|
3884
|
-
}
|
|
3885
|
-
return;
|
|
3886
|
-
}
|
|
3887
|
-
const raw = inputs?.duration;
|
|
3888
|
-
if (raw === undefined || raw === null || (typeof raw === 'string' && raw.trim() === '')) {
|
|
3889
|
-
at('DELAY_NO_DURATION', `Delay "${step}" has no \`duration\`, so it carries no wait at all and the flow runs straight `
|
|
3890
|
-
+ `past it. Give it an ISO-8601 duration — ${DELAY_PRESETS.slice(0, 8).join(', ')} … are the `
|
|
3891
|
-
+ `designer's presets, and any other exact duration works too.`, `duration: 'PT15M'`);
|
|
3892
|
-
return;
|
|
3893
|
-
}
|
|
3894
|
-
const d = String(raw).trim();
|
|
3895
|
-
if (!ANY_DURATION_RE.test(d)) {
|
|
3896
|
-
at('DELAY_NOT_ISO', `Delay "${step}"'s duration ${JSON.stringify(raw)} is not an ISO-8601 duration. The format is `
|
|
3897
|
-
+ `"P" plus a date part and/or "T" plus a time part — PT30S, PT15M, PT2H, P1D, P1W. Note the `
|
|
3898
|
-
+ `"T": P1M is a MONTH, PT1M is a minute.`, nearestDuration(d));
|
|
3899
|
-
return;
|
|
3900
|
-
}
|
|
3901
|
-
if (!/[1-9]/.test(d)) {
|
|
3902
|
-
at('DELAY_ZERO', `Delay "${step}"'s duration ${JSON.stringify(d)} is zero-length, so the step waits for no time `
|
|
3903
|
-
+ `at all. Use a non-zero duration, or drop the step.`, `duration: 'PT15M'`);
|
|
3904
|
-
return;
|
|
3905
|
-
}
|
|
3906
|
-
// Grammatical, and the platform runs it — but a month is not a fixed number
|
|
3907
|
-
// of milliseconds, so NO local rung can run the flow at all.
|
|
3908
|
-
// Rejected here rather than approximated: guessing 30 days would
|
|
3909
|
-
// put a silent disagreement with the deployed engine back into the pipeline.
|
|
3910
|
-
if (!DURATION_RE.test(d)) {
|
|
3911
|
-
at('DELAY_CALENDAR', `Delay "${step}"'s duration ${JSON.stringify(d)} is calendar-relative: how long a month or a `
|
|
3912
|
-
+ `year is depends on when it starts. The platform resolves that at run time, but the local `
|
|
3913
|
-
+ `timer takes a fixed millisecond count, so this flow could not be replayed or run offline.`, `duration: 'P30D'`);
|
|
3914
|
-
}
|
|
3915
|
-
}
|
|
3916
|
-
/** Best guess at what the author meant, for a `DELAY_NOT_ISO` suggestion. */
|
|
3917
|
-
function nearestDuration(d) {
|
|
3918
|
-
// The likeliest slip: a time-part duration written without its "T".
|
|
3919
|
-
if (/^P\d+[HMS]$/.test(d) && ANY_DURATION_RE.test(`PT${d.slice(1)}`))
|
|
3920
|
-
return `duration: 'PT${d.slice(1)}'`;
|
|
3921
|
-
const lower = d.toLowerCase();
|
|
3922
|
-
const n = /(\d+)/.exec(lower)?.[1];
|
|
3923
|
-
const word = DURATION_WORDS.find(([w]) => lower.includes(w));
|
|
3924
|
-
if (!word)
|
|
3925
|
-
return undefined;
|
|
3926
|
-
if (!n)
|
|
3927
|
-
return `duration: '${word[1]}'`;
|
|
3928
|
-
const unit = word[1].replace(/\d+/, '');
|
|
3929
|
-
return `duration: '${unit.startsWith('PT') ? `PT${n}${unit.slice(2)}` : `P${n}${unit.slice(1)}`}'`;
|
|
3930
|
-
}
|
|
3931
3797
|
/** Best guess at what the author meant, for a `SCHEDULE_NOT_ISO` suggestion. */
|
|
3932
3798
|
function nearestInterval(every) {
|
|
3933
3799
|
// By far the likeliest slip: a correct duration missing the "R/" repeat marker.
|
|
@@ -4052,7 +3918,7 @@ function armTails(steps, incoming) {
|
|
|
4052
3918
|
tails = [];
|
|
4053
3919
|
continue;
|
|
4054
3920
|
}
|
|
4055
|
-
if (st.kind === 'return' || st.kind === 'terminate' || st.kind === 'break')
|
|
3921
|
+
if (st.kind === 'return' || st.kind === 'terminate' || st.kind === 'break' || st.kind === 'continue')
|
|
4056
3922
|
return [];
|
|
4057
3923
|
if (st.kind === 'action') {
|
|
4058
3924
|
tails = actionTails(st.name, st.spec, st.options?.version);
|
|
@@ -4249,70 +4115,6 @@ const RETRY_COUNT_SOFT_MAX = 5;
|
|
|
4249
4115
|
* not a shorter timeout, it is a value the request node fails to configure with,
|
|
4250
4116
|
* and nothing downstream of `check` would say so in the author's own vocabulary.
|
|
4251
4117
|
*/
|
|
4252
|
-
/** The type vocabulary a script's `returns` may name — what the wire format accepts. */
|
|
4253
|
-
const SCRIPT_RETURN_TYPES = new Set(['string', 'number', 'boolean', 'object', 'array']);
|
|
4254
|
-
/**
|
|
4255
|
-
* A script step's `returns` — the author overriding what the body looks like.
|
|
4256
|
-
*
|
|
4257
|
-
* TypeScript already refuses a bad value at the call site, so this is the fence
|
|
4258
|
-
* for everything that does NOT go through the type checker: a `.flow.ts` loaded
|
|
4259
|
-
* as plain JS, a generated flow, a value read from config. Both mistakes below
|
|
4260
|
-
* reach the emitted file silently and are then the designer's problem instead of
|
|
4261
|
-
* the author's, which is precisely the failure this whole feature exists to undo.
|
|
4262
|
-
*/
|
|
4263
|
-
function checkScript(step, inputs, diags) {
|
|
4264
|
-
const returns = inputs?.returns;
|
|
4265
|
-
if (returns === undefined)
|
|
4266
|
-
return;
|
|
4267
|
-
if (typeof returns === 'string') {
|
|
4268
|
-
if (!SCRIPT_RETURN_TYPES.has(returns)) {
|
|
4269
|
-
diags.push({
|
|
4270
|
-
level: 'error',
|
|
4271
|
-
code: 'SCRIPT_RETURNS_UNKNOWN_TYPE',
|
|
4272
|
-
message: `Script step "${step}" declares \`returns: ${JSON.stringify(returns)}\`, which is not a type the `
|
|
4273
|
-
+ `wire format knows. Use one of ${[...SCRIPT_RETURN_TYPES].map((t) => `"${t}"`).join(', ')}, or a `
|
|
4274
|
-
+ `field map like \`{ total: "number" }\` to name an object's fields.`,
|
|
4275
|
-
step,
|
|
4276
|
-
});
|
|
4277
|
-
}
|
|
4278
|
-
return;
|
|
4279
|
-
}
|
|
4280
|
-
if (typeof returns !== 'object' || returns === null || Array.isArray(returns)) {
|
|
4281
|
-
diags.push({
|
|
4282
|
-
level: 'error',
|
|
4283
|
-
code: 'SCRIPT_RETURNS_UNKNOWN_TYPE',
|
|
4284
|
-
message: `Script step "${step}"'s \`returns\` is ${JSON.stringify(returns)}. It takes either a type name or a `
|
|
4285
|
-
+ `map of field names to types (\`{ total: "number" }\`).`,
|
|
4286
|
-
step,
|
|
4287
|
-
});
|
|
4288
|
-
return;
|
|
4289
|
-
}
|
|
4290
|
-
const fields = returns;
|
|
4291
|
-
// Same call as `APIWF_EMPTY_RETURNS` one surface over: an empty map declares
|
|
4292
|
-
// nothing, emits nothing, and reads exactly like omitting the key — so it is
|
|
4293
|
-
// almost always a half-finished edit rather than a statement.
|
|
4294
|
-
if (Object.keys(fields).length === 0) {
|
|
4295
|
-
diags.push({
|
|
4296
|
-
level: 'warning',
|
|
4297
|
-
code: 'SCRIPT_EMPTY_RETURNS',
|
|
4298
|
-
message: `Script step "${step}" declares \`returns: {}\`, which names no fields and emits nothing — the same `
|
|
4299
|
-
+ `as omitting it. Name the fields, or drop the key and let the body speak for itself.`,
|
|
4300
|
-
step,
|
|
4301
|
-
});
|
|
4302
|
-
return;
|
|
4303
|
-
}
|
|
4304
|
-
for (const [name, type] of Object.entries(fields)) {
|
|
4305
|
-
if (typeof type !== 'string' || !SCRIPT_RETURN_TYPES.has(type)) {
|
|
4306
|
-
diags.push({
|
|
4307
|
-
level: 'error',
|
|
4308
|
-
code: 'SCRIPT_RETURNS_UNKNOWN_TYPE',
|
|
4309
|
-
message: `Script step "${step}" declares field "${name}" as ${JSON.stringify(type)}, which is not a type the `
|
|
4310
|
-
+ `wire format knows. Use one of ${[...SCRIPT_RETURN_TYPES].map((t) => `"${t}"`).join(', ')}.`,
|
|
4311
|
-
step,
|
|
4312
|
-
});
|
|
4313
|
-
}
|
|
4314
|
-
}
|
|
4315
|
-
}
|
|
4316
4118
|
function checkHttp(step, inputs, diags) {
|
|
4317
4119
|
// `core.action.http` is retired on the platform side: after `registry pull
|
|
4318
4120
|
// --force` the tenant serves only `core.action.http.v2` (2.3), and a
|
|
@@ -4450,12 +4252,28 @@ function checkHttp(step, inputs, diags) {
|
|
|
4450
4252
|
}
|
|
4451
4253
|
}
|
|
4452
4254
|
}
|
|
4255
|
+
/**
|
|
4256
|
+
* Why a form-field type is refused: a product spelling of a type the SDK
|
|
4257
|
+
* models (`"string"` is the platform's own name for text), or a type
|
|
4258
|
+
* the SDK does not model at all. `unknownWhy` is the caller's reason for the
|
|
4259
|
+
* second case; the default is a human task's.
|
|
4260
|
+
*/
|
|
4261
|
+
function formFieldTypeWhy(type, unknownWhy = `which the platform does not recognize. Older runtimes read it as text, newer ones reject `
|
|
4262
|
+
+ `the form`) {
|
|
4263
|
+
const alias = sdkFormFieldType(type);
|
|
4264
|
+
return alias !== undefined
|
|
4265
|
+
? `which is the platform's spelling of the SDK's '${alias}' — the SDK writes '${alias}'`
|
|
4266
|
+
: unknownWhy;
|
|
4267
|
+
}
|
|
4453
4268
|
/** Every FormFieldType, once; `Record<FormFieldType, …>` makes tsc refuse a missing or extra one. */
|
|
4454
4269
|
const FORM_FIELD_TYPE_SET = { text: true, number: true, boolean: true, 'date-time': true };
|
|
4455
|
-
/**
|
|
4270
|
+
/** The form-field types the SDK models. */
|
|
4456
4271
|
const FORM_FIELD_TYPES = Object.keys(FORM_FIELD_TYPE_SET);
|
|
4457
4272
|
/** The `suggestion` for an unknown form-field type: the nearest valid one, else `type: 'text'`. */
|
|
4458
4273
|
function formFieldTypeHint(type) {
|
|
4274
|
+
const alias = sdkFormFieldType(type);
|
|
4275
|
+
if (alias !== undefined)
|
|
4276
|
+
return `type: '${alias}'`;
|
|
4459
4277
|
const nearest = nearestName(String(type), FORM_FIELD_TYPES);
|
|
4460
4278
|
return nearest !== undefined ? `type: '${nearest}'` : `type: 'text'`;
|
|
4461
4279
|
}
|
|
@@ -4467,18 +4285,6 @@ const RESERVED_ANSWER_IDS = new Set(['output', 'status', 'Action']);
|
|
|
4467
4285
|
* family — same availability note as do-while).
|
|
4468
4286
|
*/
|
|
4469
4287
|
const HITL_VARIANTS = ['quick-form', 'action-app', 'document-validation'];
|
|
4470
|
-
/**
|
|
4471
|
-
* An outcome's stable id — the serializer's own slug rule, mirrored so the
|
|
4472
|
-
* `outcome-<id>` port names this file validates are the ones that get emitted.
|
|
4473
|
-
*/
|
|
4474
|
-
function outcomeSlug(name, index) {
|
|
4475
|
-
const slug = String(name ?? '')
|
|
4476
|
-
.trim()
|
|
4477
|
-
.toLowerCase()
|
|
4478
|
-
.replace(/[^a-z0-9]+/g, '-')
|
|
4479
|
-
.replace(/^-+|-+$/g, '');
|
|
4480
|
-
return slug || `outcome-${index + 1}`;
|
|
4481
|
-
}
|
|
4482
4288
|
/**
|
|
4483
4289
|
* Why a base or quick-form task continues every outcome to the next step, as a
|
|
4484
4290
|
* clause for a diagnostic. `pinned` is the step's `{ version }`.
|
|
@@ -4548,10 +4354,9 @@ function reportUnreachableHitlOutcomes(choices, exprs, diags) {
|
|
|
4548
4354
|
for (const m of js.matchAll(/\$vars\.([A-Za-z0-9_]+)\s*\[/g))
|
|
4549
4355
|
readsOutcome.add(m[1]);
|
|
4550
4356
|
}
|
|
4551
|
-
for (const [step, { names, ends, variant, pinned, portsOff }] of choices) {
|
|
4357
|
+
for (const [step, { names, ids, ends, variant, pinned, portsOff }] of choices) {
|
|
4552
4358
|
if (readsOutcome.has(step))
|
|
4553
4359
|
continue;
|
|
4554
|
-
const ids = names.map((n, i) => outcomeSlug(n, i));
|
|
4555
4360
|
// The base node and quick-form draw a handle per outcome, so both can route
|
|
4556
4361
|
// it — `.stepSwitch`, or `outcomePorts: true` (a pin keeps its version). An
|
|
4557
4362
|
// app-backed variant cannot: the Action read is its only remedy.
|
|
@@ -4734,10 +4539,8 @@ function checkHitl(step, inputs, diags) {
|
|
|
4734
4539
|
return;
|
|
4735
4540
|
}
|
|
4736
4541
|
if (!FORM_FIELD_TYPES.includes(String(f?.type))) {
|
|
4737
|
-
at('HITL_FIELD_TYPE_UNKNOWN', `Human task "${step}" gives the form field "${id}" the type ${JSON.stringify(f?.type)},
|
|
4738
|
-
+
|
|
4739
|
-
+ `gets a text box and the value comes back as a string. Valid: `
|
|
4740
|
-
+ `${FORM_FIELD_TYPES.join(', ')}.`, formFieldTypeHint(f?.type));
|
|
4542
|
+
at('HITL_FIELD_TYPE_UNKNOWN', `Human task "${step}" gives the form field "${id}" the type ${JSON.stringify(f?.type)}, `
|
|
4543
|
+
+ `${formFieldTypeWhy(f?.type)}. Valid: ${FORM_FIELD_TYPES.join(', ')}.`, formFieldTypeHint(f?.type));
|
|
4741
4544
|
}
|
|
4742
4545
|
if (isAnswerField(f))
|
|
4743
4546
|
answers++; // an `output`, or an `inOut` whose reviewed value comes back
|
|
@@ -4776,8 +4579,26 @@ function checkHitl(step, inputs, diags) {
|
|
|
4776
4579
|
return;
|
|
4777
4580
|
}
|
|
4778
4581
|
const names = new Set();
|
|
4582
|
+
const ids = new Map();
|
|
4779
4583
|
outcomes.forEach((o, i) => {
|
|
4780
4584
|
const name = (typeof o === 'string' ? o : o?.name)?.trim();
|
|
4585
|
+
// Two outcomes on one id would share one `outcome-<id>` port, so the
|
|
4586
|
+
// second's edge could never be told apart. An explicit `id` can collide
|
|
4587
|
+
// with another outcome's derived slug.
|
|
4588
|
+
const id = outcomeIdOf(o, i);
|
|
4589
|
+
const explicitId = typeof o === 'object' && o !== null ? o.id : undefined;
|
|
4590
|
+
if (explicitId !== undefined && (typeof explicitId !== 'string' || explicitId === '')) {
|
|
4591
|
+
at('HITL_OUTCOME_BAD_ID', `Human task "${step}" outcome ${JSON.stringify(name ?? i)} has an id that is not a non-empty string. `
|
|
4592
|
+
+ `Leave \`id\` out to use the slug of the name.`, `{ name: ${JSON.stringify(name ?? 'Approve')} }`);
|
|
4593
|
+
}
|
|
4594
|
+
else if (ids.has(id) && ids.get(id) !== String(name ?? '')) {
|
|
4595
|
+
// Same name, same id: HITL_DUP_OUTCOME below reports it once, as a name clash.
|
|
4596
|
+
at('HITL_DUP_OUTCOME_ID', `Human task "${step}" gives outcomes "${ids.get(id)}" and "${name}" the same id "${id}", so both would `
|
|
4597
|
+
+ `leave from one 'outcome-${id}' port. Give one of them a different \`id\`.`);
|
|
4598
|
+
}
|
|
4599
|
+
else {
|
|
4600
|
+
ids.set(id, String(name ?? ''));
|
|
4601
|
+
}
|
|
4781
4602
|
if (!name) {
|
|
4782
4603
|
at('HITL_OUTCOME_NO_NAME', `Human task "${step}" has an outcome with no name (outcomes[${i}]). The result carries the `
|
|
4783
4604
|
+ `outcome's NAME — it is what "$vars.${step}.output.Action" holds and what the platform's `
|
|
@@ -4907,211 +4728,6 @@ function checkHitlRead(name, field, rest, form, step, diags) {
|
|
|
4907
4728
|
...(nearest !== undefined ? { suggestion: `out('${name}', '${nearest}')` } : {}),
|
|
4908
4729
|
});
|
|
4909
4730
|
}
|
|
4910
|
-
function checkTransform(step, inputs, diags, typeOf = new Map(), triggerId = 'start') {
|
|
4911
|
-
const at = (code, message, suggestion) => {
|
|
4912
|
-
diags.push({ level: 'error', code, message, step, ...(suggestion ? { suggestion } : {}) });
|
|
4913
|
-
};
|
|
4914
|
-
// ── collection must be a bare variable path ──
|
|
4915
|
-
const collection = inputs?.collection;
|
|
4916
|
-
if (collection === undefined || collection === null) {
|
|
4917
|
-
at('TRANSFORM_NO_COLLECTION', `Transform "${step}" has no collection. Point it at the variable or step output holding the array.`, `collection: v('catalog')`);
|
|
4918
|
-
}
|
|
4919
|
-
else if (!(collection instanceof Expr)) {
|
|
4920
|
-
// Types are stripped at run time, so `collection: [{…}]` or
|
|
4921
|
-
// `collection: 'catalog'` reaches here as a raw value.
|
|
4922
|
-
at('TRANSFORM_COLLECTION_NOT_REF', `Transform "${step}" was given a ${Array.isArray(collection) ? 'raw array' : `raw ${typeof collection}`} as its `
|
|
4923
|
-
+ `collection. That field is a variable PATH — the runtime turns the string into a lookup, so a value there `
|
|
4924
|
-
+ `resolves to nothing and the transform silently sees an empty collection.`, typeof collection === 'string' ? `collection: v('${collection}')` : `collection: v('<varName>')`);
|
|
4925
|
-
}
|
|
4926
|
-
else if (collection.literal) {
|
|
4927
|
-
at('TRANSFORM_COLLECTION_LITERAL', `Transform "${step}" has a literal collection. Inline data cannot be a path — keep the array in a variable `
|
|
4928
|
-
+ `default (\`.var('items', types.array, [...])\`) and reference it, or read an upstream step's output.`, `collection: v('items')`);
|
|
4929
|
-
}
|
|
4930
|
-
else if (!/^\$vars\.[A-Za-z0-9_]+(\.[A-Za-z0-9_]+)*$/.test(collection.js.trim())) {
|
|
4931
|
-
// A computed expression (js`…`, tmpl`…`, `.concat(…)`) is the trap that
|
|
4932
|
-
// survives every local rung: a local run resolves it to a value, so
|
|
4933
|
-
// it runs correctly there and resolves to NOTHING once deployed, where the
|
|
4934
|
-
// string is used as a lookup path.
|
|
4935
|
-
at('TRANSFORM_COLLECTION_NOT_PATH', `Transform "${step}"'s collection must be a plain reference, but it is the expression `
|
|
4936
|
-
+ `"${collection.js.trim().slice(0, 60)}". The deployed runtime uses that string as a lookup PATH, so a `
|
|
4937
|
-
+ `computed expression resolves to nothing there even though it runs locally. Compute it in a script step `
|
|
4938
|
-
+ `first, then point the transform at that step's output.`, `collection: out('<scriptStep>')`);
|
|
4939
|
-
}
|
|
4940
|
-
else {
|
|
4941
|
-
// A collection that is not an array is PASSED THROUGH untouched — no error,
|
|
4942
|
-
// no operations applied (faithful to v1). The result is a transform that
|
|
4943
|
-
// looks like it ran and changed nothing.
|
|
4944
|
-
// Only a DECLARED scalar can be judged here: a step output's type is not
|
|
4945
|
-
// known offline, so this stays quiet about those rather than guessing.
|
|
4946
|
-
const refName = declaredRefName(collection.js, triggerId);
|
|
4947
|
-
const declaredType = refName ? typeOf.get(refName) : undefined;
|
|
4948
|
-
if (declaredType && ['string', 'number', 'boolean'].includes(declaredType)) {
|
|
4949
|
-
at('TRANSFORM_COLLECTION_SCALAR', `Transform "${step}"'s collection is "${refName}", declared as ${declaredType}. A transform runs its `
|
|
4950
|
-
+ `operations over an ARRAY; anything else is passed through untouched — no error, nothing applied — so `
|
|
4951
|
-
+ `the step would look like it ran and change nothing.`, `Declare it as types.array, or point the collection at the array field: collection: out('<step>', '<field>')`);
|
|
4952
|
-
}
|
|
4953
|
-
}
|
|
4954
|
-
// ── the operation chain ──
|
|
4955
|
-
const ops = inputs?.operations;
|
|
4956
|
-
if (!Array.isArray(ops) || ops.length === 0) {
|
|
4957
|
-
at('TRANSFORM_NO_OPERATIONS', `Transform "${step}" has no operations, so it passes its collection straight through. Add at least one `
|
|
4958
|
-
+ `filter / map / groupBy operation, or drop the node.`, `operations: [{ type: 'filter', filters: [{ field: 'status', condition: 'equals', value: 'open' }] }]`);
|
|
4959
|
-
return;
|
|
4960
|
-
}
|
|
4961
|
-
checkVariant(step, inputs, ops, at);
|
|
4962
|
-
ops.forEach((op, i) => {
|
|
4963
|
-
const where = `operations[${i}]`;
|
|
4964
|
-
// Read the discriminant before the narrowing below exhausts the union — at
|
|
4965
|
-
// run time it can be anything (an author typo survives type erasure).
|
|
4966
|
-
const kind = op?.type;
|
|
4967
|
-
if (op?.type === 'filter') {
|
|
4968
|
-
if (!Array.isArray(op.filters) || op.filters.length === 0) {
|
|
4969
|
-
at('TRANSFORM_EMPTY_FILTER', `Transform "${step}" ${where} is a filter with no rules — the runtime passes every item through it.`);
|
|
4970
|
-
return;
|
|
4971
|
-
}
|
|
4972
|
-
for (const [j, f] of op.filters.entries()) {
|
|
4973
|
-
if (!f?.field && f?.field !== '')
|
|
4974
|
-
at('TRANSFORM_FILTER_NO_FIELD', `Transform "${step}" ${where}.filters[${j}] has no field, so the rule is dropped and the filter `
|
|
4975
|
-
+ `matches everything.`);
|
|
4976
|
-
checkVocab(step, `${where}.filters[${j}].condition`, f?.condition, FILTER_CONDITIONS, 'filter condition', 'passes every item through the rule', diags);
|
|
4977
|
-
if (typeof f?.value === 'string' && LOOKS_LIKE_EXPRESSION.test(f.value.trim()))
|
|
4978
|
-
at('TRANSFORM_FILTER_VALUE_EXPR', `Transform "${step}" ${where}.filters[${j}] compares against ${JSON.stringify(f.value)}. Filter values `
|
|
4979
|
-
+ `are LITERAL only — the runtime evaluates nothing here, so every item would be compared against that `
|
|
4980
|
-
+ `text and the filter would return nothing. Use a literal, or do the comparison in a script step.`, `value: 500`);
|
|
4981
|
-
// `contains` is TWO operators wearing one name — the runtime does
|
|
4982
|
-
// `Array.includes` when the FIELD holds an array and `String.includes`
|
|
4983
|
-
// otherwise, faithful to v1. Which
|
|
4984
|
-
// one runs is decided by the data, so nothing static can pick for you —
|
|
4985
|
-
// but an ARRAY on the compare side is wrong under both readings
|
|
4986
|
-
// (`['a','b']` is stringified to `a,b` for the substring test, and
|
|
4987
|
-
// `includes` looks for the array itself as one element), and it is the
|
|
4988
|
-
// shape an author reaches for when they mean "any of these".
|
|
4989
|
-
if ((f?.condition === 'contains' || f?.condition === 'not_contains') && Array.isArray(f?.value))
|
|
4990
|
-
at('TRANSFORM_CONTAINS_ARRAY_VALUE', `Transform "${step}" ${where}.filters[${j}] uses "${f.condition}" against an ARRAY. "contains" compares `
|
|
4991
|
-
+ `ONE value: if the item's field holds an array it tests membership of that value, otherwise it tests `
|
|
4992
|
-
+ `for that value as a substring — either way an array on the right matches nothing, silently. For `
|
|
4993
|
-
+ `"any of these", use one rule per value with the filter's "or" operation.`, `condition: 'contains', value: ${JSON.stringify(f.value[0] ?? 'x')}`);
|
|
4994
|
-
}
|
|
4995
|
-
return;
|
|
4996
|
-
}
|
|
4997
|
-
if (op?.type === 'map') {
|
|
4998
|
-
if (!Array.isArray(op.mappings) || op.mappings.length === 0) {
|
|
4999
|
-
at('TRANSFORM_EMPTY_MAP', `Transform "${step}" ${where} is a map with no mappings — with keepOriginalFields off that turns every `
|
|
5000
|
-
+ `item into {}.`);
|
|
5001
|
-
return;
|
|
5002
|
-
}
|
|
5003
|
-
for (const [j, m] of op.mappings.entries()) {
|
|
5004
|
-
if (!m?.field)
|
|
5005
|
-
at('TRANSFORM_MAP_NO_FIELD', `Transform "${step}" ${where}.mappings[${j}] has no field, so nothing is carried into the output.`);
|
|
5006
|
-
// Omitted is fine — it defaults to `copy`.
|
|
5007
|
-
if (m?.transformation !== undefined)
|
|
5008
|
-
checkVocab(step, `${where}.mappings[${j}].transformation`, m.transformation, TRANSFORMATIONS, 'transformation', 'copies the value unchanged', diags);
|
|
5009
|
-
}
|
|
5010
|
-
return;
|
|
5011
|
-
}
|
|
5012
|
-
if (op?.type === 'groupBy') {
|
|
5013
|
-
if (!op.groupByField)
|
|
5014
|
-
at('TRANSFORM_NO_GROUP_FIELD', `Transform "${step}" ${where} is a groupBy with no groupByField, so every item lands in one anonymous `
|
|
5015
|
-
+ `group.`);
|
|
5016
|
-
for (const [j, a] of (op.aggregations ?? []).entries()) {
|
|
5017
|
-
if (!a?.alias?.trim())
|
|
5018
|
-
at('TRANSFORM_AGG_NO_ALIAS', `Transform "${step}" ${where}.aggregations[${j}] has no alias. The runtime drops an unaliased `
|
|
5019
|
-
+ `aggregation, so its result would simply be absent from the output.`);
|
|
5020
|
-
if (a?.operation !== 'count' && !a?.field?.trim())
|
|
5021
|
-
at('TRANSFORM_AGG_NO_FIELD', `Transform "${step}" ${where}.aggregations[${j}] is "${a?.operation}" with no field. Only "count" `
|
|
5022
|
-
+ `works without one; the runtime drops the rest, so the aggregate would be missing from the output.`);
|
|
5023
|
-
checkVocab(step, `${where}.aggregations[${j}].operation`, a?.operation, AGGREGATION_OPERATIONS, 'aggregation operation', 'yields null for that aggregate', diags);
|
|
5024
|
-
}
|
|
5025
|
-
return;
|
|
5026
|
-
}
|
|
5027
|
-
at('TRANSFORM_UNKNOWN_OP', `Transform "${step}" ${where} has type ${JSON.stringify(kind)}. The runtime skips an operation it does `
|
|
5028
|
-
+ `not recognize, so the collection would pass through unchanged. Use 'filter', 'map', or 'groupBy'.`);
|
|
5029
|
-
});
|
|
5030
|
-
}
|
|
5031
|
-
/**
|
|
5032
|
-
* Diagnose the `variant` discriminator on a transform step.
|
|
5033
|
-
*
|
|
5034
|
-
* A variant emits a single-purpose node type (`core.action.transform.filter`,
|
|
5035
|
-
* `.map`, `.group-by`) instead of the generic one. The platform runs all four
|
|
5036
|
-
* node types through the SAME script generators and dispatches on the
|
|
5037
|
-
* OPERATION's type, never the node's — so a mismatch here does not fault and
|
|
5038
|
-
* does not change the answer. It produces a node that lies: a `.filter` node
|
|
5039
|
-
* that actually maps, whose operation the designer's filter editor cannot even
|
|
5040
|
-
* show. That is exactly the class this SDK catches at rung 1, because no rung
|
|
5041
|
-
* below it ever will.
|
|
5042
|
-
*/
|
|
5043
|
-
function checkVariant(step, inputs, ops, at) {
|
|
5044
|
-
const variant = inputs?.variant;
|
|
5045
|
-
if (variant === undefined || variant === null)
|
|
5046
|
-
return; // generic — chains freely
|
|
5047
|
-
const variants = Object.keys(VARIANT_OPERATION);
|
|
5048
|
-
if (typeof variant !== 'string' || !variants.includes(variant)) {
|
|
5049
|
-
at('TRANSFORM_VARIANT_UNKNOWN', `Transform "${step}" has variant ${JSON.stringify(variant)}, which is not a Transform node type. `
|
|
5050
|
-
+ `The emitted node would be "core.action.transform.${String(variant)}", which no runtime resolves. `
|
|
5051
|
-
+ `Valid: ${variants.join(', ')} — or omit \`variant\` for the generic chaining Transform.`, nearestIn(variant, variants));
|
|
5052
|
-
return;
|
|
5053
|
-
}
|
|
5054
|
-
const expected = VARIANT_OPERATION[variant];
|
|
5055
|
-
if (ops.length !== 1) {
|
|
5056
|
-
at('TRANSFORM_VARIANT_ONE_OP', `Transform "${step}" is the "${variant}" variant but carries ${ops.length} operations. A variant node runs `
|
|
5057
|
-
+ `exactly one — the designer's ${variant} editor shows a single ${expected} operation and nothing else. `
|
|
5058
|
-
+ `Drop \`variant\` to chain them in one generic Transform, or split them across one node per step.`);
|
|
5059
|
-
return;
|
|
5060
|
-
}
|
|
5061
|
-
const got = ops[0]?.type;
|
|
5062
|
-
if (got !== expected) {
|
|
5063
|
-
at('TRANSFORM_VARIANT_OP_MISMATCH', `Transform "${step}" is the "${variant}" variant but its operation is ${JSON.stringify(got)}, not `
|
|
5064
|
-
+ `"${expected}". The node type does not drive the runtime — the OPERATION does — so this would emit a `
|
|
5065
|
-
+ `node labelled "${variant}" that actually does ${JSON.stringify(got)}, and the designer's ${variant} `
|
|
5066
|
-
+ `editor could not show it. Match them, or drop \`variant\` for the generic Transform.`, typeof got === 'string' && Object.values(VARIANT_OPERATION).includes(got)
|
|
5067
|
-
? `variant: '${variants.find((v) => VARIANT_OPERATION[v] === got)}'`
|
|
5068
|
-
: `type: '${expected}'`);
|
|
5069
|
-
}
|
|
5070
|
-
}
|
|
5071
|
-
/** Nearest member of a closed vocabulary, for a "did you mean" suggestion. */
|
|
5072
|
-
function nearestIn(value, vocab) {
|
|
5073
|
-
const v = typeof value === 'string' ? value.toLowerCase() : '';
|
|
5074
|
-
return vocab.find((c) => c.replace(/[-_]/g, '') === v.replace(/[^a-z]/g, ''))
|
|
5075
|
-
?? vocab.find((c) => c.startsWith(v) || (v !== '' && v.startsWith(c)));
|
|
5076
|
-
}
|
|
5077
|
-
/** A string that is an expression the transform runtime will NOT evaluate. */
|
|
5078
|
-
const LOOKS_LIKE_EXPRESSION = /^(=js:|\$vars\.|\{\s*\$vars\.)/;
|
|
5079
|
-
// The three closed vocabularies a transform operation carries. TypeScript types
|
|
5080
|
-
// them, but an authored `.flow.ts` runs with types ERASED — so `condition:
|
|
5081
|
-
// 'greater'` reaches the runtime, whose generated script falls through on a name
|
|
5082
|
-
// it does not know and is silent about it:
|
|
5083
|
-
// condition → the rule passes every item
|
|
5084
|
-
// transformation → the value is copied unchanged
|
|
5085
|
-
// aggregation → the aggregate is null
|
|
5086
|
-
// Green at every rung, wrong answer. Checked here so the author hears it at
|
|
5087
|
-
// rung 1 — a flow that never went through this SDK gets no such warning.
|
|
5088
|
-
const FILTER_CONDITIONS = [
|
|
5089
|
-
'equals', 'not_equals',
|
|
5090
|
-
'greater_than', 'less_than', 'greater_equal', 'less_equal',
|
|
5091
|
-
'contains', 'not_contains',
|
|
5092
|
-
'starts_with', 'not_starts_with',
|
|
5093
|
-
'ends_with', 'not_ends_with',
|
|
5094
|
-
'matches_regex',
|
|
5095
|
-
'in', 'not_in',
|
|
5096
|
-
'is_null', 'is_not_null',
|
|
5097
|
-
'is_empty', 'is_not_empty',
|
|
5098
|
-
];
|
|
5099
|
-
const TRANSFORMATIONS = ['copy', 'uppercase', 'lowercase', 'trim'];
|
|
5100
|
-
const AGGREGATION_OPERATIONS = ['count', 'sum', 'average', 'min', 'max', 'first', 'last', 'collect'];
|
|
5101
|
-
/** Flag a value that is not a member of a closed vocabulary, suggesting the nearest. */
|
|
5102
|
-
function checkVocab(step, at, value, vocab, what, effect, diags) {
|
|
5103
|
-
if (typeof value === 'string' && vocab.includes(value))
|
|
5104
|
-
return;
|
|
5105
|
-
const near = nearestIn(value, vocab);
|
|
5106
|
-
diags.push({
|
|
5107
|
-
level: 'error',
|
|
5108
|
-
code: 'TRANSFORM_BAD_VOCAB',
|
|
5109
|
-
message: `Transform "${step}" ${at} is ${JSON.stringify(value)}, which is not a valid ${what}. The runtime does not `
|
|
5110
|
-
+ `recognize it and ${effect}, so the result would be silently wrong. Valid: ${vocab.join(', ')}.`,
|
|
5111
|
-
step,
|
|
5112
|
-
...(near ? { suggestion: near } : {}),
|
|
5113
|
-
});
|
|
5114
|
-
}
|
|
5115
4731
|
/**
|
|
5116
4732
|
* Flag a case value whose type cannot match the discriminant's declared type.
|
|
5117
4733
|
*
|
|
@@ -5174,100 +4790,116 @@ const LOOP_FIELDS = new Set(['currentItem', 'currentIteration', 'collection', 'o
|
|
|
5174
4790
|
* before the 2.4 default (#868); its own fields are not audited here.
|
|
5175
4791
|
*/
|
|
5176
4792
|
const LOOP_FIELDS_1_0_0 = new Set(['currentItem', 'currentIndex', 'collection', 'output', 'error']);
|
|
5177
|
-
/**
|
|
5178
|
-
|
|
5179
|
-
|
|
5180
|
-
|
|
5181
|
-
* guard: a hand-authored `Id` row satisfies the node's own "at least one field"
|
|
5182
|
-
* rule, ships in the body, and is rejected — and the rejection is only logged,
|
|
5183
|
-
* so the row comes back unchanged and the run goes green.
|
|
5184
|
-
*/
|
|
5185
|
-
const DATAFABRIC_SYSTEM_COLUMNS = new Set(['Id', 'CreateTime', 'CreatedBy', 'UpdateTime', 'UpdatedBy']);
|
|
5186
|
-
function checkDataFabricColumns(step, columns, option, diags) {
|
|
5187
|
-
const system = columns.filter((c) => DATAFABRIC_SYSTEM_COLUMNS.has(c));
|
|
5188
|
-
if (system.length) {
|
|
5189
|
-
diags.push({
|
|
5190
|
-
level: 'error', code: 'DATAFABRIC_SYSTEM_COLUMN', step,
|
|
5191
|
-
message: `"${step}" writes ${system.map((c) => JSON.stringify(c)).join(', ')} in { ${option} }. `
|
|
5192
|
-
+ 'Data Fabric assigns its system columns (Id, CreateTime, CreatedBy, UpdateTime, UpdatedBy) and rejects a '
|
|
5193
|
-
+ 'write to them — and it only LOGS the rejection, so the row comes back unchanged and the run goes green.',
|
|
5194
|
-
});
|
|
5195
|
-
}
|
|
4793
|
+
/** The registered node type for a spec's kind, or undefined (a custom FlowAction has no kind). */
|
|
4794
|
+
function registeredKind(spec) {
|
|
4795
|
+
const kind = spec?.kind;
|
|
4796
|
+
return typeof kind === 'string' ? FLOW_NODE_TYPES.byKind(kind, spec) : undefined;
|
|
5196
4797
|
}
|
|
5197
4798
|
/**
|
|
5198
|
-
*
|
|
4799
|
+
* Check one step whose kind is registered: its version, its node-local rules
|
|
4800
|
+
* (reported through the family's catalog), and the expressions it carries.
|
|
4801
|
+
* Returns its placement, or undefined when the version cannot be resolved — the
|
|
4802
|
+
* step is then reported and not checked further.
|
|
5199
4803
|
*
|
|
5200
|
-
*
|
|
5201
|
-
*
|
|
5202
|
-
*
|
|
5203
|
-
*
|
|
5204
|
-
* to
|
|
4804
|
+
* Its bindings are not checked here: unlike a contribution (`checkContribution`),
|
|
4805
|
+
* a registered type's bindings exist only once it serializes. Closing every
|
|
4806
|
+
* `<bindings.NAME>` its definition references is the node type's own contract,
|
|
4807
|
+
* which serialize enforces (#942) and the type's tests pin; an author cannot
|
|
4808
|
+
* cause the gap, so there is no author diagnostic to report.
|
|
5205
4809
|
*/
|
|
5206
|
-
function
|
|
5207
|
-
|
|
5208
|
-
|
|
5209
|
-
|
|
5210
|
-
|
|
5211
|
-
|
|
5212
|
-
|
|
5213
|
-
|
|
5214
|
-
|
|
5215
|
-
|
|
4810
|
+
function checkRegisteredStep(entry, s, exprs, errorSource, opts, diags, typeOf, triggerId) {
|
|
4811
|
+
const spec = s.spec;
|
|
4812
|
+
/** The step's node-local rules and expressions, as `impl` defines them. */
|
|
4813
|
+
const runRules = (impl) => {
|
|
4814
|
+
const ctx = {
|
|
4815
|
+
step: s.name,
|
|
4816
|
+
version: impl.version,
|
|
4817
|
+
report: (code, message, suggestion) => diags.push(catalogued(entry, s.name, code, message, suggestion)),
|
|
4818
|
+
declaredRef: (js) => {
|
|
4819
|
+
const name = declaredRefName(js, triggerId);
|
|
4820
|
+
return name ? { name, type: typeOf.get(name) } : undefined;
|
|
4821
|
+
},
|
|
4822
|
+
options: opts,
|
|
4823
|
+
};
|
|
4824
|
+
impl.check?.(spec, ctx);
|
|
4825
|
+
for (const site of impl.expressions(spec)) {
|
|
4826
|
+
if ('code' in site) {
|
|
4827
|
+
// A script body: checked for its `$vars.` refs although it is a string. No
|
|
4828
|
+
// errorSource, as for `script()` (collectActionExprs).
|
|
4829
|
+
exprs.push({ js: site.code, step: s.name });
|
|
4830
|
+
}
|
|
4831
|
+
else {
|
|
4832
|
+
pushDeepExprs(site.value, s.name, errorSource, exprs);
|
|
4833
|
+
}
|
|
4834
|
+
}
|
|
4835
|
+
return ctx;
|
|
4836
|
+
};
|
|
4837
|
+
let impl;
|
|
4838
|
+
try {
|
|
4839
|
+
impl = resolveVersion(entry.type, spec, s.options?.version, `step "${s.name}"`);
|
|
5216
4840
|
}
|
|
5217
|
-
|
|
5218
|
-
|
|
5219
|
-
|
|
5220
|
-
|
|
5221
|
-
|
|
5222
|
-
|
|
4841
|
+
catch (e) {
|
|
4842
|
+
// A select() that picks an unregistered version is the node type's bug, not the author's pin.
|
|
4843
|
+
if (isInternalSdkError(e))
|
|
4844
|
+
throw e;
|
|
4845
|
+
diags.push({ level: 'error', code: 'NODE_VERSION_UNREGISTERED', step: s.name, message: e.message });
|
|
4846
|
+
// Still report the step's other mistakes in this same pass, with the version it
|
|
4847
|
+
// would get unpinned (select(), else the default) — as decompile lifts an
|
|
4848
|
+
// unregistered version — so fixing the pin is not followed by a second round.
|
|
4849
|
+
// It stays unplaced: the pin is wrong, so no family rule may rely on it.
|
|
4850
|
+
runRules(resolveVersion(entry.type, spec, undefined, `step "${s.name}"`));
|
|
4851
|
+
return undefined;
|
|
5223
4852
|
}
|
|
4853
|
+
const ctx = runRules(impl);
|
|
4854
|
+
return { entry, impl, spec, placement: { step: s.name, kind: impl.kind, spec, version: impl.version }, ctx };
|
|
5224
4855
|
}
|
|
5225
|
-
/**
|
|
5226
|
-
|
|
5227
|
-
|
|
5228
|
-
|
|
5229
|
-
|
|
5230
|
-
function checkDataFabricQuery(step, inputs, diags) {
|
|
5231
|
-
const listOnly = ['limit', 'skip', 'sort', 'fields'].filter((k) => inputs[k] !== undefined);
|
|
5232
|
-
if (listOnly.length && inputs.resultMode !== 'multiple') {
|
|
5233
|
-
diags.push({
|
|
5234
|
-
level: 'error', code: 'DATAFABRIC_QUERY_OPTS_WITHOUT_MULTIPLE', step,
|
|
5235
|
-
message: `"${step}" sets { ${listOnly.join(', ')} }, which only a list query uses, but its resultMode is `
|
|
5236
|
-
+ `${inputs.resultMode ? JSON.stringify(inputs.resultMode) : 'unset (a single-record read)'}.`,
|
|
5237
|
-
suggestion: "resultMode: 'multiple'",
|
|
5238
|
-
});
|
|
4856
|
+
/** Every non-literal Expr reachable in `value`, as expressions to check. */
|
|
4857
|
+
function pushDeepExprs(value, step, errorSource, out) {
|
|
4858
|
+
if (value instanceof Expr) {
|
|
4859
|
+
if (!value.literal)
|
|
4860
|
+
out.push({ js: value.js, step, errorSource });
|
|
5239
4861
|
}
|
|
5240
|
-
|
|
5241
|
-
|
|
5242
|
-
|
|
5243
|
-
diags.push({
|
|
5244
|
-
level: 'error', code: 'DATAFABRIC_LIMIT_RANGE', step,
|
|
5245
|
-
message: `"${step}": limit must be a whole number between 1 and 1000 (got ${inputs.limit}). 1000 is a hard `
|
|
5246
|
-
+ 'ceiling — a larger value truncates silently, so page with { skip } instead.',
|
|
5247
|
-
});
|
|
4862
|
+
else if (Array.isArray(value)) {
|
|
4863
|
+
for (const v of value)
|
|
4864
|
+
pushDeepExprs(v, step, errorSource, out);
|
|
5248
4865
|
}
|
|
5249
|
-
|
|
5250
|
-
|
|
5251
|
-
|
|
5252
|
-
diags.push({
|
|
5253
|
-
level: 'error', code: 'DATAFABRIC_IN_REQUIRES_MULTIPLE', step,
|
|
5254
|
-
message: `"${step}" filters with the 'in' operator, which is honoured only when resultMode is 'multiple'. `
|
|
5255
|
-
+ 'On a single-record read that row compiles away — and a read whose filters compiled away matches every record.',
|
|
5256
|
-
});
|
|
4866
|
+
else if (value && typeof value === 'object') {
|
|
4867
|
+
for (const v of Object.values(value))
|
|
4868
|
+
pushDeepExprs(v, step, errorSource, out);
|
|
5257
4869
|
}
|
|
5258
|
-
|
|
5259
|
-
|
|
5260
|
-
|
|
5261
|
-
|
|
5262
|
-
|
|
5263
|
-
|
|
5264
|
-
|
|
5265
|
-
|
|
5266
|
-
|
|
5267
|
-
|
|
5268
|
-
|
|
5269
|
-
|
|
5270
|
-
|
|
4870
|
+
}
|
|
4871
|
+
/** Whether `e` is one of the SDK's own `internal SDK error:` throws, which `check` never turns into a diagnostic. */
|
|
4872
|
+
function isInternalSdkError(e) {
|
|
4873
|
+
return e instanceof Error && e.message.startsWith('internal SDK error:');
|
|
4874
|
+
}
|
|
4875
|
+
/** A diagnostic a registered node type reports, at its family catalog's severity. */
|
|
4876
|
+
function catalogued(entry, step, code, message, suggestion) {
|
|
4877
|
+
const known = Object.hasOwn(entry.family.diagnostics, code) ? entry.family.diagnostics[code] : undefined;
|
|
4878
|
+
if (!known) {
|
|
4879
|
+
throw new Error(`internal SDK error: node family "${entry.family.id}" reports ${code}, which its diagnostics catalog does not list.`);
|
|
4880
|
+
}
|
|
4881
|
+
// Key order as the per-family rules always wrote it — `check --json` prints it.
|
|
4882
|
+
return { level: known.severity, code, step, message, ...(suggestion !== undefined ? { suggestion } : {}) };
|
|
4883
|
+
}
|
|
4884
|
+
/** Run each registered family's own rules over all of its placements in this flow. */
|
|
4885
|
+
function checkRegisteredFamilies(steps, unresolved, stepNames, diags) {
|
|
4886
|
+
const byFamily = new Map();
|
|
4887
|
+
for (const placed of steps.values()) {
|
|
4888
|
+
const list = byFamily.get(placed.entry.family.id) ?? [];
|
|
4889
|
+
list.push(placed);
|
|
4890
|
+
byFamily.set(placed.entry.family.id, list);
|
|
4891
|
+
}
|
|
4892
|
+
for (const placed of byFamily.values()) {
|
|
4893
|
+
const family = placed[0].entry.family;
|
|
4894
|
+
if (!family.check)
|
|
4895
|
+
continue;
|
|
4896
|
+
const ctx = {
|
|
4897
|
+
report: (step, code, message, suggestion) => diags.push(catalogued(placed[0].entry, step, code, message, suggestion)),
|
|
4898
|
+
hasStep: (step) => stepNames.has(step),
|
|
4899
|
+
placementOf: (step) => steps.get(step)?.placement,
|
|
4900
|
+
versionUnresolved: (step) => unresolved.has(step),
|
|
4901
|
+
};
|
|
4902
|
+
family.check(placed.map((p) => p.placement), ctx);
|
|
5271
4903
|
}
|
|
5272
4904
|
}
|
|
5273
4905
|
function collectActionExprs(s, out, errorSource) {
|
|
@@ -5294,20 +4926,6 @@ function collectActionExprs(s, out, errorSource) {
|
|
|
5294
4926
|
push(val);
|
|
5295
4927
|
return;
|
|
5296
4928
|
}
|
|
5297
|
-
if (s.spec.kind === 'script') {
|
|
5298
|
-
// A script body is a plain STRING, not an Expr — so it never went through
|
|
5299
|
-
// this check, and a bad `$vars.…` ref inside one used to surface only rungs
|
|
5300
|
-
// later, as a type error about generated code (flow-builder-sdk#20). The body
|
|
5301
|
-
// isn't parsed; the `$vars.<name>` refs it contains are checked like any other.
|
|
5302
|
-
out.push({ js: s.spec.inputs.code, step: s.name });
|
|
5303
|
-
return;
|
|
5304
|
-
}
|
|
5305
|
-
if (s.spec.kind === 'transform') {
|
|
5306
|
-
// The only expression a transform carries is its collection; everything
|
|
5307
|
-
// else is a literal or a field name (see checkTransform).
|
|
5308
|
-
push(s.spec.inputs.collection);
|
|
5309
|
-
return;
|
|
5310
|
-
}
|
|
5311
4929
|
if (s.spec.kind === 'hitl') {
|
|
5312
4930
|
// A human task's only expressions are the values its shown fields display.
|
|
5313
4931
|
// Without this a typo'd reference inside a form field would reach the
|
|
@@ -5326,30 +4944,6 @@ function collectActionExprs(s, out, errorSource) {
|
|
|
5326
4944
|
push(val);
|
|
5327
4945
|
return;
|
|
5328
4946
|
}
|
|
5329
|
-
if (s.spec.kind === 'dataFabricRead' || s.spec.kind === 'dataFabricCreate'
|
|
5330
|
-
|| s.spec.kind === 'dataFabricUpdate' || s.spec.kind === 'dataFabricDelete') {
|
|
5331
|
-
// Every value a Data Fabric step carries is authored against flow scope: a
|
|
5332
|
-
// filter's comparison value, the record id, the columns written. The family
|
|
5333
|
-
// was missing here entirely, so a `$vars.` typo in any of them travelled to
|
|
5334
|
-
// the platform unchecked — and the delete verb's no-output rule below has
|
|
5335
|
-
// nothing to fire on until these are collected.
|
|
5336
|
-
if (s.spec.kind === 'dataFabricRead') {
|
|
5337
|
-
for (const f of s.spec.inputs.filters ?? [])
|
|
5338
|
-
push(f.value);
|
|
5339
|
-
return;
|
|
5340
|
-
}
|
|
5341
|
-
if (s.spec.kind === 'dataFabricCreate') {
|
|
5342
|
-
for (const val of Object.values(s.spec.inputs.values ?? {}))
|
|
5343
|
-
push(val);
|
|
5344
|
-
return;
|
|
5345
|
-
}
|
|
5346
|
-
push(s.spec.inputs.record?.byId);
|
|
5347
|
-
if (s.spec.kind === 'dataFabricUpdate') {
|
|
5348
|
-
for (const val of Object.values(s.spec.inputs.set ?? {}))
|
|
5349
|
-
push(val);
|
|
5350
|
-
}
|
|
5351
|
-
return;
|
|
5352
|
-
}
|
|
5353
4947
|
if (s.spec.kind === 'connector') {
|
|
5354
4948
|
// Same gap on the connector surface: `messageToSend: tmpl`…${v('typo')}``
|
|
5355
4949
|
// reached the runtime unchecked.
|
|
@@ -5376,6 +4970,17 @@ function collectActionExprs(s, out, errorSource) {
|
|
|
5376
4970
|
const PER_TENANT_ERROR_PORT_KINDS = new Set([
|
|
5377
4971
|
'connector', 'rpaWorkflow', 'apiWorkflow', 'agenticProcess', 'agent', 'ixpExtract',
|
|
5378
4972
|
]);
|
|
4973
|
+
/**
|
|
4974
|
+
* The ONERROR_NO_ERROR_PORT sentence for a typed step — a built-in family or a
|
|
4975
|
+
* registered node type — whose node type has no error port. One spelling for
|
|
4976
|
+
* both, so porting a family onto the registry changes no message.
|
|
4977
|
+
*/
|
|
4978
|
+
function typedNoErrorPortMessage(stepName, kind, def) {
|
|
4979
|
+
const nodeType = def ? ` (${def.nodeType}@${def.version})` : '';
|
|
4980
|
+
return `Step "${stepName}" is a ${kind} step${nodeType}, and that node type has no error port — its definition does not `
|
|
4981
|
+
+ `set supportsErrorHandling, so the edge would leave a handle the node does not have. Handle the failure `
|
|
4982
|
+
+ `downstream instead (a delay cannot fail; a wait resumes or does not).`;
|
|
4983
|
+
}
|
|
5379
4984
|
/** The five fields the platform's error envelope declares — no more, no less. */
|
|
5380
4985
|
// The five the definitions declare, plus the two every probed runtime envelope
|
|
5381
4986
|
// carried anyway (`response`, `element`) — see ErrorEnvelopeField in core/expr.ts.
|
|
@@ -5385,7 +4990,9 @@ const ERROR_ENVELOPE_FIELDS = ['code', 'message', 'detail', 'category', 'status'
|
|
|
5385
4990
|
* mistakes at build time (no previous step, two handlers on one node, an empty
|
|
5386
4991
|
* handler); these are the ones only a checker can see.
|
|
5387
4992
|
*/
|
|
5388
|
-
function checkOnError(_handler, step, diags
|
|
4993
|
+
function checkOnError(_handler, step, diags,
|
|
4994
|
+
/** The step's registry record, when its kind is registered and its version resolved. */
|
|
4995
|
+
placed) {
|
|
5389
4996
|
// The builder refuses a handler with no eligible step before it, so a missing
|
|
5390
4997
|
// source here means a hand-built `BuiltFlow`; there is nothing to say about it.
|
|
5391
4998
|
if (!step)
|
|
@@ -5439,6 +5046,35 @@ function checkOnError(_handler, step, diags) {
|
|
|
5439
5046
|
return;
|
|
5440
5047
|
}
|
|
5441
5048
|
const spec = step.spec;
|
|
5049
|
+
// A registered node type: a bundled one asks its definition at the version it
|
|
5050
|
+
// will be emitted at; a per-tenant one answers for itself (its errorPort).
|
|
5051
|
+
if (registeredKind(spec)) {
|
|
5052
|
+
// No record: the version did not resolve, which NODE_VERSION_UNREGISTERED
|
|
5053
|
+
// already reports where the step is checked.
|
|
5054
|
+
if (!placed)
|
|
5055
|
+
return;
|
|
5056
|
+
const { impl } = placed;
|
|
5057
|
+
if (impl.definition === 'per-tenant') {
|
|
5058
|
+
const port = typeof impl.errorPort === 'function' ? impl.errorPort(spec) : impl.errorPort === true;
|
|
5059
|
+
if (!port) {
|
|
5060
|
+
diags.push({
|
|
5061
|
+
level: 'error', code: 'ONERROR_NO_ERROR_PORT', step: step.name,
|
|
5062
|
+
message: declaredErrorPortRefusal(step.name, impl.typeName, impl.version),
|
|
5063
|
+
});
|
|
5064
|
+
return;
|
|
5065
|
+
}
|
|
5066
|
+
}
|
|
5067
|
+
else {
|
|
5068
|
+
const def = bundledDefinition(impl.typeName, impl.version);
|
|
5069
|
+
if (def !== undefined && !definitionHasErrorPort(def)) {
|
|
5070
|
+
diags.push({ level: 'error', code: 'ONERROR_NO_ERROR_PORT', message: typedNoErrorPortMessage(step.name, impl.kind, def), step: step.name });
|
|
5071
|
+
return;
|
|
5072
|
+
}
|
|
5073
|
+
}
|
|
5074
|
+
// The port exists; the node type may still refuse the handler (plain HTTP's rule, once ported).
|
|
5075
|
+
impl.checkErrorHandler?.(placed.spec, placed.ctx);
|
|
5076
|
+
return;
|
|
5077
|
+
}
|
|
5442
5078
|
// A typed family with a bundled definition: ask the definition serialize will
|
|
5443
5079
|
// emit, at the version it will emit (#1011). For instance `voiceAgent()`
|
|
5444
5080
|
// defaults to uipath.agent.voice@1.0 (no error port) and `{ version: '1.1' }`
|
|
@@ -5451,13 +5087,10 @@ function checkOnError(_handler, step, diags) {
|
|
|
5451
5087
|
? bundledDef !== undefined && !definitionHasErrorPort(bundledDef)
|
|
5452
5088
|
: !PER_TENANT_ERROR_PORT_KINDS.has(spec.kind);
|
|
5453
5089
|
if (noPort) {
|
|
5454
|
-
const nodeType = bundledDef ? ` (${bundledDef.nodeType}@${bundledDef.version})` : '';
|
|
5455
5090
|
diags.push({
|
|
5456
5091
|
level: 'error',
|
|
5457
5092
|
code: 'ONERROR_NO_ERROR_PORT',
|
|
5458
|
-
message:
|
|
5459
|
-
+ `set supportsErrorHandling, so the edge would leave a handle the node does not have. Handle the failure `
|
|
5460
|
-
+ `downstream instead (a delay cannot fail; a wait resumes or does not).`,
|
|
5093
|
+
message: typedNoErrorPortMessage(step.name, spec.kind, bundledDef),
|
|
5461
5094
|
step: step.name,
|
|
5462
5095
|
});
|
|
5463
5096
|
return;
|
|
@@ -5700,14 +5333,14 @@ function checkStepRefs(built, diags) {
|
|
|
5700
5333
|
index(built.steps, false, '', undefined);
|
|
5701
5334
|
/** Every ref, with where it sits. */
|
|
5702
5335
|
const found = [];
|
|
5703
|
-
const collect = (steps, from, scope, arm) => {
|
|
5336
|
+
const collect = (steps, from, scope, arm, enclosing = []) => {
|
|
5704
5337
|
let source = from;
|
|
5705
5338
|
for (const [i, s] of steps.entries()) {
|
|
5706
5339
|
if (s.kind === 'action')
|
|
5707
5340
|
source = s.name;
|
|
5708
5341
|
if (s.kind === 'stepToRef') {
|
|
5709
5342
|
found.push({
|
|
5710
|
-
target: s.target, port: s.port, from: source, scope, arm,
|
|
5343
|
+
target: s.target, port: s.port, from: source, scope, arm, enclosing,
|
|
5711
5344
|
// Only a default-port ref hands the path off, so only it can strand
|
|
5712
5345
|
// what follows; a side exit leaves the chain running.
|
|
5713
5346
|
after: s.port === 'output' ? steps.length - i - 1 : 0,
|
|
@@ -5715,23 +5348,23 @@ function checkStepRefs(built, diags) {
|
|
|
5715
5348
|
continue;
|
|
5716
5349
|
}
|
|
5717
5350
|
if (s.kind === 'stepToList')
|
|
5718
|
-
collect(s.body, source, scope, arm);
|
|
5351
|
+
collect(s.body, source, scope, arm, enclosing);
|
|
5719
5352
|
else if (s.kind === 'branch') {
|
|
5720
|
-
collect(s.then, source, scope, arm);
|
|
5721
|
-
collect(s.otherwise, source, scope, arm);
|
|
5353
|
+
collect(s.then, source, scope, arm, enclosing);
|
|
5354
|
+
collect(s.otherwise, source, scope, arm, enclosing);
|
|
5722
5355
|
}
|
|
5723
5356
|
else if (s.kind === 'switch') {
|
|
5724
|
-
s.cases.forEach((c) => collect(c.body, source, scope, arm));
|
|
5725
|
-
collect(s.default ?? [], source, scope, arm);
|
|
5357
|
+
s.cases.forEach((c) => collect(c.body, source, scope, arm, enclosing));
|
|
5358
|
+
collect(s.default ?? [], source, scope, arm, enclosing);
|
|
5726
5359
|
}
|
|
5727
5360
|
else if (s.kind === 'parallel')
|
|
5728
|
-
s.arms.forEach((a, i) => collect(a, source, scope, `${s.name}#${i}
|
|
5361
|
+
s.arms.forEach((a, i) => collect(a, source, scope, `${s.name}#${i}`, enclosing));
|
|
5729
5362
|
else if (s.kind === 'loop' || s.kind === 'doWhile')
|
|
5730
|
-
collect(s.body, source, s.name, arm);
|
|
5363
|
+
collect(s.body, source, s.name, arm, [...enclosing, s.name]);
|
|
5731
5364
|
}
|
|
5732
5365
|
};
|
|
5733
5366
|
collect(built.steps, undefined, '', undefined);
|
|
5734
|
-
for (const { target, from, after, scope, arm } of found) {
|
|
5367
|
+
for (const { target, from, after, scope, arm, enclosing } of found) {
|
|
5735
5368
|
const where = from ? `after "${from}"` : 'at the start of the flow';
|
|
5736
5369
|
const say = (level, code, message, suggestion) => {
|
|
5737
5370
|
diags.push({ level, code, message, step: from ?? 'stepToRef', ...(suggestion ? { suggestion } : {}) });
|
|
@@ -5742,6 +5375,16 @@ function checkStepRefs(built, diags) {
|
|
|
5742
5375
|
+ `${near ? ` (did you mean "${near}"?)` : ''}. It takes the same name you passed to .step().`, near ? `.stepToRef('${near}')` : undefined);
|
|
5743
5376
|
continue;
|
|
5744
5377
|
}
|
|
5378
|
+
// A ref to a container the path is INSIDE names it as a step, so the edge
|
|
5379
|
+
// enters it from its outer input: it starts the loop over from its first
|
|
5380
|
+
// item, while the iteration the path was in never finishes. What the author
|
|
5381
|
+
// means is almost always the next item, or out of the loop (#1129).
|
|
5382
|
+
if (enclosing.includes(target)) {
|
|
5383
|
+
say('error', 'REF_TO_OWN_LOOP', `.stepToRef('${target}') ${where} names the loop it is inside. That edge enters "${target}" from its outer `
|
|
5384
|
+
+ `input, which starts the loop over instead of going on to the next item. Use .continue() to go on to the `
|
|
5385
|
+
+ `next iteration, or .break() to leave the loop.`, '.continue()');
|
|
5386
|
+
continue;
|
|
5387
|
+
}
|
|
5745
5388
|
if (inPortList.has(target)) {
|
|
5746
5389
|
say('error', 'REF_INTO_PORT_LIST', `.stepToRef('${target}') ${where} targets a step inside another port's path. That path runs only when ITS `
|
|
5747
5390
|
+ `port fired — it is not somewhere another edge can jump into. Point at a step on the main path, or end `
|
|
@@ -5760,12 +5403,15 @@ function checkStepRefs(built, diags) {
|
|
|
5760
5403
|
+ `iteration, so those have no value. Point at a step after the loop, or do this work inside the body.`);
|
|
5761
5404
|
continue;
|
|
5762
5405
|
}
|
|
5763
|
-
// OUT of a loop body
|
|
5764
|
-
//
|
|
5765
|
-
//
|
|
5766
|
-
|
|
5767
|
-
|
|
5768
|
-
|
|
5406
|
+
// OUT of a loop body to a step after it. The container's own handles are
|
|
5407
|
+
// the measured ways out — `.break()` (its break handle) and `.continue()`
|
|
5408
|
+
// (its continue handle). Whether the runtime accepts an edge leaving a
|
|
5409
|
+
// bpmn:SubProcess some other way is not measured, so this warns and leaves
|
|
5410
|
+
// the verdict to `validate`.
|
|
5411
|
+
say('warning', 'REF_OUT_OF_LOOP', `.stepToRef('${target}') ${where} leaves the body of loop "${scope}" by an edge straight to a step outside it. `
|
|
5412
|
+
+ `To leave the loop, use .break() (the loop's break handle); to go on to the next item, use .continue(). `
|
|
5413
|
+
+ `Whether the runtime honours any other edge out of a loop body is not something this checker can prove: `
|
|
5414
|
+
+ `verify with uip maestro flow validate before relying on it.`);
|
|
5769
5415
|
}
|
|
5770
5416
|
// Best effort from here: the mechanism is understood, the runtime's behaviour
|
|
5771
5417
|
// is not measured, so these WARN and leave the verdict to `validate`.
|
|
@@ -5789,6 +5435,23 @@ function checkStepRefs(built, diags) {
|
|
|
5789
5435
|
}
|
|
5790
5436
|
}
|
|
5791
5437
|
}
|
|
5438
|
+
/**
|
|
5439
|
+
* LOOP_EXIT_IN_PARALLEL_ARM: a `.continue()` inside a `.parallel` arm sends
|
|
5440
|
+
* that arm's token to the container's continue handle, so it never reaches the
|
|
5441
|
+
* arm's Merge. The Merge is a bpmn:ParallelGateway that waits for every branch
|
|
5442
|
+
* it forked, and the iteration it belongs to is still running: the join never
|
|
5443
|
+
* fires and the iteration hangs. (`.break()` leaves the whole container, so
|
|
5444
|
+
* whether the runtime cancels the pending Merge is a different, unmeasured
|
|
5445
|
+
* question; it is not refused here.)
|
|
5446
|
+
*/
|
|
5447
|
+
function loopExitInArm() {
|
|
5448
|
+
return {
|
|
5449
|
+
level: 'error', code: 'LOOP_EXIT_IN_PARALLEL_ARM',
|
|
5450
|
+
message: `.continue() inside a .parallel() arm sends that arm's path to the loop's continue handle, so it never `
|
|
5451
|
+
+ `reaches the arm's Merge, which waits for every branch it forked: the join never fires and the iteration `
|
|
5452
|
+
+ `hangs. Put the .continue() after the .parallel(), on a .branch() that reads what the arms produced.`,
|
|
5453
|
+
};
|
|
5454
|
+
}
|
|
5792
5455
|
/**
|
|
5793
5456
|
* A field read off `$vars.<step>.error` — the envelope, and the one shape the
|
|
5794
5457
|
* authoring surface writes.
|