@uipath/maestro-builder-sdk 6.8.3 → 6.8.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/check.js +377 -118
- package/dist/core/actions.d.ts +20 -20
- package/dist/core/cli-spelling.d.ts +10 -0
- package/dist/core/cli-spelling.js +24 -0
- package/dist/core/hitl-routing.d.ts +129 -34
- package/dist/core/hitl-routing.js +97 -13
- package/dist/core/lookups.js +4 -2
- package/dist/core/step-ports.d.ts +8 -0
- package/dist/core/step-ports.js +37 -30
- package/dist/decompile.js +85 -14
- package/dist/registry/generators/markdown/sections/lookups.js +20 -5
- package/dist/registry/prepare/ledger.d.ts +3 -1
- package/dist/registry/prepare/ledger.js +55 -5
- package/dist/registry/prepare/messages.d.ts +8 -0
- package/dist/registry/prepare/messages.js +17 -0
- package/dist/registry/prepare/pipeline.js +4 -1
- package/dist/registry/prepare/resolve.d.ts +27 -2
- package/dist/registry/prepare/resolve.js +69 -7
- package/dist/serialize.js +259 -66
- package/lib/library-json.samples/index.json +18 -0
- package/lib/library-json.samples/uipath-getresponse-getresponse/send-newsletter@1.0.0.json +159 -0
- package/lib/library-json.samples/uipath-getresponse-getresponse/send-newsletter@1.0.0.v1def.json +323 -0
- package/lib/library-json.samples/uipath-salesforce-slack/create-group-direct-message@1.0.0.json +149 -0
- package/lib/library-json.samples/uipath-salesforce-slack/create-group-direct-message@1.0.0.v1def.json +237 -0
- package/package.json +2 -2
package/dist/check.js
CHANGED
|
@@ -11,11 +11,11 @@ import { isLookupToken, resolvedValue, unresolvedLookupMessage, } from './core/l
|
|
|
11
11
|
import { EVENT_FILTER_OPERATORS, readEventFilter, eventFilterProblem } from './event-filters.js';
|
|
12
12
|
import { extractRefs } from './core/expr-check.js';
|
|
13
13
|
import { bindingSelfNameMessage } from './core/binding-messages.js';
|
|
14
|
-
import { hitlRoutesPerOutcome } from './core/hitl-routing.js';
|
|
15
|
-
import { declaredExits, hasExits } from './core/step-ports.js';
|
|
14
|
+
import { hitlDerivesOutcomeHandles, hitlExitModel, hitlRoutesPerOutcome } from './core/hitl-routing.js';
|
|
15
|
+
import { declaredExits, hasExits, stopsAtEnd } from './core/step-ports.js';
|
|
16
16
|
import { FLOW_SCHEMA_VERSIONS } from './format-profile.js';
|
|
17
17
|
import { compareSchemaVersions, schemaVersionRefusal } from './schema-version.js';
|
|
18
|
-
import { prepareCommand } from './core/cli-spelling.js';
|
|
18
|
+
import { prepareCommand, resolveFlagArg } from './core/cli-spelling.js';
|
|
19
19
|
export function check(built, opts = {}) {
|
|
20
20
|
return checkFlow(built, [], opts);
|
|
21
21
|
}
|
|
@@ -107,14 +107,16 @@ function checkFlow(built, ancestors, opts = {}) {
|
|
|
107
107
|
const hitlOutcomeSteps = new Map();
|
|
108
108
|
const hitlOutcomeRouted = new Map();
|
|
109
109
|
// The other half of that bookkeeping: hitl steps that declare a CHOICE (more
|
|
110
|
-
// than one outcome) and
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
//
|
|
114
|
-
//
|
|
115
|
-
//
|
|
116
|
-
//
|
|
117
|
-
//
|
|
110
|
+
// than one outcome) and do not route it, so every outcome goes to the same
|
|
111
|
+
// place — the same next step (a base or quick-form task that does not route
|
|
112
|
+
// per outcome), or the one `completed` exit (an app-backed variant). Since
|
|
113
|
+
// more than one outcome infers per-outcome routing on the base node, reaching
|
|
114
|
+
// this map means that was chosen — by a variant, a pinned version, or
|
|
115
|
+
// `outcomePorts: false` — and WHICH decides what the remedy can be. Whether
|
|
116
|
+
// it is a mistake at all depends on something no single step can see —
|
|
117
|
+
// whether anything downstream reads the chosen outcome — so the decision
|
|
118
|
+
// waits until the expressions are collected (see HITL_OUTCOMES_UNREACHABLE
|
|
119
|
+
// below).
|
|
118
120
|
const hitlChoiceNoPorts = new Map();
|
|
119
121
|
// Data Fabric writes that target a record `{ fromRead: '<step>' }` — update and
|
|
120
122
|
// delete share the selector, so they share the check.
|
|
@@ -179,6 +181,30 @@ function checkFlow(built, ancestors, opts = {}) {
|
|
|
179
181
|
step: term.name,
|
|
180
182
|
});
|
|
181
183
|
}
|
|
184
|
+
// Nothing may follow a human task whose EVERY outcome ends the run, except a
|
|
185
|
+
// `.return()`. Each outcome gets its own End node — the flow runtime never
|
|
186
|
+
// reads `action`, so only the graph can end the run there — which leaves
|
|
187
|
+
// the next step with no incoming edge. A `.return()` is an End already, so
|
|
188
|
+
// the outcomes join it. Side exits attach to the task and do not count.
|
|
189
|
+
for (const [i, st] of steps.entries()) {
|
|
190
|
+
if (st.kind !== 'action' || st.spec.kind !== 'hitl')
|
|
191
|
+
continue;
|
|
192
|
+
const model = hitlExitModel(st.spec.inputs, st.options?.version);
|
|
193
|
+
if (model.shape !== 'every-outcome' || !model.exits.every((e) => e.endsProcess))
|
|
194
|
+
continue;
|
|
195
|
+
const after = steps.slice(i + 1).find((x) => x.kind !== 'stepToList' && x.kind !== 'stepToRef');
|
|
196
|
+
if (after === undefined || after.kind === 'return')
|
|
197
|
+
continue;
|
|
198
|
+
diags.push({
|
|
199
|
+
level: 'error',
|
|
200
|
+
code: 'HITL_UNREACHABLE_AFTER',
|
|
201
|
+
message: `Every outcome of human task "${st.name}" is action: 'End' `
|
|
202
|
+
+ `(${model.exits.map((e) => e.value).join(', ')}), so each one ends the run at its own End node, and `
|
|
203
|
+
+ `"${'name' in after ? after.name : after.kind}" after it can never run. Drop action: 'End' from the `
|
|
204
|
+
+ 'outcomes that should continue, or end the path here with .return().',
|
|
205
|
+
step: st.name,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
182
208
|
// The action a port-scoped edge leaves, tracked because `stepToList` and
|
|
183
209
|
// `stepToRef` FOLLOW the step they attach to rather than nesting inside it.
|
|
184
210
|
let prevAction;
|
|
@@ -224,41 +250,35 @@ function checkFlow(built, ancestors, opts = {}) {
|
|
|
224
250
|
});
|
|
225
251
|
}
|
|
226
252
|
}
|
|
227
|
-
// The mirror of HITL_OUTCOME_PORTS_OFF below: `completed` on a
|
|
228
|
-
// that
|
|
253
|
+
// The mirror of HITL_OUTCOME_PORTS_OFF below: `completed` on a base or
|
|
254
|
+
// quick-form human task that has outcomes.
|
|
229
255
|
//
|
|
230
|
-
//
|
|
231
|
-
//
|
|
232
|
-
// `
|
|
233
|
-
//
|
|
234
|
-
//
|
|
235
|
-
//
|
|
236
|
-
//
|
|
237
|
-
// inspects ports.
|
|
238
|
-
//
|
|
239
|
-
// Since more than one outcome INFERS the routing, this is now also the
|
|
240
|
-
// rule an author meets when they write the old shape on a multi-outcome
|
|
241
|
-
// task, so the message names the two ways back to a `completed` exit.
|
|
256
|
+
// The product draws one `outcome-<id>` handle per outcome on those two node
|
|
257
|
+
// types, at every definition version, and `completed` only when there are
|
|
258
|
+
// none (flow-workbench `build-handle-customization.ts`). An edge on it is
|
|
259
|
+
// stale: Studio Web and the converter remap it to the PRIMARY outcome
|
|
260
|
+
// (#3512), so every other outcome loses its path — and `flow validate`
|
|
261
|
+
// accepts it without `handleCustomization`, so nothing later catches it.
|
|
262
|
+
// `serialize` refuses to emit one; this says why, in the author's terms.
|
|
242
263
|
if ((s.kind === 'stepToList' || s.kind === 'stepToRef') && s.port === 'completed') {
|
|
243
264
|
const prevSpec = prevAction?.spec;
|
|
244
265
|
const isHitl = !!prevSpec && 'kind' in prevSpec && prevSpec.kind === 'hitl';
|
|
245
|
-
const
|
|
246
|
-
if (
|
|
247
|
-
const
|
|
248
|
-
const
|
|
249
|
-
const why = prevSpec.inputs?.exposeError === true ? 'exposeError selects'
|
|
250
|
-
: prevSpec.inputs?.outcomePorts === true ? 'outcomePorts selects'
|
|
251
|
-
: `its ${(prevSpec.inputs?.outcomes ?? []).length} outcomes select`;
|
|
266
|
+
const model = isHitl ? hitlExitModel(prevSpec.inputs, prevAction?.options?.version) : undefined;
|
|
267
|
+
if (model && model.shape !== 'placeholder') {
|
|
268
|
+
const ports = model.exits.map((e) => `"${e.port}"`).join(', ');
|
|
269
|
+
const routed = model.shape === 'per-outcome';
|
|
252
270
|
diags.push({
|
|
253
271
|
level: 'error', code: 'HITL_COMPLETED_PORT_GONE', step: prevAction?.name,
|
|
254
|
-
message: `Port "completed" does not exist on human task "${prevAction?.name}".
|
|
255
|
-
+
|
|
256
|
-
+ `
|
|
257
|
-
+
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
272
|
+
message: `Port "completed" does not exist on human task "${prevAction?.name}". It has outcomes, so `
|
|
273
|
+
+ `the product draws one handle per outcome (${ports}) and no 'completed' — an edge there is `
|
|
274
|
+
+ `remapped to the primary outcome and the others lose their path. `
|
|
275
|
+
+ (routed
|
|
276
|
+
? `The FIRST outcome ("${model.exits[0]?.port}") is what continues the main path.`
|
|
277
|
+
: `.step() after this task already continues every outcome to the next step.`),
|
|
278
|
+
suggestion: routed
|
|
279
|
+
? `route the outcomes (.stepToList('${model.exits[1]?.port ?? model.exits[0]?.port}', …)), or continue `
|
|
280
|
+
+ `every outcome to the next step with outcomePorts: false`
|
|
281
|
+
: `drop the 'completed' route — .step() continues every outcome — or route each outcome with .stepSwitch`,
|
|
262
282
|
});
|
|
263
283
|
}
|
|
264
284
|
}
|
|
@@ -273,29 +293,20 @@ function checkFlow(built, ancestors, opts = {}) {
|
|
|
273
293
|
+ `${prevAction ? `"${prevAction.name}" ` : ''}is not a human task.`,
|
|
274
294
|
});
|
|
275
295
|
}
|
|
276
|
-
else if (!hitlRoutesPerOutcome(prevSpec.inputs, prevAction?.options?.version)) {
|
|
277
|
-
// Reachable three ways now that more than one outcome infers the
|
|
278
|
-
// routing: `outcomePorts: false`, a pinned `{ version: '1.0' }`, or a
|
|
279
|
-
// variant that has no per-outcome definition to select.
|
|
280
|
-
const off = prevSpec.inputs?.outcomePorts === false ? 'outcomePorts: false turns them off'
|
|
281
|
-
: prevSpec.inputs?.variant !== undefined
|
|
282
|
-
? `the '${prevSpec.inputs.variant}' variant has no per-outcome definition version`
|
|
283
|
-
: prevAction?.options?.version !== undefined
|
|
284
|
-
? `{ version: '${prevAction.options.version}' } pins the definition that has none`
|
|
285
|
-
: 'it declares fewer than two outcomes';
|
|
286
|
-
diags.push({
|
|
287
|
-
level: 'error', code: 'HITL_OUTCOME_PORTS_OFF', step: prevAction?.name,
|
|
288
|
-
message: `Port "${s.port}" routes a per-outcome exit, but human task "${prevAction?.name}" `
|
|
289
|
-
+ `does not have them — ${off}, so its single exit is 'completed'. Per-outcome exits are the `
|
|
290
|
-
+ `node's 1.1 definition.`,
|
|
291
|
-
suggestion: prevSpec.inputs?.variant !== undefined
|
|
292
|
-
? `route on out('${prevAction?.name}', 'Action') downstream`
|
|
293
|
-
: `outcomePorts: true`,
|
|
294
|
-
});
|
|
295
|
-
}
|
|
296
296
|
else {
|
|
297
|
+
const model = hitlExitModel(prevSpec.inputs, prevAction?.options?.version);
|
|
297
298
|
const ids = (prevSpec.inputs.outcomes ?? []).map((o, i) => outcomeSlug(typeof o === 'string' ? o : o?.name, i));
|
|
298
|
-
if (
|
|
299
|
+
if (model.shape === 'placeholder') {
|
|
300
|
+
diags.push({
|
|
301
|
+
level: 'error', code: 'HITL_OUTCOME_PORTS_OFF', step: prevAction?.name,
|
|
302
|
+
message: `Port "${s.port}" routes a per-outcome exit, but human task "${prevAction?.name}" `
|
|
303
|
+
+ `has none — ${ids.length === 0 ? 'it declares no outcomes'
|
|
304
|
+
: `the '${prevSpec.inputs?.variant}' variant reads its outcomes from its app`}, so its single `
|
|
305
|
+
+ `exit is 'completed'.`,
|
|
306
|
+
suggestion: `route on out('${prevAction?.name}', 'Action') downstream`,
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
else if (!ids.includes(want)) {
|
|
299
310
|
diags.push({
|
|
300
311
|
level: 'error', code: 'HITL_OUTCOME_PORT_UNKNOWN', step: prevAction?.name,
|
|
301
312
|
message: `Port "${s.port}" routes the outcome id ${JSON.stringify(want)}, but human task `
|
|
@@ -303,6 +314,19 @@ function checkFlow(built, ancestors, opts = {}) {
|
|
|
303
314
|
+ `outcome names slugified (lowercase, non-alphanumerics to '-').`,
|
|
304
315
|
});
|
|
305
316
|
}
|
|
317
|
+
else if (model.shape === 'every-outcome') {
|
|
318
|
+
// The port is real, but it is ALREADY wired: a task that does not
|
|
319
|
+
// route per outcome continues every outcome to the next step, so a
|
|
320
|
+
// second route would put two edges on one port (#741).
|
|
321
|
+
diags.push({
|
|
322
|
+
level: 'error', code: 'HITL_OUTCOME_PORTS_OFF', step: prevAction?.name,
|
|
323
|
+
message: `Port "${s.port}" is already wired: human task "${prevAction?.name}" does not route per `
|
|
324
|
+
+ `outcome — ${fanOutWhy(model.why, prevAction?.options?.version)} — so .step() continues every `
|
|
325
|
+
+ `outcome to the next step, "${s.port}" included. A second route would put two edges on one port.`,
|
|
326
|
+
suggestion: `outcomePorts: true (then the first outcome continues and the others are routed), `
|
|
327
|
+
+ `or route every outcome with .stepSwitch`,
|
|
328
|
+
});
|
|
329
|
+
}
|
|
306
330
|
else if (prevAction) {
|
|
307
331
|
let routed = hitlOutcomeRouted.get(prevAction.name);
|
|
308
332
|
if (!routed)
|
|
@@ -384,10 +408,13 @@ function checkFlow(built, ancestors, opts = {}) {
|
|
|
384
408
|
// the whole truth there, and the rule below would be a false positive.
|
|
385
409
|
const allEnd = outcomes.length > 0
|
|
386
410
|
&& outcomes.every((o) => typeof o !== 'string' && o?.action === 'End');
|
|
387
|
-
|
|
411
|
+
// `.stepSwitch` routes every outcome as an arm, so it never belongs here.
|
|
412
|
+
if (outcomes.length > 1 && !allEnd && s0.kind !== 'stepSwitch'
|
|
388
413
|
&& !hitlRoutesPerOutcome(s.spec.inputs, s.options?.version)) {
|
|
389
414
|
hitlChoiceNoPorts.set(s.name, {
|
|
390
415
|
names: outcomes.map((o) => String((typeof o === 'string' ? o : o?.name) ?? '').trim()),
|
|
416
|
+
ends: outcomes.filter((o) => typeof o !== 'string' && o?.action === 'End')
|
|
417
|
+
.map((o) => String(o.name ?? '').trim()),
|
|
391
418
|
variant: s.spec.inputs?.variant,
|
|
392
419
|
pinned: s.options?.version,
|
|
393
420
|
portsOff: s.spec.inputs?.outcomePorts === false,
|
|
@@ -1237,6 +1264,9 @@ function checkFlow(built, ancestors, opts = {}) {
|
|
|
1237
1264
|
function checkConnectorLookups(step, spec, opts, diags) {
|
|
1238
1265
|
const specs = opts.lookups?.(spec.key, spec.action, spec.version, spec.object);
|
|
1239
1266
|
const resolutions = opts.resolutions ?? {};
|
|
1267
|
+
// One LOOKUP_LITERAL_ID per field: a multiselect of three pasted ids is one
|
|
1268
|
+
// mistake with one remedy, not three.
|
|
1269
|
+
const literalWarned = new Set();
|
|
1240
1270
|
const visit = (value, path) => {
|
|
1241
1271
|
if (isLookupToken(value)) {
|
|
1242
1272
|
// A match key the field does not declare is certainly wrong, and the
|
|
@@ -1277,7 +1307,17 @@ function checkConnectorLookups(step, spec, opts, diags) {
|
|
|
1277
1307
|
return;
|
|
1278
1308
|
}
|
|
1279
1309
|
if (Array.isArray(value)) {
|
|
1280
|
-
|
|
1310
|
+
// An element is named by the WIRE name the lookup table is keyed on —
|
|
1311
|
+
// `users[*]`, `contacts[*].id` — not by its index. `users.0` matched no
|
|
1312
|
+
// spec, so a pasted id, a token with a bad match key, or a runtime value
|
|
1313
|
+
// inside a multiselect passed every rule below unseen. A name that already
|
|
1314
|
+
// is the array's (a flat `'users[*]'` key, or a field typed as an array)
|
|
1315
|
+
// stays as it is.
|
|
1316
|
+
const last = path[path.length - 1] ?? '';
|
|
1317
|
+
const element = last.endsWith('[*]') || specs?.[path.join('.')]
|
|
1318
|
+
? path
|
|
1319
|
+
: [...path.slice(0, -1), `${last}[*]`];
|
|
1320
|
+
value.forEach((v) => visit(v, element));
|
|
1281
1321
|
return;
|
|
1282
1322
|
}
|
|
1283
1323
|
if (value && typeof value === 'object' && !(value instanceof Expr)) {
|
|
@@ -1302,6 +1342,9 @@ function checkConnectorLookups(step, spec, opts, diags) {
|
|
|
1302
1342
|
const literal = value instanceof Expr ? value.value : value;
|
|
1303
1343
|
if (typeof literal !== 'string')
|
|
1304
1344
|
return;
|
|
1345
|
+
if (literalWarned.has(field))
|
|
1346
|
+
return;
|
|
1347
|
+
literalWarned.add(field);
|
|
1305
1348
|
const { target, call } = lookupPhrases(field, lookup, spec);
|
|
1306
1349
|
diags.push({
|
|
1307
1350
|
level: 'warning',
|
|
@@ -1313,7 +1356,7 @@ function checkConnectorLookups(step, spec, opts, diags) {
|
|
|
1313
1356
|
+ `compile nor deploy can tell. Write what you mean and resolve it late:\n`
|
|
1314
1357
|
+ ` ${call}\n`
|
|
1315
1358
|
+ `then record the value once with \`${prepareCommand(spec.key)} `
|
|
1316
|
-
+ `${spec.action} --resolve ${field
|
|
1359
|
+
+ `${spec.action} --resolve ${resolveFlagArg(field, lookup.by[0] ?? 'id', '<value>')}\`.`,
|
|
1317
1360
|
step,
|
|
1318
1361
|
});
|
|
1319
1362
|
};
|
|
@@ -1530,7 +1573,7 @@ function runtimeLookupDiagnostic(step, spec, field, lookup, value) {
|
|
|
1530
1573
|
+ `neither compile nor validate can tell. Resolve the value you know instead:\n`
|
|
1531
1574
|
+ ` ${call}\n`
|
|
1532
1575
|
+ `then record it once with \`${prepareCommand(spec.key, spec.action)} `
|
|
1533
|
-
+ `--resolve ${field
|
|
1576
|
+
+ `--resolve ${resolveFlagArg(field, lookup.by[0] ?? 'id', '<value>')}\`; or read the id from a step whose `
|
|
1534
1577
|
+ `output returned one (\`${lookup.value}\`).`
|
|
1535
1578
|
+ (isEmail
|
|
1536
1579
|
? `\n\`${single.raw}\` reads \`${leaf}\`, an e-mail address, not a \`${target}\`.`
|
|
@@ -3747,15 +3790,19 @@ function nearestInterval(every) {
|
|
|
3747
3790
|
* is why the e2e task's covering assertion is structural, and why these are
|
|
3748
3791
|
* rung-1 errors rather than something the ladder catches.
|
|
3749
3792
|
*
|
|
3750
|
-
*
|
|
3751
|
-
*
|
|
3752
|
-
* exclusive
|
|
3753
|
-
*
|
|
3754
|
-
*
|
|
3755
|
-
*
|
|
3756
|
-
*
|
|
3757
|
-
*
|
|
3758
|
-
* does
|
|
3793
|
+
* The join is a `bpmn:parallelGateway`, and it waits for a token on EVERY
|
|
3794
|
+
* incoming flow (flow-v1 `gateway-join-tracker.ts:33-36`). So two mutually
|
|
3795
|
+
* exclusive edges into it — a `.branch` / `.switch` / `.stepSwitch` inside an
|
|
3796
|
+
* arm whose paths both end at the Merge — leave the join waiting for the path
|
|
3797
|
+
* that was not taken, and the run stays Running with no incident. That was
|
|
3798
|
+
* measured live on 09-01 (a Merge fed by a Decision's two ports). An older note
|
|
3799
|
+
* here said such flows "deploy and run"; they deploy, and they do not finish.
|
|
3800
|
+
* This function does NOT refuse that general shape yet — it predates the human
|
|
3801
|
+
* task rule below and is tracked as its own SDK issue. It does refuse it when a
|
|
3802
|
+
* human task's outcome edge is among the arm's alternative edges into the join
|
|
3803
|
+
* — a fan-out task at the arm's end, one empty `.stepSwitch` arm beside a body
|
|
3804
|
+
* arm, a task on one side of a `.branch` (HITL_FAN_OUT_INTO_JOIN) — and an
|
|
3805
|
+
* `action: 'End'` outcome inside an arm (HITL_END_OUTCOME_IN_PARALLEL).
|
|
3759
3806
|
*/
|
|
3760
3807
|
function checkParallel(s, diags) {
|
|
3761
3808
|
const at = (code, message, suggestion) => {
|
|
@@ -3798,6 +3845,14 @@ function checkParallel(s, diags) {
|
|
|
3798
3845
|
+ `terminate its own arm, or move the earlier step(s) before the \`.parallel(…)\` call.`, `.parallel('${s.name}', [(a) => a.terminate('${term.name}'), …])`);
|
|
3799
3846
|
}
|
|
3800
3847
|
});
|
|
3848
|
+
arms.forEach((arm, i) => {
|
|
3849
|
+
const steps = Array.isArray(arm) ? arm : [];
|
|
3850
|
+
const fanIn = hitlFanInto(steps);
|
|
3851
|
+
if (fanIn)
|
|
3852
|
+
diags.push(fanInDiag(s.name, i, steps, fanIn));
|
|
3853
|
+
for (const h of hitlEndHazards(steps))
|
|
3854
|
+
diags.push(endDiag(s.name, h));
|
|
3855
|
+
});
|
|
3801
3856
|
// An arm that ends the flow reaches an End node instead of the Merge, so it
|
|
3802
3857
|
// contributes NO incoming edge. Verified on the emitted graph: with two arms
|
|
3803
3858
|
// and one returning, the Merge has a single incoming edge and is no longer a
|
|
@@ -3821,6 +3876,180 @@ function checkParallel(s, diags) {
|
|
|
3821
3876
|
});
|
|
3822
3877
|
}
|
|
3823
3878
|
}
|
|
3879
|
+
/**
|
|
3880
|
+
* The edges a step list hands on, mirroring serialize's `emit` tail for tail.
|
|
3881
|
+
*
|
|
3882
|
+
* `.branch` / `.switch` / `.stepSwitch` hand on every arm's tails (an empty arm
|
|
3883
|
+
* hands on the port it started from), a fan-out human task one per continuing
|
|
3884
|
+
* outcome, a routed one its primary outcome; `.return` / `.terminate` / `.break`
|
|
3885
|
+
* and a `stepToRef('output', …)` hand on none, `.stepToList` leaves the path as
|
|
3886
|
+
* it was, and every other step hands on its own single edge.
|
|
3887
|
+
*/
|
|
3888
|
+
function armTails(steps, incoming) {
|
|
3889
|
+
let tails = incoming;
|
|
3890
|
+
for (const st of steps) {
|
|
3891
|
+
if (st.kind === 'stepToList')
|
|
3892
|
+
continue;
|
|
3893
|
+
if (st.kind === 'stepToRef') {
|
|
3894
|
+
if (st.port === 'output')
|
|
3895
|
+
tails = [];
|
|
3896
|
+
continue;
|
|
3897
|
+
}
|
|
3898
|
+
if (st.kind === 'return' || st.kind === 'terminate' || st.kind === 'break')
|
|
3899
|
+
return [];
|
|
3900
|
+
if (st.kind === 'action') {
|
|
3901
|
+
tails = actionTails(st.name, st.spec, st.options?.version);
|
|
3902
|
+
}
|
|
3903
|
+
else if (st.kind === 'stepSwitch') {
|
|
3904
|
+
const spec = st.spec;
|
|
3905
|
+
const outcomes = spec.kind === 'hitl' && hitlDerivesOutcomeHandles(spec.inputs?.variant);
|
|
3906
|
+
tails = st.cases.flatMap((c) => armTails(c.body, [{ from: st.name, ...(outcomes ? { outcome: String(c.value) } : {}) }])
|
|
3907
|
+
.map((t) => (outcomes && !t.humanSwitch ? { ...t, humanSwitch: { step: st.name, outcome: String(c.value) } } : t)));
|
|
3908
|
+
}
|
|
3909
|
+
else if (st.kind === 'branch') {
|
|
3910
|
+
tails = [...armTails(st.then, [{ from: st.name }]), ...armTails(st.otherwise, [{ from: st.name }])];
|
|
3911
|
+
}
|
|
3912
|
+
else if (st.kind === 'switch') {
|
|
3913
|
+
tails = [
|
|
3914
|
+
...st.cases.flatMap((c) => armTails(c.body, [{ from: st.name }])),
|
|
3915
|
+
...(st.default !== undefined ? armTails(st.default, [{ from: st.name }]) : []),
|
|
3916
|
+
];
|
|
3917
|
+
}
|
|
3918
|
+
else {
|
|
3919
|
+
tails = [{ from: st.name ?? st.kind }];
|
|
3920
|
+
}
|
|
3921
|
+
}
|
|
3922
|
+
return tails;
|
|
3923
|
+
}
|
|
3924
|
+
function actionTails(name, spec, version) {
|
|
3925
|
+
if (spec.kind !== 'hitl' || !hitlDerivesOutcomeHandles(spec.inputs?.variant))
|
|
3926
|
+
return [{ from: name }];
|
|
3927
|
+
const model = hitlExitModel(spec.inputs, version);
|
|
3928
|
+
if (model.shape === 'placeholder')
|
|
3929
|
+
return [{ from: name }];
|
|
3930
|
+
if (model.shape === 'per-outcome')
|
|
3931
|
+
return model.exits.slice(0, 1).map((e) => ({ from: name, outcome: e.value }));
|
|
3932
|
+
return model.exits.filter((e) => !e.endsProcess).map((e) => ({ from: name, outcome: e.value }));
|
|
3933
|
+
}
|
|
3934
|
+
/**
|
|
3935
|
+
* The tails of a `.parallel` arm when they would starve its join: two or more
|
|
3936
|
+
* alternative edges — only one fires — at least one of which is a human task's
|
|
3937
|
+
* outcome edge, or the end of a human-task `.stepSwitch` arm. The join waits
|
|
3938
|
+
* for a token on every incoming flow.
|
|
3939
|
+
*
|
|
3940
|
+
* However the task got there, it is the same graph: a fan-out task at the arm's
|
|
3941
|
+
* end (its outcomes are the alternatives), a `.stepSwitch` with an empty arm
|
|
3942
|
+
* beside a body arm, a complete `.stepSwitch` with nothing after it (one edge
|
|
3943
|
+
* per arm's last step), a task on one side of a `.branch`. serialize refuses the
|
|
3944
|
+
* same condition, edge for edge. Rejoining arms with NO human task in them are
|
|
3945
|
+
* the older, general case, and not refused here.
|
|
3946
|
+
*/
|
|
3947
|
+
function hitlFanInto(steps) {
|
|
3948
|
+
const tails = armTails(steps, [{ from: '(fork)' }]);
|
|
3949
|
+
return tails.length >= 2 && tails.some((t) => t.outcome !== undefined || t.humanSwitch !== undefined) ? tails : undefined;
|
|
3950
|
+
}
|
|
3951
|
+
function fanInDiag(join, arm, steps, tails) {
|
|
3952
|
+
const tagged = tails.find((t) => t.outcome !== undefined || t.humanSwitch !== undefined);
|
|
3953
|
+
const task = tagged.outcome !== undefined ? tagged.from : tagged.humanSwitch.step;
|
|
3954
|
+
if (tails.every((t) => t.from === task && t.outcome !== undefined)) {
|
|
3955
|
+
return {
|
|
3956
|
+
level: 'error', code: 'HITL_FAN_OUT_INTO_JOIN', step: task,
|
|
3957
|
+
message: `Human task "${task}" ends its path inside parallel "${join}", so its outcomes `
|
|
3958
|
+
+ `${tails.map((t) => t.outcome).join(', ')} each get their own edge into the join. Only one outcome ever `
|
|
3959
|
+
+ `fires, and the join waits for a token on every incoming edge, so the run never gets past "${join}" — with `
|
|
3960
|
+
+ `no incident, and \`flow validate\` answers Valid. Add a step after "${task}" inside the arm, so every `
|
|
3961
|
+
+ `outcome continues to that step and only its one edge reaches the join (the decision is `
|
|
3962
|
+
+ `out('${task}', 'Action')), or move the task out of the \`.parallel(…)\`.`,
|
|
3963
|
+
suggestion: `a.step('${task}', hitl(…)).step('record', …)`,
|
|
3964
|
+
};
|
|
3965
|
+
}
|
|
3966
|
+
const edges = tails.map((t) => (t.outcome !== undefined ? `outcome ${t.outcome} of "${t.from}"`
|
|
3967
|
+
: t.humanSwitch ? `"${t.from}" (on outcome ${t.humanSwitch.outcome} of "${t.humanSwitch.step}")` : `"${t.from}"`));
|
|
3968
|
+
const last = [...steps].reverse().find((x) => x.kind !== 'stepToList' && x.kind !== 'stepToRef');
|
|
3969
|
+
return {
|
|
3970
|
+
level: 'error', code: 'HITL_FAN_OUT_INTO_JOIN', step: task,
|
|
3971
|
+
message: `Parallel "${join}" arm ${arm + 1} ends on ${tails.length} alternative paths, and each gets its own edge `
|
|
3972
|
+
+ `into the join: ${edges.join(', ')}. Only one of them runs, and the join waits for a token on every incoming `
|
|
3973
|
+
+ `edge, so the run never gets past "${join}" — with no incident, and \`flow validate\` answers Valid. Add a `
|
|
3974
|
+
+ `step at the end of the arm${last?.name ? `, after "${last.name}"` : ''}, so the paths meet on that step and `
|
|
3975
|
+
+ `only its one edge reaches the join, or move human task "${task}" out of the \`.parallel(…)\`.`,
|
|
3976
|
+
suggestion: `….step('record', …) // last step of arm ${arm + 1}`,
|
|
3977
|
+
};
|
|
3978
|
+
}
|
|
3979
|
+
/**
|
|
3980
|
+
* Human tasks in one `.parallel` arm that continue every outcome while some of
|
|
3981
|
+
* them are `action: 'End'`. The End outcome's path stops at its End node, so the
|
|
3982
|
+
* join waits for the continuing path that did not run.
|
|
3983
|
+
*
|
|
3984
|
+
* Recurses into `.branch` / `.switch` / `.stepSwitch` bodies, which reach the
|
|
3985
|
+
* join too. Not into a loop body (the loop node is what reaches the join) or a
|
|
3986
|
+
* nested `.parallel` (its own join is checked on its own).
|
|
3987
|
+
*/
|
|
3988
|
+
function hitlEndHazards(steps) {
|
|
3989
|
+
const found = [];
|
|
3990
|
+
for (const st of steps) {
|
|
3991
|
+
if (st.kind === 'action' && st.spec.kind === 'hitl') {
|
|
3992
|
+
const model = hitlExitModel(st.spec.inputs, st.options?.version);
|
|
3993
|
+
if (model.shape !== 'every-outcome')
|
|
3994
|
+
continue;
|
|
3995
|
+
const ending = model.exits.filter((e) => e.endsProcess).map((e) => e.value);
|
|
3996
|
+
if (ending.length > 0 && ending.length < model.exits.length)
|
|
3997
|
+
found.push({ step: st.name, outcomes: ending, via: 'action-end' });
|
|
3998
|
+
}
|
|
3999
|
+
else if (st.kind === 'branch') {
|
|
4000
|
+
found.push(...hitlEndHazards(st.then), ...hitlEndHazards(st.otherwise));
|
|
4001
|
+
}
|
|
4002
|
+
else if (st.kind === 'switch') {
|
|
4003
|
+
for (const c of st.cases)
|
|
4004
|
+
found.push(...hitlEndHazards(c.body));
|
|
4005
|
+
if (st.default)
|
|
4006
|
+
found.push(...hitlEndHazards(st.default));
|
|
4007
|
+
}
|
|
4008
|
+
else if (st.kind === 'stepSwitch') {
|
|
4009
|
+
// An outcome with no arm gets an End (serialize's `endExit`), and so does
|
|
4010
|
+
// an arm that ends in `.return()`. Beside an arm that continues, that End
|
|
4011
|
+
// is where this arm's token stops on that outcome.
|
|
4012
|
+
const spec = st.spec;
|
|
4013
|
+
const exits = spec.kind === 'hitl' && hitlDerivesOutcomeHandles(spec.inputs?.variant)
|
|
4014
|
+
? declaredExits(st.spec, st.options?.version) : undefined;
|
|
4015
|
+
if (exits && hasExits(exits)) {
|
|
4016
|
+
const stopped = exits.filter((e) => {
|
|
4017
|
+
const c = st.cases.find((x) => x.value === e.value);
|
|
4018
|
+
return !c || (armTails(c.body, [{ from: st.name }]).length === 0 && stopsAtEnd(c.body));
|
|
4019
|
+
}).map((e) => e.value);
|
|
4020
|
+
if (stopped.length > 0 && armTails([st], [{ from: '(fork)' }]).length > 0) {
|
|
4021
|
+
found.push({ step: st.name, outcomes: stopped, via: 'step-switch' });
|
|
4022
|
+
}
|
|
4023
|
+
}
|
|
4024
|
+
for (const c of st.cases)
|
|
4025
|
+
found.push(...hitlEndHazards(c.body));
|
|
4026
|
+
}
|
|
4027
|
+
}
|
|
4028
|
+
return found;
|
|
4029
|
+
}
|
|
4030
|
+
function endDiag(join, h) {
|
|
4031
|
+
const list = h.outcomes.join(', ');
|
|
4032
|
+
if (h.via === 'step-switch') {
|
|
4033
|
+
return {
|
|
4034
|
+
level: 'error', code: 'HITL_END_OUTCOME_IN_PARALLEL', step: h.step,
|
|
4035
|
+
message: `Human task "${h.step}" is a .stepSwitch inside parallel "${join}", and ${h.outcomes.length === 1
|
|
4036
|
+
? `outcome ${list} has` : `outcomes ${list} have`} no arm, or an arm that ends in .return(). That path `
|
|
4037
|
+
+ `stops at an End node, while the join waits for a token from this arm on the path that continues, so the `
|
|
4038
|
+
+ `run never gets past "${join}" — with no incident, and \`flow validate\` answers Valid. Give every outcome `
|
|
4039
|
+
+ `an arm that continues (and move a .return() after the \`.parallel(…)\`), or move the task out of the `
|
|
4040
|
+
+ `\`.parallel(…)\`.`,
|
|
4041
|
+
suggestion: `{ value: '${h.outcomes[0]}', body: (b) => b.step(…) }`,
|
|
4042
|
+
};
|
|
4043
|
+
}
|
|
4044
|
+
return {
|
|
4045
|
+
level: 'error', code: 'HITL_END_OUTCOME_IN_PARALLEL', step: h.step,
|
|
4046
|
+
message: `Human task "${h.step}" is inside parallel "${join}", and ${h.outcomes.length === 1
|
|
4047
|
+
? `outcome ${list} is` : `outcomes ${list} are`} action: 'End'. `
|
|
4048
|
+
+ `That path stops at its own End node, while the join waits for a token from this arm on its `
|
|
4049
|
+
+ `continuing path, so the run never gets past "${join}". Drop action: 'End' there and act on `
|
|
4050
|
+
+ `out('${h.step}', 'Action') after the join, or move the task out of the \`.parallel(…)\`.`,
|
|
4051
|
+
};
|
|
4052
|
+
}
|
|
3824
4053
|
/**
|
|
3825
4054
|
* Diagnose a `transform` step.
|
|
3826
4055
|
*
|
|
@@ -4059,34 +4288,46 @@ function outcomeSlug(name, index) {
|
|
|
4059
4288
|
.replace(/^-+|-+$/g, '');
|
|
4060
4289
|
return slug || `outcome-${index + 1}`;
|
|
4061
4290
|
}
|
|
4291
|
+
/**
|
|
4292
|
+
* Why a base or quick-form task continues every outcome to the next step, as a
|
|
4293
|
+
* clause for a diagnostic. `pinned` is the step's `{ version }`.
|
|
4294
|
+
*/
|
|
4295
|
+
function fanOutWhy(why, pinned) {
|
|
4296
|
+
switch (why) {
|
|
4297
|
+
case 'outcomePorts: false': return 'outcomePorts: false says so';
|
|
4298
|
+
case 'quick-form': return 'a quick-form task does not unless it sets outcomePorts: true';
|
|
4299
|
+
case 'pinned': return `{ version: '${pinned}' } pins it without outcomePorts: true`;
|
|
4300
|
+
case 'one outcome': return 'it has a single outcome';
|
|
4301
|
+
case 'every outcome ends': return "every outcome is action: 'End'";
|
|
4302
|
+
}
|
|
4303
|
+
}
|
|
4062
4304
|
/**
|
|
4063
4305
|
* HITL_OUTCOMES_UNREACHABLE — a human task offers the reviewer a CHOICE that
|
|
4064
4306
|
* nothing downstream can act on.
|
|
4065
4307
|
*
|
|
4066
|
-
*
|
|
4067
|
-
*
|
|
4068
|
-
*
|
|
4069
|
-
*
|
|
4070
|
-
*
|
|
4071
|
-
*
|
|
4308
|
+
* A task that does not route per outcome sends every outcome to the same place:
|
|
4309
|
+
* on the base node and quick-form, one `outcome-<id>` edge per outcome, all to
|
|
4310
|
+
* the same next step; on an app-backed variant, its one `completed` exit. The
|
|
4311
|
+
* outcomes reach the reviewer and `inputs.schema.outcomes`, and then every
|
|
4312
|
+
* button the reviewer can press leads to the same step. Two outcomes, one path —
|
|
4313
|
+
* an approval whose approve and reject arms are indistinguishable is a modelling
|
|
4314
|
+
* error, not a style choice, yet it compiles and `flow validate` answers `Valid`
|
|
4072
4315
|
* (flow-builder-sdk#735).
|
|
4073
4316
|
*
|
|
4074
4317
|
* On a plain base task that is no longer reachable: more than one outcome now
|
|
4075
4318
|
* INFERS per-outcome routing (`hitlRoutesPerOutcome`, #739), because a warning
|
|
4076
4319
|
* alone did not change what agents emitted — #739 measured the diagnostic
|
|
4077
4320
|
* firing three times and the unreachable artifact shipping anyway. What is left
|
|
4078
|
-
* for this rule is the cases where
|
|
4079
|
-
* still be a mistake:
|
|
4080
|
-
*
|
|
4081
|
-
* `outcomePorts: false`.
|
|
4321
|
+
* for this rule is the cases where not routing is CHOSEN and the choice may
|
|
4322
|
+
* still be a mistake: quick-form's default, a pinned `{ version }`, an explicit
|
|
4323
|
+
* `outcomePorts: false`, and the app-backed variants.
|
|
4082
4324
|
*
|
|
4083
|
-
* In all
|
|
4084
|
-
* `out('<step>', 'Action')` feeding a `.switch()` routes the decision as data
|
|
4085
|
-
*
|
|
4086
|
-
*
|
|
4087
|
-
*
|
|
4088
|
-
*
|
|
4089
|
-
* `checkHitl`: the deciding evidence is the rest of the flow.
|
|
4325
|
+
* In all of them the shape is correct when something reads the chosen outcome:
|
|
4326
|
+
* `out('<step>', 'Action')` feeding a `.switch()` routes the decision as data.
|
|
4327
|
+
* So the rule is not "outcomes without routes", which would refuse that shape;
|
|
4328
|
+
* it is "outcomes that no route and no expression distinguishes". That is why it
|
|
4329
|
+
* runs here, after `walk`, rather than inside `checkHitl`: the deciding evidence
|
|
4330
|
+
* is the rest of the flow.
|
|
4090
4331
|
*
|
|
4091
4332
|
* A read counts when it can SEE the outcome — `.output.Action`, the whole
|
|
4092
4333
|
* answer object (`out('<step>')` / `$vars.<step>.output`, which carries
|
|
@@ -4116,35 +4357,46 @@ function reportUnreachableHitlOutcomes(choices, exprs, diags) {
|
|
|
4116
4357
|
for (const m of js.matchAll(/\$vars\.([A-Za-z0-9_]+)\s*\[/g))
|
|
4117
4358
|
readsOutcome.add(m[1]);
|
|
4118
4359
|
}
|
|
4119
|
-
for (const [step, { names, variant, pinned, portsOff }] of choices) {
|
|
4360
|
+
for (const [step, { names, ends, variant, pinned, portsOff }] of choices) {
|
|
4120
4361
|
if (readsOutcome.has(step))
|
|
4121
4362
|
continue;
|
|
4122
4363
|
const ids = names.map((n, i) => outcomeSlug(n, i));
|
|
4123
|
-
//
|
|
4124
|
-
// `outcomePorts: true`
|
|
4125
|
-
//
|
|
4126
|
-
|
|
4127
|
-
//
|
|
4128
|
-
//
|
|
4129
|
-
const
|
|
4130
|
-
|
|
4131
|
-
|
|
4132
|
-
|
|
4133
|
-
|
|
4134
|
-
|
|
4364
|
+
// The base node and quick-form draw a handle per outcome, so both can route
|
|
4365
|
+
// it — `.stepSwitch`, or `outcomePorts: true` (a pin keeps its version). An
|
|
4366
|
+
// app-backed variant cannot: the Action read is its only remedy.
|
|
4367
|
+
const canRoute = hitlDerivesOutcomeHandles(variant);
|
|
4368
|
+
// On those two, an `action: 'End'` outcome gets its own End node, so the
|
|
4369
|
+
// graph already tells it apart. Only the outcomes that CONTINUE share a step.
|
|
4370
|
+
const continuing = canRoute ? names.filter((n) => !ends.includes(n)) : names;
|
|
4371
|
+
if (continuing.length < 2)
|
|
4372
|
+
continue;
|
|
4373
|
+
const endNote = canRoute && ends.length > 0
|
|
4374
|
+
? ` (${ends.join(', ')} ${ends.length === 1 ? 'is' : 'are'} action: 'End' and end${ends.length === 1 ? 's' : ''} the run instead)`
|
|
4375
|
+
: '';
|
|
4376
|
+
const kept = !canRoute
|
|
4377
|
+
? `The "${variant}" variant reads its outcomes from its app and leaves on its single 'completed' exit`
|
|
4378
|
+
: portsOff
|
|
4379
|
+
? `outcomePorts: false continues every outcome to the next step`
|
|
4380
|
+
: variant === 'quick-form'
|
|
4381
|
+
? `A quick-form task continues every outcome to the next step unless it routes them`
|
|
4382
|
+
: `{ version: '${pinned}' } without outcomePorts: true continues every outcome to the next step`;
|
|
4135
4383
|
diags.push({
|
|
4136
4384
|
level: 'warning', code: 'HITL_OUTCOMES_UNREACHABLE', step,
|
|
4137
4385
|
message: `Human task "${step}" declares ${names.length} outcomes (${names.join(', ')}), but nothing `
|
|
4138
|
-
+ `downstream tells them apart:
|
|
4139
|
-
|
|
4140
|
-
|
|
4141
|
-
+ `out('${step}', 'Action')
|
|
4142
|
-
+ (
|
|
4143
|
-
|
|
4386
|
+
+ `downstream tells them apart: ${canRoute
|
|
4387
|
+
? `${ends.length > 0 ? `${continuing.join(', ')} continue` : 'every outcome continues'} to the same step${endNote}`
|
|
4388
|
+
: "the node leaves on its single 'completed' exit"}, and no expression reads `
|
|
4389
|
+
+ `out('${step}', 'Action') either. The outcomes reach the reviewer and the artifact; the decision `
|
|
4390
|
+
+ `does not reach the flow. ${kept}, so switch on out('${step}', 'Action') to route the decision as data`
|
|
4391
|
+
+ (canRoute
|
|
4392
|
+
? `, or give each outcome its own route (${ids.map((i) => `'outcome-${i}'`).join(', ')}) with `
|
|
4393
|
+
+ `.stepSwitch${portsOff ? ' or by dropping outcomePorts: false' : ' or outcomePorts: true'}.`
|
|
4144
4394
|
: '.'),
|
|
4145
|
-
suggestion:
|
|
4395
|
+
suggestion: portsOff
|
|
4146
4396
|
? 'drop outcomePorts: false'
|
|
4147
|
-
:
|
|
4397
|
+
: canRoute
|
|
4398
|
+
? `.stepSwitch('${step}', …), or outcomePorts: true`
|
|
4399
|
+
: `.switch('route', out('${step}', 'Action'), …)`,
|
|
4148
4400
|
});
|
|
4149
4401
|
}
|
|
4150
4402
|
}
|
|
@@ -4174,12 +4426,19 @@ function checkHitl(step, inputs, diags) {
|
|
|
4174
4426
|
}
|
|
4175
4427
|
const isApp = variant === 'action-app';
|
|
4176
4428
|
const isDocval = variant === 'document-validation';
|
|
4177
|
-
//
|
|
4178
|
-
//
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
|
|
4182
|
-
|
|
4429
|
+
// The error output is the BASE node's 1.2 definition, which no variant has.
|
|
4430
|
+
// Per-outcome exits are not a version question: quick-form draws one handle
|
|
4431
|
+
// per outcome like the base node, so it may ask for them; the two app-backed
|
|
4432
|
+
// variants read their outcomes from the app and leave on `completed`.
|
|
4433
|
+
if (variant !== undefined
|
|
4434
|
+
&& (inputs?.exposeError === true || (inputs?.outcomePorts === true && !hitlDerivesOutcomeHandles(variant)))) {
|
|
4435
|
+
at('HITL_OUTCOME_PORTS_VARIANT', inputs?.exposeError === true
|
|
4436
|
+
? `Human task "${step}" asks for exposeError, but its variant is "${variant}" — the error output `
|
|
4437
|
+
+ `exists only on the base node type's 1.2 definition.`
|
|
4438
|
+
: `Human task "${step}" asks for outcomePorts, but its variant is "${variant}", which reads its `
|
|
4439
|
+
+ `outcomes from its app and leaves on its single 'completed' exit.`, inputs?.exposeError === true
|
|
4440
|
+
? 'Omit `variant`, or drop exposeError.'
|
|
4441
|
+
: `Route on out('${step}', 'Action') downstream.`);
|
|
4183
4442
|
return;
|
|
4184
4443
|
}
|
|
4185
4444
|
if (inputs?.document !== undefined && !isDocval) {
|