@uipath/maestro-builder-sdk 6.57.0 → 6.58.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.
Files changed (66) hide show
  1. package/dist/case/subscription.d.ts +4 -2
  2. package/dist/case/subscription.js +5 -2
  3. package/dist/core/actions.js +2 -2
  4. package/dist/core/binding-messages.d.ts +2 -1
  5. package/dist/core/binding-messages.js +3 -4
  6. package/dist/core/byoa.d.ts +3 -2
  7. package/dist/core/byoa.js +3 -3
  8. package/dist/core/carried-definitions.d.ts +0 -18
  9. package/dist/core/carried-definitions.js +3 -2
  10. package/dist/core/ceql.d.ts +2 -1
  11. package/dist/core/ceql.js +3 -2
  12. package/dist/core/compile-refusal.d.ts +21 -0
  13. package/dist/core/compile-refusal.js +26 -0
  14. package/dist/core/connector-inputs.js +2 -1
  15. package/dist/core/connector-resolve-error.d.ts +2 -1
  16. package/dist/core/connector-resolve-error.js +3 -4
  17. package/dist/core/connectors.js +2 -1
  18. package/dist/core/event-filters.js +2 -1
  19. package/dist/core/hitl-routing.d.ts +7 -82
  20. package/dist/core/hitl-routing.js +95 -6
  21. package/dist/core/library.js +4 -3
  22. package/dist/core/lookups.js +6 -5
  23. package/dist/core/nodes/registry.js +3 -2
  24. package/dist/core/nodes/versioned.js +3 -2
  25. package/dist/core/process-resource-sort-order.js +1 -1
  26. package/dist/core/read-text.d.ts +0 -28
  27. package/dist/core/read-text.js +3 -2
  28. package/dist/core/refusals.d.ts +465 -0
  29. package/dist/core/refusals.js +504 -0
  30. package/dist/core/script-function.js +2 -1
  31. package/dist/core/typescript-module.js +2 -1
  32. package/dist/edit-scope.d.ts +180 -0
  33. package/dist/edit-scope.js +251 -0
  34. package/dist/flow-expr-check.d.ts +2 -1
  35. package/dist/flow-expr-check.js +4 -2
  36. package/dist/flow-sdk.js +17 -16
  37. package/dist/flow-step-tree.js +1 -1
  38. package/dist/flow-variables.js +3 -2
  39. package/dist/load.js +7 -6
  40. package/dist/merge-cli.js +4 -2
  41. package/dist/merge-compile.d.ts +5 -88
  42. package/dist/merge-compile.js +8 -138
  43. package/dist/merge.d.ts +1 -61
  44. package/dist/merge.js +1 -112
  45. package/dist/nodes/agent/flow/inline-agent.js +7 -6
  46. package/dist/nodes/connector/flow/node-type.js +2 -1
  47. package/dist/nodes/data-fabric/flow/factories.js +2 -1
  48. package/dist/nodes/data-fabric/flow/verbs.js +2 -1
  49. package/dist/nodes/delay/flow/factories.js +2 -1
  50. package/dist/nodes/document/flow/ixp.js +5 -1
  51. package/dist/nodes/hitl/flow/check.js +4 -2
  52. package/dist/nodes/hitl/flow/node-type.js +6 -5
  53. package/dist/nodes/http/flow/factories.js +2 -1
  54. package/dist/nodes/http/flow/node-type.js +6 -5
  55. package/dist/nodes/process/flow/factories.js +2 -1
  56. package/dist/nodes/queue/flow/node-type.js +3 -2
  57. package/dist/nodes/transform/flow/node-type.js +2 -1
  58. package/dist/raw-node.js +3 -2
  59. package/dist/ref-resolve.d.ts +0 -36
  60. package/dist/ref-resolve.js +9 -8
  61. package/dist/schema-version.d.ts +0 -38
  62. package/dist/schema-version.js +3 -2
  63. package/dist/sdk-resolve.js +2 -1
  64. package/dist/serialize.d.ts +5 -2
  65. package/dist/serialize.js +115 -83
  66. package/package.json +2 -2
@@ -0,0 +1,180 @@
1
+ /**
2
+ * edit-scope — which parts of a Flow the author edited, and where a finding
3
+ * sits relative to that edit.
4
+ *
5
+ * A node or subflow of the edited flow that differs from its `baseline`
6
+ * counterpart (compile(decompile(original)), the no-edit round trip) is one
7
+ * the author changed; one that equals it is untouched. `mergeFlow` takes the
8
+ * changed ones from the edited flow and restores the untouched ones from the
9
+ * original; `flow merge --baseline-from` (merge-cli.ts) sorts each draft
10
+ * refusal and `check()` finding by the same rule, so a finding on an untouched
11
+ * node is a warning and one on an edited node is an error.
12
+ *
13
+ * For instance, a canvas Slack node with no connection yet that the author did
14
+ * not touch is `kept`; the same node with a new message text is `refused`.
15
+ *
16
+ * Pure: no file access, so a portable entry (`check.ts`, `merge.ts`) can use
17
+ * it. Reading the original and compiling its baseline is merge-compile.ts's.
18
+ */
19
+ import type { Diagnostic } from './check.js';
20
+ export interface AnyNode {
21
+ id: string;
22
+ type: string;
23
+ typeVersion?: string;
24
+ parentId?: string;
25
+ ui?: unknown;
26
+ display?: unknown;
27
+ inputs?: Record<string, unknown>;
28
+ outputs?: Record<string, unknown>;
29
+ [k: string]: unknown;
30
+ }
31
+ export interface AnyEdge {
32
+ id?: string;
33
+ sourceNodeId: string;
34
+ sourcePort: string;
35
+ targetNodeId: string;
36
+ targetPort: string;
37
+ [k: string]: unknown;
38
+ }
39
+ export interface AnyFlow {
40
+ id?: string;
41
+ name?: string;
42
+ version?: string;
43
+ nodes?: AnyNode[];
44
+ edges?: AnyEdge[];
45
+ definitions?: Array<{
46
+ nodeType?: string;
47
+ [k: string]: unknown;
48
+ }>;
49
+ variables?: unknown;
50
+ subflows?: Record<string, unknown>;
51
+ bindings?: Array<{
52
+ id?: string;
53
+ [k: string]: unknown;
54
+ }>;
55
+ metadata?: unknown;
56
+ solutionId?: unknown;
57
+ projectId?: unknown;
58
+ layout?: unknown;
59
+ runtime?: unknown;
60
+ [k: string]: unknown;
61
+ }
62
+ export declare function byId(nodes: AnyNode[] | undefined): Map<string, AnyNode>;
63
+ /**
64
+ * Whether two nodes are the SAME as far as the author is concerned — same type,
65
+ * same inputs and same output mappings. Layout (`ui`), `display`, `typeVersion`,
66
+ * and the derived parts of `outputs` (type, schema, description) are ignored,
67
+ * because those are exactly the parts decompile does not carry through. An
68
+ * output's `source` is authored, not derived: it is what an End returns, so an
69
+ * edit to `.return({ ... })` changes nothing else on the node. Used only to
70
+ * decide "did the user edit this node?" against the baseline.
71
+ */
72
+ export declare function nodeUnchanged(a: AnyNode | undefined, b: AnyNode | undefined): boolean;
73
+ /**
74
+ * The keys of the subflows `mergeFlow` (merge.ts) takes from `edited` rather than
75
+ * restoring from `original`: every subflow that is new, that the original does
76
+ * not have, or that differs from its `baseline` counterpart in any way the
77
+ * author can edit — a node's inputs, output sources or label, an edge, the
78
+ * scope's `variables` or `layout` (`subflowUnchanged`, #1322). With no
79
+ * baseline, that is every subflow. A kept subflow is the original's whole: its
80
+ * nodes, edges, variables and layout.
81
+ */
82
+ export declare function subflowsTakenFromEdited(original: AnyFlow, edited: AnyFlow, baseline?: AnyFlow): string[];
83
+ export type SubflowScope = {
84
+ nodes?: AnyNode[];
85
+ edges?: AnyEdge[];
86
+ variables?: unknown;
87
+ layout?: unknown;
88
+ };
89
+ /**
90
+ * Whether merge restores `e`'s ORIGINAL node instead of taking `e`: there is a
91
+ * baseline, the original has the node, and `e` equals its baseline counterpart.
92
+ * merge.ts's `mergeFlow` node loop and {@link nodesTakenFromEdited} both ask this.
93
+ */
94
+ export declare function isKept(e: AnyNode, origNodes: Map<string, AnyNode>, baseNodes: Map<string, AnyNode>, baseline: AnyFlow | undefined): boolean;
95
+ /**
96
+ * Why a finding is an error: `edited`, its node is one the author changed or
97
+ * added; `subflow`, it sits in a subflow the edit changed, which merge takes
98
+ * whole; `unplaced`, it names no node and the original's own round trip does
99
+ * not report it, so the edit introduced it.
100
+ */
101
+ export type TakenReason = 'edited' | 'subflow' | 'unplaced';
102
+ /**
103
+ * A finding on a node merge takes from the edited flow, with the reason it
104
+ * takes it: `edited`, the author changed or added the node; `subflow`, the
105
+ * node sits in a subflow the edit changed, which merge takes whole (`subflow`
106
+ * names it).
107
+ */
108
+ export type Refused<T> = T & {
109
+ reason: TakenReason;
110
+ subflow?: string;
111
+ };
112
+ /** A finding on a node merge restores from the original; `subflow` names the untouched subflow it sits in. */
113
+ type Kept<T> = T & {
114
+ subflow?: string;
115
+ };
116
+ /**
117
+ * Sort each finding by what merge does with its node: `refused` when the node
118
+ * comes from the edited flow (reason `edited`: the author changed or added it;
119
+ * reason `subflow`: it sits in a subflow the edit changed, which merge takes
120
+ * whole), `kept` when merge restores the original node — a root node the edit
121
+ * did not touch, or any node of a subflow the edit did not touch (#1322) — and
122
+ * `unplaced` when the finding names no node merge knows (an unnamed End's
123
+ * `.return`, a flow-level finding). The caller sorts those again with
124
+ * {@link splitUnplaced}: one the original's own round trip reports too is
125
+ * printed, one the edit introduced is refused.
126
+ *
127
+ * For instance a Slack node with no connection that the author did not touch
128
+ * is `kept`; the same node with a new message text is `refused` as `edited`.
129
+ */
130
+ export declare function classifyDrafts<T extends {
131
+ nodeId: string;
132
+ subflow?: string;
133
+ }>(original: any, edited: any, baseline: any, findings: readonly T[]): {
134
+ kept: Kept<T>[];
135
+ refused: Refused<T>[];
136
+ unplaced: T[];
137
+ };
138
+ /**
139
+ * A `check()` diagnostic, placed on the node its step emitted (`''` when it
140
+ * names no node); `subflow` is the calling step when the finding is inside a
141
+ * subflow, which then decides how it is classified.
142
+ */
143
+ export type PlacedDiagnostic = Diagnostic & {
144
+ nodeId: string;
145
+ subflow?: string;
146
+ };
147
+ /**
148
+ * Place each `check()` diagnostic on the node its step emitted in `editedFlow`, so
149
+ * {@link classifyDrafts} can sort it: a root step on its node (by label, then
150
+ * id); a step inside a subflow (`check` reports it as `<caller> › <step>`, one
151
+ * segment per level) on the deepest step the path resolves to, with `subflow`
152
+ * set to the deepest subflow on that path, which is what decides; a named End
153
+ * on its End node (#1322). A diagnostic with no step, or one no node answers
154
+ * to, gets nodeId `''`.
155
+ */
156
+ export declare function placeDiagnostics(diagnostics: readonly Diagnostic[], editedFlow: any): PlacedDiagnostic[];
157
+ /** How many times each {@link findingKey} occurs, for {@link splitUnplaced}'s baseline. */
158
+ export declare function findingCounts(findings: ReadonlyArray<{
159
+ code: string;
160
+ message: string;
161
+ }>): Map<string, number>;
162
+ /**
163
+ * Sort findings merge could not place on a node by whether the edit introduced
164
+ * them: `known` when the ORIGINAL's own round trip (`baseline`, the decompiled
165
+ * original compiled and checked, as {@link findingCounts}) reports the same
166
+ * code and message — once per occurrence, so a second copy of a finding the
167
+ * original has once is `introduced` — so the finding predates the edit and
168
+ * only prints; `introduced` otherwise, which is refused like any error on an
169
+ * edited node (#1322). With no baseline findings to compare against (a
170
+ * precompiled `--baseline` file), every one is `introduced`, as merge treated
171
+ * them before.
172
+ */
173
+ export declare function splitUnplaced<T extends {
174
+ code: string;
175
+ message: string;
176
+ }>(unplaced: readonly T[], baseline: ReadonlyMap<string, number> | undefined): {
177
+ known: T[];
178
+ introduced: T[];
179
+ };
180
+ export {};
@@ -0,0 +1,251 @@
1
+ export function byId(nodes) {
2
+ return new Map((nodes ?? []).map((n) => [n.id, n]));
3
+ }
4
+ /**
5
+ * Whether two nodes are the SAME as far as the author is concerned — same type,
6
+ * same inputs and same output mappings. Layout (`ui`), `display`, `typeVersion`,
7
+ * and the derived parts of `outputs` (type, schema, description) are ignored,
8
+ * because those are exactly the parts decompile does not carry through. An
9
+ * output's `source` is authored, not derived: it is what an End returns, so an
10
+ * edit to `.return({ ... })` changes nothing else on the node. Used only to
11
+ * decide "did the user edit this node?" against the baseline.
12
+ */
13
+ export function nodeUnchanged(a, b) {
14
+ if (!a || !b)
15
+ return false;
16
+ if (a.type !== b.type)
17
+ return false;
18
+ return JSON.stringify(a.inputs ?? {}) === JSON.stringify(b.inputs ?? {})
19
+ && JSON.stringify(outputSources(a)) === JSON.stringify(outputSources(b));
20
+ }
21
+ /** Each output's `source` by output name; outputs without one are left out. */
22
+ function outputSources(n) {
23
+ const sources = {};
24
+ for (const [name, output] of Object.entries(n.outputs ?? {})) {
25
+ if (output && typeof output === 'object' && 'source' in output)
26
+ sources[name] = output.source;
27
+ }
28
+ return sources;
29
+ }
30
+ /**
31
+ * The ids of the root nodes `mergeFlow` (merge.ts) takes from `edited` rather than
32
+ * restoring from `original`: every node that is new, or that differs from its
33
+ * `baseline` counterpart. With no baseline, that is every node.
34
+ *
35
+ * For instance, with a baseline equal to `edited` and every node present in
36
+ * `original`, the answer is `[]` — a no-edit merge keeps them all.
37
+ */
38
+ function nodesTakenFromEdited(original, edited, baseline) {
39
+ const origNodes = byId(original.nodes);
40
+ const baseNodes = byId(baseline?.nodes);
41
+ return (edited.nodes ?? []).filter((e) => !isKept(e, origNodes, baseNodes, baseline)).map((e) => e.id);
42
+ }
43
+ /**
44
+ * The keys of the subflows `mergeFlow` (merge.ts) takes from `edited` rather than
45
+ * restoring from `original`: every subflow that is new, that the original does
46
+ * not have, or that differs from its `baseline` counterpart in any way the
47
+ * author can edit — a node's inputs, output sources or label, an edge, the
48
+ * scope's `variables` or `layout` (`subflowUnchanged`, #1322). With no
49
+ * baseline, that is every subflow. A kept subflow is the original's whole: its
50
+ * nodes, edges, variables and layout.
51
+ */
52
+ export function subflowsTakenFromEdited(original, edited, baseline) {
53
+ return Object.keys(edited.subflows ?? {}).filter((key) => {
54
+ const orig = original.subflows?.[key];
55
+ const base = baseline?.subflows?.[key];
56
+ return !baseline || orig === undefined || base === undefined
57
+ || !subflowUnchanged(edited.subflows[key], base);
58
+ });
59
+ }
60
+ /** A node's canvas label, when it has one. */
61
+ const labelOf = (n) => n?.display?.label;
62
+ /**
63
+ * Whether an edited subflow equals its baseline: the same node ids, each node
64
+ * unchanged (`nodeUnchanged`) and under the same label, the same edges by
65
+ * their endpoints and handles (ids are compile's own), and the same
66
+ * `variables` and `layout` byte for byte. The scope's variables carry what the
67
+ * nodes do not — a step's `{ updates }` row, a child `.input()` type, a `.var()`
68
+ * default — and a kept subflow is the original's whole, so any of them changing
69
+ * has to make the subflow a taken one.
70
+ */
71
+ function subflowUnchanged(e, b) {
72
+ const eNodes = byId(e.nodes);
73
+ const bNodes = byId(b.nodes);
74
+ if (eNodes.size !== bNodes.size)
75
+ return false;
76
+ for (const [id, n] of eNodes) {
77
+ const base = bNodes.get(id);
78
+ if (!nodeUnchanged(n, base) || labelOf(n) !== labelOf(base))
79
+ return false;
80
+ }
81
+ if (JSON.stringify(e.variables ?? null) !== JSON.stringify(b.variables ?? null))
82
+ return false;
83
+ // `layout.edges` is keyed by edge id, and ids come from one file-wide
84
+ // counter: a step added before the call shifts every id in the subflow
85
+ // without touching it. So routes are compared by the endpoints the id names
86
+ // (as the edges themselves are), and `layout.nodes` as it is.
87
+ if (JSON.stringify(layoutByEndpoints(e)) !== JSON.stringify(layoutByEndpoints(b)))
88
+ return false;
89
+ const eEdges = (e.edges ?? []).map(edgeKey).sort();
90
+ const bEdges = (b.edges ?? []).map(edgeKey).sort();
91
+ return eEdges.length === bEdges.length && eEdges.every((k, i) => k === bEdges[i]);
92
+ }
93
+ /** `<source>@<port>-><target>@<port>`: an edge's identity apart from its id. */
94
+ const edgeKey = (x) => `${x.sourceNodeId}@${x.sourcePort}->${x.targetNodeId}@${x.targetPort}`;
95
+ /** A scope's layout with its `edges` re-keyed by endpoints, so an id shift does not read as a change. */
96
+ function layoutByEndpoints(sf) {
97
+ const layout = sf.layout;
98
+ if (!layout || typeof layout !== 'object')
99
+ return layout ?? null;
100
+ const byId = new Map((sf.edges ?? []).map((x) => [x.id, edgeKey(x)]));
101
+ const { edges, ...rest } = layout;
102
+ const routes = Object.entries(edges ?? {}).map(([id, route]) => [byId.get(id) ?? id, route]).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
103
+ return { ...rest, edges: Object.fromEntries(routes) };
104
+ }
105
+ /**
106
+ * Whether merge restores `e`'s ORIGINAL node instead of taking `e`: there is a
107
+ * baseline, the original has the node, and `e` equals its baseline counterpart.
108
+ * merge.ts's `mergeFlow` node loop and {@link nodesTakenFromEdited} both ask this.
109
+ */
110
+ export function isKept(e, origNodes, baseNodes, baseline) {
111
+ return !!baseline && origNodes.has(e.id) && nodeUnchanged(e, baseNodes.get(e.id));
112
+ }
113
+ /**
114
+ * Sort each finding by what merge does with its node: `refused` when the node
115
+ * comes from the edited flow (reason `edited`: the author changed or added it;
116
+ * reason `subflow`: it sits in a subflow the edit changed, which merge takes
117
+ * whole), `kept` when merge restores the original node — a root node the edit
118
+ * did not touch, or any node of a subflow the edit did not touch (#1322) — and
119
+ * `unplaced` when the finding names no node merge knows (an unnamed End's
120
+ * `.return`, a flow-level finding). The caller sorts those again with
121
+ * {@link splitUnplaced}: one the original's own round trip reports too is
122
+ * printed, one the edit introduced is refused.
123
+ *
124
+ * For instance a Slack node with no connection that the author did not touch
125
+ * is `kept`; the same node with a new message text is `refused` as `edited`.
126
+ */
127
+ export function classifyDrafts(original, edited, baseline, findings) {
128
+ const fromEdited = new Set(nodesTakenFromEdited(original, edited, baseline));
129
+ const takenSubflows = new Set(subflowsTakenFromEdited(original, edited, baseline));
130
+ const rootIds = new Set((edited?.nodes ?? []).map((n) => n.id));
131
+ /** A subflow's own node ids → the key (the calling step) of the subflow holding them. */
132
+ const subflowOfNode = new Map();
133
+ for (const [key, sf] of Object.entries(edited?.subflows ?? {})) {
134
+ for (const n of sf?.nodes ?? [])
135
+ subflowOfNode.set(n.id, key);
136
+ }
137
+ const kept = [];
138
+ const refused = [];
139
+ const unplaced = [];
140
+ for (const f of findings) {
141
+ // A finding inside a subflow — placed on the calling step by `checkEdited`
142
+ // (`subflow` set), or a draft on one of the subflow's own nodes — is judged
143
+ // by the SUBFLOW: taken whole when the edit changed it, else the original's.
144
+ const subflow = f.subflow ?? subflowOfNode.get(f.nodeId);
145
+ if (subflow !== undefined) {
146
+ if (takenSubflows.has(subflow))
147
+ refused.push({ ...f, reason: 'subflow', subflow });
148
+ else
149
+ kept.push({ ...f, subflow });
150
+ }
151
+ else if (rootIds.has(f.nodeId)) {
152
+ if (fromEdited.has(f.nodeId))
153
+ refused.push({ ...f, reason: 'edited' });
154
+ else
155
+ kept.push(f);
156
+ }
157
+ else {
158
+ unplaced.push(f);
159
+ }
160
+ }
161
+ return { kept, refused, unplaced };
162
+ }
163
+ /**
164
+ * Place each `check()` diagnostic on the node its step emitted in `editedFlow`, so
165
+ * {@link classifyDrafts} can sort it: a root step on its node (by label, then
166
+ * id); a step inside a subflow (`check` reports it as `<caller> › <step>`, one
167
+ * segment per level) on the deepest step the path resolves to, with `subflow`
168
+ * set to the deepest subflow on that path, which is what decides; a named End
169
+ * on its End node (#1322). A diagnostic with no step, or one no node answers
170
+ * to, gets nodeId `''`.
171
+ */
172
+ export function placeDiagnostics(diagnostics, editedFlow) {
173
+ const byStep = new Map();
174
+ for (const n of editedFlow?.nodes ?? []) {
175
+ const label = n?.display?.label;
176
+ if (typeof label === 'string' && !byStep.has(label))
177
+ byStep.set(label, n.id);
178
+ }
179
+ const ids = new Set((editedFlow?.nodes ?? []).map((n) => n.id));
180
+ /** `name` as a node id in `nodes`: by label, then by id; `''` when neither answers. */
181
+ const resolveIn = (nodes, name) => {
182
+ const byLabel = nodes.find((n) => n?.display?.label === name);
183
+ if (byLabel)
184
+ return byLabel.id;
185
+ return nodes.some((n) => n?.id === name) ? name : '';
186
+ };
187
+ // `subflows` is flat, keyed by each caller's id at every depth, and check
188
+ // prefixes a finding at every level (`outer › inner › x`): the subflow that
189
+ // decides is the DEEPEST one on the path, since merge keeps or takes each
190
+ // subflow on its own (an edit inside `inner` leaves `outer` kept).
191
+ const place = (step) => {
192
+ if (step === undefined)
193
+ return { nodeId: '' };
194
+ const segments = step.split(SUBFLOW_STEP_SEPARATOR);
195
+ let scope = editedFlow?.nodes ?? [];
196
+ let subflow;
197
+ let nodeId = '';
198
+ for (const [i, segment] of segments.entries()) {
199
+ const id = i === 0 ? (byStep.get(segment) ?? (ids.has(segment) ? segment : '')) : resolveIn(scope, segment);
200
+ if (id === '')
201
+ break;
202
+ nodeId = id;
203
+ const child = i < segments.length - 1 ? editedFlow?.subflows?.[id] : undefined;
204
+ if (child === undefined)
205
+ break;
206
+ subflow = id;
207
+ scope = child.nodes ?? [];
208
+ }
209
+ return subflow !== undefined ? { nodeId: nodeId === '' ? subflow : nodeId, subflow } : { nodeId };
210
+ };
211
+ return diagnostics.map((d) => ({ ...d, ...place(d.step) }));
212
+ }
213
+ /** The identity of a finding for the introduced-or-not comparison: its code and message. */
214
+ function findingKey(f) {
215
+ return `${f.code}\n${f.message}`;
216
+ }
217
+ /** How many times each {@link findingKey} occurs, for {@link splitUnplaced}'s baseline. */
218
+ export function findingCounts(findings) {
219
+ const counts = new Map();
220
+ for (const f of findings)
221
+ counts.set(findingKey(f), (counts.get(findingKey(f)) ?? 0) + 1);
222
+ return counts;
223
+ }
224
+ /**
225
+ * Sort findings merge could not place on a node by whether the edit introduced
226
+ * them: `known` when the ORIGINAL's own round trip (`baseline`, the decompiled
227
+ * original compiled and checked, as {@link findingCounts}) reports the same
228
+ * code and message — once per occurrence, so a second copy of a finding the
229
+ * original has once is `introduced` — so the finding predates the edit and
230
+ * only prints; `introduced` otherwise, which is refused like any error on an
231
+ * edited node (#1322). With no baseline findings to compare against (a
232
+ * precompiled `--baseline` file), every one is `introduced`, as merge treated
233
+ * them before.
234
+ */
235
+ export function splitUnplaced(unplaced, baseline) {
236
+ const known = [];
237
+ const introduced = [];
238
+ const left = new Map(baseline ?? []);
239
+ for (const f of unplaced) {
240
+ const n = left.get(findingKey(f)) ?? 0;
241
+ if (n > 0) {
242
+ left.set(findingKey(f), n - 1);
243
+ known.push(f);
244
+ }
245
+ else
246
+ introduced.push(f);
247
+ }
248
+ return { known, introduced };
249
+ }
250
+ /** How `check` joins a subflow's calling step and the step inside it in a diagnostic's `step`. */
251
+ const SUBFLOW_STEP_SEPARATOR = ' › ';
@@ -1,5 +1,6 @@
1
1
  import { type ExprDiagnostic } from './core/expr-check.js';
2
2
  import type { BuiltFlow } from './flow-sdk.js';
3
+ import { CompileRefusal } from './core/compile-refusal.js';
3
4
  /** A diagnostic plus the node / field it was found in. */
4
5
  export interface LocatedDiagnostic extends ExprDiagnostic {
5
6
  where: string;
@@ -28,7 +29,7 @@ export declare function checkCookedEscapes(built: BuiltFlow): (LocatedDiagnostic
28
29
  * `message` lists every problem (so a runner that prints `err.message` shows them
29
30
  * all at once); `diagnostics` carries them structured for a caller that wants them.
30
31
  */
31
- export declare class FlowBuildError extends Error {
32
+ export declare class FlowBuildError extends CompileRefusal {
32
33
  readonly diagnostics: LocatedDiagnostic[];
33
34
  constructor(built: BuiltFlow, diagnostics: LocatedDiagnostic[]);
34
35
  }
@@ -30,6 +30,7 @@
30
30
  import { Expr, cookedEscapesOf } from './core/expr.js';
31
31
  import { checkExpression, cookedEscapeDiagnostics } from './core/expr-check.js';
32
32
  import { stepDeclaredVariables } from './flow-step-variables.js';
33
+ import { CompileRefusal } from './core/compile-refusal.js';
33
34
  const NOUN = 'input, variable, or step';
34
35
  /**
35
36
  * Every `$vars` root a reference may legitimately resolve to, across the WHOLE
@@ -269,12 +270,13 @@ export function checkCookedEscapes(built) {
269
270
  * `message` lists every problem (so a runner that prints `err.message` shows them
270
271
  * all at once); `diagnostics` carries them structured for a caller that wants them.
271
272
  */
272
- export class FlowBuildError extends Error {
273
+ export class FlowBuildError extends CompileRefusal {
273
274
  diagnostics;
274
275
  constructor(built, diagnostics) {
275
276
  const label = built.name || built.id || 'flow';
276
277
  const lines = diagnostics.map((d) => ` ✗ [${d.code}] in ${d.where}: ${d.message}${d.suggestion ? ` (did you mean "${d.suggestion}"?)` : ''}`);
277
- super(`Cannot build flow "${label}" — ${diagnostics.length} expression problem(s):\n${lines.join('\n')}`);
278
+ // The first problem's code; `diagnostics` carries every one.
279
+ super(diagnostics[0]?.code ?? 'EXPR_SYNTAX', `Cannot build flow "${label}" — ${diagnostics.length} expression problem(s):\n${lines.join('\n')}`);
278
280
  this.name = 'FlowBuildError';
279
281
  this.diagnostics = diagnostics;
280
282
  }
package/dist/flow-sdk.js CHANGED
@@ -48,6 +48,7 @@ import { checkFlowExpressions, FlowBuildError } from './flow-expr-check.js';
48
48
  import { FLOW_SCHEMA_VERSIONS } from './format-profile.js';
49
49
  import { schemaVersionRefusal } from './schema-version.js';
50
50
  import { indexCarriedDefinitions } from './core/carried-definitions.js';
51
+ import { CompileRefusal } from './core/compile-refusal.js';
51
52
  export { FlowBuildError, checkFlowExpressions } from './flow-expr-check.js';
52
53
  const TYPE_NAMES = Object.values(types);
53
54
  /**
@@ -69,7 +70,7 @@ const TYPE_NAMES = Object.values(types);
69
70
  */
70
71
  function assertShape(method, shape) {
71
72
  if (typeof shape !== 'object' || shape === null || Array.isArray(shape)) {
72
- throw new TypeError(`.${method}() takes ONE object mapping names to types — got ${Array.isArray(shape) ? 'an array' : typeof shape}`
73
+ throw new CompileRefusal('DECLARATION_SHAPE', `.${method}() takes ONE object mapping names to types — got ${Array.isArray(shape) ? 'an array' : typeof shape}`
73
74
  + `${typeof shape === 'string' ? ` (${JSON.stringify(shape)})` : ''}. `
74
75
  + `Write .${method}({ ${typeof shape === 'string' ? shape : 'name'}: types.string }), not `
75
76
  + `.${method}('name', 'string'): a string would be iterated character by character and `
@@ -89,12 +90,12 @@ function varDeclOf(method, name, value) {
89
90
  assertType(method, name, spec.type);
90
91
  if (spec.shared !== undefined) {
91
92
  if (method !== 'input') {
92
- throw new TypeError(`.${method}() declared ${JSON.stringify(name)} with { shared }, which only an input takes: it names `
93
+ throw new CompileRefusal('SHARED_INPUT_NOT_INPUT', `.${method}() declared ${JSON.stringify(name)} with { shared }, which only an input takes: it names `
93
94
  + `the flow variable a root's input is copied into. Declare the variable with .var() and feed it from `
94
95
  + `.input({ <name>: { type, shared: ${JSON.stringify(spec.shared)} } }).`);
95
96
  }
96
97
  if (typeof spec.shared !== 'string' || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(spec.shared)) {
97
- throw new TypeError(`Input ${JSON.stringify(name)}: { shared } must be a variable name (letters, digits, _), got `
98
+ throw new CompileRefusal('SHARED_INPUT_NAME_INVALID', `Input ${JSON.stringify(name)}: { shared } must be a variable name (letters, digits, _), got `
98
99
  + `${JSON.stringify(spec.shared)}.`);
99
100
  }
100
101
  }
@@ -114,7 +115,7 @@ function varDeclOf(method, name, value) {
114
115
  function assertType(method, name, type) {
115
116
  if (typeof type === 'string' && TYPE_NAMES.includes(type))
116
117
  return;
117
- throw new TypeError(`.${method}() declared ${JSON.stringify(name)} with type ${JSON.stringify(type)}, which is not `
118
+ throw new CompileRefusal('DECLARATION_TYPE_UNKNOWN', `.${method}() declared ${JSON.stringify(name)} with type ${JSON.stringify(type)}, which is not `
118
119
  + `a type. Use one of ${TYPE_NAMES.map((t) => `types.${t}`).join(', ')}.`);
119
120
  }
120
121
  // ─── Triggers (what starts the flow) ────────────────────────────────────────
@@ -374,7 +375,7 @@ class StepList {
374
375
  */
375
376
  err(field) {
376
377
  if (!this.errorSource) {
377
- throw new TypeError('err(field) with no step name is only available inside an error handler — the sub-builder '
378
+ throw new CompileRefusal('ERROR_READ_OUTSIDE_HANDLER', 'err(field) with no step name is only available inside an error handler — the sub-builder '
378
379
  + '.onError((h) => …) / .stepToList(\'error\', (h) => …) passes. Elsewhere, name the step: '
379
380
  + "err('<step>', 'message').");
380
381
  }
@@ -438,7 +439,7 @@ class StepList {
438
439
  */
439
440
  stepSwitch(name, spec, cases, options) {
440
441
  if (!Array.isArray(cases) || cases.length === 0) {
441
- throw new TypeError(`.stepSwitch('${name}'): needs at least one arm. With nothing to route, use .step().`);
442
+ throw new CompileRefusal('STEP_SWITCH_NO_ARMS', `.stepSwitch('${name}'): needs at least one arm. With nothing to route, use .step().`);
442
443
  }
443
444
  const arms = cases.map((c) => {
444
445
  const arm = new ArmBuilder();
@@ -448,7 +449,7 @@ class StepList {
448
449
  // is already named by the outcome or branch it leaves. Refusing beats
449
450
  // accepting it and dropping it on the floor.
450
451
  if (arm.armLabel !== undefined) {
451
- throw new TypeError(`.stepSwitch('${name}') arm '${c.value}': .label() has nowhere to go — a port edge carries no `
452
+ throw new CompileRefusal('STEP_SWITCH_ARM_LABEL', `.stepSwitch('${name}') arm '${c.value}': .label() has nowhere to go — a port edge carries no `
452
453
  + `label, and the exit is named by "${c.value}" already. Use options.label to rename the NODE.`);
453
454
  }
454
455
  return { value: String(c.value), body: arm.steps };
@@ -551,14 +552,14 @@ class StepList {
551
552
  // version the container emits (#1010).
552
553
  const containerPort = port === 'error' && (source?.kind === 'loop' || source?.kind === 'doWhile');
553
554
  if (!source || (source.kind !== 'action' && !containerPort)) {
554
- throw new TypeError(`${called} leaves a port of the step BEFORE it, and ${source ? `the previous step is a ${source.kind}` : 'there is no previous step'}. `
555
+ throw new CompileRefusal('PORT_NO_SOURCE_STEP', `${called} leaves a port of the step BEFORE it, and ${source ? `the previous step is a ${source.kind}` : 'there is no previous step'}. `
555
556
  + `Write .step('fetch', http({…}))${called}`
556
557
  + ' — a branch, parallel, terminate or return has no port to leave from; a .loop() or .doWhile() has one '
557
558
  + "only for 'error'.");
558
559
  }
559
560
  const taken = attached.some((s) => (s.kind === 'stepToList' || s.kind === 'stepToRef') && s.port === port);
560
561
  if (taken) {
561
- throw new TypeError(`${called} is already set on step "${source.name}"'s "${port}" port. A port takes exactly ONE outgoing edge `
562
+ throw new CompileRefusal('PORT_ALREADY_ROUTED', `${called} is already set on step "${source.name}"'s "${port}" port. A port takes exactly ONE outgoing edge `
562
563
  + `(the definition says so: maxConnections: 1), so put everything that path needs in one place.`);
563
564
  }
564
565
  return source;
@@ -611,7 +612,7 @@ class StepList {
611
612
  h.errorSource = prev.name;
612
613
  bodyFn?.(h);
613
614
  if (h.steps.length === 0) {
614
- throw new TypeError(`${called} on step "${prev.name}" has an empty ${noun}. An edge to nothing is worse than none — `
615
+ throw new CompileRefusal('PORT_EMPTY_PATH', `${called} on step "${prev.name}" has an empty ${noun}. An edge to nothing is worse than none — `
615
616
  + `the path still leaves the main flow. Return a value, terminate the run, or do the compensating work.`);
616
617
  }
617
618
  this.steps.push({ kind: 'stepToList', port, body: h.steps });
@@ -637,7 +638,7 @@ class StepList {
637
638
  */
638
639
  branch(name, cond, thenFn, elseFn, options) {
639
640
  if (options?.updates) {
640
- throw new TypeError('.branch(): { updates } is not supported here — the platform strips variable updates from '
641
+ throw new CompileRefusal('UPDATES_NOT_SUPPORTED', '.branch(): { updates } is not supported here — the platform strips variable updates from '
641
642
  + 'decision/switch/merge nodes on save. Attach them to a step before or after instead.');
642
643
  }
643
644
  const t = new ArmBuilder();
@@ -707,7 +708,7 @@ class StepList {
707
708
  */
708
709
  switch(name, on, cases, defaultFn, options) {
709
710
  if (options?.updates) {
710
- throw new TypeError('.switch(): { updates } is not supported here — the platform strips variable updates from '
711
+ throw new CompileRefusal('UPDATES_NOT_SUPPORTED', '.switch(): { updates } is not supported here — the platform strips variable updates from '
711
712
  + 'decision/switch/merge nodes on save. Attach them to a step before or after instead.');
712
713
  }
713
714
  const arms = (cases ?? []).map((c) => {
@@ -789,7 +790,7 @@ class StepList {
789
790
  */
790
791
  parallel(name, arms, options) {
791
792
  if (options?.updates) {
792
- throw new TypeError('.parallel(): { updates } is not supported here — the platform strips variable updates from '
793
+ throw new CompileRefusal('UPDATES_NOT_SUPPORTED', '.parallel(): { updates } is not supported here — the platform strips variable updates from '
793
794
  + 'decision/switch/merge nodes on save. Attach them to a step before or after instead.');
794
795
  }
795
796
  const built = (arms ?? []).map((armFn) => {
@@ -924,7 +925,7 @@ class StepList {
924
925
  const port = maybeTarget === undefined ? 'output' : portOrTarget;
925
926
  const target = maybeTarget === undefined ? portOrTarget : maybeTarget;
926
927
  if (typeof target !== 'string' || target.length === 0) {
927
- throw new TypeError(`.stepToRef() takes the NAME of the step to continue at — .stepToRef('priceIt') — and got ${JSON.stringify(target)}. `
928
+ throw new CompileRefusal('REF_TARGET_NOT_NAME', `.stepToRef() takes the NAME of the step to continue at — .stepToRef('priceIt') — and got ${JSON.stringify(target)}. `
928
929
  + `It is the same name you passed to .step() (or a .return() End's \`name\`), so the edge can be wired after the whole flow is built.`);
929
930
  }
930
931
  // A non-default port leaves the PRECEDING step, so it is guarded the same way
@@ -1103,7 +1104,7 @@ class FlowBuilder extends StepList {
1103
1104
  */
1104
1105
  trigger(spec, options) {
1105
1106
  if (options?.updates) {
1106
- throw new TypeError('.trigger(): { updates } is not supported — a trigger has not run anything to assign from.');
1107
+ throw new CompileRefusal('UPDATES_NOT_SUPPORTED', '.trigger(): { updates } is not supported — a trigger has not run anything to assign from.');
1107
1108
  }
1108
1109
  this._trigger = spec;
1109
1110
  if (options)
@@ -1263,7 +1264,7 @@ class FlowBuilder extends StepList {
1263
1264
  schemaVersion(version) {
1264
1265
  const refusal = schemaVersionRefusal(FLOW_SCHEMA_VERSIONS, version);
1265
1266
  if (refusal)
1266
- throw new Error(refusal);
1267
+ throw new CompileRefusal('SCHEMA_VERSION_UNSUPPORTED', refusal);
1267
1268
  this._schemaVersion = version;
1268
1269
  return this;
1269
1270
  }
@@ -44,7 +44,7 @@ export function childStepLists(step) {
44
44
  return [];
45
45
  default: {
46
46
  const unhandled = step;
47
- throw new Error(`childStepLists: unhandled step kind ${JSON.stringify(unhandled.kind)}`);
47
+ throw new Error(`internal SDK error: childStepLists: unhandled step kind ${JSON.stringify(unhandled.kind)}`);
48
48
  }
49
49
  }
50
50
  }
@@ -1,4 +1,5 @@
1
1
  import { globalNameCollisionMessage, globalNameCollisions } from './core/global-names.js';
2
+ import { CompileRefusal } from './core/compile-refusal.js';
2
3
  /** The keys of `variables` that hold declarations rather than name one (the map spelling). */
3
4
  export const CONTAINER_KEYS = new Set(['globals', 'nodes', 'variableUpdates']);
4
5
  const DIRECTIONS = new Set(['in', 'out', 'inout']);
@@ -8,7 +9,7 @@ export function readFlowGlobals(variables, strict = false) {
8
9
  return [];
9
10
  if (typeof variables !== 'object' || Array.isArray(variables)) {
10
11
  if (strict)
11
- throw new Error('decompile: variables must be an object.');
12
+ throw new CompileRefusal('DECOMPILE_VARIABLES_SHAPE', 'decompile: variables must be an object.');
12
13
  return [];
13
14
  }
14
15
  const source = variables;
@@ -37,7 +38,7 @@ export function readFlowGlobals(variables, strict = false) {
37
38
  }
38
39
  }
39
40
  if (strict && unknown.length > 0) {
40
- throw new Error(`decompile: unrecognized variables key${unknown.length === 1 ? '' : 's'}: `
41
+ throw new CompileRefusal('DECOMPILE_VARIABLES_SHAPE', `decompile: unrecognized variables key${unknown.length === 1 ? '' : 's'}: `
41
42
  + `${unknown.map((key) => JSON.stringify(key)).join(', ')}.`);
42
43
  }
43
44
  return globals;