@uipath/maestro-builder-sdk 5.2.0 → 5.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/case/decompile.d.ts +44 -0
- package/dist/case/decompile.js +48 -1
- package/dist/case/format-profile.js +11 -0
- package/dist/case/merge-cli.d.ts +3 -0
- package/dist/case/merge-cli.js +44 -0
- package/dist/case/merge.d.ts +18 -0
- package/dist/case/merge.js +112 -0
- package/dist/case/serialize.d.ts +23 -0
- package/dist/case/serialize.js +41 -8
- package/dist/check.js +30 -0
- package/dist/cli/commands/case.js +11 -1
- package/dist/format-profile.js +2 -0
- package/dist/schema-version.d.ts +10 -0
- package/dist/schema-version.js +14 -4
- package/package.json +17 -11
package/dist/case/decompile.d.ts
CHANGED
|
@@ -182,6 +182,50 @@ export interface CaseDecompileResult {
|
|
|
182
182
|
source: string;
|
|
183
183
|
diagnostics: CaseDecompileDiagnostic[];
|
|
184
184
|
}
|
|
185
|
+
type Preservation = {
|
|
186
|
+
topLevel?: Record<string, unknown>;
|
|
187
|
+
metadata?: Record<string, unknown>;
|
|
188
|
+
caseAppConfigFields?: {
|
|
189
|
+
fields?: Record<string, unknown>;
|
|
190
|
+
sections?: Array<{
|
|
191
|
+
id: string;
|
|
192
|
+
fields: Record<string, unknown>;
|
|
193
|
+
}>;
|
|
194
|
+
};
|
|
195
|
+
nodes?: Array<{
|
|
196
|
+
index: number;
|
|
197
|
+
node: Record<string, unknown>;
|
|
198
|
+
}>;
|
|
199
|
+
nodeOrder?: string[];
|
|
200
|
+
edges?: unknown[];
|
|
201
|
+
layout?: unknown;
|
|
202
|
+
omitLayout?: boolean;
|
|
203
|
+
variableFields?: Partial<Record<'inputs' | 'outputs' | 'inputOutputs', Record<string, Record<string, unknown>>>>;
|
|
204
|
+
legacyVariableDefaults?: Partial<Record<'inputs' | 'outputs' | 'inputOutputs', Record<string, unknown>>>;
|
|
205
|
+
variableOrder?: Partial<Record<'inputs' | 'outputs' | 'inputOutputs', string[]>>;
|
|
206
|
+
taskFields?: Array<{
|
|
207
|
+
stage: string;
|
|
208
|
+
lane: number;
|
|
209
|
+
task: string;
|
|
210
|
+
fields: Record<string, unknown>;
|
|
211
|
+
}>;
|
|
212
|
+
triggerDataFields?: Array<Record<string, unknown>>;
|
|
213
|
+
omitStagePositions?: string[];
|
|
214
|
+
omitTaskElementIds?: Array<{
|
|
215
|
+
stage: string;
|
|
216
|
+
lane: number;
|
|
217
|
+
task: string;
|
|
218
|
+
}>;
|
|
219
|
+
identities?: unknown;
|
|
220
|
+
};
|
|
221
|
+
/**
|
|
222
|
+
* Everything the ORIGINAL document owns and the typed builder does not model —
|
|
223
|
+
* ids, layout, platform metadata, annotation nodes, node order, designer
|
|
224
|
+
* variable columns. Exported because `mergeCase` needs the same extraction that
|
|
225
|
+
* `decompile` embeds in a `preserveCaseJson(...)` payload, only read from the
|
|
226
|
+
* original file at merge time instead.
|
|
227
|
+
*/
|
|
228
|
+
export declare function collectPreservation(plan: CasePlan): Preservation | undefined;
|
|
185
229
|
export declare function decompileWithDiagnostics(plan: CasePlan, options?: DecompileOptions): CaseDecompileResult;
|
|
186
230
|
export declare function decompile(plan: CasePlan, options?: DecompileOptions): string;
|
|
187
231
|
export {};
|
package/dist/case/decompile.js
CHANGED
|
@@ -899,6 +899,45 @@ function slaIdentity(sla) {
|
|
|
899
899
|
escalations: (sla.escalationRule ?? []).map((entry) => ({ ...(entry.id ? { id: entry.id } : {}) })),
|
|
900
900
|
};
|
|
901
901
|
}
|
|
902
|
+
/**
|
|
903
|
+
* Drop preservation entries the serializer reproduces on its own, so a payload
|
|
904
|
+
* that says nothing is not emitted at all.
|
|
905
|
+
*
|
|
906
|
+
* Only PROVABLE redundancies, each one a fact about the serializer rather than a
|
|
907
|
+
* guess about the document:
|
|
908
|
+
*
|
|
909
|
+
* - `identities.id` — but ONLY when `slugId` writes that exact id inline as
|
|
910
|
+
* `casePlan("…")`. It does so for a canonical `case-[A-Za-z0-9]{10}`; anything
|
|
911
|
+
* else it SLUGIFIES, and then the identity entry is the only thing restoring
|
|
912
|
+
* the document's real id. `probe--wait-rule` (`case-WaitRuleMigrationProbe`)
|
|
913
|
+
* is exactly that, and it is why this is a comparison rather than a delete.
|
|
914
|
+
* - `identities.triggers` when it is exactly `[{ id: "trigger_1" }]` — the
|
|
915
|
+
* serializer hardcodes `i === 0 ? 'trigger_1'`, so the first trigger already
|
|
916
|
+
* has that id.
|
|
917
|
+
* - empty collections — there is nothing to align.
|
|
918
|
+
* - `nodeOrder` with fewer than two ids — ordering one node is identity.
|
|
919
|
+
*
|
|
920
|
+
* Everything else stays. A designer document orders stages before its trigger,
|
|
921
|
+
* carries ids no label seeding reproduces, and needs every one of them.
|
|
922
|
+
*/
|
|
923
|
+
function prunePreservation(preservation, plan) {
|
|
924
|
+
const identities = preservation.identities;
|
|
925
|
+
if (identities) {
|
|
926
|
+
if (identities.id === slugId(plan))
|
|
927
|
+
delete identities.id;
|
|
928
|
+
if (identities.triggers?.length === 1 && identities.triggers[0]?.id === 'trigger_1')
|
|
929
|
+
delete identities.triggers;
|
|
930
|
+
for (const key of ['triggers', 'stages', 'caseExitRules', 'slaRules'])
|
|
931
|
+
if (Array.isArray(identities[key]) && identities[key].length === 0)
|
|
932
|
+
delete identities[key];
|
|
933
|
+
if (identities.variables && Object.values(identities.variables).every((rows) => !Array.isArray(rows) || rows.length === 0))
|
|
934
|
+
delete identities.variables;
|
|
935
|
+
if (Object.keys(identities).length === 0)
|
|
936
|
+
delete preservation.identities;
|
|
937
|
+
}
|
|
938
|
+
if ((preservation.nodeOrder?.length ?? 0) < 2)
|
|
939
|
+
delete preservation.nodeOrder;
|
|
940
|
+
}
|
|
902
941
|
function identityPreservation(plan) {
|
|
903
942
|
return {
|
|
904
943
|
...(plan.id ? { id: plan.id } : {}),
|
|
@@ -925,7 +964,14 @@ function identityPreservation(plan) {
|
|
|
925
964
|
])),
|
|
926
965
|
};
|
|
927
966
|
}
|
|
928
|
-
|
|
967
|
+
/**
|
|
968
|
+
* Everything the ORIGINAL document owns and the typed builder does not model —
|
|
969
|
+
* ids, layout, platform metadata, annotation nodes, node order, designer
|
|
970
|
+
* variable columns. Exported because `mergeCase` needs the same extraction that
|
|
971
|
+
* `decompile` embeds in a `preserveCaseJson(...)` payload, only read from the
|
|
972
|
+
* original file at merge time instead.
|
|
973
|
+
*/
|
|
974
|
+
export function collectPreservation(plan) {
|
|
929
975
|
const preservation = { identities: identityPreservation(plan) };
|
|
930
976
|
const topKnown = new Set(['id', 'name', 'description', 'version', 'bindings', 'variables', 'metadata', 'nodes', 'edges', 'layout']);
|
|
931
977
|
const topLevel = Object.fromEntries(Object.entries(plan).filter(([name]) => !topKnown.has(name)));
|
|
@@ -1078,6 +1124,7 @@ function collectPreservation(plan) {
|
|
|
1078
1124
|
preservation.legacyVariableDefaults = legacyVariableDefaults;
|
|
1079
1125
|
if (Object.keys(variableOrder).length)
|
|
1080
1126
|
preservation.variableOrder = variableOrder;
|
|
1127
|
+
prunePreservation(preservation, plan);
|
|
1081
1128
|
return Object.keys(preservation).length ? preservation : undefined;
|
|
1082
1129
|
}
|
|
1083
1130
|
function slugId(plan) {
|
|
@@ -60,6 +60,17 @@ export const CASE_SCHEMA_VERSIONS = {
|
|
|
60
60
|
latestKnown: CASE_FORMAT_PROFILE.validationCeiling.formatVersion,
|
|
61
61
|
migration: 'Case documents migrate FORWARD on read, through the platform\'s feature-flag-gated chain; there is no '
|
|
62
62
|
+ '`uip maestro case migrate`, and no path that writes an older grammar.',
|
|
63
|
+
// Case has no `merge`, so it cannot repeat Flow's advice. What it can say is
|
|
64
|
+
// which direction the rewrite moves and whether that is a problem, rather than
|
|
65
|
+
// sending the reader after a command that does not exist — which is what the
|
|
66
|
+
// shared wording did, and what an eval agent then worked around by copying the
|
|
67
|
+
// recompiled file over the original.
|
|
68
|
+
preserveAdvice: (_declared, writes, direction) => direction === 'newer'
|
|
69
|
+
? `Keep the original file: this build's ceiling is ${writes}, so recompiling over it is the one `
|
|
70
|
+
+ 'direction the platform will not undo for you.'
|
|
71
|
+
: `Recompiling rewrites the whole file at ${writes}. That is the direction the platform's on-read `
|
|
72
|
+
+ 'chain takes it anyway, and the round-trip is asserted converter-equivalent over the migration '
|
|
73
|
+
+ 'corpus, so the rewrite is normally safe to keep.',
|
|
63
74
|
writable: [
|
|
64
75
|
{
|
|
65
76
|
version: CASE_FORMAT_PROFILE.writeVersion,
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* case/merge-cli — merge an edited case back into the original `caseplan.json`
|
|
4
|
+
* (bin `case-merge`, surfaced as `uip maestro case merge`).
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* case-merge <original.json> <edited.json> [-o merged.json]
|
|
8
|
+
*
|
|
9
|
+
* `edited.json` is compile(decompile(original)) after your edits. See merge.ts
|
|
10
|
+
* for what each side decides and why the join is by label rather than by id.
|
|
11
|
+
*/
|
|
12
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
13
|
+
import { basename } from 'node:path';
|
|
14
|
+
import { mergeCase } from './merge.js';
|
|
15
|
+
import { runWhenInvokedDirectly } from '../cli-run.js';
|
|
16
|
+
function opt(argv, name) {
|
|
17
|
+
const i = argv.indexOf(name);
|
|
18
|
+
return i >= 0 ? argv[i + 1] : undefined;
|
|
19
|
+
}
|
|
20
|
+
export function run(argv) {
|
|
21
|
+
if (argv.length === 0 || argv[0] === '-h' || argv[0] === '--help') {
|
|
22
|
+
console.error('usage: case-merge <original.json> <edited.json> [-o merged.json]');
|
|
23
|
+
return 2;
|
|
24
|
+
}
|
|
25
|
+
const flags = new Set(['-o']);
|
|
26
|
+
const positionals = argv.filter((a, i) => !flags.has(a) && !flags.has(argv[i - 1]) && !a.startsWith('-'));
|
|
27
|
+
const [originalPath, editedPath] = positionals;
|
|
28
|
+
if (!originalPath || !editedPath) {
|
|
29
|
+
console.error('case-merge: need <original.json> and <edited.json>');
|
|
30
|
+
return 2;
|
|
31
|
+
}
|
|
32
|
+
const original = JSON.parse(readFileSync(originalPath, 'utf8'));
|
|
33
|
+
const edited = JSON.parse(readFileSync(editedPath, 'utf8'));
|
|
34
|
+
const merged = mergeCase(original, edited, {
|
|
35
|
+
warn: (message) => console.error(`case-merge: warning: ${message}`),
|
|
36
|
+
});
|
|
37
|
+
const out = opt(argv, '-o') ?? 'merged.caseplan.json';
|
|
38
|
+
writeFileSync(out, JSON.stringify(merged, null, 2) + '\n');
|
|
39
|
+
const stages = (merged.nodes ?? [])
|
|
40
|
+
.filter((node) => String(node.type ?? '').endsWith('Stage')).length;
|
|
41
|
+
console.log(`case-merge: wrote ${basename(out)} (${stages} stage(s))`);
|
|
42
|
+
return 0;
|
|
43
|
+
}
|
|
44
|
+
runWhenInvokedDirectly(import.meta.url, 'case-merge', run);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** A caseplan-shaped document. Deliberately loose: merge moves fields, it does not type them. */
|
|
2
|
+
export type CaseDocument = Record<string, unknown>;
|
|
3
|
+
export interface CaseMergeOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Called once per finding. Today the only finding is a construct the original
|
|
6
|
+
* carried an identity for that the edited document no longer has — a rename or
|
|
7
|
+
* a delete, which are indistinguishable here and which silently drop the
|
|
8
|
+
* original's ids for that construct.
|
|
9
|
+
*/
|
|
10
|
+
warn?: (message: string) => void;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Merge an edited case plan onto its original.
|
|
14
|
+
*
|
|
15
|
+
* The edited document supplies the graph; the original supplies identity and
|
|
16
|
+
* everything the typed builder does not model. Neither input is mutated.
|
|
17
|
+
*/
|
|
18
|
+
export declare function mergeCase(original: CaseDocument, edited: CaseDocument, options?: CaseMergeOptions): CaseDocument;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* merge — write a decompiled-then-edited case back into the ORIGINAL
|
|
3
|
+
* `caseplan.json`, keeping everything the original owns and the author does not.
|
|
4
|
+
*
|
|
5
|
+
* The brownfield pipeline mirrors Flow's:
|
|
6
|
+
*
|
|
7
|
+
* original.json --decompile--> Case.case.ts --(edit)--> --compile--> edited.json
|
|
8
|
+
* mergeCase(original, edited) --> merged.json
|
|
9
|
+
*
|
|
10
|
+
* ## Why Case cannot merge the way Flow does
|
|
11
|
+
*
|
|
12
|
+
* `mergeFlow` re-attaches by NODE ID, and says so: "node ids are stable by
|
|
13
|
+
* construction (decompile names every step by its node id), which is what lets
|
|
14
|
+
* every re-attach key on id." Flow ids ARE the authored step names, so a
|
|
15
|
+
* round-trip reproduces them for free.
|
|
16
|
+
*
|
|
17
|
+
* Case ids are not. `case/ids.ts` derives `Stage_` + base62(fnv1a64('stage:<label>'))
|
|
18
|
+
* because the schema demands an opaque `prefixedId`, and a document written by
|
|
19
|
+
* the designer carries ids from some other producer entirely — `Stage_Kp3mRx`
|
|
20
|
+
* is not derivable from "Review" by anyone but its author. So an edited case
|
|
21
|
+
* compiled on its own carries GENERATED ids, and id is exactly the key this
|
|
22
|
+
* merge cannot use.
|
|
23
|
+
*
|
|
24
|
+
* What it uses instead is what the preservation channel already uses: stage
|
|
25
|
+
* LABEL, task displayName, lane and position. That is not a second mechanism —
|
|
26
|
+
* it is the same one, read from the original file at merge time rather than
|
|
27
|
+
* from a `preserveCaseJson(...)` payload baked into the source.
|
|
28
|
+
*
|
|
29
|
+
* ## What each side decides
|
|
30
|
+
*
|
|
31
|
+
* edited the graph: stages, tasks, conditions, rules, variables, bindings
|
|
32
|
+
* original identity and chrome: every id, layout, platform-owned metadata,
|
|
33
|
+
* annotation nodes, node order, opaque edges, designer variable columns
|
|
34
|
+
*
|
|
35
|
+
* A construct the edit ADDED has no counterpart in the original, so it keeps the
|
|
36
|
+
* ids the serializer generated. A construct the edit DELETED simply has nothing
|
|
37
|
+
* to receive its preserved identity, and is dropped with it.
|
|
38
|
+
*
|
|
39
|
+
* ## Why this exists next to `preserveCaseJson`
|
|
40
|
+
*
|
|
41
|
+
* Both answer the same question. The payload answers it at compile time from a
|
|
42
|
+
* literal embedded in the authored file; this answers it at merge time from the
|
|
43
|
+
* original document. The payload's failure mode is that it can go stale or be
|
|
44
|
+
* hand-edited — flow-builder-sdk#719 was a reference left pointing at an id the
|
|
45
|
+
* payload had renamed away, and the eval agent that met it reached for
|
|
46
|
+
* `perl -0pi -e` against a minified blob. Identity read fresh from the original
|
|
47
|
+
* cannot drift from it.
|
|
48
|
+
*/
|
|
49
|
+
import { collectPreservation } from './decompile.js';
|
|
50
|
+
import { applyPreservation } from './serialize.js';
|
|
51
|
+
function clone(value) {
|
|
52
|
+
return JSON.parse(JSON.stringify(value));
|
|
53
|
+
}
|
|
54
|
+
/** Stage labels in a document, in order. */
|
|
55
|
+
function stageLabels(document) {
|
|
56
|
+
return (document.nodes ?? [])
|
|
57
|
+
.filter((node) => String(node.type ?? '').endsWith('Stage'))
|
|
58
|
+
.map((node) => String(node.data?.label ?? ''));
|
|
59
|
+
}
|
|
60
|
+
/** Task display names in a document, by stage label. */
|
|
61
|
+
function taskNames(document) {
|
|
62
|
+
const out = new Map();
|
|
63
|
+
for (const node of (document.nodes ?? [])) {
|
|
64
|
+
if (!String(node.type ?? '').endsWith('Stage'))
|
|
65
|
+
continue;
|
|
66
|
+
const names = new Set();
|
|
67
|
+
for (const lane of (node.data?.tasks ?? []))
|
|
68
|
+
for (const task of lane)
|
|
69
|
+
names.add(String(task.displayName ?? ''));
|
|
70
|
+
out.set(String(node.data?.label ?? ''), names);
|
|
71
|
+
}
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Report identities the merge could not place, so a rename does not silently
|
|
76
|
+
* mint a new id for a construct the original already had one for.
|
|
77
|
+
*/
|
|
78
|
+
function warnOrphanedIdentities(preservation, edited, warn) {
|
|
79
|
+
const labels = new Set(stageLabels(edited));
|
|
80
|
+
const tasks = taskNames(edited);
|
|
81
|
+
for (const stage of preservation.identities?.stages ?? []) {
|
|
82
|
+
if (!labels.has(stage.label)) {
|
|
83
|
+
warn(`stage "${stage.label}" is in the original but not in the edited case, so its ids are dropped. `
|
|
84
|
+
+ `If this was a RENAME rather than a delete, the renamed stage gets a new id and every reference `
|
|
85
|
+
+ `to the old one goes with it — rename in the original first, or keep the label.`);
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
const present = tasks.get(stage.label) ?? new Set();
|
|
89
|
+
for (const lane of stage.tasks ?? [])
|
|
90
|
+
for (const task of lane)
|
|
91
|
+
if (!present.has(task.name))
|
|
92
|
+
warn(`task "${task.name}" in stage "${stage.label}" is in the original but not in the edited case, `
|
|
93
|
+
+ `so its ids are dropped. A rename reads the same as a delete here.`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Merge an edited case plan onto its original.
|
|
98
|
+
*
|
|
99
|
+
* The edited document supplies the graph; the original supplies identity and
|
|
100
|
+
* everything the typed builder does not model. Neither input is mutated.
|
|
101
|
+
*/
|
|
102
|
+
export function mergeCase(original, edited, options = {}) {
|
|
103
|
+
const preservation = collectPreservation(original);
|
|
104
|
+
if (!preservation)
|
|
105
|
+
return clone(edited);
|
|
106
|
+
if (options.warn)
|
|
107
|
+
warnOrphanedIdentities(preservation, edited, options.warn);
|
|
108
|
+
// `applyPreservation` is what `compile` runs with the payload a source
|
|
109
|
+
// carries. Handing it the ORIGINAL's payload and the EDITED graph is the whole
|
|
110
|
+
// merge: same operation, different provenance.
|
|
111
|
+
return applyPreservation(clone(edited), preservation, { overwrite: true });
|
|
112
|
+
}
|
package/dist/case/serialize.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Library } from '../core/library.js';
|
|
2
2
|
import { Bindings } from '../core/bindings.js';
|
|
3
|
+
import { type CaseJsonPreservation } from './preserve.js';
|
|
3
4
|
import type { BuiltCase } from './case-sdk.js';
|
|
4
5
|
/** Optional wiring for connector tasks — the library that resolves a connector
|
|
5
6
|
* op, and the bindings that map its symbolic connection/folder to resources. */
|
|
@@ -8,3 +9,25 @@ export interface CaseSerializeOpts {
|
|
|
8
9
|
bindings?: Bindings;
|
|
9
10
|
}
|
|
10
11
|
export declare function serialize(built: BuiltCase, opts?: CaseSerializeOpts): Record<string, unknown>;
|
|
12
|
+
/** How `applyPreservation` treats a field the target already has. */
|
|
13
|
+
export interface ApplyPreservationOptions {
|
|
14
|
+
/**
|
|
15
|
+
* Let a preserved field overwrite one already present on the target.
|
|
16
|
+
*
|
|
17
|
+
* `compile` leaves this off: it applies the payload to a document the
|
|
18
|
+
* serializer just built, so a field already there means the channel is
|
|
19
|
+
* claiming something the typed builder owns — a contract violation worth
|
|
20
|
+
* refusing loudly.
|
|
21
|
+
*
|
|
22
|
+
* `mergeCase` turns it on, because its target has ALREADY been compiled. The
|
|
23
|
+
* designer-owned fields are present by construction there, and the whole point
|
|
24
|
+
* of the merge is that the original wins on exactly those.
|
|
25
|
+
*/
|
|
26
|
+
overwrite?: boolean;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Stamp a preservation payload onto a caseplan-shaped document. Exported for
|
|
30
|
+
* `mergeCase`, which applies the ORIGINAL's payload to the EDITED graph — the
|
|
31
|
+
* same operation compile performs with the payload the source carries.
|
|
32
|
+
*/
|
|
33
|
+
export declare function applyPreservation(result: Record<string, unknown>, preservation: CaseJsonPreservation | undefined, options?: ApplyPreservationOptions): Record<string, unknown>;
|
package/dist/case/serialize.js
CHANGED
|
@@ -294,7 +294,12 @@ function applyAuthorLayout(result, authored) {
|
|
|
294
294
|
result.layout = layout;
|
|
295
295
|
return result;
|
|
296
296
|
}
|
|
297
|
-
|
|
297
|
+
/**
|
|
298
|
+
* Stamp a preservation payload onto a caseplan-shaped document. Exported for
|
|
299
|
+
* `mergeCase`, which applies the ORIGINAL's payload to the EDITED graph — the
|
|
300
|
+
* same operation compile performs with the payload the source carries.
|
|
301
|
+
*/
|
|
302
|
+
export function applyPreservation(result, preservation, options = {}) {
|
|
298
303
|
if (!preservation)
|
|
299
304
|
return result;
|
|
300
305
|
const errors = caseJsonPreservationErrors(preservation);
|
|
@@ -306,7 +311,7 @@ function applyPreservation(result, preservation) {
|
|
|
306
311
|
const caseAppConfig = result.metadata.caseAppConfig;
|
|
307
312
|
if (caseAppConfig && preservation.caseAppConfigFields) {
|
|
308
313
|
for (const [field, value] of Object.entries(preservation.caseAppConfigFields.fields ?? {})) {
|
|
309
|
-
if (Object.prototype.hasOwnProperty.call(caseAppConfig, field))
|
|
314
|
+
if (!options.overwrite && Object.prototype.hasOwnProperty.call(caseAppConfig, field))
|
|
310
315
|
throw new Error(`preserved caseAppConfig field "${field}" collides with serializer-owned output.`);
|
|
311
316
|
caseAppConfig[field] = value;
|
|
312
317
|
}
|
|
@@ -315,7 +320,7 @@ function applyPreservation(result, preservation) {
|
|
|
315
320
|
if (!section)
|
|
316
321
|
continue;
|
|
317
322
|
for (const [field, value] of Object.entries(entry.fields)) {
|
|
318
|
-
if (Object.prototype.hasOwnProperty.call(section, field))
|
|
323
|
+
if (!options.overwrite && Object.prototype.hasOwnProperty.call(section, field))
|
|
319
324
|
throw new Error(`preserved Case App section "${entry.id}" field "${field}" collides with serializer-owned output.`);
|
|
320
325
|
section[field] = value;
|
|
321
326
|
}
|
|
@@ -333,10 +338,12 @@ function applyPreservation(result, preservation) {
|
|
|
333
338
|
const rows = variables[lane] ?? [];
|
|
334
339
|
for (const [name, fields] of Object.entries(byName ?? {})) {
|
|
335
340
|
const row = rows.find((candidate) => candidate.name === name);
|
|
341
|
+
if (!row && options.overwrite)
|
|
342
|
+
continue;
|
|
336
343
|
if (!row)
|
|
337
344
|
throw new Error(`preserved variable "${name}" was not regenerated in variables.${lane}.`);
|
|
338
345
|
for (const [field, value] of Object.entries(fields)) {
|
|
339
|
-
if (Object.prototype.hasOwnProperty.call(row, field))
|
|
346
|
+
if (!options.overwrite && Object.prototype.hasOwnProperty.call(row, field))
|
|
340
347
|
throw new Error(`preserved variable "${name}" field "${field}" collides with serializer-owned output.`);
|
|
341
348
|
row[field] = value;
|
|
342
349
|
}
|
|
@@ -346,11 +353,22 @@ function applyPreservation(result, preservation) {
|
|
|
346
353
|
const rows = variables[lane] ?? [];
|
|
347
354
|
for (const [name, value] of Object.entries(byName ?? {})) {
|
|
348
355
|
const row = rows.find((candidate) => candidate.name === name);
|
|
356
|
+
if (!row && options.overwrite)
|
|
357
|
+
continue;
|
|
349
358
|
if (!row)
|
|
350
359
|
throw new Error(`legacy variable default "${name}" was not regenerated in variables.${lane}.`);
|
|
351
360
|
const encoded = JSON.stringify(value);
|
|
352
|
-
|
|
361
|
+
// The legacy row exists to carry the NON-STRING spelling of a value the
|
|
362
|
+
// serializer now writes as a string, so it is only applicable while the two
|
|
363
|
+
// still agree. Under merge a disagreement means the author edited the
|
|
364
|
+
// default, which makes the preserved spelling stale: keep what the author
|
|
365
|
+
// wrote and drop the legacy row. Under compile it means the payload
|
|
366
|
+
// contradicts the source, which is worth refusing.
|
|
367
|
+
if (row.default !== encoded) {
|
|
368
|
+
if (options.overwrite)
|
|
369
|
+
continue;
|
|
353
370
|
throw new Error(`legacy variable default "${name}" does not match the serializer-owned default.`);
|
|
371
|
+
}
|
|
354
372
|
row.default = value;
|
|
355
373
|
}
|
|
356
374
|
}
|
|
@@ -376,7 +394,8 @@ function applyPreservation(result, preservation) {
|
|
|
376
394
|
// channel quietly overwrite typed output.
|
|
377
395
|
if (task[field] === value)
|
|
378
396
|
continue;
|
|
379
|
-
|
|
397
|
+
if (!options.overwrite)
|
|
398
|
+
throw new Error(`preserved task "${entry.stage}/${entry.task}" field "${field}" collides with serializer-owned output.`);
|
|
380
399
|
}
|
|
381
400
|
task[field] = value;
|
|
382
401
|
}
|
|
@@ -397,6 +416,8 @@ function applyPreservation(result, preservation) {
|
|
|
397
416
|
.filter((node) => node.type === 'case-management:Trigger' || node.type === 'uipath.case.trigger');
|
|
398
417
|
for (const [index, fields] of (preservation.triggerDataFields ?? []).entries()) {
|
|
399
418
|
const trigger = triggerNodes[index];
|
|
419
|
+
if (!trigger && options.overwrite)
|
|
420
|
+
continue;
|
|
400
421
|
if (!trigger)
|
|
401
422
|
throw new Error(`trigger ${index + 1} was not regenerated for preserved data fields.`);
|
|
402
423
|
Object.assign(trigger.data, fields);
|
|
@@ -485,9 +506,21 @@ function applyPreservedIdentities(result, preservation) {
|
|
|
485
506
|
replaced = replaced.replaceAll(current, wanted);
|
|
486
507
|
return replaced;
|
|
487
508
|
};
|
|
509
|
+
// Every field that carries an id GENERATED by this serialize pass has to be
|
|
510
|
+
// rewritten when preservation renames the node that id belongs to. Missing one
|
|
511
|
+
// leaves a reference pointing at an id nothing has any more.
|
|
512
|
+
//
|
|
513
|
+
// `exitToStageId` was missing, and it only bites when the preserved id DIFFERS
|
|
514
|
+
// from the generated one — i.e. on a document this SDK did not write. Round-trip
|
|
515
|
+
// an SDK-authored case and the two agree, so the rename is a no-op and nothing
|
|
516
|
+
// shows. Take a DESIGNER document, whose stage ids are not label-seeded, insert
|
|
517
|
+
// a stage and recompile — the brownfield loop the skill documents — and
|
|
518
|
+
// `exitToStage: "Decision"` resolves to the generated id, the Decision node is
|
|
519
|
+
// then renamed to its designer id, and the exit points at nothing. Silently:
|
|
520
|
+
// `case check` says OK and `uip maestro case validate` reports nothing about it.
|
|
488
521
|
const referenceFields = new Set([
|
|
489
|
-
'elementId', '
|
|
490
|
-
'originalVar', 'var', 'source', 'target',
|
|
522
|
+
'elementId', 'exitToStageId', 'selectedStageId', 'selectedStageIds', 'selectedTasksIds',
|
|
523
|
+
'slaId', 'escalationId', 'originalVar', 'var', 'source', 'target',
|
|
491
524
|
]);
|
|
492
525
|
const preservedOutputSources = [];
|
|
493
526
|
for (const node of nodes) {
|
package/dist/check.js
CHANGED
|
@@ -193,6 +193,36 @@ function checkFlow(built, ancestors, opts = {}) {
|
|
|
193
193
|
});
|
|
194
194
|
}
|
|
195
195
|
}
|
|
196
|
+
// The mirror of HITL_OUTCOME_PORTS_OFF below: `completed` on a human task
|
|
197
|
+
// that HAS opted into per-outcome exits.
|
|
198
|
+
//
|
|
199
|
+
// Turning on `outcomePorts` (or `exposeError`, which implies it) moves the
|
|
200
|
+
// step to the node's 1.1/1.2 definition, and those declare ONE source
|
|
201
|
+
// handle — `outcome-{item.id}`, repeated over `inputs.schema.outcomes`.
|
|
202
|
+
// There is no `completed` handle to leave from, so this edge is refused by
|
|
203
|
+
// the product: `flow validate` answers "Edge references undeclared source
|
|
204
|
+
// handle \"completed\" on node \"<step>\"" (measured). Without this rule
|
|
205
|
+
// `check` was silent, `compile` emitted it, and the author found out from
|
|
206
|
+
// validate — or from a consumer that inspects ports.
|
|
207
|
+
if ((s.kind === 'stepToList' || s.kind === 'stepToRef') && s.port === 'completed') {
|
|
208
|
+
const prevSpec = prevAction?.spec;
|
|
209
|
+
const isHitl = !!prevSpec && 'kind' in prevSpec && prevSpec.kind === 'hitl';
|
|
210
|
+
const routed = isHitl
|
|
211
|
+
&& (prevSpec.inputs?.outcomePorts === true || prevSpec.inputs?.exposeError === true);
|
|
212
|
+
if (routed) {
|
|
213
|
+
const first = prevSpec.inputs.outcomes?.[0];
|
|
214
|
+
const firstId = outcomeSlug(typeof first === 'string' ? first : first?.name, 0);
|
|
215
|
+
diags.push({
|
|
216
|
+
level: 'error', code: 'HITL_COMPLETED_PORT_GONE', step: prevAction?.name,
|
|
217
|
+
message: `Port "completed" does not exist on human task "${prevAction?.name}". `
|
|
218
|
+
+ `${prevSpec.inputs?.exposeError === true ? 'exposeError' : 'outcomePorts'} selects the node's `
|
|
219
|
+
+ `${prevSpec.inputs?.exposeError === true ? '1.2' : '1.1'} definition, whose only source handle is `
|
|
220
|
+
+ `"outcome-<id>" repeated over the outcomes — the 1.0 definition's single 'completed' exit is not `
|
|
221
|
+
+ `part of it. The FIRST outcome ("outcome-${firstId}") is what continues the main path.`,
|
|
222
|
+
suggestion: `route the outcomes (.stepToList('outcome-${firstId}', …)), or drop outcomePorts to keep the 'completed' exit`,
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
}
|
|
196
226
|
if ((s.kind === 'stepToList' || s.kind === 'stepToRef') && s.port.startsWith('outcome-')) {
|
|
197
227
|
const want = s.port.slice('outcome-'.length);
|
|
198
228
|
const prevSpec = prevAction?.spec;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { addValue, inferCheckMode, invoke, loadTool } from '../common.js';
|
|
2
2
|
export function registerCaseCommands(program) {
|
|
3
|
-
const family = program.command('case').description('Build and
|
|
3
|
+
const family = program.command('case').description('Build, check, and merge Case artifacts.');
|
|
4
4
|
family.command('compile')
|
|
5
5
|
.description('Compile authored TypeScript to caseplan.json.')
|
|
6
6
|
.argument('<source>', 'a .case.ts path or bare name')
|
|
@@ -57,4 +57,14 @@ export function registerCaseCommands(program) {
|
|
|
57
57
|
argv.push('--no-pipeline');
|
|
58
58
|
await invoke('case decompile', () => loadTool(import.meta.url, '../../case/decompile-cli.js'), argv);
|
|
59
59
|
});
|
|
60
|
+
family.command('merge')
|
|
61
|
+
.description('Merge an edited case plan with its original caseplan.json.')
|
|
62
|
+
.argument('<original>', 'original caseplan.json path')
|
|
63
|
+
.argument('<edited>', 'edited caseplan.json path')
|
|
64
|
+
.option('-o, --output <file>', 'merged output path')
|
|
65
|
+
.action(async (original, edited, options) => {
|
|
66
|
+
const argv = [original, edited];
|
|
67
|
+
addValue(argv, '-o', options.output);
|
|
68
|
+
await invoke('case merge', () => loadTool(import.meta.url, '../../case/merge-cli.js'), argv);
|
|
69
|
+
});
|
|
60
70
|
}
|
package/dist/format-profile.js
CHANGED
|
@@ -55,6 +55,8 @@ export const FLOW_SCHEMA_VERSIONS = {
|
|
|
55
55
|
latestKnown: '1.10',
|
|
56
56
|
migration: 'Moving a document between versions is `uip maestro flow migrate`\'s job — it owns the hops, '
|
|
57
57
|
+ 'including data repairs this SDK cannot reproduce from the source alone.',
|
|
58
|
+
preserveAdvice: () => 'Until then, merge the recompiled graph back into the ORIGINAL file with `uip maestro flow merge`, '
|
|
59
|
+
+ 'which keeps its own declared version.',
|
|
58
60
|
writable: [
|
|
59
61
|
{
|
|
60
62
|
version: '1.6',
|
package/dist/schema-version.d.ts
CHANGED
|
@@ -91,6 +91,16 @@ export interface SchemaVersionPolicy {
|
|
|
91
91
|
* refusal that names the wrong mechanism is worse than one that names none.
|
|
92
92
|
*/
|
|
93
93
|
migration: string;
|
|
94
|
+
/**
|
|
95
|
+
* What to do when a document declares a version this build cannot write, for
|
|
96
|
+
* the `// TODO:` decompile leaves behind. Family-specific for the same reason
|
|
97
|
+
* {@link migration} is: the honest answer names a command the reader actually
|
|
98
|
+
* has. Flow can be told to merge the recompiled graph back into the original,
|
|
99
|
+
* because `uip maestro flow merge` exists; Case cannot, because it has no
|
|
100
|
+
* merge. Omitted, no instruction is offered at all, which beats naming one
|
|
101
|
+
* that does not exist.
|
|
102
|
+
*/
|
|
103
|
+
preserveAdvice?: (declared: string, writes: string, direction: 'older' | 'newer') => string;
|
|
94
104
|
/**
|
|
95
105
|
* The builder method that declares it, WITHOUT parentheses (`.schemaVersion`)
|
|
96
106
|
* — every message appends its own argument list, so a value carrying `()`
|
package/dist/schema-version.js
CHANGED
|
@@ -138,11 +138,21 @@ export function preserveSchemaVersion(policy, declared) {
|
|
|
138
138
|
if (isWritableSchemaVersion(policy, version)) {
|
|
139
139
|
return { call: `${policy.method}(${JSON.stringify(version)})` };
|
|
140
140
|
}
|
|
141
|
-
const writes = policy.default ?? writableSchemaVersions(policy)[0] ?? 'nothing';
|
|
141
|
+
const writes = String(policy.default ?? writableSchemaVersions(policy)[0] ?? 'nothing');
|
|
142
|
+
// Which way the rewrite moves matters, and the message used to flatten it. A
|
|
143
|
+
// document OLDER than the write version is being carried forward, which the
|
|
144
|
+
// platform would do on read anyway; one NEWER is being pushed BACK below what
|
|
145
|
+
// the platform handed you, and that is where a construct this build does not
|
|
146
|
+
// know can be dropped.
|
|
147
|
+
const direction = isSchemaVersionSyntax(version) && isSchemaVersionSyntax(writes)
|
|
148
|
+
&& compareSchemaVersions(version, writes) > 0 ? 'newer' : 'older';
|
|
149
|
+
const advice = policy.preserveAdvice?.(version, writes, direction);
|
|
142
150
|
return {
|
|
143
151
|
problem: `the file declares ${policy.family} schema version ${JSON.stringify(version)}, which this SDK build cannot write, `
|
|
144
|
-
+ `so this source does not re-declare it and recompiling writes ${JSON.stringify(
|
|
145
|
-
+ `${
|
|
146
|
-
|
|
152
|
+
+ `so this source does not re-declare it and recompiling writes ${JSON.stringify(writes)} instead — `
|
|
153
|
+
+ `${direction === 'newer'
|
|
154
|
+
? 'a DOWNGRADE below the version the platform gave you, which can drop constructs this build does not know'
|
|
155
|
+
: 'carrying the document forward'}. `
|
|
156
|
+
+ `${policy.migration}${advice ? ` ${advice}` : ''}`,
|
|
147
157
|
};
|
|
148
158
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uipath/maestro-builder-sdk",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.3.0",
|
|
4
4
|
"description": "Build UiPath Flow, Case, and BPMN artifacts by writing TypeScript.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://docs.uipath.com/maestro",
|
|
@@ -12,11 +12,6 @@
|
|
|
12
12
|
"orchestration",
|
|
13
13
|
"case-management"
|
|
14
14
|
],
|
|
15
|
-
"repository": {
|
|
16
|
-
"type": "git",
|
|
17
|
-
"url": "git+https://github.com/UiPath/flow-builder-sdk.git",
|
|
18
|
-
"directory": "typescript/sdk"
|
|
19
|
-
},
|
|
20
15
|
"type": "module",
|
|
21
16
|
"main": "./dist/index.js",
|
|
22
17
|
"module": "./dist/index.js",
|
|
@@ -53,6 +48,7 @@
|
|
|
53
48
|
"case-compile": "./dist/case/compile-cli.js",
|
|
54
49
|
"case-check": "./dist/case/check-cli.js",
|
|
55
50
|
"case-decompile": "./dist/case/decompile-cli.js",
|
|
51
|
+
"case-merge": "./dist/case/merge-cli.js",
|
|
56
52
|
"bpmn-compile": "./dist/bpmn/compile-cli.js",
|
|
57
53
|
"bpmn-check": "./dist/bpmn/check-cli.js",
|
|
58
54
|
"bpmn-decompile": "./dist/bpmn/decompile-cli.js",
|
|
@@ -67,6 +63,20 @@
|
|
|
67
63
|
"engines": {
|
|
68
64
|
"node": ">=22.6"
|
|
69
65
|
},
|
|
66
|
+
"scripts": {
|
|
67
|
+
"build": "tsc && tsc -p cli/tsconfig.json && cp src/core-definitions.json dist/core-definitions.json && cp src/ixp-definition.json dist/ixp-definition.json && cp src/bpmn/uipath-moddle.v1.json dist/bpmn/uipath-moddle.v1.json && cp src/bpmn/registry-types.json dist/bpmn/registry-types.json && node scripts/copy-registry-generators.mjs && node scripts/extract-api-model.mjs --check && node scripts/gen-api-reference.mjs --check && node scripts/api-doc-coverage.mjs --check && node scripts/check-api-docs.mjs",
|
|
68
|
+
"check:package": "node scripts/check-package-contents.mjs",
|
|
69
|
+
"generate:api": "node scripts/extract-api-model.mjs && node scripts/gen-api-reference.mjs",
|
|
70
|
+
"docs:check": "node scripts/check-api-docs.mjs",
|
|
71
|
+
"coverage:flow": "node scripts/audit-flow-coverage.mjs",
|
|
72
|
+
"coverage:case": "node scripts/audit-case-coverage.mjs",
|
|
73
|
+
"docs:coverage": "node scripts/api-doc-coverage.mjs",
|
|
74
|
+
"docs:coverage:accept": "node scripts/api-doc-coverage.mjs --write",
|
|
75
|
+
"prepack": "npm run build",
|
|
76
|
+
"check:md-examples": "node scripts/check-md-examples.mjs",
|
|
77
|
+
"sync:definitions": "node scripts/sync-core-definitions.mjs",
|
|
78
|
+
"check:definitions": "node scripts/sync-core-definitions.mjs --check"
|
|
79
|
+
},
|
|
70
80
|
"dependencies": {
|
|
71
81
|
"bpmn-auto-layout": "^1.3.0",
|
|
72
82
|
"bpmn-moddle": "^9.0.4",
|
|
@@ -77,9 +87,5 @@
|
|
|
77
87
|
"@types/node": "^22.7.0",
|
|
78
88
|
"esbuild": "^0.28.1"
|
|
79
89
|
},
|
|
80
|
-
"gitref": "
|
|
81
|
-
"gitHead": "fe867a0e0080d40c98359688494c808eebed1c8b",
|
|
82
|
-
"publishConfig": {
|
|
83
|
-
"registry": "https://registry.npmjs.org/"
|
|
84
|
-
}
|
|
90
|
+
"gitref": "776d66e5ed1bfa43a703c7ff5684e9c5c8c26152"
|
|
85
91
|
}
|