@uipath/maestro-builder-sdk 6.14.0 → 6.14.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.
@@ -54,7 +54,7 @@
54
54
  */
55
55
  import BpmnModdle from 'bpmn-moddle';
56
56
  import { createRequire } from 'node:module';
57
- import { registryType } from './typed-node.js';
57
+ import { registryType, rowDeclaration } from './typed-node.js';
58
58
  import { errorSchema, errorSchemaWithResponse } from './bpmn-sdk.js';
59
59
  import { BPMN_SCHEMA_VERSIONS } from './format-profile.js';
60
60
  import { preserveSchemaVersion } from '../schema-version.js';
@@ -66,6 +66,15 @@ const uipathDescriptor = require('./uipath-moddle.v1.json');
66
66
  * then double every edge, so the body pins `.flowMode('explicit')` for itself.
67
67
  */
68
68
  let pinExplicitInBodies = false;
69
+ /**
70
+ * Variable id → the schema expression its declaration carries, for declarations being
71
+ * FOLDED onto the output row that fills them (`TypedOutputRow.varSchema`).
72
+ *
73
+ * Set for the length of one decompile, beside `pinExplicitInBodies` and for the same
74
+ * reason: the row emitters are reached through a dozen element emitters that have no
75
+ * business carrying a document-wide table.
76
+ */
77
+ let foldedVarSchemas;
69
78
  // ─── Source emitters ──────────────────────────────────────────────────────────
70
79
  /**
71
80
  * A JS/TS string literal.
@@ -191,6 +200,14 @@ function opts(entries) {
191
200
  const parts = entries.filter(([, v]) => v !== undefined).map(([k, v]) => `${key(k)}: ${v}`);
192
201
  return parts.length ? `{ ${parts.join(', ')} }` : '';
193
202
  }
203
+ /**
204
+ * The same option pairs as {@link opts}, but as the TAIL of a literal that already has
205
+ * a field — `{ id: 'x'` + `, name: 'y', … }` — for the array forms.
206
+ */
207
+ function inner(entries) {
208
+ const parts = entries.filter(([, v]) => v !== undefined).map(([k, v]) => `${key(k)}: ${v}`);
209
+ return parts.length ? `, ${parts.join(', ')}` : '';
210
+ }
194
211
  /** A record literal, or `undefined` when the record is empty. */
195
212
  function record(r) {
196
213
  if (!r || Object.keys(r).length === 0)
@@ -199,6 +216,17 @@ function record(r) {
199
216
  .map(([k, v]) => `${key(k)}: ${typeof v === 'string' ? str(v) : literal(v)}`)
200
217
  .join(', ')} }`;
201
218
  }
219
+ /**
220
+ * An array-form call — `.bindings([…])`, `.vars([…])`, `.schemas([…])` — one entry per
221
+ * line, indented under the method.
222
+ *
223
+ * The singular calls still exist and still work; this is what `decompile` writes,
224
+ * because a real process declares dozens of each and a column of repeated method
225
+ * names is a wall to read past rather than a table to read.
226
+ */
227
+ function arrayCall(method, entries, indent) {
228
+ return `.${method}([\n${entries.map((e) => `${indent} ${e},`).join('\n')}\n${indent}])`;
229
+ }
202
230
  /** A collector for the fluent chain's lines. */
203
231
  class Chain {
204
232
  indent;
@@ -436,6 +464,8 @@ function outputRowsNeedSpellingOut(payload) {
436
464
  row.custom !== undefined ||
437
465
  row.description !== undefined ||
438
466
  row.target !== undefined ||
467
+ // A folded declaration lives in `varSchema`, which only the long form has.
468
+ (row.var !== undefined && foldedVarSchemas?.has(row.var) === true) ||
439
469
  (row.var !== undefined && row.var !== row.name));
440
470
  }
441
471
  /** The output rows as `TypedOutputRow` option source, in document order. */
@@ -455,6 +485,8 @@ function emitOutputRows(payload) {
455
485
  // schema is carried, never interpreted, so nothing here can reorder its keys
456
486
  // or drop a field this SDK does not know about.
457
487
  ['schema', row.body !== undefined ? jsonLiteral(row.body) : undefined],
488
+ // The declaration this row fills, folded onto the row — see `derivedVariableIds`.
489
+ ['varSchema', row.var !== undefined ? foldedVarSchemas?.get(row.var) : undefined],
458
490
  ]));
459
491
  return `[${rendered.join(', ')}]`;
460
492
  }
@@ -1672,33 +1704,77 @@ defaultScope = () => ownerId) {
1672
1704
  return;
1673
1705
  const groups = [
1674
1706
  ['input', 'input'],
1675
- ['inputOutput', 'var'],
1707
+ ['inputOutput', 'inputOutput'],
1676
1708
  ['output', 'output'],
1677
1709
  ];
1678
- for (const [prop, method] of groups) {
1710
+ // One `.vars([…])` for all three directions, in document order, so the block reads as
1711
+ // the table the variables panel shows. `direction` is written only where it is not
1712
+ // the `inputOutput` default — the shape `.var()` has always had.
1713
+ const entries = [];
1714
+ for (const [prop, direction] of groups) {
1679
1715
  for (const v of vars[prop] ?? []) {
1680
1716
  if (skip.has(v.id))
1681
1717
  continue;
1682
1718
  const type = varType(v.type);
1683
1719
  const def = v.default === undefined ? undefined : decodeDefault(v.default, v.type);
1684
- chain.add(`.${method}(${str(v.id)}, ${str(type)}${optionalArg(opts([
1720
+ entries.push(`{ id: ${str(v.id)}, type: ${str(type)}${inner([
1721
+ ['direction', direction === 'inputOutput' ? undefined : str(direction)],
1685
1722
  ['name', v.name !== undefined && v.name !== v.id ? str(v.name) : undefined],
1686
1723
  ['default', def],
1687
1724
  ['schema', v.body !== undefined ? (plan?.ref.get(String(v.body)) ?? jsonLiteral(v.body)) : undefined],
1688
1725
  ['required', v.required !== undefined ? 'true' : undefined],
1726
+ ['custom', v.custom !== undefined ? 'true' : undefined],
1727
+ ['internal', v.internal !== undefined ? 'true' : undefined],
1728
+ ['subType', v.subType !== undefined ? str(v.subType) : undefined],
1689
1729
  ['elementId', emitElementId(v.elementId, defaultScope(prop, v), rehome)],
1690
- ]))})`);
1730
+ ])} }`);
1691
1731
  }
1692
1732
  }
1733
+ if (entries.length)
1734
+ chain.add(arrayCall('vars', entries, chain.indent));
1693
1735
  }
1694
- /** A variable `default` attribute back into the TS literal it came from. */
1736
+ /**
1737
+ * Whether anything in the document reads this binding.
1738
+ *
1739
+ * Deliberately textual and deliberately over-eager: a reference is written
1740
+ * `=bindings.<id>` in a context value, but it can also sit inside a CDATA body, a
1741
+ * script, or designer metadata, and a structural walk that missed one would DELETE a
1742
+ * live binding. Matching the id anywhere in the raw XML, on a word boundary, can only
1743
+ * err by keeping a dead row.
1744
+ *
1745
+ * The id is escaped because a binding id is free text: a hand-authored one is a label
1746
+ * (`slack`, `shared`), not the `b`-prefixed nonce the designer generates.
1747
+ */
1748
+ function bindingIsReferenced(xml, id) {
1749
+ if (!id)
1750
+ return true;
1751
+ return new RegExp(String.raw `bindings\.${id.replace(/[.*+?^${}()|[\]\\]/g, String.raw `\$&`)}\b`).test(xml);
1752
+ }
1753
+ /**
1754
+ * A variable `default` attribute back into the TS literal it came from.
1755
+ *
1756
+ * A default is not always a literal of the declared type: the product lets one be an
1757
+ * `=`-EXPRESSION, evaluated at instance start, whatever the variable's type. Such a
1758
+ * default has to survive as its own text. The boolean arm used to decide `raw ===
1759
+ * 'true' ? 'true' : 'false'`, which is total — so a real export's
1760
+ * `type="boolean" default="=js:true"` came back `default="false"`, silently inverting
1761
+ * a process input that gates a whole branch (`claims-intake-hitl`'s
1762
+ * `in_usePlaceholderData`). The number arm already fell through to a string on
1763
+ * anything non-finite, so it was right by accident; this makes the rule explicit for
1764
+ * every type.
1765
+ */
1695
1766
  function decodeDefault(raw, wire) {
1767
+ if (raw.startsWith('='))
1768
+ return str(raw); // an expression, not a value of `wire`
1696
1769
  if (wire === 'json')
1697
1770
  return raw; // already JSON — emit it verbatim
1698
1771
  if (wire === 'number')
1699
1772
  return Number.isFinite(Number(raw)) ? String(Number(raw)) : str(raw);
1773
+ // Only the two literals are booleans. Anything else — an empty `default=""`, which
1774
+ // a real export writes to mean "no value" — is text, and collapsing it to `false`
1775
+ // is the same silent inversion as the expression case above.
1700
1776
  if (wire === 'boolean' || wire === 'bool')
1701
- return raw === 'true' ? 'true' : 'false';
1777
+ return raw === 'true' || raw === 'false' ? raw : str(raw);
1702
1778
  return str(raw);
1703
1779
  }
1704
1780
  // ─── Entry point ──────────────────────────────────────────────────────────────
@@ -1805,44 +1881,82 @@ export async function decompile(xml, options = {}) {
1805
1881
  // Bindings next: a typed node may reference one, and reading the declarations
1806
1882
  // before the nodes keeps the emitted source in dependency order.
1807
1883
  const bindings = (host.container.extensionElements?.values ?? []).find((x) => x.$type === 'uipath:Bindings');
1884
+ const droppedBindings = [];
1885
+ const bindingEntries = [];
1808
1886
  for (const b of bindings?.binding ?? []) {
1809
- chain.add(`.binding(${str(b.id)}, ${opts([
1887
+ if (!options.keepUnusedBindings && !bindingIsReferenced(xml, b.id)) {
1888
+ droppedBindings.push(b.id);
1889
+ continue;
1890
+ }
1891
+ bindingEntries.push(`{ id: ${str(b.id)}${inner([
1810
1892
  ['name', b.name !== undefined && b.name !== b.id ? str(b.name) : undefined],
1811
1893
  ['value', b.default !== undefined ? str(b.default) : undefined],
1812
1894
  ['resource', b.resource !== undefined && b.resource !== 'custom' ? str(b.resource) : undefined],
1813
1895
  ['resourceKey', b.resourceKey !== undefined && b.resourceKey !== b.default ? str(b.resourceKey) : undefined],
1814
1896
  ['propertyAttribute', b.propertyAttribute !== undefined && b.propertyAttribute !== 'value' ? str(b.propertyAttribute) : undefined],
1815
1897
  ['resourceSubType', b.resourceSubType !== undefined ? str(b.resourceSubType) : undefined],
1816
- ])})`);
1898
+ ])} }`);
1817
1899
  }
1818
- // Variables the serializer DERIVES from a node's output are re-derived on
1819
- // recompile, so echoing them would double-declare. Skip exactly those.
1820
- const derived = derivedVariableIds(process);
1900
+ if (bindingEntries.length)
1901
+ chain.add(arrayCall('bindings', bindingEntries, chain.indent));
1902
+ if (droppedBindings.length)
1903
+ options.onPrune?.(droppedBindings);
1821
1904
  // Count the bodies first: whether one is written inline or shared depends on how
1822
1905
  // many variables carry it, which is only knowable across the whole document.
1823
1906
  const varsExt = (host.container.extensionElements?.values ?? []).find((x) => x.$type === 'uipath:Variables');
1907
+ // Variables the serializer DERIVES from a node's output are re-derived on
1908
+ // recompile, so echoing them would double-declare. Skip exactly those.
1909
+ //
1910
+ // The declarations come from `host.container`, not from `process`: a pool-shaped
1911
+ // export hangs them off the `bpmn:collaboration`, and reading them off the process
1912
+ // found none, so NOTHING was recognised as derived and every declaration was echoed
1913
+ // by hand. That was invisible — an echoed declaration takes the id before
1914
+ // `implicitNodeVars` can claim it, so the output was correct, just 112 lines of it.
1915
+ const { ids: derived, folds } = derivedVariableIds(process, varsExt, host.rehome);
1824
1916
  const schemaPlan = planSchemas([
1825
1917
  ...(varsExt?.input ?? []),
1826
1918
  ...(varsExt?.inputOutput ?? []),
1827
1919
  ...(varsExt?.output ?? []),
1828
1920
  ]);
1829
- for (const [id, body] of schemaPlan.declare)
1830
- chain.add(`.schema(${str(id)}, ${body})`);
1831
- emitVariablesInto(chain, host.container, host.rehome?.to ?? host.container.id, derived, host.rehome, schemaPlan, publicDeclarationScopeOf(process, host.rehome?.to ?? host.container.id));
1832
- if (options.style === 'nested') {
1833
- const sequence = options.sequenceMode ?? true;
1834
- const summary = { handlers: 0, branches: 0, nets: 0, leftFlat: [], renamedFlows: [], sequence };
1835
- pinExplicitInBodies = sequence;
1836
- try {
1837
- emitScopeNested(chain, process, 0, indent, summary, options.relabelFlows ?? true, sequence);
1921
+ const schemaEntries = [...schemaPlan.declare].map(([id, body]) => `{ id: ${str(id)}, body: ${body} }`);
1922
+ if (schemaEntries.length)
1923
+ chain.add(arrayCall('schemas', schemaEntries, chain.indent));
1924
+ // A folded body is written the way a declaration's would be — through the shared
1925
+ // `.schema()` plan when the document repeats it, inline when it does not — so
1926
+ // folding a declaration onto its row does not un-share a schema 27 nodes use.
1927
+ foldedVarSchemas = new Map([...folds].map(([id, body]) => [id, schemaPlan.ref.get(String(body)) ?? jsonLiteral(body)]));
1928
+ // A folded declaration is not written as a `.var()`, so its POSITION in the block
1929
+ // would be lost — derived declarations are appended after the declared ones, and the
1930
+ // designer's order is not recoverable from anything else (it follows the owning
1931
+ // element's document order in none of the five corpus files). Carry it in the
1932
+ // preservation block, which is where state like this goes and what says "not for
1933
+ // hand-editing". Only when something was actually folded: a document of explicit
1934
+ // `.var()` calls already states its order by their position, and the extra line would
1935
+ // be noise.
1936
+ if (derived.size) {
1937
+ const ids = [...(varsExt?.input ?? []), ...(varsExt?.inputOutput ?? []), ...(varsExt?.output ?? [])].map((v) => v.id);
1938
+ chain.add(`.preserve({ variableOrder: [${ids.map(str).join(', ')}] })`);
1939
+ }
1940
+ try {
1941
+ emitVariablesInto(chain, host.container, host.rehome?.to ?? host.container.id, derived, host.rehome, schemaPlan, publicDeclarationScopeOf(process, host.rehome?.to ?? host.container.id));
1942
+ if (options.style === 'nested') {
1943
+ const sequence = options.sequenceMode ?? true;
1944
+ const summary = { handlers: 0, branches: 0, nets: 0, leftFlat: [], renamedFlows: [], sequence };
1945
+ pinExplicitInBodies = sequence;
1946
+ try {
1947
+ emitScopeNested(chain, process, 0, indent, summary, options.relabelFlows ?? true, sequence);
1948
+ }
1949
+ finally {
1950
+ pinExplicitInBodies = false;
1951
+ }
1952
+ options.onLift?.(summary);
1838
1953
  }
1839
- finally {
1840
- pinExplicitInBodies = false;
1954
+ else {
1955
+ emitScope(chain, process, 0, indent);
1841
1956
  }
1842
- options.onLift?.(summary);
1843
1957
  }
1844
- else {
1845
- emitScope(chain, process, 0, indent);
1958
+ finally {
1959
+ foldedVarSchemas = undefined;
1846
1960
  }
1847
1961
  const importFrom = options.importSpecifier ?? './bpmn-sdk.js';
1848
1962
  const imported = ['bpmn', ...[...schemaPlan.imports].sort()].join(', ');
@@ -1876,8 +1990,10 @@ export async function decompile(xml, options = {}) {
1876
1990
  * the HITL case in the round-trip table missed it only because its mapped row writes
1877
1991
  * `decision` while its declared variable is `o`.
1878
1992
  */
1879
- function derivedVariableIds(process) {
1880
- // var id -> [owner element id, wire type the derived declaration carries]
1993
+ function derivedVariableIds(process, varsExt,
1994
+ /** The collaboration→process re-parenting, so `elementId` is compared post-move. */
1995
+ rehome) {
1996
+ // var id -> the declaration `implicitNodeVars` would derive for it
1881
1997
  const candidates = new Map();
1882
1998
  const walk = (container) => {
1883
1999
  for (const el of container.flowElements ?? []) {
@@ -1887,11 +2003,29 @@ function derivedVariableIds(process) {
1887
2003
  // not derived. A script task's `outputs` rows ARE — serialize declares each as a
1888
2004
  // `string` owned by the task, because the validator does not read the row as a
1889
2005
  // declaration — so they are candidates with the type that shape carries.
1890
- if (payload && type && type !== 'BPMN.Variables') {
1891
- const wire = type === 'BPMN.ScriptTask' ? 'string' : 'json';
1892
- for (const row of payload.output ?? [])
1893
- if (row.var)
1894
- candidates.set(row.var, [el.id, wire]);
2006
+ // `implicitNodeVars` derives from a node the SDK models as TYPED — a connector,
2007
+ // a connector event, an external task, a trigger start, a script task. It derives
2008
+ // nothing from an element whose payload merely DECORATES it: a boundary event's
2009
+ // `=Error` row is written by `errorVar` sugar, or declared by hand, but never by
2010
+ // the derivation. So the same G1 discriminator that picks the emitter picks the
2011
+ // candidates, and the two sets stay equal.
2012
+ //
2013
+ // Without it this set grew wider than serialize's the moment the schema fold let
2014
+ // a bodied declaration through: ten `error<n>` declarations owned by boundary
2015
+ // events in `claims-triage` were skipped as derived and then nobody derived them
2016
+ // — they vanished from the recompiled process.
2017
+ if (payload && type && type !== 'BPMN.Variables' && typeDescribesElement(localName(el.$type), type)) {
2018
+ for (const row of payload.output ?? []) {
2019
+ if (!row.var)
2020
+ continue;
2021
+ // A script task's rows are NOT spelled-out `outputRows` on the way back —
2022
+ // `implicitNodeVars` derives its declarations from `outputs` keys, so they
2023
+ // carry the id as the name and that arm's own wire type. Every other typed
2024
+ // node's rows go through `rowDeclaration`, the same call the serializer makes.
2025
+ candidates.set(row.var, type === 'BPMN.ScriptTask'
2026
+ ? { owner: el.id, wire: 'string' }
2027
+ : { owner: el.id, ...rowDeclaration({ name: row.name, type: row.type, schema: row.body }) });
2028
+ }
1895
2029
  }
1896
2030
  if (el.$type === 'bpmn:SubProcess')
1897
2031
  walk(el);
@@ -1905,18 +2039,38 @@ function derivedVariableIds(process) {
1905
2039
  // — including the older shape with no `elementId`, which recompiles identically only
1906
2040
  // because `emitVariablesInto` then writes it as `elementId: null`.
1907
2041
  const out = new Set();
1908
- const vars = (process.extensionElements?.values ?? []).find((x) => x.$type === 'uipath:Variables');
1909
- for (const v of vars?.inputOutput ?? []) {
2042
+ // Declarations whose only difference from a derived one is the schema body they
2043
+ // carry — foldable onto the row as `varSchema`, which derives them exactly.
2044
+ const folds = new Map();
2045
+ for (const v of varsExt?.inputOutput ?? []) {
1910
2046
  const c = candidates.get(v.id);
1911
2047
  if (!c)
1912
2048
  continue;
1913
- if (v.name !== undefined && v.name !== v.id)
2049
+ // The derived declaration takes the ROW's name when it has one, the id otherwise.
2050
+ if ((v.name ?? v.id) !== (c.name ?? v.id))
1914
2051
  continue;
1915
- if (v.type !== c[1])
2052
+ if (v.type !== c.wire)
1916
2053
  continue;
1917
- if (v.default !== undefined || v.elementId !== c[0])
2054
+ const elementId = rehome && v.elementId === rehome.from ? rehome.to : v.elementId;
2055
+ if (v.default !== undefined || elementId !== c.owner)
1918
2056
  continue;
2057
+ // `implicitNodeVars` writes none of these, so a declaration carrying one was not
2058
+ // derived however well the rest of it matches — and omitting it here re-derived a
2059
+ // plain copy, dropping the attribute. 22 declarations in the corpus.
2060
+ if (v.custom !== undefined || v.internal !== undefined || v.subType !== undefined)
2061
+ continue;
2062
+ // The body. A declaration with none matches a row with none. One that carries a
2063
+ // body is still derivable IF the row can be made to state it — `varSchema` — and
2064
+ // the row does not already claim a different body for the variable. This is the
2065
+ // common case by a wide margin: 409 declarations in the corpus carry a body
2066
+ // against 28 rows, so without the fold almost every structured output stays an
2067
+ // explicit `.var()`.
2068
+ if (String(v.body ?? '') !== String(c.schema ?? '')) {
2069
+ if (c.schema !== undefined || v.body === undefined)
2070
+ continue;
2071
+ folds.set(v.id, v.body);
2072
+ }
1919
2073
  out.add(v.id);
1920
2074
  }
1921
- return out;
2075
+ return { ids: out, folds };
1922
2076
  }
@@ -18,7 +18,7 @@ import { createRequire } from 'node:module';
18
18
  import { connectorOutputVars, } from './bpmn-sdk.js';
19
19
  import { createConnectorTask } from './connector.js';
20
20
  import { createConnectorEvent, createExternalTask, intsvcOutputVar } from './intsvc.js';
21
- import { createExtensionPayload, createTypedNode, typedNodeOutputVar, verbatimOutputRows } from './typed-node.js';
21
+ import { createExtensionPayload, createTypedNode, rowDeclaration, typedNodeOutputVar, verbatimOutputRows } from './typed-node.js';
22
22
  import { typedNodeDeclaredVars } from './bpmn-sdk.js';
23
23
  const require = createRequire(import.meta.url);
24
24
  const uipathDescriptor = require('./uipath-moddle.v1.json');
@@ -84,6 +84,7 @@ export async function serialize(built, opts = {}) {
84
84
  library: opts.library,
85
85
  bindings: opts.bindings,
86
86
  schemas: built.schemas,
87
+ variableOrder: built.preservation?.variableOrder,
87
88
  bindingSink: new Map(),
88
89
  varTypeOf: (varId) => authoredVarType.get(varId),
89
90
  };
@@ -104,7 +105,7 @@ export async function serialize(built, opts = {}) {
104
105
  // effect — then assemble the process extensionElements from the result.
105
106
  const flowElements = buildScope(ctx, built.nodes, built.flows);
106
107
  const processExt = [];
107
- const varsExt = buildVariablesExt(ctx, rootVars, built.id, publicDeclarationScope(built.nodes, built.id));
108
+ const varsExt = buildVariablesExt(ctx, rootVars, built.id, publicDeclarationScope(built.nodes, built.id), built.preservation?.variableOrder);
108
109
  if (varsExt)
109
110
  processExt.push(varsExt);
110
111
  // Author-declared bindings first, then the ones connector wiring derived —
@@ -507,7 +508,7 @@ function createSubProcess(ctx, n) {
507
508
  // DUPLICATE_DOCUMENT_ID the emitter goes out of its way to avoid everywhere else
508
509
  // (the XML parser drops the second element that reuses an id).
509
510
  const enclosing = [...n.variables, ...[...ctx.varType.keys()].map((id) => ({ id }))];
510
- const varsExt = buildVariablesExt(ctx, [...n.variables, ...implicitNodeVars(n.nodes, enclosing)], n.id);
511
+ const varsExt = buildVariablesExt(ctx, [...n.variables, ...implicitNodeVars(n.nodes, enclosing)], n.id, undefined, ctx.variableOrder);
511
512
  if (varsExt)
512
513
  values.push(varsExt);
513
514
  if (values.length)
@@ -629,11 +630,13 @@ function assignmentSource(value) {
629
630
  export function implicitNodeVars(nodes, declared) {
630
631
  const taken = new Set(declared.map((v) => v.id));
631
632
  const out = [];
632
- const claim = (id, ownerId, type = 'object') => {
633
+ const claim = (id, ownerId, type = 'object',
634
+ /** The row that implies it, when one spelled the declaration out — see `rowDeclaration`. */
635
+ from) => {
633
636
  if (!id || taken.has(id))
634
637
  return;
635
638
  taken.add(id);
636
- out.push({ id, name: id, type, direction: 'inputOutput', elementId: ownerId });
639
+ out.push({ id, name: from?.name ?? id, type, direction: 'inputOutput', elementId: ownerId, schema: from?.schema });
637
640
  };
638
641
  for (const n of nodes) {
639
642
  if (n.kind === 'connector') {
@@ -647,8 +650,23 @@ export function implicitNodeVars(nodes, declared) {
647
650
  // skips re-declaring EVERY output row's variable, so a row claimed by neither
648
651
  // side vanishes from the recompiled process. Measured on a Slack connector —
649
652
  // `send_Error` was declared in the original and absent after the round trip.
650
- for (const varId of typedNodeDeclaredVars(n))
651
- claim(varId, n.id);
653
+ //
654
+ // A SPELLED-OUT row states the declaration too — its `name`, `type` and schema
655
+ // body are what the product copies onto the variable — so the derived one is
656
+ // built from the row rather than left as a bare `json` named after its id. That
657
+ // is what keeps a node's own outputs inside the node: 83 of the 112 declarations
658
+ // in `claims-intake-hitl` are this shape, and every one of them had to be
659
+ // restated as a `.var()` because the derivation could not reach the row.
660
+ if (n.outputRows) {
661
+ for (const row of n.outputRows) {
662
+ const d = rowDeclaration(row);
663
+ claim(row.var, n.id, d.type, d);
664
+ }
665
+ }
666
+ else {
667
+ for (const varId of typedNodeDeclaredVars(n))
668
+ claim(varId, n.id);
669
+ }
652
670
  }
653
671
  else if (n.kind === 'connectorEvent' || n.kind === 'externalTask') {
654
672
  // Same reason as the typed and connector arms: the node's output rows
@@ -808,12 +826,15 @@ function defaultAttr(value, mappedType) {
808
826
  */
809
827
  function buildVariablesExt(ctx, vars, ownerId,
810
828
  /** The default scope of a declaration the author left unscoped; the owner when absent. */
811
- scopeOf = () => ownerId) {
829
+ scopeOf = () => ownerId,
830
+ /** Declaration order pinned by `.preserve()`; ids it omits keep their relative order, after. */
831
+ order) {
812
832
  const { m } = ctx;
813
833
  if (!vars.length)
814
834
  return undefined;
815
835
  const groups = { input: [], inputOutput: [], output: [] };
816
- for (const v of vars) {
836
+ const ordered = orderVars(vars, order);
837
+ for (const v of ordered) {
817
838
  const el = m.create('uipath:Variable', {
818
839
  id: v.id,
819
840
  name: v.name,
@@ -823,6 +844,10 @@ scopeOf = () => ownerId) {
823
844
  // Carried, never interpreted — a schema field this SDK does not model survives.
824
845
  body: schemaBody(ctx, v.schema),
825
846
  required: v.required ? 'true' : undefined,
847
+ // Designer bookkeeping, carried rather than interpreted — see `VarOpts.custom`.
848
+ custom: v.custom ? 'true' : undefined,
849
+ internal: v.internal ? 'true' : undefined,
850
+ subType: v.subType,
826
851
  });
827
852
  groups[v.direction].push(el);
828
853
  }
@@ -835,6 +860,24 @@ scopeOf = () => ownerId) {
835
860
  props.output = groups.output;
836
861
  return m.create('uipath:Variables', props);
837
862
  }
863
+ /**
864
+ * Declarations in the order `.preserve({ variableOrder })` pinned, stably.
865
+ *
866
+ * A derived declaration is appended to the author's list (`rootVars`), so without a
867
+ * pinned order every variable a node's output row fills lands after every declared one
868
+ * — which is not where the document had it, and the variables panel is a list a person
869
+ * reads. An id the order omits keeps its relative position, after the ones it names,
870
+ * so a partial order is still useful and an unknown id is never dropped.
871
+ */
872
+ function orderVars(vars, order) {
873
+ if (!order?.length)
874
+ return vars;
875
+ const rank = new Map(order.map((id, i) => [id, i]));
876
+ return vars
877
+ .map((v, i) => ({ v, i, r: rank.get(v.id) ?? Number.POSITIVE_INFINITY }))
878
+ .sort((a, b) => a.r - b.r || a.i - b.i)
879
+ .map((e) => e.v);
880
+ }
838
881
  /**
839
882
  * Where an unscoped PUBLIC declaration belongs at the top level of a process.
840
883
  *
@@ -15,6 +15,7 @@
15
15
  */
16
16
  import type BpmnModdle from 'bpmn-moddle';
17
17
  import type { ModdleElement } from 'bpmn-moddle';
18
+ import { type TypeDesc } from '../core/expr.js';
18
19
  type El = ModdleElement;
19
20
  /** One context field of a typed node, as the registry describes it. */
20
21
  export interface RegistryContextField {
@@ -206,6 +207,42 @@ export interface TypedInputRow {
206
207
  /** The row's CDATA body, for a `json` row — the value itself, or a string when the exact bytes matter. */
207
208
  body?: unknown;
208
209
  }
210
+ /**
211
+ * The variable declaration a spelled-out output row implies.
212
+ *
213
+ * A node's output row and the declaration that receives it are two halves of one
214
+ * fact, and the product writes them that way: the row's `name`, `type` and schema
215
+ * body are copied onto the `uipath:inputOutput` it fills. Measured over
216
+ * `research/bpmn-roundtrip/corpus/`, 688 of 842 declarations are exactly this — a
217
+ * node's own output, attributed to the node, with no `custom` flag — against 119 an
218
+ * author typed into the variables panel.
219
+ *
220
+ * So the row is the source of truth and the declaration is derived from it, rather
221
+ * than the author stating the same `name`/`type`/schema twice. `implicitNodeVars`
222
+ * builds the declaration from this, and `derivedVariableIds` uses `wire` to decide
223
+ * that a declaration on disk looks derived and must not be echoed. **Both call this
224
+ * function**: the two sets have to be equal, and the way they stayed unequal before
225
+ * was each computing the shape for itself.
226
+ *
227
+ * A row's type is carried onto the declaration only when it NAMES a variable type.
228
+ * A row may instead be typed with the node's registry type — `type="Actions.HITL"` on
229
+ * the response row `typedNodeOutputVar` derives — and the variable behind that is
230
+ * `json`, not a type called `Actions.HITL`. `undefined` lands there too: `json` is
231
+ * what every derived declaration carried before the row was consulted, and a row with
232
+ * no type states nothing to the contrary.
233
+ *
234
+ * Where a row does name one, the product agrees with it: 776 of the 796 row/declaration
235
+ * pairs in the corpus carry the same type. The 20 that differ (a `json` row against a
236
+ * `jsonSchema` declaration, and four one-offs) simply fail the match and stay explicit
237
+ * `.var()` lines, which is the safe direction — a declaration that does not look
238
+ * derived is the author's.
239
+ */
240
+ export declare function rowDeclaration(row: TypedOutputRow): {
241
+ type: TypeDesc;
242
+ wire: string;
243
+ name?: string;
244
+ schema?: unknown;
245
+ };
209
246
  export interface TypedOutputRow {
210
247
  /** Row name, e.g. `response` or `Error`. */
211
248
  name: string;
@@ -217,6 +254,21 @@ export interface TypedOutputRow {
217
254
  source?: string;
218
255
  /** The row's body — a JSON Schema for a `jsonSchema` row. */
219
256
  schema?: unknown;
257
+ /**
258
+ * The schema body of the VARIABLE this row fills — not the row's own `schema`.
259
+ *
260
+ * The two are different places on the wire, and the product uses the second one:
261
+ * across `research/bpmn-roundtrip/corpus/` only 28 output rows carry a body of their
262
+ * own, while 409 declarations do. So a `jsonSchema` output is normally a bare row
263
+ * plus a `uipath:inputOutput` carrying the shape — which is why stating the row was
264
+ * not enough to derive the declaration, and why a node's outputs had to be restated
265
+ * as `.var()` lines beside it.
266
+ *
267
+ * Setting this keeps the whole declaration on the row: `implicitNodeVars` builds the
268
+ * variable with this as its body, and nothing about the emitted XML changes — the
269
+ * body still lands on the declaration, never on the row.
270
+ */
271
+ varSchema?: unknown;
220
272
  /**
221
273
  * Mark the row `custom="true"` — the product's flag for a row an author added
222
274
  * rather than one the node's type declares.
@@ -1,5 +1,6 @@
1
1
  import { createRequire } from 'node:module';
2
2
  import { resolveFromRegistry } from './registry-fallback.js';
3
+ import { types } from '../core/expr.js';
3
4
  const require = createRequire(import.meta.url);
4
5
  const REGISTRY = require('./registry-types.json');
5
6
  /** Every extension type in the snapshot, sorted. */
@@ -115,6 +116,48 @@ export const REGISTRY_GAPS = {
115
116
  // same spec, opposite enforcement.
116
117
  'Maestro.ReceiveMessageEvent': { outputRowsFixed: true },
117
118
  };
119
+ /**
120
+ * The variable declaration a spelled-out output row implies.
121
+ *
122
+ * A node's output row and the declaration that receives it are two halves of one
123
+ * fact, and the product writes them that way: the row's `name`, `type` and schema
124
+ * body are copied onto the `uipath:inputOutput` it fills. Measured over
125
+ * `research/bpmn-roundtrip/corpus/`, 688 of 842 declarations are exactly this — a
126
+ * node's own output, attributed to the node, with no `custom` flag — against 119 an
127
+ * author typed into the variables panel.
128
+ *
129
+ * So the row is the source of truth and the declaration is derived from it, rather
130
+ * than the author stating the same `name`/`type`/schema twice. `implicitNodeVars`
131
+ * builds the declaration from this, and `derivedVariableIds` uses `wire` to decide
132
+ * that a declaration on disk looks derived and must not be echoed. **Both call this
133
+ * function**: the two sets have to be equal, and the way they stayed unequal before
134
+ * was each computing the shape for itself.
135
+ *
136
+ * A row's type is carried onto the declaration only when it NAMES a variable type.
137
+ * A row may instead be typed with the node's registry type — `type="Actions.HITL"` on
138
+ * the response row `typedNodeOutputVar` derives — and the variable behind that is
139
+ * `json`, not a type called `Actions.HITL`. `undefined` lands there too: `json` is
140
+ * what every derived declaration carried before the row was consulted, and a row with
141
+ * no type states nothing to the contrary.
142
+ *
143
+ * Where a row does name one, the product agrees with it: 776 of the 796 row/declaration
144
+ * pairs in the corpus carry the same type. The 20 that differ (a `json` row against a
145
+ * `jsonSchema` declaration, and four one-offs) simply fail the match and stay explicit
146
+ * `.var()` lines, which is the safe direction — a declaration that does not look
147
+ * derived is the author's.
148
+ */
149
+ export function rowDeclaration(row) {
150
+ const type = (() => {
151
+ if (row.type === undefined || row.type === 'json')
152
+ return 'object';
153
+ if (row.type === 'bool')
154
+ return 'boolean';
155
+ return Object.hasOwn(types, row.type) ? row.type : 'object';
156
+ })();
157
+ // The serializer's `mapType`, which collapses the three structured descriptors.
158
+ const wire = type === 'object' || type === 'array' || type === 'any' ? 'json' : type;
159
+ return { type, wire, name: row.name, schema: row.varSchema };
160
+ }
118
161
  /**
119
162
  * An output name reduced to something an expression can actually read.
120
163
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uipath/maestro-builder-sdk",
3
- "version": "6.14.0",
3
+ "version": "6.14.1",
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",
@@ -92,5 +92,5 @@
92
92
  "@types/node": "^22.7.0",
93
93
  "esbuild": "^0.28.1"
94
94
  },
95
- "gitref": "84bd49a28ffaae311bcb7b3207a311dfeaf1c08f"
95
+ "gitref": "00178a53b6c63ad810cda275b16ac8170422f9ae"
96
96
  }