@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/core/actions.d.ts
CHANGED
|
@@ -564,16 +564,16 @@ export interface HitlInputs {
|
|
|
564
564
|
/**
|
|
565
565
|
* The completion buttons, e.g. `['Approve', 'Reject']`. At least one, or the
|
|
566
566
|
* reviewer has no way to complete the task and the flow waits forever. The
|
|
567
|
-
* first is the primary (default) one.
|
|
568
|
-
*
|
|
569
|
-
*
|
|
570
|
-
*
|
|
571
|
-
*
|
|
572
|
-
*
|
|
567
|
+
* first is the primary (default) one. Each outcome is its own exit,
|
|
568
|
+
* `outcome-<slug>`: the default node and `'quick-form'` never leave on
|
|
569
|
+
* `completed` once they have outcomes. On the default node MORE THAN ONE
|
|
570
|
+
* outcome routes per outcome: the first continues the main path and the rest
|
|
571
|
+
* are routed with `.stepToList('outcome-<slug>', …)`. With `'quick-form'`,
|
|
572
|
+
* {@link HitlInputs.outcomePorts | outcomePorts}`: false`, an explicit
|
|
573
|
+
* `{ version }` or one outcome, `.step()` continues EVERY outcome instead.
|
|
573
574
|
*
|
|
574
575
|
* Which one they pressed is also `out('<step>', 'Action')` — that is what the
|
|
575
|
-
* platform's own routing conditions compare against
|
|
576
|
-
* exit branches on it downstream.
|
|
576
|
+
* platform's own routing conditions compare against.
|
|
577
577
|
*/
|
|
578
578
|
outcomes: Outcome[];
|
|
579
579
|
/** Who gets the task and how. Omit for the definition's default delivery. */
|
|
@@ -581,25 +581,25 @@ export interface HitlInputs {
|
|
|
581
581
|
/** Action Center labels, comma-separated (the platform stores one string). */
|
|
582
582
|
labels?: string;
|
|
583
583
|
/**
|
|
584
|
-
* Whether
|
|
585
|
-
*
|
|
586
|
-
* the useful value is `false` — an opt back into the single exit, for a task
|
|
587
|
-
* that routes the decision as DATA by reading `out('<step>', 'Action')`.
|
|
584
|
+
* Whether the task ROUTES per outcome. Each outcome has its own exit either
|
|
585
|
+
* way (`outcome-<id>`, the name slugified: `'Approve'` → `outcome-approve`).
|
|
588
586
|
*
|
|
589
|
-
*
|
|
590
|
-
*
|
|
591
|
-
*
|
|
592
|
-
*
|
|
593
|
-
*
|
|
594
|
-
*
|
|
595
|
-
*
|
|
587
|
+
* `true`: the FIRST outcome continues the main path and the others are routed
|
|
588
|
+
* with `.stepToList('outcome-<id>', …)`, or the run stalls when the reviewer
|
|
589
|
+
* picks one. The default node with more than one outcome already does this,
|
|
590
|
+
* on its **1.1** definition unless a `{ version }` pins another; `'quick-form'`
|
|
591
|
+
* supports it on its one definition. `false`: EVERY outcome continues to the
|
|
592
|
+
* next step on its own edge — a task that routes the decision as DATA by
|
|
593
|
+
* reading `out('<step>', 'Action')`; `'quick-form'` does this by default.
|
|
594
|
+
* `'action-app'` and `'document-validation'` read their outcomes from the app
|
|
595
|
+
* and leave on their one `completed` exit, so `true` is refused there.
|
|
596
596
|
*/
|
|
597
597
|
outcomePorts?: boolean;
|
|
598
598
|
/**
|
|
599
599
|
* Expose the task's runtime error as a bindable `out('<step>', 'error')`
|
|
600
600
|
* object (`{ code, message, detail, category, status }`). Selects the node's
|
|
601
601
|
* **1.2** definition — which also routes per outcome, so this implies
|
|
602
|
-
* {@link HitlInputs.outcomePorts | outcomePorts}.
|
|
602
|
+
* {@link HitlInputs.outcomePorts | outcomePorts}. Default node only.
|
|
603
603
|
*/
|
|
604
604
|
exposeError?: boolean;
|
|
605
605
|
/**
|
|
@@ -37,3 +37,13 @@ export declare function cliSpelling(env?: CliSpellingEnv): string;
|
|
|
37
37
|
* ```
|
|
38
38
|
*/
|
|
39
39
|
export declare function prepareCommand(...args: string[]): string;
|
|
40
|
+
/**
|
|
41
|
+
* The argument of one `--resolve <field>:<by>=<value>`, single-quoted when the
|
|
42
|
+
* shell would otherwise mangle it.
|
|
43
|
+
*
|
|
44
|
+
* @param field - The lookup field's wire name, e.g. `channel` or `users[*]`.
|
|
45
|
+
* @param by - The match key, e.g. `profile.email`.
|
|
46
|
+
* @param value - The value to match, or the `<value>` placeholder.
|
|
47
|
+
* @returns `channel:profile.email=d@x.com` as is; `'users[*]:profile.real_name=Coder Evals'` quoted.
|
|
48
|
+
*/
|
|
49
|
+
export declare function resolveFlagArg(field: string, by: string, value: string): string;
|
|
@@ -60,3 +60,27 @@ export function cliSpelling(env = ambientEnv()) {
|
|
|
60
60
|
export function prepareCommand(...args) {
|
|
61
61
|
return [cliSpelling(), 'registry', 'prepare', ...args.filter(Boolean)].join(' ');
|
|
62
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* Characters that make a `--resolve` argument unsafe to paste unquoted. `[*]`
|
|
65
|
+
* is a shell glob (every multiselect wire name, `users[*]`), whitespace splits
|
|
66
|
+
* a real name (`Coder Evals`) into two arguments, and quotes, `$`, backslash
|
|
67
|
+
* and the control operators change what the shell hands over. The `<value>`
|
|
68
|
+
* placeholder a template prints is stripped before the test, so a template
|
|
69
|
+
* with nothing else unsafe keeps its bytes.
|
|
70
|
+
*/
|
|
71
|
+
const SHELL_UNSAFE = /[\s'"`$\\[\]*?;&|(){}!#~<>]/;
|
|
72
|
+
/**
|
|
73
|
+
* The argument of one `--resolve <field>:<by>=<value>`, single-quoted when the
|
|
74
|
+
* shell would otherwise mangle it.
|
|
75
|
+
*
|
|
76
|
+
* @param field - The lookup field's wire name, e.g. `channel` or `users[*]`.
|
|
77
|
+
* @param by - The match key, e.g. `profile.email`.
|
|
78
|
+
* @param value - The value to match, or the `<value>` placeholder.
|
|
79
|
+
* @returns `channel:profile.email=d@x.com` as is; `'users[*]:profile.real_name=Coder Evals'` quoted.
|
|
80
|
+
*/
|
|
81
|
+
export function resolveFlagArg(field, by, value) {
|
|
82
|
+
const arg = `${field}:${by}=${value}`;
|
|
83
|
+
if (!SHELL_UNSAFE.test(arg.replaceAll('<value>', '')))
|
|
84
|
+
return arg;
|
|
85
|
+
return `'${arg.replaceAll("'", `'\\''`)}'`;
|
|
86
|
+
}
|
|
@@ -1,38 +1,73 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
2
|
+
* How a human task leaves its node — the one model `serialize`, `check` and
|
|
3
|
+
* `.stepSwitch` (`step-ports.ts`) all read, so the three cannot drift. They did
|
|
4
|
+
* drift once, in the direction that matters least visibly: `check` keyed on the
|
|
5
|
+
* `outcomePorts` FLAG while `serialize` was about to infer it, which would have
|
|
6
|
+
* made `check` refuse per-outcome wiring that `compile` then emitted.
|
|
7
|
+
*
|
|
8
|
+
* **The product's rule, which the model follows.** On the base node
|
|
9
|
+
* (`uipath.human-in-the-loop`, `type: 'quick'`) and on the quick-form variant,
|
|
10
|
+
* the designer draws ONE handle per entry in `inputs.schema.outcomes`, named
|
|
11
|
+
* `outcome-<id>`, whatever the node's `typeVersion` — and draws the manifest's
|
|
12
|
+
* static `completed` handle only when there are no outcomes
|
|
13
|
+
* (flow-workbench `build-handle-customization.ts:102-124`; it derives them on
|
|
14
|
+
* every render and deliberately never saves them). A leftover `completed` edge
|
|
15
|
+
* on such a node is a stale placeholder: since flow-workbench #3512 the load
|
|
16
|
+
* path and the converter remap it to the PRIMARY outcome
|
|
17
|
+
* (`hitl-edge-reconciliation.ts:52-63`), so every other outcome loses its path at
|
|
18
|
+
* run time. So a task with outcomes never leaves on `completed` here. The two
|
|
19
|
+
* app-backed variants are different: action-app and document-validation read
|
|
20
|
+
* their outcomes from `inputs.app`, and `completed` really is the only handle
|
|
21
|
+
* they render (`hitl-edge-reconciliation.ts:68-74`).
|
|
22
|
+
*
|
|
23
|
+
* (This replaces an older premise — "the definition VERSION selects the port
|
|
24
|
+
* shape, and the variants have no per-outcome version" (#729). The manifests do
|
|
25
|
+
* declare only `completed` statically; the designer just never reads that list
|
|
26
|
+
* for these two node types once outcomes exist.)
|
|
27
|
+
*
|
|
28
|
+
* **Three shapes**, and what `.step()` after the task means in each — ".step()
|
|
29
|
+
* continues from the task's tails":
|
|
30
|
+
*
|
|
31
|
+
* - `per-outcome`: the task ROUTES per outcome. One tail, the PRIMARY (first)
|
|
32
|
+
* outcome's `outcome-<id>`; the author routes the rest with `.stepToList` /
|
|
33
|
+
* `.stepToRef`, or all of them as `.stepSwitch` arms.
|
|
34
|
+
* - `every-outcome`: the task does NOT route per outcome. One tail PER outcome,
|
|
35
|
+
* so `.step()` continues every outcome to the next step ("fan-out") — the
|
|
36
|
+
* decision reaches the flow as data, `out('<step>', 'Action')`, in the handles
|
|
37
|
+
* the product draws. An `action: 'End'` outcome is the exception: the flow
|
|
38
|
+
* runtime never reads `action` (flow-workbench `hitl-serialization.ts` does
|
|
39
|
+
* not; only the agent storage projection does), so the graph is the only thing
|
|
40
|
+
* that can end the run there, and `serialize` gives it an End node.
|
|
41
|
+
* - `placeholder`: `completed` — action-app, document-validation, and a task
|
|
42
|
+
* with no outcomes.
|
|
43
|
+
*
|
|
44
|
+
* **More than one outcome infers `per-outcome`** on the base node (#739): a task
|
|
45
|
+
* that publishes a choice and then routes every button the same way was the
|
|
46
|
+
* modelling error behind flow-builder-sdk#735, and a warning alone did not
|
|
47
|
+
* change what agents emitted. Five things keep a base or quick-form task on
|
|
48
|
+
* `every-outcome` instead, in this order:
|
|
49
|
+
*
|
|
50
|
+
* 1. `outcomePorts: false` — the explicit "decision as data" choice.
|
|
51
|
+
* 2. `variant: 'quick-form'`. Not a product limit — quick-form draws the same
|
|
52
|
+
* per-outcome handles — but its authors write `.step()` or a `.switch` on the
|
|
53
|
+
* Action after it and mean "every outcome continues" (every quick-form source
|
|
54
|
+
* in the 09-23 baseline does), so inferring primary-only there would strand
|
|
55
|
+
* the other outcomes. It routes per outcome when asked: `outcomePorts: true`,
|
|
56
|
+
* or `.stepSwitch`. Its definition version never moves.
|
|
57
|
+
* 3. An explicit `{ version }` pin. `decompile` pins every node and writes
|
|
58
|
+
* `outcomePorts: true` whenever a task's outcomes lead to different places, so
|
|
59
|
+
* a pinned task that reaches `.step()` without the flag is a round-tripped
|
|
60
|
+
* artifact whose outcomes all go to one step, or a legacy lone `completed`
|
|
61
|
+
* edge, which meant "every outcome continues" before #3512. Both are this
|
|
62
|
+
* shape. The pin also wins the version: `outcomePorts: true` on a pinned 1.0
|
|
63
|
+
* routes per outcome on the 1.0 node, which `flow validate` accepts.
|
|
64
|
+
* 4. A single outcome — an acknowledgement, not a choice.
|
|
65
|
+
* 5. Every outcome carrying `action: 'End'` — there is no continuation to
|
|
66
|
+
* distinguish, and each outcome gets its End.
|
|
67
|
+
*
|
|
68
|
+
* `exposeError: true` or `outcomePorts: true` select `per-outcome` on every node
|
|
69
|
+
* type, including the two app-backed variants, so that `serialize` and `check`
|
|
70
|
+
* refuse them there with their own message rather than silently ignoring them.
|
|
36
71
|
*/
|
|
37
72
|
import type { HitlInputs } from './actions.js';
|
|
38
73
|
/**
|
|
@@ -44,4 +79,64 @@ import type { HitlInputs } from './actions.js';
|
|
|
44
79
|
* nothing else recognises.
|
|
45
80
|
*/
|
|
46
81
|
export declare function outcomeSlug(name: unknown, index: number): string;
|
|
82
|
+
/** One outcome's exit. */
|
|
83
|
+
export interface HitlOutcomeExit {
|
|
84
|
+
/** The outcome NAME — what `.stepSwitch` arms and `out('<step>', 'Action')` compare against. */
|
|
85
|
+
value: string;
|
|
86
|
+
/** The emitted source handle, `outcome-<id>`. */
|
|
87
|
+
port: string;
|
|
88
|
+
/** `action: 'End'`: the author says this outcome ends the run. */
|
|
89
|
+
endsProcess: boolean;
|
|
90
|
+
}
|
|
91
|
+
/** Why a base or quick-form task continues every outcome, phrased for a diagnostic. */
|
|
92
|
+
export type HitlFanOutReason = 'outcomePorts: false' | 'quick-form' | 'pinned' | 'one outcome' | 'every outcome ends';
|
|
93
|
+
export type HitlExitModel = {
|
|
94
|
+
shape: 'placeholder';
|
|
95
|
+
} | {
|
|
96
|
+
shape: 'every-outcome';
|
|
97
|
+
exits: HitlOutcomeExit[];
|
|
98
|
+
why: HitlFanOutReason;
|
|
99
|
+
} | {
|
|
100
|
+
shape: 'per-outcome';
|
|
101
|
+
exits: HitlOutcomeExit[];
|
|
102
|
+
};
|
|
103
|
+
/**
|
|
104
|
+
* The two human-task node types whose exits the product derives from
|
|
105
|
+
* `inputs.schema.outcomes`. Keyed on TYPE, never on whether `schema.outcomes` is
|
|
106
|
+
* present: the SDK writes `schema.outcomes` on an action-app task too (its
|
|
107
|
+
* `Action` output is enumerated over them), and that task's `completed` is real.
|
|
108
|
+
*/
|
|
109
|
+
export declare const OUTCOME_HANDLE_NODE_TYPES: ReadonlySet<string>;
|
|
110
|
+
/** The same test on the authored side: no variant (the base node), or quick-form. */
|
|
111
|
+
export declare function hitlDerivesOutcomeHandles(variant: unknown): boolean;
|
|
112
|
+
export declare function hitlExitModel(inputs: HitlInputs | undefined, pinnedVersion?: string): HitlExitModel;
|
|
113
|
+
/** Does this task route PER OUTCOME (primary continues, the rest are routed)? */
|
|
47
114
|
export declare function hitlRoutesPerOutcome(inputs: HitlInputs | undefined, pinnedVersion?: string): boolean;
|
|
115
|
+
type NodeLike = {
|
|
116
|
+
id?: unknown;
|
|
117
|
+
type?: unknown;
|
|
118
|
+
inputs?: any;
|
|
119
|
+
};
|
|
120
|
+
type EdgeLike = {
|
|
121
|
+
sourceNodeId?: unknown;
|
|
122
|
+
sourcePort?: unknown;
|
|
123
|
+
};
|
|
124
|
+
/**
|
|
125
|
+
* Why an edge on `port` cannot leave `node`, or `undefined` when it can.
|
|
126
|
+
*
|
|
127
|
+
* Only the two node types whose handles the product derives from the outcomes
|
|
128
|
+
* are judged, and only once they HAVE outcomes: then `completed` is a handle
|
|
129
|
+
* the product does not draw, and an `outcome-<id>` naming no declared outcome is
|
|
130
|
+
* one `flow validate` refuses ("Edge references undeclared source handle"). A
|
|
131
|
+
* real outcome named "Completed" is `outcome-completed`, which is fine — the
|
|
132
|
+
* bare `completed` never is.
|
|
133
|
+
*/
|
|
134
|
+
export declare function hitlExitViolation(node: NodeLike, port: string): string | undefined;
|
|
135
|
+
/**
|
|
136
|
+
* The post-emit half of the guard: every edge of an emitted scope, whatever
|
|
137
|
+
* path produced it (a tail, a `.stepToList` seed, a parked `.stepToRef`), is held
|
|
138
|
+
* to {@link hitlExitViolation}. Fails closed — a serializer that emitted one of
|
|
139
|
+
* these would ship a flow that validates and then drops outcomes at run time.
|
|
140
|
+
*/
|
|
141
|
+
export declare function assertHitlExitInvariant(nodes: readonly NodeLike[], edges: readonly EdgeLike[]): void;
|
|
142
|
+
export {};
|
|
@@ -14,20 +14,104 @@ export function outcomeSlug(name, index) {
|
|
|
14
14
|
.replace(/^-+|-+$/g, '');
|
|
15
15
|
return slug || `outcome-${index + 1}`;
|
|
16
16
|
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
/**
|
|
18
|
+
* The two human-task node types whose exits the product derives from
|
|
19
|
+
* `inputs.schema.outcomes`. Keyed on TYPE, never on whether `schema.outcomes` is
|
|
20
|
+
* present: the SDK writes `schema.outcomes` on an action-app task too (its
|
|
21
|
+
* `Action` output is enumerated over them), and that task's `completed` is real.
|
|
22
|
+
*/
|
|
23
|
+
export const OUTCOME_HANDLE_NODE_TYPES = new Set([
|
|
24
|
+
'uipath.human-in-the-loop',
|
|
25
|
+
'uipath.human-in-the-loop.quick-form',
|
|
26
|
+
]);
|
|
27
|
+
/** The same test on the authored side: no variant (the base node), or quick-form. */
|
|
28
|
+
export function hitlDerivesOutcomeHandles(variant) {
|
|
29
|
+
return variant === undefined || variant === 'quick-form';
|
|
30
|
+
}
|
|
31
|
+
export function hitlExitModel(inputs, pinnedVersion) {
|
|
32
|
+
const exits = (inputs?.outcomes ?? []).map((o, i) => {
|
|
33
|
+
const name = typeof o === 'string' ? o : o?.name;
|
|
34
|
+
return {
|
|
35
|
+
value: String(name ?? ''),
|
|
36
|
+
port: `outcome-${outcomeSlug(name, i)}`,
|
|
37
|
+
endsProcess: typeof o !== 'string' && o?.action === 'End',
|
|
38
|
+
};
|
|
39
|
+
});
|
|
21
40
|
if (inputs?.exposeError === true || inputs?.outcomePorts === true)
|
|
22
|
-
return
|
|
41
|
+
return { shape: 'per-outcome', exits };
|
|
42
|
+
if (!hitlDerivesOutcomeHandles(inputs?.variant) || exits.length === 0)
|
|
43
|
+
return { shape: 'placeholder' };
|
|
44
|
+
const every = (why) => ({ shape: 'every-outcome', exits, why });
|
|
23
45
|
if (inputs?.outcomePorts === false)
|
|
24
|
-
return false;
|
|
25
|
-
if (inputs?.variant
|
|
26
|
-
return
|
|
46
|
+
return every('outcomePorts: false');
|
|
47
|
+
if (inputs?.variant === 'quick-form')
|
|
48
|
+
return every('quick-form');
|
|
27
49
|
if (pinnedVersion !== undefined)
|
|
28
|
-
return
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
50
|
+
return every('pinned');
|
|
51
|
+
if (exits.length === 1)
|
|
52
|
+
return every('one outcome');
|
|
53
|
+
if (exits.every((e) => e.endsProcess))
|
|
54
|
+
return every('every outcome ends');
|
|
55
|
+
return { shape: 'per-outcome', exits };
|
|
56
|
+
}
|
|
57
|
+
/** Does this task route PER OUTCOME (primary continues, the rest are routed)? */
|
|
58
|
+
export function hitlRoutesPerOutcome(inputs, pinnedVersion) {
|
|
59
|
+
return hitlExitModel(inputs, pinnedVersion).shape === 'per-outcome';
|
|
60
|
+
}
|
|
61
|
+
function outcomeIdsOf(node) {
|
|
62
|
+
let schema = node.inputs?.schema;
|
|
63
|
+
if (typeof schema === 'string') {
|
|
64
|
+
try {
|
|
65
|
+
schema = JSON.parse(schema);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return [];
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const outcomes = Array.isArray(schema?.outcomes) ? schema.outcomes : [];
|
|
72
|
+
return outcomes.map((o) => o?.id).filter((id) => typeof id === 'string' && id !== '');
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Why an edge on `port` cannot leave `node`, or `undefined` when it can.
|
|
76
|
+
*
|
|
77
|
+
* Only the two node types whose handles the product derives from the outcomes
|
|
78
|
+
* are judged, and only once they HAVE outcomes: then `completed` is a handle
|
|
79
|
+
* the product does not draw, and an `outcome-<id>` naming no declared outcome is
|
|
80
|
+
* one `flow validate` refuses ("Edge references undeclared source handle"). A
|
|
81
|
+
* real outcome named "Completed" is `outcome-completed`, which is fine — the
|
|
82
|
+
* bare `completed` never is.
|
|
83
|
+
*/
|
|
84
|
+
export function hitlExitViolation(node, port) {
|
|
85
|
+
if (!OUTCOME_HANDLE_NODE_TYPES.has(String(node.type)))
|
|
86
|
+
return undefined;
|
|
87
|
+
const ids = outcomeIdsOf(node);
|
|
88
|
+
if (ids.length === 0)
|
|
89
|
+
return undefined;
|
|
90
|
+
const declared = ids.map((i) => `outcome-${i}`).join(', ');
|
|
91
|
+
if (port === 'completed') {
|
|
92
|
+
return `human task "${String(node.id)}" has outcomes (${declared}), so the product draws no `
|
|
93
|
+
+ `'completed' handle on it — an edge there is remapped to the primary outcome and every other `
|
|
94
|
+
+ `outcome loses its path.`;
|
|
95
|
+
}
|
|
96
|
+
if (port.startsWith('outcome-') && !ids.includes(port.slice('outcome-'.length))) {
|
|
97
|
+
return `human task "${String(node.id)}" has no outcome for port "${port}" — it declares ${declared}.`;
|
|
98
|
+
}
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* The post-emit half of the guard: every edge of an emitted scope, whatever
|
|
103
|
+
* path produced it (a tail, a `.stepToList` seed, a parked `.stepToRef`), is held
|
|
104
|
+
* to {@link hitlExitViolation}. Fails closed — a serializer that emitted one of
|
|
105
|
+
* these would ship a flow that validates and then drops outcomes at run time.
|
|
106
|
+
*/
|
|
107
|
+
export function assertHitlExitInvariant(nodes, edges) {
|
|
108
|
+
const byId = new Map(nodes.map((n) => [String(n.id), n]));
|
|
109
|
+
for (const e of edges) {
|
|
110
|
+
const node = byId.get(String(e.sourceNodeId));
|
|
111
|
+
if (!node)
|
|
112
|
+
continue;
|
|
113
|
+
const why = hitlExitViolation(node, String(e.sourcePort));
|
|
114
|
+
if (why)
|
|
115
|
+
throw new Error(`serialize: ${why}`);
|
|
116
|
+
}
|
|
33
117
|
}
|
package/dist/core/lookups.js
CHANGED
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
* degenerate classes the generator excludes and why generating for them would
|
|
28
28
|
* make the surface worse rather than more complete.
|
|
29
29
|
*/
|
|
30
|
-
import { prepareCommand } from './cli-spelling.js';
|
|
30
|
+
import { prepareCommand, resolveFlagArg } from './cli-spelling.js';
|
|
31
31
|
/** True when `v` is an unresolved {@link LookupToken}. */
|
|
32
32
|
export function isLookupToken(v) {
|
|
33
33
|
return (typeof v === 'object' && v !== null &&
|
|
@@ -192,7 +192,9 @@ export function unresolvedLookupMessage(token) {
|
|
|
192
192
|
return (`input "${field}" is an unresolved lookup (${by}=${JSON.stringify(value)}). ` +
|
|
193
193
|
`Compile does not make network calls, so the value has to be resolved first:\n` +
|
|
194
194
|
` ${prepareCommand(key, action)} \\\n` +
|
|
195
|
-
|
|
195
|
+
// Quoted only when the shell would mangle it: a multiselect's `users[*]`
|
|
196
|
+
// is a glob and a real name has spaces, and the last line is meant to paste.
|
|
197
|
+
` --resolve ${resolveFlagArg(field, by, String(value))}`);
|
|
196
198
|
}
|
|
197
199
|
// ─── Classification, mirroring `_lookups.py` ─────────────────────────────────
|
|
198
200
|
//
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Step } from '../flow-sdk.js';
|
|
1
2
|
/** One author-named exit: the value an arm matches on, and the port it wires. */
|
|
2
3
|
export interface DeclaredExit {
|
|
3
4
|
/** What `.stepSwitch()`'s arm `value` matches — the outcome or branch NAME. */
|
|
@@ -34,4 +35,11 @@ type SpecLike = {
|
|
|
34
35
|
export declare function declaredExits(spec: SpecLike | undefined, pinnedVersion?: string): DeclaredExit[] | NoExits;
|
|
35
36
|
/** Narrowing helper — `declaredExits` returns one or the other. */
|
|
36
37
|
export declare function hasExits(r: DeclaredExit[] | NoExits): r is DeclaredExit[];
|
|
38
|
+
/**
|
|
39
|
+
* Whether a step list that hands on no tail got there by reaching an End — a
|
|
40
|
+
* `.return()`, on some path — rather than by stopping the whole run
|
|
41
|
+
* (`.terminate`) or leaving a loop (`.break`). Only the first leaves a sibling
|
|
42
|
+
* `.parallel` join waiting: a terminate ends the run, join included.
|
|
43
|
+
*/
|
|
44
|
+
export declare function stopsAtEnd(steps: readonly Step[]): boolean;
|
|
37
45
|
export {};
|
package/dist/core/step-ports.js
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* handling and outcome routing in one list, where an omitted arm would mean two
|
|
21
21
|
* different things.
|
|
22
22
|
*/
|
|
23
|
-
import {
|
|
23
|
+
import { hitlExitModel } from './hitl-routing.js';
|
|
24
24
|
/**
|
|
25
25
|
* The author-declared exits of `spec`, or a `NoExits` explaining why there are
|
|
26
26
|
* none to route.
|
|
@@ -29,32 +29,29 @@ export function declaredExits(spec, pinnedVersion) {
|
|
|
29
29
|
const kind = spec && 'kind' in spec ? spec.kind : undefined;
|
|
30
30
|
if (kind === 'hitl') {
|
|
31
31
|
const inputs = spec?.inputs;
|
|
32
|
-
const
|
|
33
|
-
if (
|
|
34
|
-
|
|
35
|
-
}
|
|
36
|
-
if (!hitlRoutesPerOutcome(inputs, pinnedVersion)) {
|
|
37
|
-
// WHY it kept the single exit decides what the author can do about it, so
|
|
32
|
+
const model = hitlExitModel(inputs, pinnedVersion);
|
|
33
|
+
if (model.shape === 'placeholder') {
|
|
34
|
+
// WHY it has no outcome exits decides what the author can do about it, so
|
|
38
35
|
// the caller gets the specific one rather than "ports are off".
|
|
39
|
-
if (inputs?.
|
|
40
|
-
return {
|
|
41
|
-
reason: 'outcomePorts: false keeps its single `completed` exit',
|
|
42
|
-
suggestion: 'Drop `outcomePorts: false`.',
|
|
43
|
-
};
|
|
36
|
+
if ((inputs?.outcomes ?? []).length === 0) {
|
|
37
|
+
return { reason: 'it declares no outcomes', suggestion: `outcomes: ['Approve', 'Reject']` };
|
|
44
38
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
39
|
+
return {
|
|
40
|
+
reason: `the '${inputs?.variant}' variant reads its outcomes from its app and leaves on its single 'completed' exit`,
|
|
41
|
+
suggestion: `Use .step() and switch on out('<step>', 'Action').`,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
// A quick-form task and a pinned one continue every outcome under `.step()`,
|
|
45
|
+
// but their outcome handles are real, so `.stepSwitch` routes them. The
|
|
46
|
+
// other fan-out reasons leave nothing to route.
|
|
47
|
+
if (model.shape === 'every-outcome' && model.why !== 'quick-form' && model.why !== 'pinned') {
|
|
48
|
+
if (model.why === 'outcomePorts: false') {
|
|
52
49
|
return {
|
|
53
|
-
reason:
|
|
54
|
-
suggestion: 'Drop
|
|
50
|
+
reason: 'outcomePorts: false continues every outcome to the next step',
|
|
51
|
+
suggestion: 'Drop `outcomePorts: false`.',
|
|
55
52
|
};
|
|
56
53
|
}
|
|
57
|
-
if (
|
|
54
|
+
if (model.why === 'one outcome') {
|
|
58
55
|
return {
|
|
59
56
|
reason: 'a single outcome has a single exit, so there is nothing to route',
|
|
60
57
|
suggestion: 'Use .step() — the next step follows it unconditionally.',
|
|
@@ -65,14 +62,11 @@ export function declaredExits(spec, pinnedVersion) {
|
|
|
65
62
|
suggestion: 'Use .step().',
|
|
66
63
|
};
|
|
67
64
|
}
|
|
68
|
-
return
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
...(typeof o !== 'string' && o?.action === 'End' ? { endsProcess: true } : {}),
|
|
74
|
-
};
|
|
75
|
-
});
|
|
65
|
+
return model.exits.map((e) => ({
|
|
66
|
+
value: e.value,
|
|
67
|
+
port: e.port,
|
|
68
|
+
...(e.endsProcess ? { endsProcess: true } : {}),
|
|
69
|
+
}));
|
|
76
70
|
}
|
|
77
71
|
if (kind === 'http') {
|
|
78
72
|
const branches = (spec?.inputs?.branches
|
|
@@ -100,3 +94,16 @@ export function declaredExits(spec, pinnedVersion) {
|
|
|
100
94
|
export function hasExits(r) {
|
|
101
95
|
return Array.isArray(r);
|
|
102
96
|
}
|
|
97
|
+
/**
|
|
98
|
+
* Whether a step list that hands on no tail got there by reaching an End — a
|
|
99
|
+
* `.return()`, on some path — rather than by stopping the whole run
|
|
100
|
+
* (`.terminate`) or leaving a loop (`.break`). Only the first leaves a sibling
|
|
101
|
+
* `.parallel` join waiting: a terminate ends the run, join included.
|
|
102
|
+
*/
|
|
103
|
+
export function stopsAtEnd(steps) {
|
|
104
|
+
const has = (list, kinds) => list.some((st) => kinds.has(st.kind)
|
|
105
|
+
|| (st.kind === 'branch' && (has(st.then, kinds) || has(st.otherwise, kinds)))
|
|
106
|
+
|| ((st.kind === 'switch' || st.kind === 'stepSwitch') && st.cases.some((c) => has(c.body, kinds)))
|
|
107
|
+
|| (st.kind === 'switch' && st.default !== undefined && has(st.default, kinds)));
|
|
108
|
+
return has(steps, new Set(['return'])) && !has(steps, new Set(['terminate', 'break']));
|
|
109
|
+
}
|