@uipath/maestro-builder-sdk 6.13.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.
@@ -378,6 +378,12 @@ export interface BpmnVarDecl {
378
378
  schema?: unknown;
379
379
  /** Mark the declaration `required="true"`, as a process input can be. */
380
380
  required?: boolean;
381
+ /** The product's "an author added this" flag — see {@link VarOpts.custom}. */
382
+ custom?: boolean;
383
+ /** The product's "the platform owns this" flag — see {@link VarOpts.internal}. */
384
+ internal?: boolean;
385
+ /** A refinement of `type` the designer carries — see {@link VarOpts.subType}. */
386
+ subType?: string;
381
387
  }
382
388
  /**
383
389
  * A declared external identifier the process needs supplied at deploy time — a
@@ -393,9 +399,9 @@ export interface BindingDecl {
393
399
  resource: string;
394
400
  /** Which property of that resource is wanted, e.g. `'ConnectionId'`, `'Key'`. */
395
401
  propertyAttribute: string;
396
- /** The value used when nothing overrides it — what an offline run resolves to. */
402
+ /** The value `=bindings.<id>` resolves to — see {@link BindingOpts.value}, the `value` option. */
397
403
  default?: string;
398
- /** The resource's key, when it differs from `default`. */
404
+ /** Which resource this is bound to; never resolved — see {@link BindingOpts.resourceKey}. */
399
405
  resourceKey?: string;
400
406
  /**
401
407
  * Narrows the resource, e.g. `'Agent'` for an agent process. Required by some
@@ -403,6 +409,11 @@ export interface BindingDecl {
403
409
  */
404
410
  resourceSubType?: string;
405
411
  }
412
+ /** One entry of {@link BpmnBuilder.bindings} — a `.binding()` call written as data. */
413
+ export interface BindingEntry extends BindingOpts {
414
+ /** The binding's id — what `=bindings.<id>` refers to. */
415
+ id: string;
416
+ }
406
417
  /** A definitions-level `bpmn:message` declaration, referenced by message events. */
407
418
  export interface MessageDecl {
408
419
  /** The declaration's id — what an event's `messageRef` points at. */
@@ -419,6 +430,50 @@ export interface ErrorDecl {
419
430
  /** The error code the runtime matches on. */
420
431
  code?: string;
421
432
  }
433
+ /**
434
+ * Non-authoring state retained by decompile/recompile.
435
+ *
436
+ * The BPMN counterpart of the Case SDK's `CaseJsonPreservation`, and named to match it:
437
+ * one block, carried rather than authored, that a reader can skip.
438
+ *
439
+ * Everything here is data the source document carried that NOTHING derives and nothing
440
+ * executes. It is not a feature to reach for: `decompile` writes it, an author leaves
441
+ * it alone, and deleting it changes no behaviour — only whether the file still matches
442
+ * the one it came from.
443
+ *
444
+ * It deliberately cannot carry anything authorable. A position, a type, a condition, a
445
+ * schema — those are the typed builder's job, and hiding one here would make it
446
+ * invisible to `check` and to whoever reads the source.
447
+ *
448
+ * Distinct from LAYOUT, which this does not hold. A diagram is content: a person reads
449
+ * it, and the Flow SDK lets an author set it (`.layout()`). The members here have no
450
+ * meaning apart from "what the file said".
451
+ */
452
+ export interface BpmnPreservation {
453
+ /**
454
+ * The order declarations are written in, by id.
455
+ *
456
+ * Needed because a declaration folded onto the output row that fills it has no line
457
+ * of its own to carry its position, and derived declarations would otherwise land
458
+ * after every declared one. No rule recovers the real order: it is the designer's
459
+ * creation order, and it follows the owning element's document order in none of the
460
+ * five exports in `research/bpmn-roundtrip/corpus/` (47, 36, 19, 16 and 22
461
+ * inversions).
462
+ *
463
+ * Nothing reads it at run time — the engine seeds declarations into one scope by id,
464
+ * and a process input is matched by display `name`, not position. `uip maestro bpmn
465
+ * validate` answers `Success` on a reordered block. What it buys is the variables
466
+ * panel listing things as the source did, and a `bpmn merge` whose diff covers what
467
+ * was edited rather than all hundred declarations.
468
+ *
469
+ * It applies WITHIN each direction group — `uipath:input`, then `uipath:inputOutput`,
470
+ * then `uipath:output` — which is how the product writes them. Ids it omits keep
471
+ * their relative order, after the ones it names, and an id it does not know is
472
+ * ignored. `check` reports all three ways of getting it wrong, because every one of
473
+ * them used to be silent.
474
+ */
475
+ variableOrder?: string[];
476
+ }
422
477
  /**
423
478
  * The built in-memory process graph `.build()` returns — the IR `serialize()`
424
479
  * walks into `.bpmn` XML.
@@ -444,6 +499,8 @@ export interface BuiltBpmn {
444
499
  metadata?: ProcessMetadata;
445
500
  /** Named schemas from `.schema()`, which a row's `schema` may reference by id. */
446
501
  schemas: Map<string, unknown>;
502
+ /** Non-authoring state carried from the source — see {@link BpmnBuilder.preserve}. */
503
+ preservation?: BpmnPreservation;
447
504
  }
448
505
  /**
449
506
  * Process-level metadata — the `uipath:*` children of the process's own
@@ -887,13 +944,45 @@ export interface SubProcessOpts extends ActivityOpts {
887
944
  */
888
945
  payload?: ExtensionPayload;
889
946
  }
947
+ /**
948
+ * One entry of {@link ScopeBuilder.vars} — a `.var()` / `.input()` / `.output()` call
949
+ * written as data.
950
+ */
951
+ export interface VarEntry extends VarOpts {
952
+ /** The variable's id — what expressions read as `vars.<id>`. */
953
+ id: string;
954
+ /** A `types.*` descriptor. */
955
+ type: TypeDesc;
956
+ /** Which declaration this is. Defaults to `'inputOutput'`, the `.var()` kind. */
957
+ direction?: VarDirection;
958
+ }
959
+ /** One entry of {@link BpmnBuilder.schemas} — a `.schema()` call written as data. */
960
+ export interface SchemaEntry {
961
+ /** The schema's id, referenced by a `schema: '<id>'` option. */
962
+ id: string;
963
+ /** The JSON Schema itself. */
964
+ body: unknown;
965
+ }
890
966
  /** Options for `.binding()`. */
891
967
  export interface BindingOpts {
892
968
  /** Display name the designer shows. Defaults to the binding's id. */
893
969
  name?: string;
894
970
  /**
895
- * The value the binding resolves to when nothing overrides it — which is what
896
- * an offline run reads, so it is the one field a local run needs.
971
+ * **The value.** What `=bindings.<id>` evaluates to, and the only field of a binding
972
+ * the runtime reads.
973
+ *
974
+ * Resolution is `supplied[id] ?? supplied[name] ?? value` — a deploy, or
975
+ * `flow-debug --bindings`, overrides it; absent an override this is used; with
976
+ * neither the binding is UNRESOLVED and a dispatching run refuses to start, naming
977
+ * every one (`local-engine/src/v2/dispatch/bindings.ts`). So a binding with no
978
+ * `value` is a promise that something else will supply it.
979
+ *
980
+ * Real exports leave it out for exactly one thing: a `folderPath`, which is not
981
+ * knowable when the resource is picked. 524 of the 528 bindings in
982
+ * `research/bpmn-roundtrip/corpus/` follow "no `value` iff `propertyAttribute` is
983
+ * `folderPath`".
984
+ *
985
+ * Not to be confused with {@link BindingOpts.resourceKey}, which is never resolved.
897
986
  */
898
987
  value?: string;
899
988
  /**
@@ -905,7 +994,33 @@ export interface BindingOpts {
905
994
  resource?: string;
906
995
  /** Which property of that resource is wanted. Defaults to `'value'`. */
907
996
  propertyAttribute?: string;
908
- /** The resource's key, when it differs from {@link BindingOpts.value}. */
997
+ /**
998
+ * **Which resource this is bound to** — design-time provenance, never resolved.
999
+ *
1000
+ * Nothing at run time reads it: it identifies the tenant resource the binding was
1001
+ * bound to, so a deploy can re-resolve {@link BindingOpts.value} against the target
1002
+ * tenant and the canvas can show what the binding points at.
1003
+ *
1004
+ * It equals `value` exactly when the property asked for IS the resource's identity —
1005
+ * a queue is identified by its name, so a `name` binding's key and value are the
1006
+ * same string (196 of 202 in the corpus). It differs whenever they are different
1007
+ * things:
1008
+ *
1009
+ * - a `folderKey` binding's key is the CONNECTION's key, because the folder is
1010
+ * resolved through the connection, while its value is the folder's own key
1011
+ * (9 of 9; `folderCompanionKeys` in `serialize.ts` exists to write this);
1012
+ * - a compound process key `<folderPath>.<name>` projects onto the pair of bindings
1013
+ * as its two halves — the `folderPath` binding's value is the folder, the `name`
1014
+ * binding's value is the name;
1015
+ * - a renamed resource keeps the key it was bound under and tracks the new name in
1016
+ * `value`.
1017
+ *
1018
+ * **The author-side `bindings.json` uses this field the other way round** — there
1019
+ * `resourceKey` is the real tenant value to inline and `default` is an offline
1020
+ * placeholder, which is why `Bindings.resolve()` prefers `resourceKey` while the
1021
+ * engine reads only `value`. Both are right for their own file; `connector.ts`
1022
+ * translates between them. This option is the WIRE meaning.
1023
+ */
909
1024
  resourceKey?: string;
910
1025
  /**
911
1026
  * Narrows the resource, e.g. `'Agent'` for an agent process.
@@ -1414,6 +1529,33 @@ export interface VarOpts {
1414
1529
  schema?: unknown;
1415
1530
  /** Mark the declaration `required="true"`, as a process input can be. */
1416
1531
  required?: boolean;
1532
+ /**
1533
+ * The product's `custom="true"` flag: an author added this declaration, as opposed
1534
+ * to its coming with a node's contract.
1535
+ *
1536
+ * The designer writes it on every variable a person typed into the variables panel
1537
+ * and on no variable it derived from a node's output row, which makes it the one
1538
+ * attribute that says which of the two a declaration is. Measured over
1539
+ * `research/bpmn-roundtrip/corpus/`: 119 of 842 declarations carry it, and the split
1540
+ * is clean — a node-output declaration never does.
1541
+ *
1542
+ * Nothing executes differently either way; it is designer bookkeeping, carried so an
1543
+ * import round-trips and so `implicitNodeVars` has an explicit signal rather than an
1544
+ * inferred one.
1545
+ */
1546
+ custom?: boolean;
1547
+ /**
1548
+ * The product's `internal="true"` flag: the platform owns this declaration, not the
1549
+ * author — `internalCustomInstanceId` and its kind. 3 in the corpus. Carried, not
1550
+ * interpreted.
1551
+ */
1552
+ internal?: boolean;
1553
+ /**
1554
+ * A refinement of `type` the designer carries, e.g. `subType="object"` on a
1555
+ * `jsonSchema` variable that holds an object rather than an array. 5 in the corpus.
1556
+ * Carried, not interpreted.
1557
+ */
1558
+ subType?: string;
1417
1559
  }
1418
1560
  /**
1419
1561
  * Boundary events declared on the activity they attach to — the callback every
@@ -2772,6 +2914,32 @@ declare abstract class ScopeBuilder {
2772
2914
  * @returns This builder, so calls chain.
2773
2915
  */
2774
2916
  output(id: string, type: TypeDesc, opts?: VarOpts): this;
2917
+ /**
2918
+ * Several declarations as one call — the array form of `.var()` / `.input()` /
2919
+ * `.output()`.
2920
+ *
2921
+ * Identical in effect, and there for how it READS: a real process declares dozens,
2922
+ * and one `.var(…)` per line is a wall of repeated method names that the eye has to
2923
+ * scan past to find the data. As an array the block indents, the prefix is written
2924
+ * once, and the declarations line up as the table they are. `decompile` emits this
2925
+ * form; the singular calls are unchanged and can be mixed with it freely.
2926
+ *
2927
+ * Each entry's `direction` picks which of the three it is, defaulting to
2928
+ * `inputOutput` — so one block can carry a process's inputs, its internal state and
2929
+ * its outputs in the order the document had them.
2930
+ *
2931
+ * ```ts
2932
+ * .vars([
2933
+ * { id: 'invoiceId', type: 'string', direction: 'input', required: true },
2934
+ * { id: 'total', type: 'number', default: 0 },
2935
+ * { id: 'approved', type: 'boolean', direction: 'output' },
2936
+ * ])
2937
+ * ```
2938
+ *
2939
+ * @param entries - The declarations, in the order they should be written.
2940
+ * @returns This builder, so calls chain.
2941
+ */
2942
+ vars(entries: VarEntry[]): this;
2775
2943
  private declareVar;
2776
2944
  }
2777
2945
  /**
@@ -3042,6 +3210,24 @@ export declare class BpmnBuilder extends ScopeBuilder {
3042
3210
  private readonly _id;
3043
3211
  private _name?;
3044
3212
  private _metadata?;
3213
+ /** Non-authoring state pinned by `.preserve()`. */
3214
+ private _preservation?;
3215
+ /**
3216
+ * Carry non-authoring state from a source document — **written by `decompile`; a
3217
+ * greenfield author never needs this.**
3218
+ *
3219
+ * See {@link BpmnPreservation} for what belongs here and why. In one line: data the
3220
+ * document carried that no rule derives and nothing executes, kept so a
3221
+ * decompile/edit/recompile loop produces the file it started from. An agent editing
3222
+ * such a file should leave the call alone — there is nothing in it to reason about,
3223
+ * and no behaviour to change by changing it.
3224
+ *
3225
+ * Calling it twice REPLACES the block, matching `.layout()` on the Flow side.
3226
+ *
3227
+ * @param preservation - The carried state — see {@link BpmnPreservation}.
3228
+ * @returns This builder, so calls chain.
3229
+ */
3230
+ preserve(preservation: BpmnPreservation): this;
3045
3231
  /**
3046
3232
  * @param _id - The process id, as passed to `bpmn()`.
3047
3233
  * @returns The top-level builder.
@@ -3127,10 +3313,23 @@ export declare class BpmnBuilder extends ScopeBuilder {
3127
3313
  * ```
3128
3314
  */
3129
3315
  schema(id: string, schema: unknown): this;
3316
+ /**
3317
+ * Several named schemas as one call — the array form of {@link BpmnBuilder.schema}.
3318
+ *
3319
+ * Same reason as {@link ScopeBuilder.vars}: a process that repeats one shape across
3320
+ * twenty nodes declares a handful of these, and the array indents them into a block
3321
+ * instead of a column of repeated prefixes. `decompile` emits this form.
3322
+ *
3323
+ * @param entries - The schemas to declare.
3324
+ * @returns This builder, so calls chain.
3325
+ */
3326
+ schemas(entries: SchemaEntry[]): this;
3130
3327
  /**
3131
3328
  * Declare an external identifier the process needs supplied — a base URL, a
3132
3329
  * folder path, a process name (`uipath:binding`). Expressions read it as
3133
- * `=bindings.<id>`, and an offline run resolves it to `value`.
3330
+ * `=bindings.<id>`, and it resolves to `value` unless a deploy or `--bindings`
3331
+ * overrides it — with neither, the binding is unresolved and a dispatching run
3332
+ * refuses to start.
3134
3333
  *
3135
3334
  * @remarks
3136
3335
  * A connector's `connection`/`folder` already produce their own bindings at
@@ -3148,10 +3347,22 @@ export declare class BpmnBuilder extends ScopeBuilder {
3148
3347
  * ```
3149
3348
  *
3150
3349
  * @param id - The binding's id — what `=bindings.<id>` refers to.
3151
- * @param opts - Display `name`, the `value` it defaults to, and which resource it addresses.
3350
+ * @param opts - Display `name`, the `value` it resolves to, and which resource it addresses.
3152
3351
  * @returns This builder, so calls chain.
3153
3352
  */
3154
3353
  binding(id: string, opts?: BindingOpts): this;
3354
+ /**
3355
+ * Several bindings as one call — the array form of {@link BpmnBuilder.binding}.
3356
+ *
3357
+ * This is the block the array form was wanted for: bindings come in pairs, one per
3358
+ * resource the process touches, and a real export carries dozens — 73 in
3359
+ * `InvoiceApproval.bpmn.ts`, which was the single largest run of repeated method
3360
+ * names in the file. `decompile` emits this form.
3361
+ *
3362
+ * @param entries - The bindings to declare, in document order.
3363
+ * @returns This builder, so calls chain.
3364
+ */
3365
+ bindings(entries: BindingEntry[]): this;
3155
3366
  /**
3156
3367
  * Finish the process and return the graph the serializer turns into XML.
3157
3368
  *
@@ -2097,6 +2097,36 @@ class ScopeBuilder {
2097
2097
  output(id, type, opts = {}) {
2098
2098
  return this.declareVar(id, type, 'output', opts);
2099
2099
  }
2100
+ /**
2101
+ * Several declarations as one call — the array form of `.var()` / `.input()` /
2102
+ * `.output()`.
2103
+ *
2104
+ * Identical in effect, and there for how it READS: a real process declares dozens,
2105
+ * and one `.var(…)` per line is a wall of repeated method names that the eye has to
2106
+ * scan past to find the data. As an array the block indents, the prefix is written
2107
+ * once, and the declarations line up as the table they are. `decompile` emits this
2108
+ * form; the singular calls are unchanged and can be mixed with it freely.
2109
+ *
2110
+ * Each entry's `direction` picks which of the three it is, defaulting to
2111
+ * `inputOutput` — so one block can carry a process's inputs, its internal state and
2112
+ * its outputs in the order the document had them.
2113
+ *
2114
+ * ```ts
2115
+ * .vars([
2116
+ * { id: 'invoiceId', type: 'string', direction: 'input', required: true },
2117
+ * { id: 'total', type: 'number', default: 0 },
2118
+ * { id: 'approved', type: 'boolean', direction: 'output' },
2119
+ * ])
2120
+ * ```
2121
+ *
2122
+ * @param entries - The declarations, in the order they should be written.
2123
+ * @returns This builder, so calls chain.
2124
+ */
2125
+ vars(entries) {
2126
+ for (const e of entries)
2127
+ this.declareVar(e.id, e.type, e.direction ?? 'inputOutput', e);
2128
+ return this;
2129
+ }
2100
2130
  declareVar(id, type, direction, opts) {
2101
2131
  this._vars.push({
2102
2132
  id,
@@ -2107,6 +2137,9 @@ class ScopeBuilder {
2107
2137
  elementId: opts.elementId,
2108
2138
  schema: opts.schema,
2109
2139
  required: opts.required,
2140
+ custom: opts.custom,
2141
+ internal: opts.internal,
2142
+ subType: opts.subType,
2110
2143
  });
2111
2144
  return this;
2112
2145
  }
@@ -2433,6 +2466,27 @@ export class BpmnBuilder extends ScopeBuilder {
2433
2466
  _id;
2434
2467
  _name;
2435
2468
  _metadata;
2469
+ /** Non-authoring state pinned by `.preserve()`. */
2470
+ _preservation;
2471
+ /**
2472
+ * Carry non-authoring state from a source document — **written by `decompile`; a
2473
+ * greenfield author never needs this.**
2474
+ *
2475
+ * See {@link BpmnPreservation} for what belongs here and why. In one line: data the
2476
+ * document carried that no rule derives and nothing executes, kept so a
2477
+ * decompile/edit/recompile loop produces the file it started from. An agent editing
2478
+ * such a file should leave the call alone — there is nothing in it to reason about,
2479
+ * and no behaviour to change by changing it.
2480
+ *
2481
+ * Calling it twice REPLACES the block, matching `.layout()` on the Flow side.
2482
+ *
2483
+ * @param preservation - The carried state — see {@link BpmnPreservation}.
2484
+ * @returns This builder, so calls chain.
2485
+ */
2486
+ preserve(preservation) {
2487
+ this._preservation = preservation;
2488
+ return this;
2489
+ }
2436
2490
  /**
2437
2491
  * @param _id - The process id, as passed to `bpmn()`.
2438
2492
  * @returns The top-level builder.
@@ -2536,10 +2590,27 @@ export class BpmnBuilder extends ScopeBuilder {
2536
2590
  this._schemas.set(id, schema);
2537
2591
  return this;
2538
2592
  }
2593
+ /**
2594
+ * Several named schemas as one call — the array form of {@link BpmnBuilder.schema}.
2595
+ *
2596
+ * Same reason as {@link ScopeBuilder.vars}: a process that repeats one shape across
2597
+ * twenty nodes declares a handful of these, and the array indents them into a block
2598
+ * instead of a column of repeated prefixes. `decompile` emits this form.
2599
+ *
2600
+ * @param entries - The schemas to declare.
2601
+ * @returns This builder, so calls chain.
2602
+ */
2603
+ schemas(entries) {
2604
+ for (const e of entries)
2605
+ this._schemas.set(e.id, e.body);
2606
+ return this;
2607
+ }
2539
2608
  /**
2540
2609
  * Declare an external identifier the process needs supplied — a base URL, a
2541
2610
  * folder path, a process name (`uipath:binding`). Expressions read it as
2542
- * `=bindings.<id>`, and an offline run resolves it to `value`.
2611
+ * `=bindings.<id>`, and it resolves to `value` unless a deploy or `--bindings`
2612
+ * overrides it — with neither, the binding is unresolved and a dispatching run
2613
+ * refuses to start.
2543
2614
  *
2544
2615
  * @remarks
2545
2616
  * A connector's `connection`/`folder` already produce their own bindings at
@@ -2557,7 +2628,7 @@ export class BpmnBuilder extends ScopeBuilder {
2557
2628
  * ```
2558
2629
  *
2559
2630
  * @param id - The binding's id — what `=bindings.<id>` refers to.
2560
- * @param opts - Display `name`, the `value` it defaults to, and which resource it addresses.
2631
+ * @param opts - Display `name`, the `value` it resolves to, and which resource it addresses.
2561
2632
  * @returns This builder, so calls chain.
2562
2633
  */
2563
2634
  binding(id, opts = {}) {
@@ -2575,6 +2646,22 @@ export class BpmnBuilder extends ScopeBuilder {
2575
2646
  });
2576
2647
  return this;
2577
2648
  }
2649
+ /**
2650
+ * Several bindings as one call — the array form of {@link BpmnBuilder.binding}.
2651
+ *
2652
+ * This is the block the array form was wanted for: bindings come in pairs, one per
2653
+ * resource the process touches, and a real export carries dozens — 73 in
2654
+ * `InvoiceApproval.bpmn.ts`, which was the single largest run of repeated method
2655
+ * names in the file. `decompile` emits this form.
2656
+ *
2657
+ * @param entries - The bindings to declare, in document order.
2658
+ * @returns This builder, so calls chain.
2659
+ */
2660
+ bindings(entries) {
2661
+ for (const e of entries)
2662
+ this.binding(e.id, e);
2663
+ return this;
2664
+ }
2578
2665
  /**
2579
2666
  * Finish the process and return the graph the serializer turns into XML.
2580
2667
  *
@@ -2643,6 +2730,7 @@ export class BpmnBuilder extends ScopeBuilder {
2643
2730
  nodes: this._nodes,
2644
2731
  flows: this._flows,
2645
2732
  schemas: this._schemas,
2733
+ preservation: this._preservation,
2646
2734
  };
2647
2735
  // First-level validation, intrinsic to running the SDK: catch a reference to
2648
2736
  // an undeclared `vars.<name>` HERE — the one thing `uip maestro bpmn validate`
@@ -64,6 +64,7 @@ export function check(built) {
64
64
  const seenIds = new Set();
65
65
  checkScope(built.nodes, built.flows, 'process', diags, seenIds, true);
66
66
  checkScopedVars(built, diags);
67
+ checkVariableOrder(built, diags);
67
68
  checkDocumentIds(built, diags);
68
69
  checkFolderBindings(built, diags);
69
70
  checkEntryPointId(built, diags);
@@ -304,6 +305,91 @@ function checkDocumentIds(built, diags) {
304
305
  for (const e of built.errors)
305
306
  claim(e.id, `error "${e.name}"`);
306
307
  }
308
+ /**
309
+ * A pinned declaration order must name the declarations it is ordering.
310
+ *
311
+ * `.preserve({ variableOrder })` is round-trip bookkeeping — `decompile` writes it because a
312
+ * declaration folded onto its output row has no line of its own to carry its
313
+ * position, and nothing derives that position (it is the designer's creation order,
314
+ * which follows the owning element's document order in none of the five exports in
315
+ * `research/bpmn-roundtrip/corpus/`). An author never writes one.
316
+ *
317
+ * Which is exactly why it needs checking. Every malformed list is applied in silence:
318
+ * an id no declaration has is ignored, a DUPLICATE is harmless, and a PARTIAL list
319
+ * promotes what it names and demotes everything else — the mistake an author editing
320
+ * a decompiled file makes by adding a variable and not the array. Nothing else
321
+ * reports it: the order changes no behaviour, `uip maestro bpmn validate` answers
322
+ * `Success` on a reordered block, and the only symptom is a variables panel that
323
+ * lists things differently than the file it came from.
324
+ *
325
+ * Warnings, not errors, because nothing it can say makes the process invalid.
326
+ */
327
+ function checkVariableOrder(built, diags) {
328
+ const order = built.preservation?.variableOrder;
329
+ if (!order?.length)
330
+ return;
331
+ // Two sets, because the order applies to ONE `uipath:variables` block. `root` is what
332
+ // the order governs and so what it has to be complete over; `anywhere` also holds the
333
+ // sub-process blocks, whose ids are legal to name (they rank last and keep their
334
+ // relative order) and must not be reported as unknown.
335
+ //
336
+ // Without that split, every process with a sub-process block was INCOMPLETE by
337
+ // construction — `billing-disputes` reported its 24 sub-process declarations as left
338
+ // out, when a sub-process declaration is never folded and so already states its
339
+ // position by its line.
340
+ const root = new Set();
341
+ // A derived declaration is not in `built.variables` — it is created at serialize
342
+ // time from the row that fills it — so ask the derivation itself rather than
343
+ // re-deriving the rule here, which is how the two sets drifted apart before.
344
+ for (const v of [...built.variables, ...implicitNodeVars(built.nodes, built.variables)])
345
+ root.add(v.id);
346
+ const anywhere = new Set(root);
347
+ const collectNested = (nodes) => {
348
+ for (const n of nodes) {
349
+ if (n.kind !== 'subProcess')
350
+ continue;
351
+ for (const v of n.variables)
352
+ anywhere.add(v.id);
353
+ for (const v of implicitNodeVars(n.nodes, n.variables))
354
+ anywhere.add(v.id);
355
+ collectNested(n.nodes);
356
+ }
357
+ };
358
+ collectNested(built.nodes);
359
+ const seen = new Set();
360
+ for (const id of order) {
361
+ if (!anywhere.has(id))
362
+ diags.push({
363
+ level: 'warning',
364
+ code: 'VARIABLE_ORDER_UNKNOWN_ID',
365
+ message: `.preserve({ variableOrder }) names "${id}", which no declaration has. It is ignored — ` +
366
+ `the order is round-trip bookkeeping written by decompile, so a stale id usually means ` +
367
+ `a variable was renamed or removed without updating the list.`,
368
+ where: id,
369
+ });
370
+ if (seen.has(id))
371
+ diags.push({
372
+ level: 'warning',
373
+ code: 'VARIABLE_ORDER_DUPLICATE_ID',
374
+ message: `.preserve({ variableOrder }) names "${id}" more than once. The first position wins; drop the rest.`,
375
+ where: id,
376
+ });
377
+ seen.add(id);
378
+ }
379
+ // The dangerous shape: a list that names SOME of them. Everything it leaves out is
380
+ // written after everything it names, which reorders declarations the author never
381
+ // touched — and looks like nothing went wrong.
382
+ const missing = [...root].filter((id) => !seen.has(id));
383
+ if (missing.length)
384
+ diags.push({
385
+ level: 'warning',
386
+ code: 'VARIABLE_ORDER_INCOMPLETE',
387
+ message: `.preserve({ variableOrder }) leaves out ${missing.length} declaration(s) — ${missing.slice(0, 5).join(', ')}` +
388
+ `${missing.length > 5 ? `, and ${missing.length - 5} more` : ''}. ` +
389
+ `They are written after every id the list names, which reorders the block. List them all, or drop the call.`,
390
+ where: missing[0],
391
+ });
392
+ }
307
393
  /**
308
394
  * A variable scoped with `elementId` must name an element that exists.
309
395
  *
@@ -3,7 +3,7 @@
3
3
  * decompile-cli — `<Name>.bpmn` → `<Name>.bpmn.ts`
4
4
  *
5
5
  * Usage:
6
- * node --experimental-strip-types decompile-cli.js <Name.bpmn> [-o Name.bpmn.ts] [--import <specifier>] [--style explicit|nested] [--keep-flow-ids]
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
7
  *
8
8
  * The reverse of compile-cli: reads a `.bpmn` and emits builder source whose
9
9
  * default export re-serializes to the same XML. `--import` sets the module
@@ -42,7 +42,7 @@ function opt(argv, name) {
42
42
  */
43
43
  export async function run(argv) {
44
44
  if (argv.length === 0 || argv[0] === '-h' || argv[0] === '--help') {
45
- console.error('usage: decompile <Name.bpmn> [-o Name.bpmn.ts] [--import <specifier>] [--style explicit|nested] [--keep-flow-ids]');
45
+ console.error('usage: decompile <Name.bpmn> [-o Name.bpmn.ts] [--import <specifier>] [--style explicit|nested] [--keep-flow-ids] [--keep-unused-bindings]');
46
46
  return 2;
47
47
  }
48
48
  const style = opt(argv, '--style') ?? 'explicit';
@@ -67,15 +67,19 @@ export async function run(argv) {
67
67
  }
68
68
  }
69
69
  const keepFlowIds = argv.includes('--keep-flow-ids');
70
+ const keepUnusedBindings = argv.includes('--keep-unused-bindings');
70
71
  const flags = new Set(['-o', '--import', '--style']);
71
- const input = argv.find((a, i) => a !== '--keep-flow-ids' && !flags.has(a) && !flags.has(argv[i - 1]));
72
+ const bare = new Set(['--keep-flow-ids', '--keep-unused-bindings']);
73
+ const input = argv.find((a, i) => !bare.has(a) && !flags.has(a) && !flags.has(argv[i - 1]));
72
74
  if (!input) {
73
75
  console.error('decompile: missing <Name.bpmn>');
74
76
  return 2;
75
77
  }
76
78
  const xml = readFileSync(input, 'utf8');
77
79
  const importSpecifier = opt(argv, '--import') ?? BPMN_IMPORT;
78
- let source = await decompile(xml, { importSpecifier });
80
+ let pruned = [];
81
+ const base0 = { importSpecifier, keepUnusedBindings, onPrune: (ids) => (pruned = ids) };
82
+ let source = await decompile(xml, base0);
79
83
  let lifted;
80
84
  let fallback;
81
85
  if (style === 'nested') {
@@ -86,7 +90,7 @@ export async function run(argv) {
86
90
  const sdkEntry = fileURLToPath(new URL('./bpmn-sdk.js', import.meta.url));
87
91
  for (const sequenceMode of [true, false]) {
88
92
  let summary;
89
- const nested = await decompile(xml, { importSpecifier, style: 'nested', relabelFlows: !keepFlowIds, sequenceMode, onLift: (s) => (summary = s) });
93
+ const nested = await decompile(xml, { ...base0, style: 'nested', relabelFlows: !keepFlowIds, sequenceMode, onLift: (s) => (summary = s) });
90
94
  const renamed = new Map((summary?.renamedFlows ?? []).map((r) => [r.from, r.to]));
91
95
  const verdict = await sameGraph(source, nested, sdkEntry, renamed);
92
96
  if (verdict.same) {
@@ -102,6 +106,11 @@ export async function run(argv) {
102
106
  const out = opt(argv, '-o') ?? defaultSourceOut(`${base}.bpmn.ts`);
103
107
  writeFileSync(out, source);
104
108
  console.log(`decompile: wrote ${out}`);
109
+ if (pruned.length) {
110
+ // Never silent: these are declarations the source had and the output does not.
111
+ console.log(`decompile: dropped ${pruned.length} binding(s) nothing references — ${pruned.slice(0, 8).join(', ')}` +
112
+ `${pruned.length > 8 ? `, and ${pruned.length - 8} more` : ''}. Pass --keep-unused-bindings to echo them.`);
113
+ }
105
114
  if (lifted) {
106
115
  const flat = lifted.leftFlat.length ? `; left flat: ${lifted.leftFlat.map((r) => `${r.id} (${r.reason})`).join('; ')}` : '';
107
116
  const renamed = lifted.renamedFlows.length
@@ -48,6 +48,35 @@ export interface BpmnDecompileOptions {
48
48
  sequenceMode?: boolean;
49
49
  /** Receives what the nested style lifted and what it left flat, and why. */
50
50
  onLift?: (summary: LiftSummary) => void;
51
+ /**
52
+ * Echo binding declarations that NOTHING in the document references. Defaults to
53
+ * `false` — they are dropped, and {@link BpmnDecompileOptions.onPrune} is told.
54
+ *
55
+ * The designer writes a binding pair every time a resource is picked and collects
56
+ * none of them, so a real export accumulates rows that no `=bindings.<id>` reads:
57
+ * 298 of 528 across `research/bpmn-roundtrip/corpus/`, including four `folderPath`
58
+ * rows for one queue and a whole earlier naming of two others. There is no way to
59
+ * remove one from the canvas, so an author who never decompiles is stuck with them.
60
+ *
61
+ * They are not inert. A binding with no `value` is UNRESOLVED, and the engine
62
+ * refuses to start a dispatching run listing every one of them
63
+ * (`local-engine/src/v2/dispatch/bindings.ts`, `describeMissingBindings`) — 105 on
64
+ * `billing-disputes`, 74 of which nothing references. `uip maestro bpmn validate`
65
+ * reports none of this: it answers `Success` on every corpus file.
66
+ *
67
+ * Dropping one is safe in the direction that matters. Solution resources are
68
+ * derived by walking NODES and matching each one's connection back to a binding
69
+ * (`connectionResources` in `bindings-v2.ts`), so a row no node uses provisions
70
+ * nothing. And "referenced" is decided over the whole document text, so any mention
71
+ * at all keeps the row — the failure mode is keeping a dead binding, never dropping
72
+ * a live one.
73
+ *
74
+ * Set `true` (`--keep-unused-bindings`) for a byte-exact round trip, which is what
75
+ * `research/bpmn-roundtrip/roundtrip.sh` measures.
76
+ */
77
+ keepUnusedBindings?: boolean;
78
+ /** Receives the binding ids dropped as unreferenced — see {@link BpmnDecompileOptions.keepUnusedBindings}. */
79
+ onPrune?: (droppedBindingIds: string[]) => void;
51
80
  }
52
81
  /** What `style: 'nested'` did — see {@link BpmnDecompileOptions.style}. */
53
82
  export interface LiftSummary {