@uipath/maestro-builder-sdk 6.43.0 → 6.44.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api-index.md +66 -65
- package/dist/api-members.md +228 -226
- package/dist/check.js +101 -7
- package/dist/core/actions.d.ts +9 -0
- package/dist/core/form-field-type.d.ts +7 -0
- package/dist/core/form-field-type.js +12 -2
- package/dist/decompile.js +204 -34
- package/dist/flow-expr-check.js +5 -0
- package/dist/flow-node-type.d.ts +9 -0
- package/dist/flow-sdk.d.ts +40 -5
- package/dist/flow-sdk.js +14 -1
- package/dist/flow-step-variables.d.ts +4 -0
- package/dist/flow-step-variables.js +22 -0
- package/dist/loop-version.d.ts +23 -6
- package/dist/loop-version.js +37 -15
- package/dist/merge.d.ts +5 -3
- package/dist/merge.js +20 -10
- package/dist/node-placement.d.ts +6 -5
- package/dist/node-placement.js +25 -10
- package/dist/nodes/hitl/flow/check.js +15 -0
- package/dist/nodes/hitl/flow/node-type.js +18 -3
- package/dist/nodes/process/flow/node-type.js +17 -2
- package/dist/serialize.js +56 -15
- package/package.json +2 -2
package/dist/check.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
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, parseIxpProjectType } from './flow-sdk.js';
|
|
8
|
+
import { Expr, SCHEDULE_PRESETS, parseIxpProjectType, parallelFork } 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';
|
|
@@ -26,10 +26,11 @@ import { terminateStepClashMessage, terminateTwiceMessage } from './flow-termina
|
|
|
26
26
|
import { endContentKey } from './flow-end-content.js';
|
|
27
27
|
import { FLOW_NODE_TYPES } from './flow-node-registry.js';
|
|
28
28
|
import { stepExits } from './flow-step-exits.js';
|
|
29
|
+
import { stepDeclaredVariables } from './flow-step-variables.js';
|
|
29
30
|
import { plainHttpOnErrorMessage } from './nodes/http/flow/family.js';
|
|
30
31
|
import { resolveVersion, resolveVersionCarried } from './core/nodes/versioned.js';
|
|
31
32
|
import { declaredErrorPortRefusal } from './core/error-port.js';
|
|
32
|
-
import { bodyUsesBreak, loopParallelWarning, loopVersionRefusal } from './loop-version.js';
|
|
33
|
+
import { bodyUsesBreak, loopParallelWarning, loopVersionRefusal, resolveLoopDefinition, loopVersion } from './loop-version.js';
|
|
33
34
|
const newResourceIdLedger = () => ({ explicit: new Map(), taken: new Map(), agentSources: new Map() });
|
|
34
35
|
/** INLINE_AGENT_RESOURCE_NODE_ID for each explicit `nodeId` another node of the file also gets. */
|
|
35
36
|
function resourceNodeIdClashes(ledger) {
|
|
@@ -337,6 +338,11 @@ function checkFlow(built, ancestors, opts = {}, ledger = newResourceIdLedger())
|
|
|
337
338
|
}
|
|
338
339
|
stepNames.add(s.name);
|
|
339
340
|
declared.add(s.name); // a step's output is referenceable by name
|
|
341
|
+
// …and so is every variable the step declares by running: a quick
|
|
342
|
+
// form's asked field lands in its `variable` (#1309).
|
|
343
|
+
if (s.kind === 'action')
|
|
344
|
+
for (const v of stepDeclaredVariables(s.spec, s.options?.version))
|
|
345
|
+
declared.add(v);
|
|
340
346
|
}
|
|
341
347
|
if ((s.kind === 'stepToList' || s.kind === 'stepToRef') && s.port === 'error')
|
|
342
348
|
checkOnError(s, prevPortSource, diags, prevPortSource && checkedSteps.get(prevPortSource.name), carriedLookup([built, ...ancestors]));
|
|
@@ -687,6 +693,20 @@ function checkFlow(built, ancestors, opts = {}, ledger = newResourceIdLedger())
|
|
|
687
693
|
}
|
|
688
694
|
else if (s.kind === 'parallel') {
|
|
689
695
|
parallelNames.add(s.name);
|
|
696
|
+
// The fork node is a node too (#1310): its name is a step name for
|
|
697
|
+
// clashes, and a read of it is a read of a merge — nothing.
|
|
698
|
+
const fork = parallelFork(s.options);
|
|
699
|
+
if (fork) {
|
|
700
|
+
if (stepNames.has(fork.name)) {
|
|
701
|
+
diags.push({
|
|
702
|
+
level: 'error', code: 'DUP_STEP', step: s.name,
|
|
703
|
+
message: `Duplicate step name "${fork.name}": the fork of parallel "${s.name}" and another step share it.`,
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
stepNames.add(fork.name);
|
|
707
|
+
declared.add(fork.name);
|
|
708
|
+
parallelNames.add(fork.name);
|
|
709
|
+
}
|
|
690
710
|
checkParallel(s, diags);
|
|
691
711
|
for (const arm of s.arms)
|
|
692
712
|
walk(arm, inLoop ? 'arm' : false);
|
|
@@ -701,10 +721,14 @@ function checkFlow(built, ancestors, opts = {}, ledger = newResourceIdLedger())
|
|
|
701
721
|
addExpr(s.options.completionCondition.js, s.name);
|
|
702
722
|
// The same refusal serialize throws, from the same helper: an option or
|
|
703
723
|
// a `.break()` the pinned version's definition cannot express (#1186).
|
|
704
|
-
|
|
724
|
+
// Against the definition the loop compiles against — bundled, else
|
|
725
|
+
// carried — as serialize resolves it, so a carried 1.0's `.break()`
|
|
726
|
+
// and `parallel: true` are judged here and not first at compile (#1311).
|
|
727
|
+
const loopDef = resolveLoopDefinition(loopVersion(s.options), carriedLookup([built, ...ancestors]));
|
|
728
|
+
const versionRefusal = loopVersionRefusal(s.name, s.options, bodyUsesBreak(s.body), loopDef);
|
|
705
729
|
if (versionRefusal)
|
|
706
730
|
diags.push({ level: 'error', code: 'LOOP_VERSION_UNSUPPORTED', step: s.name, message: versionRefusal });
|
|
707
|
-
const parallelWarning = loopParallelWarning(s.name, s.options);
|
|
731
|
+
const parallelWarning = loopParallelWarning(s.name, s.options, loopDef);
|
|
708
732
|
if (parallelWarning)
|
|
709
733
|
diags.push({ level: 'warning', code: 'LOOP_PARALLEL_LEGACY', step: s.name, message: parallelWarning });
|
|
710
734
|
loopNames.set(s.name, s.options?.version === '1.0.0' ? LOOP_FIELDS_1_0_0 : LOOP_FIELDS);
|
|
@@ -3316,8 +3340,22 @@ function checkStepRefs(built, diags, reservedIds = []) {
|
|
|
3316
3340
|
s.cases.forEach((c) => index(c.body, portList, scope, arm));
|
|
3317
3341
|
index(s.default ?? [], portList, scope, arm);
|
|
3318
3342
|
}
|
|
3319
|
-
else if (s.kind === 'parallel')
|
|
3343
|
+
else if (s.kind === 'parallel') {
|
|
3344
|
+
// The fork node (`{ fork }`, #1310) is a node in this scope like the
|
|
3345
|
+
// join: a `.stepToRef(<fork>)` is an edge serialize resolves.
|
|
3346
|
+
const fork = parallelFork(s.options);
|
|
3347
|
+
if (fork) {
|
|
3348
|
+
stepNamesSeen.add(fork.name);
|
|
3349
|
+
if (!order.has(fork.name))
|
|
3350
|
+
order.set(fork.name, seq);
|
|
3351
|
+
if (portList)
|
|
3352
|
+
inPortList.add(fork.name);
|
|
3353
|
+
scopeOf.set(fork.name, scope);
|
|
3354
|
+
if (arm)
|
|
3355
|
+
armOf.set(fork.name, arm);
|
|
3356
|
+
}
|
|
3320
3357
|
s.arms.forEach((a, i) => index(a, portList, scope, `${s.name}#${i}`));
|
|
3358
|
+
}
|
|
3321
3359
|
// A loop body is a bpmn:SubProcess — its own containment scope.
|
|
3322
3360
|
else if (s.kind === 'loop' || s.kind === 'doWhile')
|
|
3323
3361
|
index(s.body, portList, s.name, arm);
|
|
@@ -3406,12 +3444,27 @@ function checkStepRefs(built, diags, reservedIds = []) {
|
|
|
3406
3444
|
s.cases.forEach((c) => collect(c.body, source, scope, arm, enclosing));
|
|
3407
3445
|
collect(s.default ?? [], source, scope, arm, enclosing);
|
|
3408
3446
|
}
|
|
3409
|
-
else if (s.kind === 'parallel')
|
|
3410
|
-
|
|
3447
|
+
else if (s.kind === 'parallel') {
|
|
3448
|
+
// Arms that all end in `.stepToRef(<one step>)` (or stop) have no Merge:
|
|
3449
|
+
// serialize writes none when no arm hands it a tail, and the refs ARE the
|
|
3450
|
+
// join — the canvas shape of two steps feeding one (#1310). A ref there
|
|
3451
|
+
// is not leaving the arm without reaching its merge.
|
|
3452
|
+
const refJoinTarget = parallelRefJoin(s.arms);
|
|
3453
|
+
s.arms.forEach((a, i) => {
|
|
3454
|
+
const key = `${s.name}#${i}`;
|
|
3455
|
+
if (refJoinTarget !== undefined)
|
|
3456
|
+
refJoin.set(key, refJoinTarget);
|
|
3457
|
+
collect(a, source, scope, key, enclosing);
|
|
3458
|
+
});
|
|
3459
|
+
}
|
|
3411
3460
|
else if (s.kind === 'loop' || s.kind === 'doWhile')
|
|
3412
3461
|
collect(s.body, source, s.name, arm, [...enclosing, s.name]);
|
|
3413
3462
|
}
|
|
3414
3463
|
};
|
|
3464
|
+
/** The step every arm of a parallel refs instead of a Merge, by `<parallel>#<arm>`. */
|
|
3465
|
+
const refJoin = new Map();
|
|
3466
|
+
/** `<parallel>\0<target>` pairs PARALLEL_REF_JOIN has been said for. */
|
|
3467
|
+
const refJoinSaid = new Set();
|
|
3415
3468
|
collect(built.steps, undefined, '', undefined);
|
|
3416
3469
|
for (const ep of built.entryPoints ?? [])
|
|
3417
3470
|
collect(ep.steps, ep.id, '', undefined);
|
|
@@ -3498,6 +3551,22 @@ function checkStepRefs(built, diags, reservedIds = []) {
|
|
|
3498
3551
|
+ `on is a bpmn:ParallelGateway, which waits for every branch it forked — an extra token arriving inside `
|
|
3499
3552
|
+ `an arm is not one of them, so the join may never settle. Verify with uip maestro flow validate.`);
|
|
3500
3553
|
}
|
|
3554
|
+
else if (arm !== undefined && refJoin.get(arm) === target) {
|
|
3555
|
+
// The arms meet at a plain step, not a Merge: that step has one incoming
|
|
3556
|
+
// edge per arm, and the runtime starts a step once per token that
|
|
3557
|
+
// arrives (only a gateway waits for every incoming flow). So it runs
|
|
3558
|
+
// once per arm, which is not what `.parallel`'s join promises. Said
|
|
3559
|
+
// once per parallel, not once per arm (#1310).
|
|
3560
|
+
const parallel = arm.slice(0, arm.lastIndexOf('#'));
|
|
3561
|
+
if (!refJoinSaid.has(`${parallel}\0${target}`)) {
|
|
3562
|
+
refJoinSaid.add(`${parallel}\0${target}`);
|
|
3563
|
+
say('warning', 'PARALLEL_REF_JOIN', `"${target}" is where every arm of parallel "${parallel}" ends by .stepToRef, so it has one incoming edge `
|
|
3564
|
+
+ `per arm and no Merge node. The runtime starts a step once per token that arrives, so "${target}" runs `
|
|
3565
|
+
+ `once per arm — not once after all arms, as .parallel's Merge would. To run it once, end each arm on a `
|
|
3566
|
+
+ `step of its own and put "${target}" after the .parallel. A canvas flow drawn this way behaves the same, `
|
|
3567
|
+
+ `so leave a decompiled one as it is.`);
|
|
3568
|
+
}
|
|
3569
|
+
}
|
|
3501
3570
|
else if (arm !== undefined) {
|
|
3502
3571
|
say('warning', 'REF_OUT_OF_PARALLEL_ARM', `.stepToRef('${target}') ${where} leaves a parallel arm without reaching its Merge. That Merge waits for `
|
|
3503
3572
|
+ `every branch it forked, so the token this arm owed it never arrives and the join may hang. Verify with `
|
|
@@ -3648,3 +3717,28 @@ function checkContribution(node, requestedVersion, step, ownError, diags) {
|
|
|
3648
3717
|
return;
|
|
3649
3718
|
diags.push({ level: 'error', code: 'CONTRIBUTION_BINDING_UNRESOLVED', step, message: plan.problems.join(' ') });
|
|
3650
3719
|
}
|
|
3720
|
+
/**
|
|
3721
|
+
* The one step every arm of a `.parallel` ends by referring to, when the arms
|
|
3722
|
+
* join there instead of on a Merge: each arm ends in `.stepToRef(<that step>)`
|
|
3723
|
+
* on its default port, or stops (`.return()`, `.terminate()`), and at least one
|
|
3724
|
+
* refs. `undefined` when some arm has a tail (serialize then emits the Merge) or
|
|
3725
|
+
* the refs disagree. The decompiler writes a step fed by several steps this way
|
|
3726
|
+
* (#1310).
|
|
3727
|
+
*/
|
|
3728
|
+
function parallelRefJoin(arms) {
|
|
3729
|
+
let target;
|
|
3730
|
+
for (const arm of arms) {
|
|
3731
|
+
const last = arm[arm.length - 1];
|
|
3732
|
+
if (last === undefined)
|
|
3733
|
+
return undefined;
|
|
3734
|
+
// An arm that ends the run or the iteration hands the join nothing either.
|
|
3735
|
+
if (last.kind === 'return' || last.kind === 'terminate' || last.kind === 'break' || last.kind === 'continue')
|
|
3736
|
+
continue;
|
|
3737
|
+
if (last.kind !== 'stepToRef' || last.port !== 'output')
|
|
3738
|
+
return undefined;
|
|
3739
|
+
if (target !== undefined && target !== last.target)
|
|
3740
|
+
return undefined;
|
|
3741
|
+
target = last.target;
|
|
3742
|
+
}
|
|
3743
|
+
return target;
|
|
3744
|
+
}
|
package/dist/core/actions.d.ts
CHANGED
|
@@ -228,6 +228,15 @@ export interface AskedField extends FormFieldSettings {
|
|
|
228
228
|
direction: 'output';
|
|
229
229
|
/** Reviewer must fill it in before the task can be completed. */
|
|
230
230
|
required?: boolean;
|
|
231
|
+
/**
|
|
232
|
+
* The flow variable the answer lands in, read downstream as `$vars.<variable>`
|
|
233
|
+
* (`v('<variable>')`) — the name only, without the `vars.` prefix. Defaults to
|
|
234
|
+
* the field id. Studio Web names it `<Step>_<field>` (`P_comments`) and later
|
|
235
|
+
* steps read `$vars.P_comments` or bind `vars.P_comments`; declaring it here is
|
|
236
|
+
* what makes those reads resolve, so a designer-saved approval flow round-trips
|
|
237
|
+
* through the SDK. The answer is also always `out('<step>', '<id>')`.
|
|
238
|
+
*/
|
|
239
|
+
variable?: string;
|
|
231
240
|
}
|
|
232
241
|
/**
|
|
233
242
|
* A value the reviewer READS **and can correct**: pre-filled from the flow,
|
|
@@ -205,6 +205,13 @@ export declare function formFieldSettingsSource(settings: FormFieldSettings, val
|
|
|
205
205
|
* neither (the agent fills the form).
|
|
206
206
|
*/
|
|
207
207
|
export type FormRowSite = 'human task' | 'escalation';
|
|
208
|
+
/**
|
|
209
|
+
* The variable NAME an asked field's `variable` row key names — `P_comments`
|
|
210
|
+
* for `vars.P_comments` — or `undefined` when the value is not a `vars.<name>`
|
|
211
|
+
* path to one identifier. That is the only shape the SDK writes (the field's
|
|
212
|
+
* `variable` option, else its id), so anything else is reported as a loss.
|
|
213
|
+
*/
|
|
214
|
+
export declare function formFieldVariableName(variable: unknown): string | undefined;
|
|
208
215
|
/**
|
|
209
216
|
* The keys of a form row the SDK cannot carry into source, each with why.
|
|
210
217
|
* Empty when decompile → compile writes the row back as it was. The lifts
|
|
@@ -462,6 +462,16 @@ export function liftFormFieldSettings(row) {
|
|
|
462
462
|
export function formFieldSettingsSource(settings, value) {
|
|
463
463
|
return FORM_FIELD_SETTINGS_KEYS.map((key) => [key, settings[key] === undefined ? undefined : value(settings[key])]);
|
|
464
464
|
}
|
|
465
|
+
/**
|
|
466
|
+
* The variable NAME an asked field's `variable` row key names — `P_comments`
|
|
467
|
+
* for `vars.P_comments` — or `undefined` when the value is not a `vars.<name>`
|
|
468
|
+
* path to one identifier. That is the only shape the SDK writes (the field's
|
|
469
|
+
* `variable` option, else its id), so anything else is reported as a loss.
|
|
470
|
+
*/
|
|
471
|
+
export function formFieldVariableName(variable) {
|
|
472
|
+
const m = typeof variable === 'string' ? /^vars\.([A-Za-z_$][\w$]*)$/.exec(variable) : null;
|
|
473
|
+
return m ? m[1] : undefined;
|
|
474
|
+
}
|
|
465
475
|
/**
|
|
466
476
|
* The keys of a form row the SDK cannot carry into source, each with why.
|
|
467
477
|
* Empty when decompile → compile writes the row back as it was. The lifts
|
|
@@ -521,8 +531,8 @@ export function formFieldRowLoss(row, site) {
|
|
|
521
531
|
say(key, 'on an escalation form, which the SDK writes without variables');
|
|
522
532
|
else if (direction !== 'output')
|
|
523
533
|
say(key, 'on a shown or pre-filled field, which the SDK writes without one');
|
|
524
|
-
else if (row.variable
|
|
525
|
-
say(key, `the SDK writes 'vars.${id}'
|
|
534
|
+
else if (formFieldVariableName(row.variable) === undefined)
|
|
535
|
+
say(key, `not a 'vars.<name>' path, which is what the SDK writes ('vars.${id}' by default)`);
|
|
526
536
|
}
|
|
527
537
|
}
|
|
528
538
|
return loss;
|
package/dist/decompile.js
CHANGED
|
@@ -191,14 +191,30 @@ function shippedVersions(type) {
|
|
|
191
191
|
* The options argument of `.branch()`, `.switch()` and `.parallel()`: the exact
|
|
192
192
|
* `version`, and a canvas `label` that differs from the node id (what compile
|
|
193
193
|
* writes when none is given). No `updates`: those constructs refuse them.
|
|
194
|
+
*
|
|
195
|
+
* `node` is the construct's own node (a `.parallel`'s join); `fork` is the
|
|
196
|
+
* merge node a designer-drawn parallel split leaves from, written as
|
|
197
|
+
* `{ fork }` (#1310). A `.parallel` whose arms rejoin on a plain step has no
|
|
198
|
+
* join node, so `node` may be absent; the fork then supplies the version.
|
|
194
199
|
*/
|
|
195
|
-
function forkOptsArg(node) {
|
|
200
|
+
function forkOptsArg(node, fork) {
|
|
196
201
|
const parts = [];
|
|
197
|
-
const ver = verClause(node);
|
|
202
|
+
const ver = node ? verClause(node) : fork ? verClause(fork) : '';
|
|
198
203
|
if (ver)
|
|
199
204
|
parts.push(ver);
|
|
200
|
-
if (hasRenamedLabel(node, node.id))
|
|
205
|
+
if (node && hasRenamedLabel(node, node.id))
|
|
201
206
|
parts.push(`label: ${tsLiteral(node.display.label)}`);
|
|
207
|
+
if (fork) {
|
|
208
|
+
// The fork's own version, when the join's clause does not already say it
|
|
209
|
+
// (no join node, or a different version on each).
|
|
210
|
+
const forkVer = node && node.typeVersion !== fork.typeVersion ? verClause(fork) : '';
|
|
211
|
+
const forkParts = [
|
|
212
|
+
`name: ${tsLiteral(fork.id)}`,
|
|
213
|
+
...(hasRenamedLabel(fork, fork.id) ? [`label: ${tsLiteral(fork.display.label)}`] : []),
|
|
214
|
+
...(forkVer ? [forkVer] : []),
|
|
215
|
+
];
|
|
216
|
+
parts.push(forkParts.length === 1 ? `fork: ${tsLiteral(fork.id)}` : `fork: { ${forkParts.join(', ')} }`);
|
|
217
|
+
}
|
|
202
218
|
return parts.length ? `, { ${parts.join(', ')} }` : '';
|
|
203
219
|
}
|
|
204
220
|
/**
|
|
@@ -322,6 +338,19 @@ function unwrapSource(v) {
|
|
|
322
338
|
}
|
|
323
339
|
return undefined;
|
|
324
340
|
}
|
|
341
|
+
/**
|
|
342
|
+
* The JS body of a condition slot (a decision's `expression`, a switch case's
|
|
343
|
+
* `expression`), whichever spelling the file uses: a bare JS string (the classic
|
|
344
|
+
* CLI), a `{ type: 'jsExpression', expression }` envelope (what Studio Web
|
|
345
|
+
* writes), or a `=js:`-prefixed string, with or without a space after the colon
|
|
346
|
+
* (`=js: ($vars.x ?? 0) > 0`). Before this, the envelope was stringified to
|
|
347
|
+
* `[object Object]` and the prefixed spelling was kept inside the template, so
|
|
348
|
+
* every designer-saved decision came back as source compile refused (#1308).
|
|
349
|
+
*/
|
|
350
|
+
function conditionText(v) {
|
|
351
|
+
const raw = unwrapSource(v);
|
|
352
|
+
return raw === undefined ? String(v ?? '') : wireExprBody(raw).trim();
|
|
353
|
+
}
|
|
325
354
|
/** A plain (boolean/number) input value, unwrapping a literal EV envelope if present. */
|
|
326
355
|
function unwrapPlain(v) {
|
|
327
356
|
if (v && typeof v === 'object' && 'expression' in v && v.type === 'literal') {
|
|
@@ -1513,7 +1542,11 @@ function forkRegion(edges, stop, scope, ipdom, ctx, refInto) {
|
|
|
1513
1542
|
const targets = edges.map((e) => e.targetNodeId);
|
|
1514
1543
|
const join = commonPostDominator(targets, ipdom);
|
|
1515
1544
|
const joinNode = join !== undefined ? ctx.graph.byId.get(join) : undefined;
|
|
1516
|
-
|
|
1545
|
+
// Arms may rejoin at the region's own continuation (`stop`) when that is a
|
|
1546
|
+
// plain step: the arms end in `.stepToRef(stop)` and nothing follows them
|
|
1547
|
+
// here. Not at a merge node that is `stop`: the enclosing construct continues
|
|
1548
|
+
// from it, so a `.parallel` inside this arm cannot also own it.
|
|
1549
|
+
if (join === undefined || joinNode === undefined || !parallelJoinable(joinNode) || (join === stop && joinNode.type === T.merge)) {
|
|
1517
1550
|
if (ctx.o.bestEffort === false)
|
|
1518
1551
|
throw new Error(`decompile: unstructured fan-out (${edges.length} edges).`);
|
|
1519
1552
|
ctx.graph.accounted.add(edges[0]);
|
|
@@ -1525,22 +1558,75 @@ function forkRegion(edges, stop, scope, ipdom, ctx, refInto) {
|
|
|
1525
1558
|
}
|
|
1526
1559
|
for (const e of edges)
|
|
1527
1560
|
ctx.graph.accounted.add(e);
|
|
1528
|
-
const fork = parallelSeg(join, joinNode, targets, scope, ipdom, ctx, refInto);
|
|
1561
|
+
const fork = parallelSeg(join, joinNode, targets, scope, ipdom, ctx, refInto, undefined, edges[0].sourceNodeId);
|
|
1529
1562
|
if (fork.next.brk || fork.next.cont)
|
|
1530
1563
|
return [fork.seg, fork.next.brk ? '.break()' : '.continue()'];
|
|
1531
1564
|
return [fork.seg, ...region(fork.next.cursor, stop, scope, ipdom, ctx, refInto)];
|
|
1532
1565
|
}
|
|
1566
|
+
/**
|
|
1567
|
+
* Whether a fan-out whose arms all reconverge at `join` folds to a
|
|
1568
|
+
* `.parallel(...)`: at a merge node, the SDK's own join; at any other step (a
|
|
1569
|
+
* step, a decision, a loop, …), the canvas shape of several steps feeding one
|
|
1570
|
+
* (#1310), which the arms spell by ending in `.stepToRef(join)` — serialize
|
|
1571
|
+
* writes no merge node when no arm hands it a tail. Not at an End or a
|
|
1572
|
+
* Terminate: arms that all end share the End by name (#1231/#1239) and nothing
|
|
1573
|
+
* follows, which is another fold (#1267).
|
|
1574
|
+
*/
|
|
1575
|
+
function parallelJoinable(join) {
|
|
1576
|
+
return join.type !== T.end && join.type !== T.terminate;
|
|
1577
|
+
}
|
|
1533
1578
|
/**
|
|
1534
1579
|
* The `.parallel(join, [arms])` segment for a fork whose `targets` (one per
|
|
1535
|
-
* edge; the caller accounts those edges) meet at
|
|
1536
|
-
*
|
|
1537
|
-
*
|
|
1538
|
-
*
|
|
1580
|
+
* edge; the caller accounts those edges) meet at `join`. Each arm is the
|
|
1581
|
+
* region up to `join`.
|
|
1582
|
+
*
|
|
1583
|
+
* At a merge node, `join` names the `.parallel` and `next` is where the walk
|
|
1584
|
+
* goes after it: its `output` edge, accounted by `continueOn` (`brk` / `cont`
|
|
1585
|
+
* when that edge leaves through the enclosing loop's `break` / spelled
|
|
1586
|
+
* `continue`). At any other step (#1310) the arms end in `.stepToRef(join)`,
|
|
1587
|
+
* the `.parallel` gets a fresh name (`forkName`, after `source`) because no
|
|
1588
|
+
* node carries one, and `next` is `join` itself: the main path emits it, and
|
|
1589
|
+
* its only incoming edges are the arms' refs.
|
|
1590
|
+
*
|
|
1591
|
+
* `fork` is the merge node a designer-drawn split leaves from, written as
|
|
1592
|
+
* `{ fork }` (#1310); the caller accounts its outgoing edges.
|
|
1539
1593
|
*/
|
|
1540
|
-
function parallelSeg(join, joinNode, targets, scope, ipdom, ctx, refInto) {
|
|
1541
|
-
const
|
|
1542
|
-
const
|
|
1543
|
-
|
|
1594
|
+
function parallelSeg(join, joinNode, targets, scope, ipdom, ctx, refInto, fork, source) {
|
|
1595
|
+
const atMerge = joinNode.type === T.merge;
|
|
1596
|
+
const name = atMerge ? join : forkName(source ?? fork?.id, ctx);
|
|
1597
|
+
const arms = targets.map((t, i) => {
|
|
1598
|
+
const segs = region(t, join, scope, ipdom, ctx, refInto);
|
|
1599
|
+
// The arm hands the join a tail only when its walk ran up to the join; a
|
|
1600
|
+
// path that already left (`.break()`, `.continue()`, a ref, an End, a
|
|
1601
|
+
// Terminate) needs no ref — and gets none.
|
|
1602
|
+
if (!atMerge && !endsPath(segs[segs.length - 1]))
|
|
1603
|
+
segs.push(`.stepToRef(${tsLiteral(join)})`);
|
|
1604
|
+
return armCb(`a${i}`, segs);
|
|
1605
|
+
});
|
|
1606
|
+
const seg = `.parallel(${tsLiteral(name)}, [\n${arms.map((a) => reindent(a, ' ')).join(',\n')},\n]${forkOptsArg(atMerge ? joinNode : undefined, fork)})`;
|
|
1607
|
+
return { seg, next: atMerge ? continueOn(joinNode, 'output', ctx, refInto) : { cursor: join } };
|
|
1608
|
+
}
|
|
1609
|
+
/** Whether a chain segment ends its path, so nothing may follow it in the same list. */
|
|
1610
|
+
function endsPath(seg) {
|
|
1611
|
+
return seg !== undefined && /^\.(stepToRef|break|continue|return|terminate)\(/.test(seg);
|
|
1612
|
+
}
|
|
1613
|
+
/**
|
|
1614
|
+
* A name for a `.parallel(...)` that creates no merge node: `<source>Fork`,
|
|
1615
|
+
* numbered when that is a node id, a declared input, variable or output, or a
|
|
1616
|
+
* name already minted. No node carries it (the arms join on a plain step by
|
|
1617
|
+
* ref), but it must still not collide with any of those: check reads a
|
|
1618
|
+
* `$vars.<name>` of a parallel's name as a read of its merge, so a variable
|
|
1619
|
+
* of that name would fail PARALLEL_HAS_NO_OUTPUT on recompile.
|
|
1620
|
+
*/
|
|
1621
|
+
function forkName(source, ctx) {
|
|
1622
|
+
const base = `${source ?? 'parallel'}Fork`;
|
|
1623
|
+
const globals = new Set(readFlowGlobals(ctx.flow.variables).map((g) => g.name ?? g.id));
|
|
1624
|
+
const taken = (n) => ctx.graph.byId.has(n) || globals.has(n) || (ctx.forkNames?.has(n) ?? false);
|
|
1625
|
+
let name = base;
|
|
1626
|
+
for (let i = 2; taken(name); i++)
|
|
1627
|
+
name = `${base}${i}`;
|
|
1628
|
+
(ctx.forkNames ??= new Set()).add(name);
|
|
1629
|
+
return name;
|
|
1544
1630
|
}
|
|
1545
1631
|
/** Re-indent every non-empty line of a (possibly multi-line) segment by `pad`. */
|
|
1546
1632
|
function reindent(seg, pad) {
|
|
@@ -1707,21 +1793,50 @@ function region(cursor, stop, scope, ipdom, ctx, refInto) {
|
|
|
1707
1793
|
return segs;
|
|
1708
1794
|
}
|
|
1709
1795
|
if (node.type === T.merge) {
|
|
1710
|
-
// A
|
|
1711
|
-
//
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1796
|
+
// A merge node with several `output` edges is a FORK the designer drew
|
|
1797
|
+
// (#1310): Studio Web's parallel split is a `core.logic.merge` with one
|
|
1798
|
+
// incoming edge and one edge per arm, the arms rejoining at a second
|
|
1799
|
+
// merge node (or at a plain step). `.parallel(join, arms, { fork })`
|
|
1800
|
+
// recreates both nodes and every edge.
|
|
1801
|
+
const outs = scopeSuccEdges(ctx.graph, node.id, scope).filter((e) => e.sourcePort === 'output');
|
|
1802
|
+
const join = outs.length >= 2 ? ipdom.get(node.id) : undefined;
|
|
1803
|
+
const joinNode = join !== undefined && join !== EXIT ? ctx.graph.byId.get(join) : undefined;
|
|
1804
|
+
// As in `forkRegion`: a plain-step join may be the region's own `stop`.
|
|
1805
|
+
if (join !== undefined && joinNode !== undefined && parallelJoinable(joinNode) && !(join === stop && joinNode.type === T.merge)) {
|
|
1806
|
+
const targets = outs.map((e) => follow(ctx.graph, node.id, e.targetNodeId));
|
|
1807
|
+
const fork = parallelSeg(join, joinNode, targets, scope, ipdom, ctx, refInto, node);
|
|
1808
|
+
segs.push(fork.seg);
|
|
1715
1809
|
flagDropped(node, ctx, segs);
|
|
1716
|
-
|
|
1810
|
+
if (fork.next.brk || fork.next.cont) {
|
|
1811
|
+
segs.push(fork.next.brk ? '.break()' : '.continue()');
|
|
1812
|
+
return segs;
|
|
1813
|
+
}
|
|
1814
|
+
cursor = fork.next.cursor;
|
|
1815
|
+
continue;
|
|
1816
|
+
}
|
|
1817
|
+
// A merge node with one edge in and one out is a pass-through the
|
|
1818
|
+
// designer left in place (a join whose arms were later removed). The
|
|
1819
|
+
// layout names it, so it is kept: it falls through to the regular step
|
|
1820
|
+
// path below and comes back as the raw node of its own definition
|
|
1821
|
+
// (#1310). Skipping it dropped the node, and compile then refused the
|
|
1822
|
+
// `.layout()` key. A bare merge with several edges in (a join whose fork
|
|
1823
|
+
// was not detected) is still skipped — best effort.
|
|
1824
|
+
const ins = ctx.graph.inEdges(node.id).filter((e) => scope.has(e.sourceNodeId));
|
|
1825
|
+
if (outs.length !== 1 || ins.length !== 1) {
|
|
1826
|
+
const next = continueOn(node, 'output', ctx, refInto);
|
|
1827
|
+
if (next.brk || next.cont) {
|
|
1828
|
+
segs.push(next.brk ? '.break()' : '.continue()');
|
|
1829
|
+
flagDropped(node, ctx, segs);
|
|
1830
|
+
return segs;
|
|
1831
|
+
}
|
|
1832
|
+
cursor = next.cursor;
|
|
1833
|
+
flagDropped(node, ctx, segs);
|
|
1834
|
+
continue;
|
|
1717
1835
|
}
|
|
1718
|
-
cursor = next.cursor;
|
|
1719
|
-
flagDropped(node, ctx, segs);
|
|
1720
|
-
continue;
|
|
1721
1836
|
}
|
|
1722
1837
|
if (node.type === T.decision) {
|
|
1723
1838
|
const r = ipdom.get(node.id) ?? stop;
|
|
1724
|
-
const cond = emitBareExpr(
|
|
1839
|
+
const cond = emitBareExpr(conditionText((node.inputs ?? {}).expression), ctx.imp, ctx.inputNames);
|
|
1725
1840
|
const thenSegs = forkRegion(portEdges(ctx.graph, node.id, 'true'), r, scope, ipdom, ctx, refInto);
|
|
1726
1841
|
const elseSegs = forkRegion(portEdges(ctx.graph, node.id, 'false'), r, scope, ipdom, ctx, refInto);
|
|
1727
1842
|
const labels = armLabels(node);
|
|
@@ -1804,7 +1919,14 @@ function region(cursor, stop, scope, ipdom, ctx, refInto) {
|
|
|
1804
1919
|
}
|
|
1805
1920
|
if (node.type === T.subflow) {
|
|
1806
1921
|
segs.push(`.step(${tsLiteral(node.id)}, ${emitSubflow(node, ctx)}${optsArg(node, ctx)})`);
|
|
1807
|
-
|
|
1922
|
+
// A subflow step guards its body like any step: `core.subflow` declares
|
|
1923
|
+
// the `error` handle, and `.onError()` after the step authors it. Without
|
|
1924
|
+
// this the handler's edge had no spelling and a recompile dropped it.
|
|
1925
|
+
const subflowSucc = scopeSucc(ctx.graph, node.id, scope);
|
|
1926
|
+
const subflowHandler = errorHandlerSeg(node.id, subflowSucc, scope, ipdom, ctx);
|
|
1927
|
+
if (subflowHandler)
|
|
1928
|
+
segs.push(subflowHandler);
|
|
1929
|
+
cursor = follow(ctx.graph, node.id, subflowSucc[0]);
|
|
1808
1930
|
if (cursor === undefined)
|
|
1809
1931
|
accountImplicitTail(node, ctx, refInto);
|
|
1810
1932
|
flagDropped(node, ctx, segs);
|
|
@@ -1920,11 +2042,13 @@ function region(cursor, stop, scope, ipdom, ctx, refInto) {
|
|
|
1920
2042
|
flagDropped(node, ctx, segs);
|
|
1921
2043
|
continue;
|
|
1922
2044
|
}
|
|
1923
|
-
// Parallel: the fork's ipdom is the
|
|
2045
|
+
// Parallel: the fork's ipdom is the join — the merge node of a `.parallel`,
|
|
2046
|
+
// or a plain step several steps feed (#1310); each successor is one arm.
|
|
1924
2047
|
const join = ipdom.get(node.id);
|
|
1925
2048
|
const joinNode = join !== undefined ? ctx.graph.byId.get(join) : undefined;
|
|
1926
|
-
|
|
1927
|
-
|
|
2049
|
+
// A plain-step join may be the region's own `stop`: the arms end in refs to it and the walk ends there.
|
|
2050
|
+
if (join && join !== EXIT && joinNode && parallelJoinable(joinNode)) {
|
|
2051
|
+
const fork = parallelSeg(join, joinNode, succ.map((t) => follow(ctx.graph, node.id, t)), scope, ipdom, ctx, refInto, undefined, node.id);
|
|
1928
2052
|
segs.push(fork.seg);
|
|
1929
2053
|
flagDropped(node, ctx, segs);
|
|
1930
2054
|
if (fork.next.brk || fork.next.cont) {
|
|
@@ -2164,7 +2288,7 @@ function emitSwitch(node, ipdom, scope, ctx, stop, refInto) {
|
|
|
2164
2288
|
// First pass: is every case one shared `<disc> === <literal>`? If not, every
|
|
2165
2289
|
// case is emitted as `when:` with its expression as written.
|
|
2166
2290
|
const parsed = cases.map((c) => {
|
|
2167
|
-
const p = parseSwitchCase(
|
|
2291
|
+
const p = parseSwitchCase(conditionText(c.expression));
|
|
2168
2292
|
if (p) {
|
|
2169
2293
|
if (disc === undefined)
|
|
2170
2294
|
disc = p.lhs;
|
|
@@ -2186,13 +2310,13 @@ function emitSwitch(node, ipdom, scope, ctx, stop, refInto) {
|
|
|
2186
2310
|
// edges leave from the ORIGINAL node's handles, which merge keeps (#1130).
|
|
2187
2311
|
const idPart = typeof c.id === 'string' && c.id !== '' && c.id !== `case_${caseIndex}` ? `, id: ${tsLiteral(c.id)}` : '';
|
|
2188
2312
|
const head = nonUniform || !p
|
|
2189
|
-
? `when: ${emitBareExpr(
|
|
2313
|
+
? `when: ${emitBareExpr(conditionText(c.expression), ctx.imp, ctx.inputNames)}`
|
|
2190
2314
|
: `value: ${p.value}`;
|
|
2191
2315
|
return `{ ${head}${labelPart}${idPart}, body: ${armCb('b', armSegs)} }`;
|
|
2192
2316
|
});
|
|
2193
2317
|
// A case with no condition has no `when:` that compiles: emitting `js\`\``
|
|
2194
2318
|
// would fail at compile ([EXPR_SYNTAX]) with no warning here.
|
|
2195
|
-
const empty = cases.filter((c) =>
|
|
2319
|
+
const empty = cases.filter((c) => conditionText(c.expression) === '');
|
|
2196
2320
|
if (empty.length > 0) {
|
|
2197
2321
|
const listing = empty.map((c) => tsLiteral(c.label ?? c.id)).join(', ');
|
|
2198
2322
|
const message = `switch ${tsLiteral(node.id)} has a case with no condition (${listing}). `
|
|
@@ -2365,7 +2489,10 @@ function layoutSeg(flow, nodes, edges) {
|
|
|
2365
2489
|
*/
|
|
2366
2490
|
function emitSubflow(node, ctx) {
|
|
2367
2491
|
const child = ctx.flow.subflows?.[node.id];
|
|
2368
|
-
|
|
2492
|
+
// `errorHandlingEnabled` is the flag serialize sets for `.onError()`, not a
|
|
2493
|
+
// child input; `.onError(...)` after the step puts it back.
|
|
2494
|
+
const { errorHandlingEnabled: _handled, ...inputs } = (node.inputs ?? {});
|
|
2495
|
+
const inputsSrc = emitValMap(inputs, ctx.imp, ctx.inputNames) || '{}';
|
|
2369
2496
|
if (!child) {
|
|
2370
2497
|
if (ctx.o.bestEffort === false)
|
|
2371
2498
|
throw new Error(`decompile: subflow "${node.id}" has no body in subflows[].`);
|
|
@@ -2381,7 +2508,52 @@ function emitSubflow(node, ctx) {
|
|
|
2381
2508
|
* own scope; the child's trigger is its `<caller>Start` node, written as
|
|
2382
2509
|
* `.trigger(manual(), { label })` only when its label is not `SUBFLOW_START_LABEL`.
|
|
2383
2510
|
*/
|
|
2384
|
-
function emitChildFlow(name,
|
|
2511
|
+
function emitChildFlow(name, childFile, ctx) {
|
|
2512
|
+
// Body: the child's trigger is its own manual Start; walk from its successor.
|
|
2513
|
+
const trigger = (childFile.nodes ?? []).find((n) => isTrigger(n.type) && !(childFile.edges ?? []).some((e) => e.targetNodeId === n.id));
|
|
2514
|
+
// The child's nodes read its inputs through that Start (`$vars.<caller>Start.
|
|
2515
|
+
// output.<input>`); read it back as `start`, the one name both `input()` and
|
|
2516
|
+
// check know a child's trigger by (#1313). Not when a child node is itself
|
|
2517
|
+
// named `start`: the rename would then point the reads at that node.
|
|
2518
|
+
const readAsStart = trigger !== undefined && trigger.id !== DEFAULT_TRIGGER_ID
|
|
2519
|
+
&& !(childFile.nodes ?? []).some((n) => n.id === DEFAULT_TRIGGER_ID);
|
|
2520
|
+
const child = readAsStart ? readChildStartAsStart(childFile, trigger.id) : childFile;
|
|
2521
|
+
const outerTriggerId = currentTriggerId;
|
|
2522
|
+
currentTriggerId = readAsStart ? DEFAULT_TRIGGER_ID : (trigger?.id ?? DEFAULT_TRIGGER_ID);
|
|
2523
|
+
try {
|
|
2524
|
+
return emitChildFlowIn(name, child, trigger, ctx);
|
|
2525
|
+
}
|
|
2526
|
+
finally {
|
|
2527
|
+
currentTriggerId = outerTriggerId;
|
|
2528
|
+
}
|
|
2529
|
+
}
|
|
2530
|
+
/**
|
|
2531
|
+
* A child flow's reads of its own Start node, `$vars.<caller>Start…`, read as
|
|
2532
|
+
* `$vars.start…`. serialize names a child's trigger `<caller>Start` and renames
|
|
2533
|
+
* `start` to it in the child scope, so `input('x')` — which lowers to
|
|
2534
|
+
* `$vars.start.output.x` — reaches it; this is the inverse. Rewritten in code
|
|
2535
|
+
* spans (`renameVarRoot`: `=js:` values, bare `$vars.` paths, the raw-JS
|
|
2536
|
+
* fields) and in the BINDING slots that hold a variable path instead of an
|
|
2537
|
+
* expression: a form row's `binding` (`vars.<caller>Start.output.x`), an agent
|
|
2538
|
+
* input's `binding`, and an action app's `appInputBindings` (`=vars.…`). Only
|
|
2539
|
+
* those: a literal string elsewhere that happens to read `vars.<caller>Start…`
|
|
2540
|
+
* (a variable's default, a label) is data and stays as written.
|
|
2541
|
+
*/
|
|
2542
|
+
function readChildStartAsStart(child, startId) {
|
|
2543
|
+
const renamed = renameVarRoot(child, startId, DEFAULT_TRIGGER_ID);
|
|
2544
|
+
const path = new RegExp(`^(=?vars\\.)${startId.replace(/[$]/g, '\\$&')}(?![A-Za-z0-9_$])`);
|
|
2545
|
+
const walk = (x, key, parentKey) => {
|
|
2546
|
+
if (typeof x === 'string')
|
|
2547
|
+
return key === 'binding' || parentKey === 'appInputBindings' ? x.replace(path, `$1${DEFAULT_TRIGGER_ID}`) : x;
|
|
2548
|
+
if (Array.isArray(x))
|
|
2549
|
+
return x.map((y) => walk(y, key, parentKey));
|
|
2550
|
+
if (x && typeof x === 'object')
|
|
2551
|
+
return Object.fromEntries(Object.entries(x).map(([k, y]) => [k, walk(y, k, key)]));
|
|
2552
|
+
return x;
|
|
2553
|
+
};
|
|
2554
|
+
return walk(renamed);
|
|
2555
|
+
}
|
|
2556
|
+
function emitChildFlowIn(name, child, trigger, ctx) {
|
|
2385
2557
|
const globals = readFlowGlobals(child.variables, runStrict);
|
|
2386
2558
|
const childCtx = {
|
|
2387
2559
|
graph: new Graph(child.nodes ?? [], child.edges ?? []),
|
|
@@ -2405,8 +2577,6 @@ function emitChildFlow(name, child, ctx) {
|
|
|
2405
2577
|
const def = g.defaultValue !== undefined ? `, ${tsLiteral(g.defaultValue)}` : '';
|
|
2406
2578
|
parts.push(`.var(${tsLiteral(g.name ?? g.id)}, ${declExpr(g, false, ctx.imp)}${def})`);
|
|
2407
2579
|
}
|
|
2408
|
-
// Body: the child's trigger is its own manual Start; walk from its successor.
|
|
2409
|
-
const trigger = (child.nodes ?? []).find((n) => isTrigger(n.type) && !(child.edges ?? []).some((e) => e.targetNodeId === n.id));
|
|
2410
2580
|
if (trigger?.type === T.triggerManual && hasRenamedLabel(trigger, SUBFLOW_START_LABEL)) {
|
|
2411
2581
|
parts.push(`.trigger(${ctx.imp.use('manual')}(), { ${triggerOptParts(trigger, SUBFLOW_START_LABEL).join(', ')} })`);
|
|
2412
2582
|
}
|
package/dist/flow-expr-check.js
CHANGED
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
*/
|
|
30
30
|
import { Expr, cookedEscapesOf } from './core/expr.js';
|
|
31
31
|
import { checkExpression, cookedEscapeDiagnostics } from './core/expr-check.js';
|
|
32
|
+
import { stepDeclaredVariables } from './flow-step-variables.js';
|
|
32
33
|
const NOUN = 'input, variable, or step';
|
|
33
34
|
/**
|
|
34
35
|
* Every `$vars` root a reference may legitimately resolve to, across the WHOLE
|
|
@@ -77,6 +78,10 @@ function addStepNames(steps, roots) {
|
|
|
77
78
|
// they are edges, not nodes, so there is nothing to reference them by.
|
|
78
79
|
if (s.kind !== 'return' && s.kind !== 'stepToRef' && s.kind !== 'stepToList' && s.kind !== 'break' && s.kind !== 'continue')
|
|
79
80
|
roots.add(s.name);
|
|
81
|
+
// A step declares the variables its fields land in too (`check.ts` mirrors this, #1309).
|
|
82
|
+
if (s.kind === 'action' || s.kind === 'stepSwitch')
|
|
83
|
+
for (const v of stepDeclaredVariables(s.spec, s.options?.version))
|
|
84
|
+
roots.add(v);
|
|
80
85
|
switch (s.kind) {
|
|
81
86
|
case 'stepToList':
|
|
82
87
|
addStepNames(s.body, roots);
|
package/dist/flow-node-type.d.ts
CHANGED
|
@@ -441,6 +441,15 @@ export interface FlowNodeType<Spec> extends NodeType<Spec, FlowNodeContribution,
|
|
|
441
441
|
* one of these (`.onError()` routes it).
|
|
442
442
|
*/
|
|
443
443
|
exits?(spec: Spec, pinnedVersion?: string): DeclaredExit[] | NoExits;
|
|
444
|
+
/**
|
|
445
|
+
* The flow variables a step of this type DECLARES by running: names a later
|
|
446
|
+
* `$vars.<name>` read resolves to that are neither a `.var()` nor a step
|
|
447
|
+
* name. A quick form's asked field lands its answer in its `variable`
|
|
448
|
+
* (`vars.<Step>_<field>`, as the designer names it), so the task declares
|
|
449
|
+
* that variable. `check` adds them to its declared set (#1309). Omitted: the
|
|
450
|
+
* type declares none.
|
|
451
|
+
*/
|
|
452
|
+
declares?(spec: Spec, pinnedVersion?: string): string[];
|
|
444
453
|
/**
|
|
445
454
|
* Whether `port` is a forward handle the PRODUCT derives for this spec, which
|
|
446
455
|
* the definition does not list — a human task's `outcome-<id>` handles, drawn
|