@uipath/maestro-builder-sdk 6.37.0 → 6.38.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.
@@ -163,6 +163,10 @@ export type BpmnNode = {
163
163
  inputs: Record<string, unknown>;
164
164
  /** Variable the response lands in; defaults to `<id>_response`. */
165
165
  outputVar?: string;
166
+ /** Variable the error payload lands in; defaults to `<id>_Error`. */
167
+ errorVar?: string;
168
+ /** Extra mapped output rows, by the variable each writes — see `BpmnConnectorOpts.outputs`. */
169
+ outputs?: Record<string, string | ConnectorOutputRow>;
166
170
  /** `=`-expression that, when true, skips this activity (`uipath:activity/@skipCondition`). */
167
171
  skipCondition?: string;
168
172
  }) | (ActivityNodeFields & {
@@ -3140,6 +3144,20 @@ declare abstract class ScopeBuilder {
3140
3144
  vars(entries: VarEntry[]): this;
3141
3145
  private declareVar;
3142
3146
  }
3147
+ /**
3148
+ * One extra output row on a connector task, spelled out — the object form of an entry in
3149
+ * {@link BpmnConnectorOpts}' `outputs`.
3150
+ */
3151
+ export interface ConnectorOutputRow {
3152
+ /** The `=`-expression against the result, e.g. `'=response.Id'`. */
3153
+ source: string;
3154
+ /** The row's `name`; defaults to the variable. The designer names a row after the field it reads. */
3155
+ name?: string;
3156
+ /** The row's `type`; defaults to the variable's declared type, else `string`. */
3157
+ type?: string;
3158
+ /** The designer's `custom="true"` mark on a row the author added in the variables panel. */
3159
+ custom?: boolean;
3160
+ }
3143
3161
  /**
3144
3162
  * `.connector()` options: the shared connector options, plus the BPMN-only
3145
3163
  * display `name` and output-variable override.
@@ -3164,6 +3182,21 @@ export type BpmnConnectorOpts = ConnectorOpts & ActivityOpts & {
3164
3182
  * always lands in `<id>_Error`.
3165
3183
  */
3166
3184
  outputVar?: string;
3185
+ /**
3186
+ * Variable the error payload lands in. Defaults to `<id>_Error`, the name every
3187
+ * `.connector()` has always written; an import names whatever the file bound
3188
+ * (`error12`), so decompile can lift a connector task without renaming its variable.
3189
+ */
3190
+ errorVar?: string;
3191
+ /**
3192
+ * Extra output rows beside the response and the Error, by the variable each
3193
+ * writes: `{ invoiceId: '=response.Id' }` writes `<uipath:output name="invoiceId"
3194
+ * source="=response.Id" var="invoiceId"/>`; the object form names and types the row
3195
+ * (`{ id5: { source: '=response.Id', name: 'Id', type: 'string' } }`), which is how a
3196
+ * designer-added row reads back from an import. Each variable is declared for you
3197
+ * unless you declared it.
3198
+ */
3199
+ outputs?: Record<string, string | ConnectorOutputRow>;
3167
3200
  };
3168
3201
  /** A sub-process body: the same graph methods, plus an internal node builder. */
3169
3202
  export declare class SubProcessBuilder extends ScopeBuilder {
@@ -943,9 +943,13 @@ class ScopeBuilder {
943
943
  object: spec.object,
944
944
  inputs: spec.inputs,
945
945
  outputVar: bpmnOpts.outputVar,
946
+ errorVar: bpmnOpts.errorVar,
947
+ outputs: bpmnOpts.outputs,
946
948
  skipCondition: bpmnOpts.skipCondition,
949
+ tags: bpmnOpts.tags,
947
950
  retry: bpmnOpts.retry,
948
951
  loop: bpmnOpts.loop,
952
+ errorMapping: bpmnOpts.errorMapping,
949
953
  }, body);
950
954
  }
951
955
  /**
@@ -99,6 +99,21 @@ function checkConnectors(built, opts, diags) {
99
99
  checkConnectorLookups(where, n, opts, EQUALS_EXPR_DIALECT, report);
100
100
  checkConnectorSchema(where, n, opts, EQUALS_EXPR_DIALECT, report);
101
101
  checkConnectorContract(where, n, opts, report);
102
+ // Two rows writing one variable: an `outputs` key that is also the response or
103
+ // error variable. Both rows would be emitted and the later write wins at run time.
104
+ const response = n.outputVar ?? `${n.id}_response`;
105
+ const error = n.errorVar ?? `${n.id}_Error`;
106
+ for (const varId of Object.keys(n.outputs ?? {})) {
107
+ if (varId !== response && varId !== error)
108
+ continue;
109
+ report({
110
+ level: 'error',
111
+ code: 'CONNECTOR_OUTPUT_VAR_CLASH',
112
+ message: `${where}: outputs maps "${varId}", which is already the task's ${varId === response ? 'response' : 'error'} variable ` +
113
+ `(\`${varId === response ? 'outputVar' : 'errorVar'}\`); two rows would write it. Map the extra row to another variable, ` +
114
+ `or drop it and read \`vars.${varId}\` directly.`,
115
+ });
116
+ }
102
117
  }
103
118
  }
104
119
  /**
@@ -0,0 +1,32 @@
1
+ import type { Library } from '../core/library.js';
2
+ import type { ConnectorLiftSummary } from './decompile.js';
3
+ type Moddle = any;
4
+ /** The outcome of verifying one decompile's lifted connector tasks. */
5
+ export interface ConnectorLiftVerdict {
6
+ /** Lifted elements whose recompiled task differs beyond the regenerated fields, with what differed. */
7
+ failed: Map<string, string>;
8
+ }
9
+ /**
10
+ * The part of a connector task that a lift must reproduce, as one comparable
11
+ * string. Row order does not matter (rows are sorted by name); a scalar row's
12
+ * `type` attribute and the response row's schema body do not either.
13
+ */
14
+ export declare function canonicalConnectorTask(el: Moddle): string;
15
+ /**
16
+ * Whether each lifted connector task recompiles to the task the file carried, the
17
+ * regenerated fields aside.
18
+ *
19
+ * Each call is compiled ON ITS OWN — the decompiled preamble (bindings, variables,
20
+ * schemas), a start event, the one `.connector()` line, an end event — rather than
21
+ * the whole process, so a lift the library refuses (a body field it does not carry)
22
+ * is the only one refused, and a process with no other way to compile (a start event
23
+ * missing its connection, say) still gets its connector tasks verified.
24
+ *
25
+ * @param originalXml - The `.bpmn` that was decompiled.
26
+ * @param summary - What the lift did: the ids, the calls, and the preamble to compile them under.
27
+ * @param sdkEntry - Absolute path of the `bpmn-sdk.js` to build against.
28
+ * @param library - The connector library the lift named operations through.
29
+ * @returns The lifted elements that did not come back the same, each with the reason.
30
+ */
31
+ export declare function verifyConnectorLifts(originalXml: string, summary: Pick<ConnectorLiftSummary, 'lifted' | 'calls' | 'preamble'>, sdkEntry: string, library: Library): Promise<ConnectorLiftVerdict>;
32
+ export {};
@@ -0,0 +1,220 @@
1
+ /**
2
+ * connector-lift-verify — the check that makes decompile's connector lift safe to write.
3
+ *
4
+ * A connector task written back as `.connector(key, action, inputs, …)` is NOT
5
+ * byte-identical on recompile, by design: the SDK regenerates what the canvas wrote
6
+ * and the file carried — the `metadata` blob (`designTimeMetadata`, `metadataState`,
7
+ * `telemetryData`), the `operation` verb, the `type` of each scalar row, the response
8
+ * row's schema body, `activityConfigurationVersion`. Everything else about the task
9
+ * must come back exactly: the element and its name, every other context row, every
10
+ * payload row's name, target and value, every output row's name, source and variable.
11
+ * This builds the lifted source, compiles it with the same library, and compares each
12
+ * lifted element against the original under exactly that list — so a lift that lost
13
+ * an input, picked the wrong action or renamed a variable is refused, element by
14
+ * element, and `decompile-cli` writes those verbatim with the reason.
15
+ *
16
+ * Same stance as `lift-verify.ts` for `--style nested`: the lift's rules are an
17
+ * argument, this is the measurement.
18
+ */
19
+ import BpmnModdle from 'bpmn-moddle';
20
+ import { createRequire } from 'node:module';
21
+ import { buildSource } from './lift-verify.js';
22
+ import { serialize } from './serialize.js';
23
+ const require = createRequire(import.meta.url);
24
+ const uipathDescriptor = require('./uipath-moddle.v1.json');
25
+ /**
26
+ * Context rows the SDK regenerates from the library on compile; not compared.
27
+ * `activity` is the op identity the SDK writes beside `objectName` (the platform
28
+ * validator requires it, exports omit it) and `operation` the library's verb where
29
+ * the canvas writes the curated name; `metadata` is the canvas blob; the
30
+ * configuration version is #1287's stamp.
31
+ */
32
+ const REGENERATED_CONTEXT = new Set(['metadata', 'operation', 'activity', 'activityConfigurationVersion']);
33
+ function stableJson(value) {
34
+ return JSON.stringify(value, (_key, v) => v && typeof v === 'object' && !Array.isArray(v)
35
+ ? Object.fromEntries(Object.keys(v).sort().map((k) => [k, v[k]]))
36
+ : v);
37
+ }
38
+ /** A row body as a comparable string: parsed JSON when it is JSON, the text otherwise. */
39
+ function bodyText(body) {
40
+ if (body === undefined || body === null)
41
+ return undefined;
42
+ const text = String(body);
43
+ try {
44
+ return stableJson(JSON.parse(text));
45
+ }
46
+ catch {
47
+ return text;
48
+ }
49
+ }
50
+ function payloadOf(el) {
51
+ return (el.extensionElements?.values ?? []).find((x) => x.$type === 'uipath:Activity');
52
+ }
53
+ /**
54
+ * The part of a connector task that a lift must reproduce, as one comparable
55
+ * string. Row order does not matter (rows are sorted by name); a scalar row's
56
+ * `type` attribute and the response row's schema body do not either.
57
+ */
58
+ export function canonicalConnectorTask(el) {
59
+ const payload = payloadOf(el);
60
+ const context = Object.fromEntries((payload?.context?.input ?? [])
61
+ .filter((r) => !REGENERATED_CONTEXT.has(r.name))
62
+ .map((r) => [r.name, r.body !== undefined ? bodyText(r.body) : (r.value ?? '')]));
63
+ const inputs = (payload?.input ?? [])
64
+ .map((r) => ({ name: r.name, target: r.target ?? null, value: r.body !== undefined ? bodyText(r.body) : String(r.value ?? '') }))
65
+ // `.connector()` always writes the request body row, `{}` when nothing fills it;
66
+ // an export writes none for an operation with no body. The same request either way.
67
+ .filter((r) => !(r.name === 'body' && r.target === 'body' && r.value === '{}'))
68
+ .sort((a, b) => String(a.name).localeCompare(String(b.name)));
69
+ const outputs = (payload?.output ?? [])
70
+ .map((r) => ({ name: r.name, source: r.source ?? null, var: r.var ?? null, custom: r.custom ?? null, target: r.target ?? null }))
71
+ .sort((a, b) => String(a.name).localeCompare(String(b.name)) || String(a.var).localeCompare(String(b.var)));
72
+ // The activity's other extensions — tags, retry, error mapping — and its loop
73
+ // characteristics: options `.connector()` takes, so a lift that lost one is refused.
74
+ const extensions = (el.extensionElements?.values ?? [])
75
+ .filter((x) => x.$type !== 'uipath:Activity' && x.$type !== 'uipath:EntryPointId')
76
+ .map(plain)
77
+ .sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b)));
78
+ return stableJson({
79
+ element: el.$type,
80
+ name: el.name ?? null,
81
+ type: payload?.type?.value ?? null,
82
+ skipCondition: payload?.skipCondition ?? null,
83
+ context,
84
+ inputs,
85
+ outputs,
86
+ extensions,
87
+ loop: el.loopCharacteristics ? plain(el.loopCharacteristics) : null,
88
+ });
89
+ }
90
+ /** A moddle element as plain data: its own properties, recursively, without the `$`-prefixed bookkeeping. */
91
+ function plain(value) {
92
+ if (Array.isArray(value))
93
+ return value.map(plain);
94
+ if (value && typeof value === 'object') {
95
+ const out = { $type: value.$type };
96
+ for (const [k, v] of Object.entries(value)) {
97
+ if (k.startsWith('$'))
98
+ continue;
99
+ out[k] = plain(v);
100
+ }
101
+ return out;
102
+ }
103
+ return value;
104
+ }
105
+ /**
106
+ * Every variable id the document declares or a node's output row writes. An isolated
107
+ * call still reads the process's variables in its expressions (`vars.response11[0].Id`),
108
+ * and the build's expression check refuses a read of an undeclared one — so the ones
109
+ * the preamble does not already declare are stubbed into the verification source.
110
+ */
111
+ function declaredVariableIds(root) {
112
+ const ids = new Set();
113
+ const fromExt = (owner) => {
114
+ for (const ext of owner.extensionElements?.values ?? []) {
115
+ if (ext.$type !== 'uipath:Variables')
116
+ continue;
117
+ for (const list of [ext.input, ext.inputOutput, ext.output])
118
+ for (const v of list ?? [])
119
+ if (v.id)
120
+ ids.add(v.id);
121
+ }
122
+ };
123
+ const walk = (container) => {
124
+ fromExt(container);
125
+ for (const el of container.flowElements ?? []) {
126
+ fromExt(el);
127
+ for (const ext of el.extensionElements?.values ?? [])
128
+ for (const row of ext.output ?? [])
129
+ if (row.var)
130
+ ids.add(row.var);
131
+ if (el.$type === 'bpmn:SubProcess')
132
+ walk(el);
133
+ }
134
+ };
135
+ for (const r of root.rootElements ?? []) {
136
+ if (r.$type === 'bpmn:Process')
137
+ walk(r);
138
+ else
139
+ fromExt(r);
140
+ }
141
+ return [...ids];
142
+ }
143
+ function elementsById(root) {
144
+ const out = new Map();
145
+ const walk = (container) => {
146
+ for (const el of container.flowElements ?? []) {
147
+ out.set(el.id, el);
148
+ if (el.$type === 'bpmn:SubProcess')
149
+ walk(el);
150
+ }
151
+ };
152
+ for (const r of root.rootElements ?? [])
153
+ if (r.$type === 'bpmn:Process')
154
+ walk(r);
155
+ return out;
156
+ }
157
+ /**
158
+ * Whether each lifted connector task recompiles to the task the file carried, the
159
+ * regenerated fields aside.
160
+ *
161
+ * Each call is compiled ON ITS OWN — the decompiled preamble (bindings, variables,
162
+ * schemas), a start event, the one `.connector()` line, an end event — rather than
163
+ * the whole process, so a lift the library refuses (a body field it does not carry)
164
+ * is the only one refused, and a process with no other way to compile (a start event
165
+ * missing its connection, say) still gets its connector tasks verified.
166
+ *
167
+ * @param originalXml - The `.bpmn` that was decompiled.
168
+ * @param summary - What the lift did: the ids, the calls, and the preamble to compile them under.
169
+ * @param sdkEntry - Absolute path of the `bpmn-sdk.js` to build against.
170
+ * @param library - The connector library the lift named operations through.
171
+ * @returns The lifted elements that did not come back the same, each with the reason.
172
+ */
173
+ export async function verifyConnectorLifts(originalXml, summary, sdkEntry, library) {
174
+ const failed = new Map();
175
+ if (summary.lifted.length === 0)
176
+ return { failed };
177
+ const moddle = new BpmnModdle({ uipath: uipathDescriptor });
178
+ const original = (await moddle.fromXML(originalXml, { lax: true })).rootElement;
179
+ const before = elementsById(original);
180
+ // Declared by the preamble means a `.var()` call or a `.vars([{ id, … }])` entry — not
181
+ // a mention in `.preserve({ variableOrder })`, which lists derived declarations the
182
+ // preamble does not write.
183
+ const declares = (v) => {
184
+ const q = JSON.stringify(v).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
185
+ return new RegExp(`\\.var\\(${q}|\\{ id: ${q}, (?:name|type|direction)`).test(summary.preamble);
186
+ };
187
+ const stubs = declaredVariableIds(original)
188
+ .filter((v) => !declares(v))
189
+ .map((v) => ` .var(${JSON.stringify(v)}, "object")`)
190
+ .join('\n');
191
+ for (const id of summary.lifted) {
192
+ const call = summary.calls.get(id);
193
+ const a = before.get(id);
194
+ if (!call || !a) {
195
+ failed.set(id, call ? 'not in the original' : 'the lift recorded no call for it');
196
+ continue;
197
+ }
198
+ const source = `${summary.preamble}\n${stubs ? `${stubs}\n` : ''} .startEvent("verify_start")\n ${call}\n .endEvent("verify_end")\n` +
199
+ ` .sequenceFlow("verify_start", ${JSON.stringify(id)})\n .sequenceFlow(${JSON.stringify(id)}, "verify_end")\n .build();\n`;
200
+ let recompiled;
201
+ try {
202
+ recompiled = await serialize(await buildSource(source, sdkEntry), { library });
203
+ }
204
+ catch (e) {
205
+ // The first three lines: the refusal and, for a build error, the diagnostic under it.
206
+ failed.set(id, `the lifted call did not compile: ${e.message.split('\n').slice(0, 3).join(' ').trim()}`);
207
+ continue;
208
+ }
209
+ const b = elementsById((await moddle.fromXML(recompiled, { lax: true })).rootElement).get(id);
210
+ if (!b) {
211
+ failed.set(id, 'not in the recompiled process');
212
+ continue;
213
+ }
214
+ const ca = canonicalConnectorTask(a);
215
+ const cb = canonicalConnectorTask(b);
216
+ if (ca !== cb)
217
+ failed.set(id, `recompiled task differs:\n was ${ca}\n now ${cb}`);
218
+ }
219
+ return { failed };
220
+ }
@@ -33,6 +33,18 @@ export interface ConnectorCtx {
33
33
  library?: Library;
34
34
  bindings?: Bindings;
35
35
  bindingSink: Map<string, Record<string, unknown>>;
36
+ /**
37
+ * The bindings the author declared in source (`.binding()`), by id. A connector
38
+ * naming one of them takes its `resourceKey` and `default` from the declaration,
39
+ * the way it takes them from a bindings.json entry: without this, a `.binding('c',
40
+ * { value: '<guid>' })` paired with `connection: 'c'` wrote `resourceKey="c"` into
41
+ * the task's context — the id where every real export writes the connection's key.
42
+ */
43
+ declaredBindings?: ReadonlyMap<string, {
44
+ name?: string;
45
+ default?: string;
46
+ resourceKey?: string;
47
+ }>;
36
48
  /**
37
49
  * Declared Maestro `type` for a variable id, for typing an author-mapped output
38
50
  * row. Optional so the connector emitter (which writes its own rows) is
@@ -116,6 +116,21 @@ export function createConnectorTask(ctx, n) {
116
116
  },
117
117
  }),
118
118
  }),
119
+ // Extra mapped rows — `outputs: { <var>: '=response.<field>' }`, or the object
120
+ // form naming and typing the row — after the two the library implies, where
121
+ // every export puts the rows an author added in the designer.
122
+ ...Object.entries(n.outputs ?? {}).map(([varId, spec]) => {
123
+ const row = typeof spec === 'string' ? { source: spec } : spec;
124
+ const props = {
125
+ name: row.name ?? varId,
126
+ type: row.type ?? ctx.varTypeOf?.(varId) ?? 'string',
127
+ source: row.source,
128
+ var: varId,
129
+ };
130
+ if (row.custom)
131
+ props.custom = true;
132
+ return m.create('uipath:Output', props);
133
+ }),
119
134
  ],
120
135
  });
121
136
  return m.create('bpmn:SendTask', {
@@ -180,7 +195,7 @@ export function connectorBindingRef(ctx, name, resource, propertyAttribute, disp
180
195
  function bindingRef(ctx, name, resource, propertyAttribute, displayName, companionKey) {
181
196
  if (!name)
182
197
  return undefined;
183
- const entry = ctx.bindings?.entry(name);
198
+ const entry = (ctx.bindings?.entry(name) ?? ctx.declaredBindings?.get(name));
184
199
  const id = entry?.id ?? name;
185
200
  // `default` is the deploy-time VALUE the runtime resolves, not the author's symbolic
186
201
  // name for the binding. An entry that states only a `resourceKey` — which is what a
@@ -15,7 +15,7 @@ import { typedNodeAdditionalOutputVars, typedNodeOutputVar } from './typed-node.
15
15
  * @internal
16
16
  */
17
17
  export function connectorOutputVars(n) {
18
- return { response: n.outputVar ?? `${n.id}_response`, error: `${n.id}_Error` };
18
+ return { response: n.outputVar ?? `${n.id}_response`, error: n.errorVar ?? `${n.id}_Error` };
19
19
  }
20
20
  /**
21
21
  * The variables a typed node writes — the ONE definition of that set.
@@ -54,7 +54,7 @@ export function typedNodeDeclaredVars(n) {
54
54
  export function nodeDeclaredVars(n) {
55
55
  if (n.kind === 'connector') {
56
56
  const { response, error } = connectorOutputVars(n);
57
- return [response, error];
57
+ return [response, error, ...Object.keys(n.outputs ?? {})];
58
58
  }
59
59
  if (n.kind === 'typed')
60
60
  return typedNodeDeclaredVars(n);
@@ -4,6 +4,15 @@
4
4
  *
5
5
  * Usage:
6
6
  * node --experimental-strip-types decompile-cli.js <Name.bpmn> [-o Name.bpmn.ts] [--import <specifier>] [--style explicit|nested] [--keep-flow-ids] [--keep-unused-bindings]
7
+ * [--library <library-json dir>] [--connectors-local <overlay dir>] [--connectors lift|verbatim]
8
+ *
9
+ * `--connectors lift` (the default when a library opens — `--library`, else the
10
+ * cache `registry pull` filled) writes a connector task the library can name as
11
+ * `.connector(key, action, inputs, { connection, folder, … })`, and verifies each
12
+ * lift by recompiling: a task that does not come back the same, the fields the
13
+ * SDK regenerates aside, is written verbatim with the reason. `--connectors
14
+ * verbatim` keeps every task as the file spells it, which is also what happens
15
+ * with no library at all.
7
16
  *
8
17
  * The reverse of compile-cli: reads a `.bpmn` and emits builder source whose
9
18
  * default export re-serializes to the same XML. `--import` sets the module
@@ -29,6 +38,8 @@ import { defaultSourceOut, writeSource } from '../workdir.js';
29
38
  import { flagValue, runWhenInvokedDirectly } from '../cli-run.js';
30
39
  import { decompile } from './decompile.js';
31
40
  import { sameGraph } from './lift-verify.js';
41
+ import { verifyConnectorLifts } from './connector-lift-verify.js';
42
+ import { openCliLibrary } from '../core/cli-library.js';
32
43
  import { BPMN_IMPORT } from '../package-name.js';
33
44
  import { fileURLToPath } from 'node:url';
34
45
  /**
@@ -62,9 +73,14 @@ export async function run(argv) {
62
73
  return 2;
63
74
  }
64
75
  }
76
+ const connectors = flagValue(argv, '--connectors') ?? 'lift';
77
+ if (connectors !== 'lift' && connectors !== 'verbatim') {
78
+ console.error(`decompile: --connectors must be lift or verbatim, not "${connectors}"`);
79
+ return 2;
80
+ }
65
81
  const keepFlowIds = argv.includes('--keep-flow-ids');
66
82
  const keepUnusedBindings = argv.includes('--keep-unused-bindings');
67
- const flags = new Set(['-o', '--import', '--style']);
83
+ const flags = new Set(['-o', '--import', '--style', '--library', '--connectors-local', '--connectors']);
68
84
  const bare = new Set(['--keep-flow-ids', '--keep-unused-bindings']);
69
85
  const input = argv.find((a, i) => !bare.has(a) && !flags.has(a) && !flags.has(argv[i - 1]));
70
86
  if (!input) {
@@ -74,8 +90,34 @@ export async function run(argv) {
74
90
  const xml = readFileSync(input, 'utf8');
75
91
  const importSpecifier = flagValue(argv, '--import') ?? BPMN_IMPORT;
76
92
  let pruned = [];
77
- const base0 = { importSpecifier, keepUnusedBindings, onPrune: (ids) => (pruned = ids) };
78
- let source = await decompile(xml, base0);
93
+ // The connector lift: name each connector task through the library, then keep only
94
+ // the lifts that verify. One decompile to plan, one verify, and — only when a lift
95
+ // was refused — one more decompile with those tasks pinned verbatim. The pinned set
96
+ // then rides into the nested-style passes below, so the flat and nested sources the
97
+ // nested verify compares make the same connector decisions.
98
+ const library = connectors === 'lift' ? openCliLibrary(argv) : undefined;
99
+ const sdkEntry = fileURLToPath(new URL('./bpmn-sdk.js', import.meta.url));
100
+ let connectorSummary;
101
+ let verbatimConnectors = new Set();
102
+ const base0 = {
103
+ importSpecifier,
104
+ keepUnusedBindings,
105
+ onPrune: (ids) => (pruned = ids),
106
+ ...(library ? { library, connectors: 'lift' } : {}),
107
+ onConnectorLift: (s) => (connectorSummary = s),
108
+ };
109
+ let source = await decompile(xml, { ...base0, verbatimConnectors });
110
+ if (library && connectorSummary && connectorSummary.lifted.length > 0) {
111
+ const verdict = await verifyConnectorLifts(xml, connectorSummary, sdkEntry, library);
112
+ if (verdict.failed.size > 0) {
113
+ verbatimConnectors = new Set(verdict.failed.keys());
114
+ source = await decompile(xml, { ...base0, verbatimConnectors });
115
+ // The summary from the second pass says "did not verify"; carry the measured reason.
116
+ if (connectorSummary) {
117
+ connectorSummary.verbatim = connectorSummary.verbatim.map((v) => ({ id: v.id, reason: verdict.failed.get(v.id) ?? v.reason }));
118
+ }
119
+ }
120
+ }
79
121
  let lifted;
80
122
  let fallback;
81
123
  if (style === 'nested') {
@@ -83,10 +125,11 @@ export async function run(argv) {
83
125
  // package's own entry, whatever specifier the written file carries — the check is
84
126
  // about the graph, not about module resolution. The flow ids the lift renamed are
85
127
  // the one sanctioned difference.
86
- const sdkEntry = fileURLToPath(new URL('./bpmn-sdk.js', import.meta.url));
87
128
  for (const sequenceMode of [true, false]) {
88
129
  let summary;
89
- const nested = await decompile(xml, { ...base0, style: 'nested', relabelFlows: !keepFlowIds, sequenceMode, onLift: (s) => (summary = s) });
130
+ const nested = await decompile(xml, {
131
+ ...base0, verbatimConnectors, style: 'nested', relabelFlows: !keepFlowIds, sequenceMode, onLift: (s) => (summary = s),
132
+ });
90
133
  const renamed = new Map((summary?.renamedFlows ?? []).map((r) => [r.from, r.to]));
91
134
  const verdict = await sameGraph(source, nested, sdkEntry, renamed);
92
135
  if (verdict.same) {
@@ -107,6 +150,15 @@ export async function run(argv) {
107
150
  console.log(`decompile: dropped ${pruned.length} binding(s) nothing references — ${pruned.slice(0, 8).join(', ')}` +
108
151
  `${pruned.length > 8 ? `, and ${pruned.length - 8} more` : ''}. Pass --keep-unused-bindings to echo them.`);
109
152
  }
153
+ if (connectorSummary && (connectorSummary.lifted.length || connectorSummary.verbatim.length)) {
154
+ const kept = connectorSummary.verbatim.length
155
+ ? `; verbatim: ${connectorSummary.verbatim.map((v) => `${v.id} (${v.reason.split('\n')[0]})`).join('; ')}`
156
+ : '';
157
+ console.log(`decompile: connectors — lifted ${connectorSummary.lifted.length} to .connector()${kept}`);
158
+ }
159
+ else if (connectors === 'lift' && !library) {
160
+ console.log('decompile: no connector library found, connector tasks written verbatim — pass --library <library-json dir> to lift them');
161
+ }
110
162
  if (lifted) {
111
163
  const flat = lifted.leftFlat.length ? `; left flat: ${lifted.leftFlat.map((r) => `${r.id} (${r.reason})`).join('; ')}` : '';
112
164
  const renamed = lifted.renamedFlows.length
@@ -118,7 +170,17 @@ export async function run(argv) {
118
170
  else if (fallback) {
119
171
  console.log(`decompile: --style nested could not be verified, wrote the flat form — ${fallback}`);
120
172
  }
121
- reportResult({ path: resolve(out), style: lifted ? 'nested' : 'explicit', ...(lifted ? { lifted } : {}), ...(fallback ? { fallback } : {}) });
173
+ reportResult({
174
+ path: resolve(out),
175
+ style: lifted ? 'nested' : 'explicit',
176
+ ...(lifted ? { lifted } : {}),
177
+ ...(fallback ? { fallback } : {}),
178
+ // What the lift did, when a library was in play: the ids, not the preamble and
179
+ // calls the verifier consumed.
180
+ ...(library && connectorSummary && (connectorSummary.lifted.length || connectorSummary.verbatim.length)
181
+ ? { connectors: { lifted: connectorSummary.lifted, verbatim: connectorSummary.verbatim } }
182
+ : {}),
183
+ });
122
184
  return 0;
123
185
  }
124
186
  runWhenInvokedDirectly(import.meta.url, 'decompile', run);
@@ -1,7 +1,31 @@
1
+ import type { Library } from '../core/library.js';
1
2
  /** Options for {@link decompile}. */
2
3
  export interface BpmnDecompileOptions {
3
4
  /** Module specifier for the emitted `import { bpmn }` line. */
4
5
  importSpecifier?: string;
6
+ /**
7
+ * The connector library. With one, an `Intsvc.ActivityExecution` the library can
8
+ * name comes back as `.connector(key, action, inputs, { connection, folder, … })` —
9
+ * the call an author writes — instead of the verbatim `.activity()` form; see
10
+ * {@link BpmnDecompileOptions.connectors}. Without one, every connector task stays
11
+ * verbatim, as before.
12
+ */
13
+ library?: Library;
14
+ /**
15
+ * How a connector task is written back when a library is given. `'lift'` (the
16
+ * default) names the operation through the library and drops what the SDK
17
+ * regenerates on compile — the `metadata` blob the canvas writes
18
+ * (`designTimeMetadata`, `metadataState`), the CRUD `operation` verb, row types,
19
+ * the response row's schema body. `'verbatim'` keeps every row as the file spells
20
+ * it. A lift is best effort by construction and safe only with the check
21
+ * `decompile-cli` runs (`verifyConnectorLifts`): the lifted call must compile to the
22
+ * same task, those regenerated fields aside, or the element is written verbatim.
23
+ */
24
+ connectors?: 'lift' | 'verbatim';
25
+ /** Connector tasks to write verbatim whatever `connectors` says — the ones a verify refused. */
26
+ verbatimConnectors?: ReadonlySet<string>;
27
+ /** Called with what the connector lift did, after decompiling. */
28
+ onConnectorLift?: (summary: ConnectorLiftSummary) => void;
5
29
  /**
6
30
  * `'explicit'` (the default) renders the graph flat: one call per element, one
7
31
  * `.sequenceFlow()` per edge. `'nested'` LIFTS the regions that the builder's
@@ -103,4 +127,23 @@ export interface LiftSummary {
103
127
  /** Whether the top level was written in `.flowMode('sequence')`. */
104
128
  sequence: boolean;
105
129
  }
130
+ /** What the connector lift did on one decompile — see `BpmnDecompileOptions.connectors`. */
131
+ export interface ConnectorLiftSummary {
132
+ /** Element ids written as `.connector()`. */
133
+ lifted: string[];
134
+ /** Connector tasks written verbatim, each with the reason. */
135
+ verbatim: Array<{
136
+ id: string;
137
+ reason: string;
138
+ }>;
139
+ /**
140
+ * The source up to the first element — the import, the process call, its metadata,
141
+ * schemas, bindings and variables — so a verifier can compile one lifted call on its
142
+ * own (`verifyConnectorLifts`) instead of the whole process, where one refused lift
143
+ * would take every other down with it.
144
+ */
145
+ preamble: string;
146
+ /** Each lifted call's source line, by element id. */
147
+ calls: Map<string, string>;
148
+ }
106
149
  export declare function decompile(xml: string, options?: BpmnDecompileOptions): Promise<string>;