@uipath/maestro-builder-sdk 6.66.2 → 6.66.4

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.
@@ -4,7 +4,7 @@ import { CASE_FORMAT_PROFILE, CASE_SCHEMA_VERSIONS } from './format-profile.js';
4
4
  import { EXTERNAL_TASK_CONTEXT, externalDescriptorRefusal, externalHybridRefusal, externalServiceType, isExternalExecutionMode } from './external.js';
5
5
  import { preserveSchemaVersion } from '../schema-version.js';
6
6
  import { ownEntry } from '../core/own-entry.js';
7
- import { isUnrestorableSlotId } from './preserve.js';
7
+ import { isUnrestorableSlotId, taskFingerprint } from './preserve.js';
8
8
  import { isInSolutionKey, SOLUTION_FOLDER } from './resource-identity.js';
9
9
  import { offlineBindingLabel } from './ids.js';
10
10
  const TASK_KIND_METHOD = {
@@ -1172,7 +1172,14 @@ function refuseUnsupportedAuthoring(plan) {
1172
1172
  function conditionIdentity(condition) {
1173
1173
  return {
1174
1174
  ...(condition.id ? { id: condition.id } : {}),
1175
- rules: (condition.rules ?? []).map((group) => group.map((rule) => ({ ...(rule.id ? { id: rule.id } : {}) }))),
1175
+ rules: (condition.rules ?? []).map((group) => group.map((rule) => ({
1176
+ ...(rule.id ? { id: rule.id } : {}),
1177
+ // A wait-for-connector rule's rows take their ids from the plan-wide pool,
1178
+ // so deleting an earlier output would renumber them.
1179
+ ...(rule.uipath?.outputs?.length
1180
+ ? { outputs: rule.uipath.outputs.map((row) => ({ name: row.name ?? '', ...(row.id ? { id: row.id } : {}) })) }
1181
+ : {}),
1182
+ }))),
1176
1183
  };
1177
1184
  }
1178
1185
  function slaIdentity(sla) {
@@ -1231,20 +1238,25 @@ function identityPreservation(plan) {
1231
1238
  return {
1232
1239
  ...(plan.id ? { id: plan.id } : {}),
1233
1240
  triggers: (plan.nodes ?? []).filter(isTrigger).map((node) => ({ ...(node.id ? { id: node.id } : {}) })),
1234
- stages: (plan.nodes ?? []).filter(isStage).map((stage) => ({
1235
- label: stage.data?.label ?? '',
1236
- ...(stage.id ? { id: stage.id } : {}),
1237
- tasks: (stage.data?.tasks ?? []).map((lane) => lane.map((task) => ({
1238
- name: task.displayName ?? '',
1239
- ...(task.id ? { id: task.id } : {}),
1240
- entryConditions: (task.entryConditions ?? []).map(conditionIdentity),
1241
- inputs: (task.data?.inputs ?? []).map((row) => ({ name: row.name ?? '', ...(row.id ? { id: row.id } : {}) })),
1242
- outputs: (task.data?.outputs ?? []).map((row) => ({ name: row.name ?? '', ...(row.id ? { id: row.id } : {}) })),
1243
- }))),
1244
- entryConditions: (stage.data?.entryConditions ?? []).map(conditionIdentity),
1245
- exitConditions: (stage.data?.exitConditions ?? []).map(conditionIdentity),
1246
- slaRules: (stage.data?.slaRules ?? []).map(slaIdentity),
1247
- })),
1241
+ stages: (plan.nodes ?? []).filter(isStage).map((stage) => {
1242
+ const shape = (task) => JSON.stringify([task.data?.inputs, task.data?.outputs].map((rows) => (rows ?? []).map((row) => row.name ?? '')));
1243
+ const shapes = (stage.data?.tasks ?? []).flat().map(shape);
1244
+ return {
1245
+ label: stage.data?.label ?? '',
1246
+ ...(stage.id ? { id: stage.id } : {}),
1247
+ tasks: (stage.data?.tasks ?? []).map((lane) => lane.map((task) => ({
1248
+ name: task.displayName ?? '',
1249
+ ...(task.id ? { id: task.id } : {}),
1250
+ entryConditions: (task.entryConditions ?? []).map(conditionIdentity),
1251
+ inputs: (task.data?.inputs ?? []).map((row) => ({ name: row.name ?? '', ...(row.id ? { id: row.id } : {}) })),
1252
+ outputs: (task.data?.outputs ?? []).map((row) => ({ name: row.name ?? '', ...(row.id ? { id: row.id } : {}) })),
1253
+ ...(shapes.filter((other) => other === shape(task)).length > 1 ? { fingerprint: taskFingerprint(task) } : {}),
1254
+ }))),
1255
+ entryConditions: (stage.data?.entryConditions ?? []).map(conditionIdentity),
1256
+ exitConditions: (stage.data?.exitConditions ?? []).map(conditionIdentity),
1257
+ slaRules: (stage.data?.slaRules ?? []).map(slaIdentity),
1258
+ };
1259
+ }),
1248
1260
  caseExitRules: (plan.metadata?.caseExitRules ?? []).map(conditionIdentity),
1249
1261
  slaRules: (plan.metadata?.slaRules ?? []).map(slaIdentity),
1250
1262
  variables: Object.fromEntries(['inputs', 'outputs', 'inputOutputs'].map((lane) => [
@@ -1374,7 +1386,6 @@ export function collectPreservation(plan) {
1374
1386
  preservation.omitTaskElementIds = omitTaskElementIds;
1375
1387
  if (omitStagePositions.length)
1376
1388
  preservation.omitStagePositions = omitStagePositions;
1377
- const produced = producedVariableNames(plan);
1378
1389
  const variableKnown = new Set(['id', 'name', 'type', 'default', 'body', '_jsonSchema', 'elementId', 'var', 'custom']);
1379
1390
  const variableFields = {};
1380
1391
  const legacyVariableDefaults = {};
@@ -1390,7 +1401,9 @@ export function collectPreservation(plan) {
1390
1401
  for (const field of Object.keys(fields))
1391
1402
  if (!['subType', 'canonicalId', 'camelizedId', 'variableType', 'internal'].includes(field))
1392
1403
  throw new Error(`variable "${row.name}" has unsupported field "${field}", which is not allowed in the collision-refusing preservation channel.`);
1393
- if (lane === 'inputOutputs' && row.custom && !produced.has(row.name))
1404
+ // Carried even while an output writes the variable, so deleting its last
1405
+ // writer keeps the flag; `applyPreservation` reads an equal value as agreement.
1406
+ if (lane === 'inputOutputs' && row.custom)
1394
1407
  fields.custom = true;
1395
1408
  const formalOutput = (plan.variables?.outputs ?? []).find((candidate) => candidate.name === row.name);
1396
1409
  if (lane === 'inputOutputs' && formalOutput && row.default !== undefined && formalOutput.default !== row.default)
@@ -3,9 +3,9 @@ export type CaseDocument = Record<string, unknown>;
3
3
  export interface CaseMergeOptions {
4
4
  /**
5
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.
6
+ * carried an identity for that the edited document no longer has — a delete, or
7
+ * a rename `followRenames` could not pair — which silently drops the original's
8
+ * ids for that construct.
9
9
  */
10
10
  warn?: (message: string) => void;
11
11
  }
@@ -22,9 +22,11 @@
22
22
  * merge cannot use.
23
23
  *
24
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.
25
+ * LABEL, task displayName, lane and position, with `followRenames` first
26
+ * re-pointing a stage or task the edit renamed or moved (see its doc) so it keeps
27
+ * its ids. That is not a second mechanism — it is the same one, read from the
28
+ * original file at merge time rather than from a `preserveCaseJson(...)` payload
29
+ * baked into the source.
28
30
  *
29
31
  * ## What each side decides
30
32
  *
@@ -47,7 +49,7 @@
47
49
  * cannot drift from it.
48
50
  */
49
51
  import { collectPreservation } from './decompile.js';
50
- import { applyPreservation } from './serialize.js';
52
+ import { applyPreservation, followRenames } from './serialize.js';
51
53
  import { jsonClone } from '../core/json-clone.js';
52
54
  /** Stage labels in a document, in order. */
53
55
  function stageLabels(document) {
@@ -79,8 +81,9 @@ function warnOrphanedIdentities(preservation, edited, warn) {
79
81
  for (const stage of preservation.identities?.stages ?? []) {
80
82
  if (!labels.has(stage.label)) {
81
83
  warn(`stage "${stage.label}" is in the original but not in the edited case, so its ids are dropped. `
82
- + `If this was a RENAME rather than a delete, the renamed stage gets a new id and every reference `
83
- + `to the old one goes with it — rename in the original first, or keep the label.`);
84
+ + `A renamed stage keeps its ids while it still holds one of its tasks by name, or when the edit `
85
+ + `leaves as many new stages as it removed. Neither holds here, so if this was a RENAME, the renamed `
86
+ + `stage gets a new id and every reference to the old one goes with it: keep a task's name, or keep the label.`);
84
87
  continue;
85
88
  }
86
89
  const present = tasks.get(stage.label) ?? new Set();
@@ -88,7 +91,8 @@ function warnOrphanedIdentities(preservation, edited, warn) {
88
91
  for (const task of lane)
89
92
  if (!present.has(task.name))
90
93
  warn(`task "${task.name}" in stage "${stage.label}" is in the original but not in the edited case, `
91
- + `so its ids are dropped. A rename reads the same as a delete here.`);
94
+ + `so its ids are dropped. A renamed or moved task keeps its ids only when its input and output `
95
+ + `rows keep their names; with those changed too, a rename reads the same as a delete.`);
92
96
  }
93
97
  }
94
98
  /**
@@ -102,7 +106,7 @@ export function mergeCase(original, edited, options = {}) {
102
106
  if (!preservation)
103
107
  return jsonClone(edited);
104
108
  if (options.warn)
105
- warnOrphanedIdentities(preservation, edited, options.warn);
109
+ warnOrphanedIdentities(followRenames(edited, preservation), edited, options.warn);
106
110
  // `applyPreservation` is what `compile` runs with the payload a source
107
111
  // carries. Handing it the ORIGINAL's payload and the EDITED graph is the whole
108
112
  // merge: same operation, different provenance.
@@ -33,6 +33,10 @@ interface ConditionIdentity {
33
33
  id?: string;
34
34
  rules?: Array<Array<{
35
35
  id?: string;
36
+ outputs?: Array<{
37
+ name: string;
38
+ id?: string;
39
+ }>;
36
40
  }>>;
37
41
  }
38
42
  interface SlaIdentity {
@@ -56,11 +60,19 @@ interface StageIdentity {
56
60
  name: string;
57
61
  id?: string;
58
62
  }>;
63
+ /** `taskFingerprint`, for a task a sibling shares its row names with. */
64
+ fingerprint?: string;
59
65
  }>>;
60
66
  entryConditions?: ConditionIdentity[];
61
67
  exitConditions?: ConditionIdentity[];
62
68
  slaRules?: SlaIdentity[];
63
69
  }
70
+ /**
71
+ * A task less its rows, its ids and every display name (a condition's defaults
72
+ * to its task's): what renaming it leaves alone, so a rename beside a deleted
73
+ * sibling of the same shape can be told from the delete.
74
+ */
75
+ export declare function taskFingerprint(task: object): string;
64
76
  interface CaseIdentityPreservation {
65
77
  id?: string;
66
78
  triggers?: Array<{
@@ -1,3 +1,4 @@
1
+ import { stableId } from '../core/stable-id.js';
1
2
  /**
2
3
  * A preserved `inputs[]` / `outputs[]` slot id copied verbatim from its name is
3
4
  * the STRICT_FORMAL_ARG_ID defect, not an identity to keep; the minted id stands.
@@ -5,6 +6,26 @@
5
6
  export function isUnrestorableSlotId(lane, row) {
6
7
  return lane !== 'inputOutputs' && typeof row.id === 'string' && row.id === row.name;
7
8
  }
9
+ /**
10
+ * A task less its rows, its ids and every display name (a condition's defaults
11
+ * to its task's): what renaming it leaves alone, so a rename beside a deleted
12
+ * sibling of the same shape can be told from the delete.
13
+ */
14
+ export function taskFingerprint(task) {
15
+ const canonical = (value) => {
16
+ if (Array.isArray(value))
17
+ return value.map(canonical);
18
+ if (!value || typeof value !== 'object')
19
+ return value;
20
+ return Object.fromEntries(Object.entries(value)
21
+ .filter(([key]) => key !== 'id' && key !== 'elementId' && key !== 'displayName')
22
+ .sort(([a], [b]) => (a < b ? -1 : 1))
23
+ .map(([key, inner]) => [key, canonical(inner)]));
24
+ };
25
+ const { data, ...rest } = task;
26
+ const { inputs: _inputs, outputs: _outputs, ...fields } = data ?? {};
27
+ return stableId(JSON.stringify(canonical({ ...rest, data: fields })));
28
+ }
8
29
  const preserved = new WeakMap();
9
30
  /** Attach decompiler-owned, non-authoring wire data to a built case. */
10
31
  export function preserveCaseJson(built, payload) {
@@ -31,3 +31,19 @@ export interface ApplyPreservationOptions {
31
31
  * same operation compile performs with the payload the source carries.
32
32
  */
33
33
  export declare function applyPreservation(result: Record<string, unknown>, preservation: CaseJsonPreservation | undefined, options?: ApplyPreservationOptions): Record<string, unknown>;
34
+ /**
35
+ * The payload with each stage label and task name an edit renamed rewritten to
36
+ * the one compiled, so a renamed stage or task keeps its ids and wire fields
37
+ * instead of reading as a delete plus an add. Stages left over on both sides
38
+ * pair by the most task names shared, then the fewest not shared, then in order
39
+ * when as many are left on each side; a task left over pairs with one in the
40
+ * same stage whose input and output rows have the same names, and a task gone
41
+ * from its stage that another stage now holds under the same name and rows
42
+ * moved there.
43
+ *
44
+ * `wanted` is the payload's side (identities read from the original plan),
45
+ * `compiled` this compile's. Per stage, `missing` tasks are the payload's that
46
+ * the compiled stage no longer names, `extra` tasks the compiled stage's that
47
+ * the payload does not.
48
+ */
49
+ export declare function followRenames(result: Record<string, unknown>, preservation: CaseJsonPreservation): CaseJsonPreservation;
@@ -9,7 +9,7 @@
9
9
  * name) are resolved here to the generated ids.
10
10
  */
11
11
  import { IdGen, offlineBindingSeed } from './ids.js';
12
- import { isUnrestorableSlotId } from './preserve.js';
12
+ import { isUnrestorableSlotId, taskFingerprint } from './preserve.js';
13
13
  import { v31TaskSets } from './task-sets.js';
14
14
  import { camelizeName, defaultActionOutputVar } from './action-output-var.js';
15
15
  import { buildConnectorInputs, nestConnectorBody, replayInputsOf, transportHttpMethod } from '../core/connector-inputs.js';
@@ -23,6 +23,7 @@ import { EXTERNAL_TASK_DESCRIPTOR_CONTRACT, EXTERNAL_TASK_HYBRID_UNSUPPORTED, ex
23
23
  import { CASE_FORMAT_PROFILE, CASE_SCHEMA_VERSIONS } from './format-profile.js';
24
24
  import { isResourceLabelRefusal, resolveResourceRef, resourceIdentity } from './resource-identity.js';
25
25
  import { ownEntry } from '../core/own-entry.js';
26
+ import { stableId } from '../core/stable-id.js';
26
27
  import { resolveSchemaVersion } from '../schema-version.js';
27
28
  import { caseJsonPreservation, caseJsonPreservationErrors, } from './preserve.js';
28
29
  import { businessEventProblems } from './business-event-shape.js';
@@ -30,6 +31,13 @@ import { readEventFilterLeaf, eventFilterTreeLeaf } from '../core/event-filters.
30
31
  import { responseJsonSchema } from '../core/connector-response-schema.js';
31
32
  import { ruleShapeProblems } from './rule-options.js';
32
33
  import { CompileRefusal } from '../core/compile-refusal.js';
34
+ /** The ids a serialize pass hashed from element ids, by the document it returned. */
35
+ const ELEMENT_SEEDED_IDS = new WeakMap();
36
+ function elementSeededId(resolver, elementId, suffix) {
37
+ const id = stableId(`${elementId}${suffix}`);
38
+ resolver.elementSeededIds.push({ elementId, suffix, id });
39
+ return id;
40
+ }
33
41
  /**
34
42
  * Condition `displayName` allocator.
35
43
  *
@@ -102,6 +110,7 @@ export function serialize(built, opts = {}) {
102
110
  escalationIdByName: new Map(),
103
111
  businessEvents: new Set((built.businessEvents ?? []).map((event) => event.name)),
104
112
  conditionName: conditionNamer(built),
113
+ elementSeededIds: [],
105
114
  };
106
115
  // Preallocate SLA + escalation ids (before any rule is emitted) so a
107
116
  // `sla-status-change` rule can resolve the `slaId`/`escalationId` it points at.
@@ -282,6 +291,7 @@ export function serialize(built, opts = {}) {
282
291
  }
283
292
  : {}),
284
293
  };
294
+ ELEMENT_SEEDED_IDS.set(result, resolver.elementSeededIds);
285
295
  const plan = stampConnectorConfigVersion(applyAuthorLayout(applyPreservation(result, caseJsonPreservation(built)), built.layout));
286
296
  // Last, over the finished document: V32 requires case-unique condition names,
287
297
  // and an author (or a decompiled pre-V32 plan) can repeat one. Rename the
@@ -396,6 +406,7 @@ export function applyPreservation(result, preservation, options = {}) {
396
406
  const errors = caseJsonPreservationErrors(preservation);
397
407
  if (errors.length)
398
408
  throw new CompileRefusal('PRESERVATION_COLLISION', `invalid Case JSON preservation payload: ${errors.join('; ')}`);
409
+ preservation = followRenames(result, preservation);
399
410
  applyPreservedIdentities(result, preservation);
400
411
  Object.assign(result, preservation.topLevel ?? {});
401
412
  Object.assign(result.metadata, preservation.metadata ?? {});
@@ -431,9 +442,15 @@ export function applyPreservation(result, preservation, options = {}) {
431
442
  const row = rows.find((candidate) => candidate.name === name);
432
443
  if (!row && options.overwrite)
433
444
  continue;
445
+ // `custom` alone has nothing to mark when no row is written: an older SDK's
446
+ // companion for a variable an output writes, which this one migrates away.
447
+ if (!row && Object.keys(fields).every((field) => field === 'custom'))
448
+ continue;
434
449
  if (!row)
435
450
  throw new CompileRefusal('PRESERVATION_TARGET_MISSING', `preserved variable "${name}" was not regenerated in variables.${lane}.`);
436
451
  for (const [field, value] of Object.entries(fields)) {
452
+ if (row[field] === value)
453
+ continue;
437
454
  if (!options.overwrite && Object.prototype.hasOwnProperty.call(row, field))
438
455
  throw new CompileRefusal('PRESERVATION_FIELD_COLLISION', `preserved variable "${name}" field "${field}" collides with serializer-owned output.`);
439
456
  row[field] = value;
@@ -524,14 +541,171 @@ export function applyPreservation(result, preservation, options = {}) {
524
541
  nodes.sort((left, right) => (order.get(String(left.id ?? '')) ?? Number.MAX_SAFE_INTEGER)
525
542
  - (order.get(String(right.id ?? '')) ?? Number.MAX_SAFE_INTEGER));
526
543
  }
544
+ // A stage the payload names that was not regenerated was deleted by the edit:
545
+ // its edges and canvas position go with it.
546
+ const nodeIds = new Set(result.nodes.map((node) => node.id));
547
+ const gone = new Set((preservation.identities?.stages ?? []).flatMap((stage) => (stage.id && !nodeIds.has(stage.id) ? [stage.id] : [])));
527
548
  if (preservation.edges !== undefined)
528
- result.edges = preservation.edges;
549
+ result.edges = preservation.edges.filter((edge) => !gone.has(edge?.source) && !gone.has(edge?.target));
529
550
  if (preservation.layout !== undefined)
530
- result.layout = preservation.layout;
551
+ result.layout = withoutLayoutNodes(preservation.layout, gone);
531
552
  else if (preservation.omitLayout)
532
553
  delete result.layout;
533
554
  return result;
534
555
  }
556
+ function withoutLayoutNodes(layout, gone) {
557
+ const nodes = layout?.nodes;
558
+ if (!gone.size || !nodes || typeof nodes !== 'object' || !Object.keys(nodes).some((id) => gone.has(id)))
559
+ return layout;
560
+ return { ...layout, nodes: Object.fromEntries(Object.entries(nodes).filter(([id]) => !gone.has(id))) };
561
+ }
562
+ /**
563
+ * The payload with each stage label and task name an edit renamed rewritten to
564
+ * the one compiled, so a renamed stage or task keeps its ids and wire fields
565
+ * instead of reading as a delete plus an add. Stages left over on both sides
566
+ * pair by the most task names shared, then the fewest not shared, then in order
567
+ * when as many are left on each side; a task left over pairs with one in the
568
+ * same stage whose input and output rows have the same names, and a task gone
569
+ * from its stage that another stage now holds under the same name and rows
570
+ * moved there.
571
+ *
572
+ * `wanted` is the payload's side (identities read from the original plan),
573
+ * `compiled` this compile's. Per stage, `missing` tasks are the payload's that
574
+ * the compiled stage no longer names, `extra` tasks the compiled stage's that
575
+ * the payload does not.
576
+ */
577
+ export function followRenames(result, preservation) {
578
+ const wanted = preservation.identities?.stages ?? [];
579
+ if (!wanted.length)
580
+ return preservation;
581
+ const compiled = (result.nodes ?? []).filter((node) => String(node.type ?? '').endsWith('Stage'));
582
+ const labelOf = (stage) => String(stage.data?.label ?? '');
583
+ const tasksOf = (stage) => (stage.data?.tasks ?? []).flat().filter((task) => task && typeof task === 'object');
584
+ const compiledLabels = new Set(compiled.map(labelOf));
585
+ const wantedLabels = new Set(wanted.map((stage) => stage.label));
586
+ const unmatchedCompiled = compiled.filter((stage) => !wantedLabels.has(labelOf(stage)));
587
+ const stageLabels = new Map();
588
+ const paired = new Set();
589
+ const unmatchedWanted = wanted.filter((stage) => !compiledLabels.has(stage.label));
590
+ // Ranked across every pair, so a deleted stage that shares one task name with
591
+ // a renamed stage cannot take it from the stage that shares more.
592
+ const stagePairs = unmatchedWanted.flatMap((stage, from) => {
593
+ const names = new Set((stage.tasks ?? []).flat().map((task) => task.name));
594
+ return unmatchedCompiled.flatMap((candidate, to) => {
595
+ const tasks = tasksOf(candidate);
596
+ const shared = tasks.filter((task) => names.has(String(task.displayName ?? ''))).length;
597
+ return shared ? [{ from, to, shared, unshared: names.size + tasks.length - 2 * shared }] : [];
598
+ });
599
+ });
600
+ stagePairs.sort((a, b) => b.shared - a.shared || a.unshared - b.unshared || a.from - b.from || a.to - b.to);
601
+ for (const { from, to } of stagePairs) {
602
+ if (stageLabels.has(unmatchedWanted[from].label) || paired.has(unmatchedCompiled[to]))
603
+ continue;
604
+ paired.add(unmatchedCompiled[to]);
605
+ stageLabels.set(unmatchedWanted[from].label, labelOf(unmatchedCompiled[to]));
606
+ }
607
+ const unpairedWanted = unmatchedWanted.filter((stage) => !stageLabels.has(stage.label));
608
+ const unpairedCompiled = unmatchedCompiled.filter((stage) => !paired.has(stage));
609
+ if (unpairedWanted.length === unpairedCompiled.length)
610
+ unpairedWanted.forEach((stage, index) => stageLabels.set(stage.label, labelOf(unpairedCompiled[index])));
611
+ const rowNames = (inputs, outputs) => JSON.stringify([inputs, outputs].map((rows) => (Array.isArray(rows) ? rows : []).map((row) => String(row?.name ?? ''))));
612
+ const nameOf = (task) => String(task.displayName ?? '');
613
+ const stageSides = wanted.map((stage) => {
614
+ const target = compiled.find((candidate) => labelOf(candidate) === (stageLabels.get(stage.label) ?? stage.label));
615
+ const tasks = (stage.tasks ?? []).flat();
616
+ const current = target ? tasksOf(target) : [];
617
+ const currentNames = new Set(current.map(nameOf));
618
+ const names = new Set(tasks.map((task) => task.name));
619
+ return { stage, target, missing: tasks.filter((task) => !currentNames.has(task.name)), extra: current.filter((task) => !names.has(nameOf(task))) };
620
+ });
621
+ const targeted = new Set(stageSides.map((side) => side.target));
622
+ const extraTasks = [
623
+ ...stageSides.flatMap(({ target, extra }) => (target ? extra.map((task) => ({ stage: labelOf(target), task })) : [])),
624
+ ...compiled.filter((stage) => !targeted.has(stage)).flatMap((stage) => tasksOf(stage).map((task) => ({ stage: labelOf(stage), task }))),
625
+ ];
626
+ const missingTasks = stageSides.flatMap(({ stage, missing }) => missing.map((task) => ({ stage: stage.label, task })));
627
+ const moves = new Map();
628
+ const claimed = new Set();
629
+ for (const { stage, task } of missingTasks) {
630
+ const rows = rowNames(task.inputs, task.outputs);
631
+ const to = extraTasks.filter((extra) => nameOf(extra.task) === task.name && rowNames(extra.task.data?.inputs, extra.task.data?.outputs) === rows);
632
+ if (to.length !== 1 || missingTasks.filter((other) => other.task.name === task.name).length !== 1)
633
+ continue;
634
+ if (!moves.has(stage))
635
+ moves.set(stage, new Map());
636
+ moves.get(stage).set(task.name, to[0].stage);
637
+ claimed.add(to[0].task);
638
+ }
639
+ const movedTo = (label, name) => moves.get(label)?.get(name);
640
+ const taskNames = new Map();
641
+ for (const side of stageSides) {
642
+ if (!side.target)
643
+ continue;
644
+ const missing = side.missing.filter((task) => !movedTo(side.stage.label, task.name));
645
+ const extra = side.extra
646
+ .filter((task) => !claimed.has(task))
647
+ .map((task) => ({ name: nameOf(task), rows: rowNames(task.data?.inputs, task.data?.outputs), task }));
648
+ // A deleted sibling can share a renamed task's rows, so the one whose content
649
+ // the renamed task still has wins, then the old name closest to the new one,
650
+ // then the earlier task.
651
+ const candidates = missing.flatMap((task, from) => extra.flatMap((candidate, to) => candidate.rows === rowNames(task.inputs, task.outputs)
652
+ ? [{ from, to, same: Number(task.fingerprint !== undefined && task.fingerprint === taskFingerprint(candidate.task)), score: nameAffinity(task.name, candidate.name) }]
653
+ : []));
654
+ candidates.sort((a, b) => b.same - a.same || b.score - a.score || a.from - b.from || a.to - b.to);
655
+ const renames = new Map();
656
+ const taken = new Set();
657
+ for (const { from, to } of candidates) {
658
+ if (renames.has(missing[from].name) || taken.has(to))
659
+ continue;
660
+ renames.set(missing[from].name, extra[to].name);
661
+ taken.add(to);
662
+ }
663
+ if (renames.size)
664
+ taskNames.set(side.stage.label, renames);
665
+ }
666
+ if (!stageLabels.size && !taskNames.size && !moves.size)
667
+ return preservation;
668
+ const stageName = (label) => stageLabels.get(label) ?? label;
669
+ const taskName = (label, name) => taskNames.get(label)?.get(name) ?? name;
670
+ const ref = (entry) => ({ ...entry, stage: movedTo(entry.stage, entry.task) ?? stageName(entry.stage), task: taskName(entry.stage, entry.task) });
671
+ const stages = wanted.map((stage) => ({
672
+ ...stage,
673
+ label: stageName(stage.label),
674
+ ...(stage.tasks
675
+ ? { tasks: stage.tasks.map((lane) => lane.filter((task) => !movedTo(stage.label, task.name)).map((task) => ({ ...task, name: taskName(stage.label, task.name) }))) }
676
+ : {}),
677
+ }));
678
+ for (const { stage: from, task } of missingTasks) {
679
+ const to = movedTo(from, task.name);
680
+ if (!to)
681
+ continue;
682
+ const target = stages.find((stage) => stage.label === to);
683
+ if (target)
684
+ target.tasks = [...(target.tasks ?? []), [task]];
685
+ else
686
+ stages.push({ label: to, tasks: [[task]] });
687
+ }
688
+ return {
689
+ ...preservation,
690
+ identities: {
691
+ ...preservation.identities,
692
+ stages,
693
+ },
694
+ ...(preservation.taskFields ? { taskFields: preservation.taskFields.map(ref) } : {}),
695
+ ...(preservation.omitTaskElementIds ? { omitTaskElementIds: preservation.omitTaskElementIds.map(ref) } : {}),
696
+ ...(preservation.omitStagePositions ? { omitStagePositions: preservation.omitStagePositions.map(stageName) } : {}),
697
+ };
698
+ }
699
+ /** How much of a renamed name survives: the longer of the shared prefix and the shared suffix. */
700
+ function nameAffinity(before, after) {
701
+ let prefix = 0;
702
+ while (prefix < before.length && before[prefix] === after[prefix])
703
+ prefix++;
704
+ let suffix = 0;
705
+ while (suffix < before.length && suffix < after.length && before.at(-1 - suffix) === after.at(-1 - suffix))
706
+ suffix++;
707
+ return Math.max(prefix, suffix);
708
+ }
535
709
  function applyPreservedIdentities(result, preservation) {
536
710
  const identities = preservation.identities;
537
711
  if (!identities)
@@ -543,11 +717,70 @@ function applyPreservedIdentities(result, preservation) {
543
717
  replacements.set(current.id, wanted);
544
718
  current.id = wanted;
545
719
  };
720
+ const nodes = result.nodes;
721
+ const stages = nodes.filter((node) => String(node.type).endsWith('Stage'));
722
+ const bagField = CASE_FORMAT_PROFILE.waitConnectorRuleBagField;
723
+ const ruleOutputRows = (conditions) => (conditions ?? []).flatMap((condition) => (condition?.rules ?? []).flat().flatMap((rule) => rule?.[bagField]?.outputs ?? []));
724
+ // Every output row id in the plan, task and rule rows alike, kept current as
725
+ // preserved ids are taken. Output ids are unique plan-wide (`claimOutputId`); a
726
+ // preserved id that another row already holds would undo that. Example: an
727
+ // older SDK wrote two action rows both `{ id: 'action', var: 'action' }`; this
728
+ // pass gives the second `action2`, and taking its preserved `action` would make
729
+ // it a duplicate.
730
+ const heldOutputIds = new Set();
731
+ for (const stage of stages) {
732
+ const rows = [
733
+ ...ruleOutputRows(stage.data?.entryConditions),
734
+ ...ruleOutputRows(stage.data?.exitConditions),
735
+ ...(stage.data?.tasks ?? []).flat().flatMap((task) => [...(task?.data?.outputs ?? []), ...ruleOutputRows(task?.entryConditions)]),
736
+ ];
737
+ for (const row of rows)
738
+ if (typeof row?.id === 'string')
739
+ heldOutputIds.add(row.id);
740
+ }
741
+ for (const row of ruleOutputRows(result.metadata?.caseExitRules))
742
+ if (typeof row?.id === 'string')
743
+ heldOutputIds.add(row.id);
744
+ const takeOutputRows = (outputRows, wantedRows) => {
745
+ // Match by name when unique on both sides, else by position.
746
+ const unique = (names, name) => names.filter((n) => n === name).length === 1;
747
+ const currentNames = outputRows.map((r) => r?.name);
748
+ const wantedNames = wantedRows.map((r) => r.name);
749
+ const named = new Set(wantedRows.filter((r) => unique(wantedNames, r.name) && unique(currentNames, r.name)).map((r) => r.name));
750
+ wantedRows.forEach((row, index) => {
751
+ const target = named.has(row.name)
752
+ ? outputRows.find((r) => r?.name === row.name)
753
+ // A positional match never takes a row a name match owns.
754
+ : named.has(outputRows[index]?.name) ? undefined : outputRows[index];
755
+ const currentId = target?.id;
756
+ // An own-name row's id IS its variable's name (`emitTaskOutput`). A
757
+ // preserved id that differs — an `o<hash>` slot from a plan an older
758
+ // SDK wrote — would rename the variable out from under every
759
+ // `=vars.<name>` reader, so the typed rule wins and the row migrates.
760
+ if (target && OWN_NAME_OUTPUT_ROWS.has(target) && row.id !== target.id)
761
+ return;
762
+ if (target && typeof row.id === 'string' && row.id !== currentId && heldOutputIds.has(row.id))
763
+ return;
764
+ take(target, row.id);
765
+ if (target && typeof row.id === 'string' && typeof currentId === 'string') {
766
+ heldOutputIds.delete(currentId);
767
+ heldOutputIds.add(row.id);
768
+ }
769
+ if (typeof currentId === 'string' && typeof row.id === 'string' && target?.value === currentId) {
770
+ target.value = row.id;
771
+ }
772
+ });
773
+ };
546
774
  const takeConditions = (current, wanted) => {
547
775
  (wanted ?? []).forEach((condition, index) => {
548
776
  const target = current?.[index];
549
777
  take(target, condition.id);
550
- (condition.rules ?? []).forEach((group, groupIndex) => group.forEach((rule, ruleIndex) => take(target?.rules?.[groupIndex]?.[ruleIndex], rule.id)));
778
+ (condition.rules ?? []).forEach((group, groupIndex) => group.forEach((rule, ruleIndex) => {
779
+ const ruleTarget = target?.rules?.[groupIndex]?.[ruleIndex];
780
+ take(ruleTarget, rule.id);
781
+ if (rule.outputs)
782
+ takeOutputRows(ruleTarget?.[bagField]?.outputs ?? [], rule.outputs);
783
+ }));
551
784
  });
552
785
  };
553
786
  const takeSlas = (current, wanted) => {
@@ -558,22 +791,8 @@ function applyPreservedIdentities(result, preservation) {
558
791
  });
559
792
  };
560
793
  take(result, identities.id);
561
- const nodes = result.nodes;
562
794
  const triggerNodes = nodes.filter((node) => node.type === 'case-management:Trigger' || node.type === 'uipath.case.trigger');
563
795
  (identities.triggers ?? []).forEach((trigger, index) => take(triggerNodes[index], trigger.id));
564
- const stages = nodes.filter((node) => String(node.type).endsWith('Stage'));
565
- // Every task-output row id in the plan, kept current as preserved ids are
566
- // taken. Output ids are unique plan-wide (`claimOutputId`); a preserved id that
567
- // another row already holds would undo that. Example: an older SDK wrote two
568
- // action rows both `{ id: 'action', var: 'action' }`; this pass gives the
569
- // second `action2`, and taking its preserved `action` would make it a duplicate.
570
- const heldOutputIds = new Set();
571
- for (const stage of stages)
572
- for (const lane of stage.data?.tasks ?? [])
573
- for (const task of lane)
574
- for (const row of task?.data?.outputs ?? [])
575
- if (typeof row?.id === 'string')
576
- heldOutputIds.add(row.id);
577
796
  for (const stageIdentity of identities.stages ?? []) {
578
797
  const stage = stages.find((candidate) => candidate.data?.label === stageIdentity.label);
579
798
  take(stage, stageIdentity.id);
@@ -585,36 +804,7 @@ function applyPreservedIdentities(result, preservation) {
585
804
  take(task, taskIdentity.id);
586
805
  takeConditions(task?.entryConditions, taskIdentity.entryConditions);
587
806
  (taskIdentity.inputs ?? []).forEach((row, index) => take(task?.data?.inputs?.[index], row.id));
588
- const outputRows = task?.data?.outputs ?? [];
589
- const wantedRows = taskIdentity.outputs ?? [];
590
- // Match by name when unique on both sides, else by position.
591
- const unique = (names, name) => names.filter((n) => n === name).length === 1;
592
- const currentNames = outputRows.map((r) => r?.name);
593
- const wantedNames = wantedRows.map((r) => r.name);
594
- const named = new Set(wantedRows.filter((r) => unique(wantedNames, r.name) && unique(currentNames, r.name)).map((r) => r.name));
595
- wantedRows.forEach((row, index) => {
596
- const target = named.has(row.name)
597
- ? outputRows.find((r) => r?.name === row.name)
598
- // A positional match never takes a row a name match owns.
599
- : named.has(outputRows[index]?.name) ? undefined : outputRows[index];
600
- const currentId = target?.id;
601
- // An own-name row's id IS its variable's name (`emitTaskOutput`). A
602
- // preserved id that differs — an `o<hash>` slot from a plan an older
603
- // SDK wrote — would rename the variable out from under every
604
- // `=vars.<name>` reader, so the typed rule wins and the row migrates.
605
- if (target && OWN_NAME_OUTPUT_ROWS.has(target) && row.id !== target.id)
606
- return;
607
- if (target && typeof row.id === 'string' && row.id !== currentId && heldOutputIds.has(row.id))
608
- return;
609
- take(target, row.id);
610
- if (target && typeof row.id === 'string' && typeof currentId === 'string') {
611
- heldOutputIds.delete(currentId);
612
- heldOutputIds.add(row.id);
613
- }
614
- if (typeof currentId === 'string' && typeof row.id === 'string' && target?.value === currentId) {
615
- target.value = row.id;
616
- }
617
- });
807
+ takeOutputRows(task?.data?.outputs ?? [], taskIdentity.outputs ?? []);
618
808
  }));
619
809
  }
620
810
  const metadata = result.metadata;
@@ -627,11 +817,12 @@ function applyPreservedIdentities(result, preservation) {
627
817
  return;
628
818
  take(variables[lane]?.find((candidate) => candidate.name === row.name), row.id);
629
819
  });
820
+ const longestFirst = [...replacements].sort((a, b) => b[0].length - a[0].length);
630
821
  const replace = (value) => {
631
822
  if (replacements.has(value))
632
823
  return replacements.get(value);
633
824
  let replaced = value;
634
- for (const [current, wanted] of [...replacements].sort((a, b) => b[0].length - a[0].length))
825
+ for (const [current, wanted] of longestFirst)
635
826
  replaced = replaced.replaceAll(current, wanted);
636
827
  return replaced;
637
828
  };
@@ -696,6 +887,33 @@ function applyPreservedIdentities(result, preservation) {
696
887
  rewrite(result);
697
888
  for (const { row, source } of preservedOutputSources)
698
889
  row.source = source;
890
+ // An id hashed from a generated element id is re-minted from the preserved one,
891
+ // wherever it is spelled, including inside a `=jsonString:` configuration.
892
+ const reseeded = new Map();
893
+ for (const entry of ELEMENT_SEEDED_IDS.get(result) ?? []) {
894
+ const elementId = replace(entry.elementId);
895
+ if (elementId !== entry.elementId)
896
+ reseeded.set(entry.id, stableId(`${elementId}${entry.suffix}`));
897
+ }
898
+ if (reseeded.size)
899
+ reseed(result, reseeded);
900
+ }
901
+ function reseed(value, ids) {
902
+ if (!value || typeof value !== 'object')
903
+ return;
904
+ const entries = Array.isArray(value) ? value.map((entry, index) => [index, entry]) : Object.entries(value);
905
+ for (const [key, entry] of entries) {
906
+ if (typeof entry !== 'string') {
907
+ reseed(entry, ids);
908
+ continue;
909
+ }
910
+ let next = entry;
911
+ for (const [from, to] of ids)
912
+ if (next.includes(from))
913
+ next = next.replaceAll(from, to);
914
+ if (next !== entry)
915
+ value[key] = next;
916
+ }
699
917
  }
700
918
  function emitVariables(vars, ids, triggerNodeByRef) {
701
919
  const inputs = [];
@@ -1091,7 +1309,7 @@ function resolvedSubscriptionBag(kind, spec, resolver, elementId, where) {
1091
1309
  const folder = folderBindingRef(resolver, spec.folder, connection, { attribute: 'folder', where, target });
1092
1310
  if (folder)
1093
1311
  context.push(io('folderKey', 'string', { value: `=bindings.${folder.id}` }));
1094
- context.push(io('method', 'string', {}), io('path', 'string', {}), io('objectName', 'string', { value: op.objectName ?? spec.event }), io('operation', 'string', { value: op.eventOperation ?? spec.event }), io('metadata', 'json', { body: subscriptionMetadata(rc, op, spec.filters ?? [], elementId) }));
1312
+ context.push(io('method', 'string', {}), io('path', 'string', {}), io('objectName', 'string', { value: op.objectName ?? spec.event }), io('operation', 'string', { value: op.eventOperation ?? spec.event }), io('metadata', 'json', { body: subscriptionMetadata(rc, op, spec.filters ?? [], elementId, resolver) }));
1095
1313
  return {
1096
1314
  serviceType: kind === 'trigger' ? CASE_FORMAT_PROFILE.eventServiceType : CASE_FORMAT_PROFILE.waitConnectorServiceType,
1097
1315
  context,
@@ -1106,7 +1324,7 @@ function eventInput(where, elementId) {
1106
1324
  ? { name: 'body', type: 'json', target: 'body', body: { queryParams: where }, var: 'vbody', id: 'vbody', elementId }
1107
1325
  : { name: 'body', type: 'json', target: 'body', value: '', var: 'vbody', id: 'vbody', elementId };
1108
1326
  }
1109
- function subscriptionMetadata(rc, op, filters, elementId) {
1327
+ function subscriptionMetadata(rc, op, filters, elementId, resolver) {
1110
1328
  const instanceParameters = {
1111
1329
  connectorKey: rc.connectorKey,
1112
1330
  objectName: op.objectName ?? rc.nodeType.split('.').pop(),
@@ -1131,7 +1349,7 @@ function subscriptionMetadata(rc, op, filters, elementId) {
1131
1349
  const { read } = judged;
1132
1350
  return {
1133
1351
  ...eventFilterTreeLeaf(read),
1134
- uiId: stableUuid(`${elementId}:${read.field}:${String(read.value)}:${index}`),
1352
+ uiId: elementSeededId(resolver, elementId, `:${read.field}:${String(read.value)}:${index}`),
1135
1353
  };
1136
1354
  }).map((leaf) => ({
1137
1355
  id: leaf.id,
@@ -1140,7 +1358,7 @@ function subscriptionMetadata(rc, op, filters, elementId) {
1140
1358
  uiId: leaf.uiId,
1141
1359
  })),
1142
1360
  groups: [],
1143
- uuId: stableUuid(`${elementId}:filters`),
1361
+ uuId: elementSeededId(resolver, elementId, ':filters'),
1144
1362
  } : null;
1145
1363
  const configuration = {
1146
1364
  essentialConfiguration: {
@@ -1182,16 +1400,6 @@ function subscriptionMetadata(rc, op, filters, elementId) {
1182
1400
  },
1183
1401
  };
1184
1402
  }
1185
- function stableUuid(seed) {
1186
- let h1 = 0x811c9dc5;
1187
- let h2 = 0x01000193;
1188
- for (let index = 0; index < seed.length; index++) {
1189
- h1 = (h1 ^ seed.charCodeAt(index)) * 0x01000193 >>> 0;
1190
- h2 = (h2 + seed.charCodeAt(index) * (index + 7)) >>> 0;
1191
- }
1192
- const hex = (value, length) => value.toString(16).padStart(8, '0').slice(0, length);
1193
- return `${hex(h1, 8)}-${hex(h2, 4)}-4${hex(h1 >>> 8, 3)}-8${hex(h2 >>> 8, 3)}-${hex(h1 ^ h2, 8)}${hex(h2, 4)}`;
1194
- }
1195
1403
  // ─── Reference-mode task io-binding (data.inputs[] / data.outputs[]) ───────────
1196
1404
  //
1197
1405
  // Wire shape verified against a real `uip maestro case` caseplan (the io-binding
package/dist/decompile.js CHANGED
@@ -48,6 +48,7 @@ import { OUTCOME_HANDLE_NODE_TYPES } from './core/hitl-routing.js';
48
48
  import { contributionPort, forwardFanOut } from './core/definition-handles.js';
49
49
  import { STICKY_NOTE_TYPE } from './core/definitionless-node-types.js';
50
50
  import { eventFilterFromTreeLeaf } from './core/event-filters.js';
51
+ import { referencedBindingNames } from './core/contribution-bindings.js';
51
52
  /**
52
53
  * The trigger id of the flow currently being decompiled, so a reference through a
53
54
  * RENAMED trigger is still recognized as `input('x')` rather than falling back to
@@ -89,6 +90,8 @@ let runRawManifests = [];
89
90
  * definition and the node keeps its version even when the author edits it.
90
91
  */
91
92
  let runCarried = new Map();
93
+ /** The file's top-level `bindings[]` rows, by id, for {@link resolveTemplateBindings}. */
94
+ let runBindings = new Map();
92
95
  let runStrict = false;
93
96
  /** `DecompileOptions.warn` for the current run, and the messages already sent to it (one line per finding). */
94
97
  let runWarn;
@@ -1040,6 +1043,81 @@ function emitEventSubscription(node, imp, inputNames) {
1040
1043
  ['folder', folder !== undefined ? tsLiteral(folder) : undefined],
1041
1044
  ]) + unboundMarker(node, { connection: conn, folder }, connector, event);
1042
1045
  }
1046
+ /**
1047
+ * The definition a `rawNode` carries for `node`. A per-tenant template
1048
+ * (`<bindings.NAME>` placeholders, empty `resourceKey`) comes back as a copy
1049
+ * resolved with the node's own key and defaults (#1512); any other `def` comes
1050
+ * back unchanged.
1051
+ *
1052
+ * For instance, a template context row `queueName: "<bindings.name>"`, on a node
1053
+ * whose `queueName` row is `=bindings.bQueueName`, takes `bQueueName`'s
1054
+ * `resourceKey` and `default`. The placeholders stay, for compile to close.
1055
+ *
1056
+ * `reason` says why a template was left unresolved, when the node is bound but
1057
+ * not fully: compile's refusal names only the empty key, not what the node lacks.
1058
+ */
1059
+ function resolveTemplateBindings(def, node) {
1060
+ const model = def?.model;
1061
+ const names = referencedBindingNames(model);
1062
+ const resource = model?.bindings?.resource;
1063
+ if (names.length === 0 || typeof resource !== 'string' || !resource || resource === 'connection')
1064
+ return { def };
1065
+ if (typeof model.bindings.resourceKey === 'string' && model.bindings.resourceKey)
1066
+ return { def };
1067
+ const nodeModel = node.model;
1068
+ const nodeContext = Array.isArray(nodeModel?.context) ? nodeModel.context : [];
1069
+ const nodeValues = Array.isArray(nodeModel?.bindings?.values) ? nodeModel.bindings.values : [];
1070
+ const keys = new Map();
1071
+ const defaults = new Map();
1072
+ const missing = [];
1073
+ for (const name of names) {
1074
+ // Matched by the context row's own `name` (`queueName`): the canvas rewrites its value.
1075
+ const fields = model.context.filter((c) => c?.value === `<bindings.${name}>`).map((c) => c?.name);
1076
+ const ref = nodeContext.find((c) => fields.includes(c?.name) && typeof c?.value === 'string');
1077
+ const id = typeof ref?.value === 'string' ? /^=bindings\.(.+)$/.exec(ref.value)?.[1] : undefined;
1078
+ const row = id !== undefined ? runBindings.get(id) : undefined;
1079
+ let key = row?.resource === resource ? row.resourceKey : undefined;
1080
+ let value = row?.resource === resource ? row.default : undefined;
1081
+ if (typeof key !== 'string' || !key)
1082
+ key = nodeModel?.bindings?.resource === resource ? nodeModel.bindings.resourceKey : undefined;
1083
+ if (typeof value !== 'string')
1084
+ value = nodeValues.find((v) => v?.name === name)?.default;
1085
+ if (typeof key === 'string' && key)
1086
+ keys.set(name, key);
1087
+ else
1088
+ missing.push(`no resourceKey for "${name}"`);
1089
+ if (typeof value === 'string')
1090
+ defaults.set(name, value);
1091
+ else
1092
+ missing.push(`no default for "${name}"`);
1093
+ }
1094
+ const label = `node "${node.id}" (${def?.nodeType}@${def?.version})`;
1095
+ // Nothing bound at all: compile's own refusal already says what to do.
1096
+ if (keys.size === 0 && defaults.size === 0)
1097
+ return { def };
1098
+ if (missing.length > 0) {
1099
+ return { def, reason: `${label} is bound to a ${resource}, but its binding has ${missing.join(', ')}, so its definition stays the template.` };
1100
+ }
1101
+ const distinct = [...new Set(keys.values())];
1102
+ if (distinct.length !== 1) {
1103
+ const each = [...keys].map(([n, k]) => `"${n}" → ${k}`).join(', ');
1104
+ return { def, reason: `${label} names more than one ${resource} (${each}), so its definition stays the template.` };
1105
+ }
1106
+ const values = Array.isArray(model.bindings.values) ? model.bindings.values : [];
1107
+ return {
1108
+ def: {
1109
+ ...def,
1110
+ model: {
1111
+ ...model,
1112
+ bindings: {
1113
+ ...model.bindings,
1114
+ resourceKey: distinct[0],
1115
+ values: values.map((v) => (defaults.has(v?.name) ? { ...v, default: defaults.get(v.name) } : v)),
1116
+ },
1117
+ },
1118
+ },
1119
+ };
1120
+ }
1043
1121
  /** Best-effort placeholder for a node type the reverse-map does not yet cover. */
1044
1122
  function unsupportedSpec(node, imp, o) {
1045
1123
  // The file's own definition for this node — the manifest the platform served.
@@ -1053,7 +1131,10 @@ function unsupportedSpec(node, imp, o) {
1053
1131
  // `mock()` for a flow that genuinely contains one.
1054
1132
  const constName = `${camel(node.id)}Definition`;
1055
1133
  if (!runRawManifests.some(([n]) => n === constName)) {
1056
- runRawManifests.push([constName, JSON.stringify(def, null, 2)]);
1134
+ const resolved = resolveTemplateBindings(def, node);
1135
+ if (resolved.reason !== undefined)
1136
+ warnOnce(resolved.reason);
1137
+ runRawManifests.push([constName, JSON.stringify(resolved.def, null, 2)]);
1057
1138
  }
1058
1139
  const inputs = node.inputs ?? {};
1059
1140
  const inputsSrc = Object.keys(inputs).length > 0 ? JSON.stringify(inputs, null, 2) : undefined;
@@ -2937,6 +3018,11 @@ export function decompile(flow, options = {}) {
2937
3018
  runDefs.set(d.nodeType, d);
2938
3019
  }
2939
3020
  runCarried = carriedPairs(flow);
3021
+ // Best effort: a malformed `bindings` section (not an array) indexes nothing.
3022
+ const rows = flow.bindings;
3023
+ runBindings = new Map((Array.isArray(rows) ? rows : [])
3024
+ .filter((b) => typeof b?.id === 'string')
3025
+ .map((b) => [b.id, b]));
2940
3026
  const imp = new Imports();
2941
3027
  const graph = new Graph(flow.nodes ?? [], flow.edges ?? [], flow.variables);
2942
3028
  const globals = readFlowGlobals(flow.variables, runStrict);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uipath/maestro-builder-sdk",
3
- "version": "6.66.2",
3
+ "version": "6.66.4",
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",
@@ -100,5 +100,5 @@
100
100
  "@types/node": "^22.7.0",
101
101
  "esbuild": "^0.28.1"
102
102
  },
103
- "gitref": "f698c1379d7c348671e6bf0df5b211be77e8f1c6"
103
+ "gitref": "af11f232febf9ed683dec9f44b4ae7fe3c2426fe"
104
104
  }