@uipath/maestro-builder-sdk 6.11.6 → 6.12.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.
@@ -60,6 +60,12 @@ import { BPMN_SCHEMA_VERSIONS } from './format-profile.js';
60
60
  import { preserveSchemaVersion } from '../schema-version.js';
61
61
  const require = createRequire(import.meta.url);
62
62
  const uipathDescriptor = require('./uipath-moddle.v1.json');
63
+ /**
64
+ * Set while the nested style writes a sequence-mode root: a sub-process body the
65
+ * flat renderer writes inherits the mode, and its trailing `.sequenceFlow()`s would
66
+ * then double every edge, so the body pins `.flowMode('explicit')` for itself.
67
+ */
68
+ let pinExplicitInBodies = false;
63
69
  // ─── Source emitters ──────────────────────────────────────────────────────────
64
70
  /**
65
71
  * A JS/TS string literal.
@@ -203,6 +209,15 @@ class Chain {
203
209
  add(call) {
204
210
  this.lines.push(`${this.indent}${call}`);
205
211
  }
212
+ /**
213
+ * Put a call at the FRONT of the chain.
214
+ *
215
+ * For the declarations that belong at the top but are only known once the body has
216
+ * been walked — `.flowIds()`, which lists the implied edges the walk decided to pin.
217
+ */
218
+ addFirst(call) {
219
+ this.lines.unshift(`${this.indent}${call}`);
220
+ }
206
221
  get all() {
207
222
  return this.lines;
208
223
  }
@@ -463,14 +478,23 @@ function emitTypedNode(chain, el, payload, type) {
463
478
  const ctx = contextOf(payload);
464
479
  const inputRows = payload.input ?? [];
465
480
  let inputs;
481
+ let verbatimInputs;
466
482
  if (spec.inputPattern === 'separateInputs') {
467
- // A row's value is in its BODY or its `value` attribute, and which one is
468
- // decided by the field's declared type — see `payloadInput` in typed-node.ts.
469
- // Reading only the body silently produced `{}` for every scalar row, so a
470
- // string correlation reference round-tripped to `value="[object Object]"`.
471
- // Nothing before this authored a separateInputs type with a declared scalar
472
- // field, so every row really was a body and the bug had nowhere to appear.
473
- inputs = Object.fromEntries(inputRows.map((r) => [r.name, r.body !== undefined ? safeJson(r.body) : r.value]));
483
+ if (inputRowsNeedSpellingOut(inputRows, spec)) {
484
+ // A row the record cannot reproduce — a connector parameter whose `target` is
485
+ // `query` rather than its own name, a type the registry does not declare — is
486
+ // spelled out as `inputRows`, the way context and output rows are.
487
+ verbatimInputs = emitInputRows(inputRows);
488
+ }
489
+ else {
490
+ // A row's value is in its BODY or its `value` attribute, and which one is
491
+ // decided by the field's declared type — see `payloadInput` in typed-node.ts.
492
+ // Reading only the body silently produced `{}` for every scalar row, so a
493
+ // string correlation reference round-tripped to `value="[object Object]"`.
494
+ // Nothing before this authored a separateInputs type with a declared scalar
495
+ // field, so every row really was a body and the bug had nowhere to appear.
496
+ inputs = Object.fromEntries(inputRows.map((r) => [r.name, r.body !== undefined ? safeJson(r.body) : r.value]));
497
+ }
474
498
  }
475
499
  else if (inputRows.length) {
476
500
  inputs = safeJson(inputRows[0].body);
@@ -488,6 +512,7 @@ function emitTypedNode(chain, el, payload, type) {
488
512
  ['contextRows', verbatimContext],
489
513
  ['inputSchema', payload.context?.inputSchema?.body !== undefined ? jsonLiteral(payload.context.inputSchema.body) : undefined],
490
514
  ['inputs', record(inputs)],
515
+ ['inputRows', verbatimInputs],
491
516
  ['outputRows', verbatimRows],
492
517
  ['outputVar', verbatimRows === undefined && outputVar !== undefined && outputVar !== defaultVar ? str(outputVar) : undefined],
493
518
  ['outputs', verbatimRows === undefined ? record(extras) : undefined],
@@ -499,6 +524,70 @@ function emitTypedNode(chain, el, payload, type) {
499
524
  ['loop', emitLoop(el)],
500
525
  ]))})`);
501
526
  }
527
+ /**
528
+ * Whether `inputs` — a name→value record — would reproduce these `separateInputs`
529
+ * rows, or whether they need spelling out as `inputRows`.
530
+ *
531
+ * The record derives a row's target and type (the registry's declaration, else the
532
+ * key and `json`-for-structured / `string`-for-scalar — see `inputRows` in
533
+ * typed-node.ts), so a row is only reproducible when it carries exactly those. A
534
+ * connector's flat parameter rows never do: `name="start" … target="query"`.
535
+ */
536
+ function inputRowsNeedSpellingOut(rows, spec) {
537
+ const declared = new Map(spec.inputs.map((f) => [f.name, f]));
538
+ return rows.some((r) => {
539
+ const field = declared.get(r.name);
540
+ if (r.value === undefined && r.body === undefined)
541
+ return true;
542
+ if (r.target !== (field?.target ?? r.name))
543
+ return true;
544
+ const derivedType = field?.type ?? (r.body !== undefined ? 'json' : 'string');
545
+ return (r.type ?? 'string') !== derivedType;
546
+ });
547
+ }
548
+ /** The payload rows as `TypedInputRow` option source, in document order. */
549
+ function emitInputRows(rows) {
550
+ if (!rows.length)
551
+ return undefined;
552
+ return `[${rows
553
+ .map((r) => opts([
554
+ ['name', str(r.name)],
555
+ ['type', r.type !== undefined ? str(r.type) : undefined],
556
+ ['target', r.target !== undefined ? str(r.target) : undefined],
557
+ ['value', r.value !== undefined ? str(r.value) : undefined],
558
+ ['body', r.body !== undefined ? (r.type === 'json' ? jsonLiteral(r.body) : str(String(r.body))) : undefined],
559
+ ]))
560
+ .join(', ')}]`;
561
+ }
562
+ /**
563
+ * The serializer's default scope for a public declaration, read off the artifact —
564
+ * the mirror of `publicDeclarationScope` in serialize.ts, so a declaration the
565
+ * platform scopes the way the SDK would is emitted without an `elementId`.
566
+ */
567
+ function publicDeclarationScopeOf(process, processId) {
568
+ const elements = process.flowElements ?? [];
569
+ // "Manual" must mean the same thing here as in the emitter, which asks
570
+ // `n.event === undefined && n.trigger === undefined`. Excluding only a `uipath:Event`
571
+ // payload did not match it: a trigger start is emitted through its REGISTRY spec, and
572
+ // `Intsvc.TimerTrigger`'s tag is `uipath:activity`, so a trigger with no timer came
573
+ // back as a `bpmn:StartEvent` with empty `eventDefinitions` and a `uipath:Activity`
574
+ // payload — manual to this side, not manual to the emitter. The round trip then moved
575
+ // a public input's `elementId` from the trigger start to the process. Excluding ANY
576
+ // typed payload makes the two agree for every shape the SDK can emit.
577
+ const manualStarts = elements.filter((el) => el.$type === 'bpmn:StartEvent' && !(el.eventDefinitions ?? []).length && payloadOf(el) === undefined);
578
+ const ends = elements.filter((el) => el.$type === 'bpmn:EndEvent');
579
+ return (kind, v) => {
580
+ if (kind === 'input')
581
+ return manualStarts.length === 1 ? manualStarts[0].id : processId;
582
+ if (kind === 'output') {
583
+ const writers = ends.filter((el) => (payloadOf(el)?.output ?? []).some((row) => row.var === v.id));
584
+ if (writers.length === 1)
585
+ return writers[0].id;
586
+ return ends.length === 1 ? ends[0].id : processId;
587
+ }
588
+ return processId;
589
+ };
590
+ }
502
591
  /** Mirrors the emitter's own default-variable naming, so a default is not echoed. */
503
592
  function identifierize(name) {
504
593
  const words = name.split(/[^A-Za-z0-9]+/).filter(Boolean);
@@ -521,6 +610,15 @@ function safeJson(body) {
521
610
  return body;
522
611
  }
523
612
  }
613
+ /**
614
+ * The `uipath:scriptVersion` a script task carries — `'v1'` when it carries none,
615
+ * because that is what an absent element means to the runtime (`IsVersionAtLeast`
616
+ * treats it as the old, spread-result contract).
617
+ */
618
+ function scriptVersionOf(el) {
619
+ const sv = (el.extensionElements?.values ?? []).find((x) => x.$type === 'uipath:ScriptVersion');
620
+ return typeof sv?.value === 'string' && sv.value.trim() !== '' ? sv.value : 'v1';
621
+ }
524
622
  /** A `uipath:mapping` script task. */
525
623
  function emitScriptTask(chain, el, payload, type) {
526
624
  const args = (payload?.input ?? []).find((r) => r.name === 'args');
@@ -537,6 +635,10 @@ function emitScriptTask(chain, el, payload, type) {
537
635
  ['name', el.name !== undefined ? str(el.name) : undefined],
538
636
  ['script', str(el.script ?? '')],
539
637
  ['scriptFormat', el.scriptFormat && el.scriptFormat !== 'JavaScript' ? str(el.scriptFormat) : undefined],
638
+ // The builder's default is v3; anything else is load-bearing (it selects the
639
+ // runtime's result shape) and must survive the round trip. An absent element
640
+ // is spelled `'v1'`, the meaning it has to the runtime.
641
+ ['scriptVersion', scriptVersionOf(el) !== 'v3' ? str(scriptVersionOf(el)) : undefined],
540
642
  // Only when it is neither the contract's value nor the invalid one this SDK
541
643
  // used to emit. `BPMN.ScriptTask` is NORMALISED AWAY rather than preserved:
542
644
  // the runtime contract has no such type, so reproducing it would propagate a
@@ -594,6 +696,666 @@ function emitScope(chain, container, depth, indent) {
594
696
  ]))})`);
595
697
  }
596
698
  }
699
+ // ─── The lift: flat regions → the nesting constructs (`style: 'nested'`) ───────
700
+ //
701
+ // Every construct the builder offers lowers to plain nodes and flows, so a flat
702
+ // graph can be read back into the construct wherever the pattern holds exactly.
703
+ // The rules below are deliberately conservative — each one names a shape the
704
+ // construct is KNOWN to lower to — and `decompile-cli` still builds both sources
705
+ // and compares the graphs before it writes the nested one. Anything the rules
706
+ // cannot prove stays flat, in the same file, next to what was lifted.
707
+ const LIFT_GATEWAYS = new Set(['exclusiveGateway', 'inclusiveGateway', 'parallelGateway', 'eventBasedGateway']);
708
+ function scopeGraph(container) {
709
+ const g = { elements: [], byId: new Map(), flows: [], outs: new Map(), ins: new Map() };
710
+ for (const el of container.flowElements ?? []) {
711
+ if (el.$type === 'bpmn:SequenceFlow') {
712
+ g.flows.push(el);
713
+ const s = el.sourceRef?.id;
714
+ const t = el.targetRef?.id;
715
+ if (s !== undefined)
716
+ g.outs.set(s, [...(g.outs.get(s) ?? []), el]);
717
+ if (t !== undefined)
718
+ g.ins.set(t, [...(g.ins.get(t) ?? []), el]);
719
+ continue;
720
+ }
721
+ g.elements.push(el);
722
+ g.byId.set(el.id, el);
723
+ }
724
+ return g;
725
+ }
726
+ /** The id the builder derives for an implied edge between a flow's ends. */
727
+ function derivedFlowId(f) {
728
+ return `Flow_${f.sourceRef?.id}_${f.targetRef?.id}`;
729
+ }
730
+ /** Whether a flow's id is the one the builder derives, so an implied edge reproduces it. */
731
+ function derivableFlow(f) {
732
+ return f.id === derivedFlowId(f);
733
+ }
734
+ /**
735
+ * Consume a flow into a construct: it is no longer written as `.sequenceFlow()`, and
736
+ * when its id is not the derived one the recompile renames it — recorded on the
737
+ * summary so the CLI can verify the graphs modulo exactly those ids and report them.
738
+ */
739
+ function implyFlow(plan, f) {
740
+ plan.impliedFlows.add(f.id);
741
+ if (derivableFlow(f))
742
+ return;
743
+ // The id is not the one the construct derives. PIN it rather than let the recompile
744
+ // rename it: `bpmn merge` keys every element by id, flows included, so a renamed flow
745
+ // reads as a new one and the original — with its diagram edge — is deleted, and any
746
+ // gateway `default` preserved from the original is left naming an id that is gone.
747
+ // `.flowIds()` keeps the edge implied and gives it the id it came with.
748
+ if (plan.relabel)
749
+ plan.pinned.push({ source: f.sourceRef?.id, target: f.targetRef?.id, id: f.id });
750
+ else
751
+ plan.summary.renamedFlows.push({ from: f.id, to: derivedFlowId(f) });
752
+ }
753
+ /**
754
+ * The first flow of an arm that carries a label the construct cannot keep, or
755
+ * undefined. An implied edge has no `name`; only a `choose` arm's ENTRY flow keeps its
756
+ * label (as `label`). An empty `name=""` — designer residue on many imports — counts
757
+ * as no label, and is dropped on a relabelled edge.
758
+ */
759
+ function labelledImpliedFlow(flows, entryKeepsLabel, kept = new Set()) {
760
+ return flows.find((f, i) => typeof f.name === 'string' && f.name !== '' && !(i === 0 && entryKeepsLabel) && !kept.has(f.id));
761
+ }
762
+ /** The flows a lift keeps the label of — every `choose` arm's entry flow, nested constructs' included. */
763
+ function labelKeptFlows(lift, into = new Set()) {
764
+ for (const a of lift.arms) {
765
+ if (lift.construct === 'choose')
766
+ into.add(a.flow.id);
767
+ for (const item of a.arm.items)
768
+ if ('lift' in item)
769
+ labelKeptFlows(item.lift, into);
770
+ }
771
+ return into;
772
+ }
773
+ function isEventSubProcess(el) {
774
+ return el.$type === 'bpmn:SubProcess' && el.triggeredByEvent === true;
775
+ }
776
+ /** An element an arm may contain: something with one way in and one way out, or an end. */
777
+ function isArmNode(el) {
778
+ const kind = localName(el.$type);
779
+ return !LIFT_GATEWAYS.has(kind) && kind !== 'boundaryEvent' && kind !== 'startEvent' && !isEventSubProcess(el);
780
+ }
781
+ /**
782
+ * Follow one path from `entry` as far as an arm can own it.
783
+ *
784
+ * The arm owns a chain of elements each with exactly one incoming flow (the
785
+ * previous one) and exactly one outgoing, and it ends at an end event, at a
786
+ * `joinOk` gateway, or — when the path reaches something it cannot own (a merge
787
+ * point, a consumed element, an element that fans out) — by jumping there with
788
+ * `.goto()`. A gateway the path alone leads into is lifted INSIDE the arm as a
789
+ * nested construct, and the arm carries on from that construct's exit when it has
790
+ * one. Every flow it implies must carry the derived id unless the lift may relabel.
791
+ *
792
+ * `taken` is shared with the caller and grows as the arm claims elements, so a
793
+ * nested construct's arms and the arms that follow see what this one owns.
794
+ */
795
+ function walkArm(plan, entry, taken, joinOk, emitted) {
796
+ const { g, relabel } = plan;
797
+ const fail = (reason) => ({ ok: false, reason, elements: [], items: [], flows: [], labelKept: new Set(), end: 'terminal' });
798
+ const elements = [];
799
+ const items = [];
800
+ const flows = [];
801
+ const labelKept = new Set();
802
+ const done = (end, extra = {}) => ({ ok: true, elements, items, flows, labelKept, end, ...extra });
803
+ let f = entry;
804
+ let tgt;
805
+ for (let guard = 0; guard < 10_000; guard++) {
806
+ if (f !== undefined) {
807
+ if (!relabel && !derivableFlow(f))
808
+ return fail(`flow "${f.id}" is not named Flow_<source>_<target>, which an implied edge would be`);
809
+ flows.push(f);
810
+ tgt = g.byId.get(f.targetRef?.id);
811
+ if (!tgt)
812
+ return fail(`flow "${f.id}" leaves the scope`);
813
+ }
814
+ if (!tgt)
815
+ return fail('the path does not end');
816
+ const incoming = (g.ins.get(tgt.id) ?? []).length;
817
+ // A join MERGES: a same-kind gateway only this path leads into is not the
818
+ // construct's join but a decision of its own, lifted inside the arm below.
819
+ if (joinOk(tgt) && incoming > 1)
820
+ return done('join', { join: tgt });
821
+ if (incoming > 1 || taken.has(tgt.id) || emitted.has(tgt.id))
822
+ return done('goto', { gotoTarget: tgt.id });
823
+ if (LIFT_GATEWAYS.has(localName(tgt.$type))) {
824
+ // A decision only this path leads to: lift it here, as the arm's next statement.
825
+ taken.add(tgt.id);
826
+ const lift = planBranch(plan, tgt, emitted, false, taken);
827
+ if (!lift) {
828
+ taken.delete(tgt.id);
829
+ return done('goto', { gotoTarget: tgt.id });
830
+ }
831
+ for (const a of lift.arms)
832
+ for (const el of a.arm.elements)
833
+ taken.add(el.id);
834
+ if (lift.join)
835
+ taken.add(lift.join.id);
836
+ flows.push(...lift.consumedFlows);
837
+ labelKeptFlows(lift, labelKept);
838
+ items.push({ lift });
839
+ if (!lift.continuation)
840
+ return done('terminal');
841
+ // The construct's exit flow is already consumed; carry on AT the continuation.
842
+ f = undefined;
843
+ tgt = lift.continuation;
844
+ continue;
845
+ }
846
+ if (!isArmNode(tgt))
847
+ return done('goto', { gotoTarget: tgt.id });
848
+ if (localName(tgt.$type) === 'endEvent') {
849
+ elements.push(tgt);
850
+ items.push({ el: tgt });
851
+ taken.add(tgt.id);
852
+ return done('terminal');
853
+ }
854
+ const outs = g.outs.get(tgt.id) ?? [];
855
+ // An element that fans out (or dead-ends) is not an arm's to own; the arm jumps to
856
+ // it and the top level writes it with its flows explicit.
857
+ if (outs.length !== 1)
858
+ return done('goto', { gotoTarget: tgt.id });
859
+ elements.push(tgt);
860
+ items.push({ el: tgt });
861
+ taken.add(tgt.id);
862
+ f = outs[0];
863
+ }
864
+ return fail('the path does not end');
865
+ }
866
+ /** Whether an event carries exactly one authorable definition, and which. */
867
+ function singleDefinition(el) {
868
+ const defs = el.eventDefinitions ?? [];
869
+ if (defs.length !== 1)
870
+ return undefined;
871
+ switch (defs[0].$type) {
872
+ case 'bpmn:ErrorEventDefinition':
873
+ return 'error';
874
+ case 'bpmn:TimerEventDefinition':
875
+ return 'timer';
876
+ case 'bpmn:MessageEventDefinition':
877
+ return 'message';
878
+ default:
879
+ return undefined;
880
+ }
881
+ }
882
+ /**
883
+ * Boundary events, lifted first: an activity's handlers are part of how the
884
+ * activity is rendered wherever it lands — at the top level or inside an arm.
885
+ */
886
+ function planHandlers(plan) {
887
+ const { g } = plan;
888
+ for (const b of g.elements) {
889
+ if (localName(b.$type) !== 'boundaryEvent')
890
+ continue;
891
+ const host = g.byId.get(b.attachedToRef?.id);
892
+ const reject = (reason) => {
893
+ plan.summary.leftFlat.push({ id: b.id, reason });
894
+ };
895
+ if (!host || !isArmNode(host) || localName(host.$type) === 'endEvent') {
896
+ reject('attached to nothing an activity body can own');
897
+ continue;
898
+ }
899
+ if (!singleDefinition(b)) {
900
+ reject('carries no single error, timer or message definition');
901
+ continue;
902
+ }
903
+ const outs = g.outs.get(b.id) ?? [];
904
+ if (outs.length !== 1) {
905
+ reject(`has ${outs.length} outgoing flows; a handler's path leaves once`);
906
+ continue;
907
+ }
908
+ const trial = new Set(plan.taken);
909
+ const arm = walkArm(plan, outs[0], trial, () => false, new Set());
910
+ if (!arm.ok) {
911
+ reject(arm.reason ?? 'path not liftable');
912
+ continue;
913
+ }
914
+ const labelled = labelledImpliedFlow(arm.flows, false, arm.labelKept);
915
+ if (labelled) {
916
+ reject(`flow "${labelled.id}" on the handler's path carries the label "${labelled.name}", which an implied edge cannot`);
917
+ continue;
918
+ }
919
+ plan.taken.add(b.id);
920
+ for (const id of trial)
921
+ plan.taken.add(id);
922
+ for (const f of arm.flows)
923
+ implyFlow(plan, f);
924
+ plan.handlers.set(host.id, [...(plan.handlers.get(host.id) ?? []), { boundary: b, arm }]);
925
+ plan.summary.handlers++;
926
+ }
927
+ }
928
+ /**
929
+ * A gateway and its arms as one construct, or nothing.
930
+ *
931
+ * @param mustTerminate - Inside an event sub-process body the construct is the last
932
+ * statement, so every arm has to end; a join would need a continuation.
933
+ */
934
+ function planBranch(plan, gw, emitted, mustTerminate, seedTaken) {
935
+ const { g } = plan;
936
+ const kind = localName(gw.$type);
937
+ const reject = (reason) => {
938
+ plan.summary.leftFlat.push({ id: gw.id, reason });
939
+ return undefined;
940
+ };
941
+ const outs = g.outs.get(gw.id) ?? [];
942
+ if (outs.length < 2)
943
+ return reject('has fewer than two outgoing flows');
944
+ const construct = kind === 'parallelGateway' ? 'fork' : kind === 'eventBasedGateway' ? 'race' : 'choose';
945
+ const joinKind = construct === 'fork' ? 'parallelGateway' : construct === 'race' ? 'exclusiveGateway' : kind;
946
+ const defaultId = gw.default?.id;
947
+ if (construct === 'choose') {
948
+ for (const f of outs) {
949
+ const conditioned = f.conditionExpression?.body !== undefined;
950
+ if (f.id === defaultId && conditioned)
951
+ return reject(`default flow "${f.id}" also carries a condition`);
952
+ if (f.id !== defaultId && !conditioned)
953
+ return reject(`flow "${f.id}" has neither a condition nor the default`);
954
+ }
955
+ }
956
+ else if (outs.some((f) => f.conditionExpression?.body !== undefined)) {
957
+ return reject('a fork or race arm cannot carry a condition');
958
+ }
959
+ // An arm may own only what nothing has placed yet: an element already written at
960
+ // the top level (document order put it before its gateway) is reached with `.goto()`,
961
+ // not written a second time. Before flow ids could be relabelled almost no arm
962
+ // lifted on a real import, which is how this went unmeasured.
963
+ // A nested lift (this gateway met inside an arm) walks against the enclosing arm's
964
+ // ownership too, or its arms could claim what that arm already owns. The set is a
965
+ // trial copy: a rejected lift leaves nothing claimed.
966
+ const taken = new Set([...plan.taken, ...emitted, ...(seedTaken ?? [])]);
967
+ const arms = [];
968
+ const joinOk = (el) => LIFT_GATEWAYS.has(localName(el.$type)) && localName(el.$type) === joinKind && el.id !== gw.id;
969
+ for (const f of outs) {
970
+ const arm = walkArm(plan, f, taken, joinOk, emitted);
971
+ if (!arm.ok)
972
+ return reject(`arm from "${f.id}": ${arm.reason}`);
973
+ const labelled = labelledImpliedFlow(arm.flows, construct === 'choose', arm.labelKept);
974
+ if (labelled)
975
+ return reject(`flow "${labelled.id}" inside an arm carries the label "${labelled.name}", which an implied edge cannot`);
976
+ arms.push({ flow: f, arm, otherwise: f.id === defaultId });
977
+ }
978
+ // The join is the one gateway two or more arms reach; an arm that reaches a gateway
979
+ // alone, or a different one, jumps there instead — the builder writes a join only
980
+ // for two or more, and `.goto()` is exactly a flow to an element the arm does not own.
981
+ const reached = new Map();
982
+ for (const a of arms)
983
+ if (a.arm.end === 'join')
984
+ reached.set(a.arm.join.id, (reached.get(a.arm.join.id) ?? 0) + 1);
985
+ const shared = [...reached.entries()].filter(([, n]) => n >= 2).map(([id]) => id);
986
+ if (shared.length > 1)
987
+ return reject('arms rejoin at more than one shared gateway');
988
+ for (const a of arms) {
989
+ if (a.arm.end === 'join' && a.arm.join.id !== shared[0]) {
990
+ a.arm = { ...a.arm, end: 'goto', gotoTarget: a.arm.join.id, join: undefined };
991
+ }
992
+ }
993
+ const joins = arms.filter((a) => a.arm.end === 'join');
994
+ const joinIds = new Set(joins.map((a) => a.arm.join.id));
995
+ if (arms.filter((a) => a.arm.end === 'join' && a.arm.elements.length === 0).length > 1)
996
+ return reject('more than one empty arm');
997
+ let join;
998
+ let continuation;
999
+ const consumedFlows = [];
1000
+ if (joinIds.size === 1) {
1001
+ if (mustTerminate)
1002
+ return reject('rejoins, where the enclosing body needs every arm to end');
1003
+ join = joins[0].arm.join;
1004
+ if ((g.ins.get(join.id) ?? []).length !== joins.length)
1005
+ return reject(`join "${join.id}" has incoming flows from outside the arms`);
1006
+ const jOuts = g.outs.get(join.id) ?? [];
1007
+ if (jOuts.length !== 1)
1008
+ return reject(`join "${join.id}" has ${jOuts.length} outgoing flows`);
1009
+ if (!plan.relabel && !derivableFlow(jOuts[0]))
1010
+ return reject(`flow "${jOuts[0].id}" out of the join is not named Flow_<source>_<target>`);
1011
+ if (labelledImpliedFlow([jOuts[0]], false))
1012
+ return reject(`flow "${jOuts[0].id}" out of the join carries a label, which an implied edge cannot`);
1013
+ continuation = g.byId.get(jOuts[0].targetRef?.id);
1014
+ if (!continuation)
1015
+ return reject('the join leaves the scope');
1016
+ if (emitted.has(continuation.id) || plan.taken.has(continuation.id)) {
1017
+ return reject(`continuation "${continuation.id}" was already placed, so the construct's exit could not reach it`);
1018
+ }
1019
+ consumedFlows.push(jOuts[0]);
1020
+ }
1021
+ for (const a of arms)
1022
+ consumedFlows.push(...a.arm.flows);
1023
+ return { gateway: gw, construct, inclusive: kind === 'inclusiveGateway', arms, join, continuation, consumedFlows };
1024
+ }
1025
+ /** Every element a lift owns — its arms' elements, nested constructs' included, and its joins. */
1026
+ function ownedByLift(lift) {
1027
+ const out = [lift.gateway.id];
1028
+ if (lift.join)
1029
+ out.push(lift.join.id);
1030
+ for (const a of lift.arms) {
1031
+ for (const el of a.arm.elements)
1032
+ out.push(el.id);
1033
+ for (const item of a.arm.items)
1034
+ if ('lift' in item)
1035
+ out.push(...ownedByLift(item.lift));
1036
+ }
1037
+ return out;
1038
+ }
1039
+ /** Every element a lift's arms jump to, nested constructs' included — the places the top level continues from. */
1040
+ function gotoTargetsOf(lift) {
1041
+ const out = [];
1042
+ for (const a of lift.arms) {
1043
+ if (a.arm.end === 'goto' && a.arm.gotoTarget)
1044
+ out.push(a.arm.gotoTarget);
1045
+ for (const item of a.arm.items)
1046
+ if ('lift' in item)
1047
+ out.push(...gotoTargetsOf(item.lift));
1048
+ }
1049
+ return out;
1050
+ }
1051
+ /** Render one element's call without a leading indent, its lifted handlers written into its body. */
1052
+ function renderElementNested(plan, el, depth, indent) {
1053
+ const tmp = new Chain('');
1054
+ emitElement(tmp, el, depth, indent);
1055
+ let call = tmp.all.join('\n');
1056
+ const handlers = plan.handlers.get(el.id);
1057
+ if (handlers?.length) {
1058
+ const pad = indent.repeat(depth + 2);
1059
+ const lines = handlers.map((h) => renderHandler(plan, h, depth + 2, indent));
1060
+ call = `${call.replace(/\)$/, '')}, (t) => {\n${pad}${lines.join(`\n${pad}`)}\n${indent.repeat(depth + 1)}})`;
1061
+ }
1062
+ return call;
1063
+ }
1064
+ /** The lines of an arm's body after `b`, one per element, or the closing `.goto()`. */
1065
+ function renderArmLines(plan, arm, depth, indent) {
1066
+ const lines = arm.items.map((item) => ('el' in item ? renderElementNested(plan, item.el, depth, indent) : renderBranch(plan, item.lift, depth, indent)));
1067
+ if (arm.end === 'goto')
1068
+ lines.push(`.goto(${str(arm.gotoTarget)})`);
1069
+ return lines;
1070
+ }
1071
+ function renderArmBody(plan, arm, depth, indent) {
1072
+ const lines = renderArmLines(plan, arm, depth, indent);
1073
+ if (!lines.length)
1074
+ return undefined;
1075
+ const pad = indent.repeat(depth + 1);
1076
+ return `(b) => b\n${pad}${lines.join(`\n${pad}`)}`;
1077
+ }
1078
+ function renderHandler(plan, h, depth, indent) {
1079
+ const b = h.boundary;
1080
+ const host = b.attachedToRef?.id;
1081
+ const def = singleDefinition(b);
1082
+ const method = def === 'error' ? 'onError' : def === 'timer' ? 'onTimer' : 'onMessage';
1083
+ const defExpr = eventOptions(b).find(([k]) => k === def)?.[1] ?? 'true';
1084
+ const payload = payloadOf(b);
1085
+ const type = extensionTypeOf(payload);
1086
+ const optsText = opts([
1087
+ ['id', b.id !== `${host}_${method}` ? str(b.id) : undefined],
1088
+ ['name', b.name !== undefined ? str(b.name) : undefined],
1089
+ ['interrupting', def !== 'error' && b.cancelActivity === false ? 'false' : undefined],
1090
+ ['payload', payload && type ? opts([['type', str(type)], ['outputRows', emitOutputRows(payload) ?? '[]']]) : undefined],
1091
+ ]);
1092
+ const body = renderArmBody(plan, h.arm, depth, indent);
1093
+ const args = [defExpr, ...(optsText !== '' && optsText !== '{}' ? [optsText] : []), ...(body ? [body] : [])];
1094
+ return `t.${method}(${args.join(', ')});`;
1095
+ }
1096
+ function renderBranch(plan, lift, depth, indent) {
1097
+ plan.summary.branches++;
1098
+ // The chain already indents the statement by one level; arms sit one level inside
1099
+ // it, their bodies one further, and the closing bracket lines up with the statement.
1100
+ const pad = indent.repeat(depth + 2);
1101
+ const gw = lift.gateway;
1102
+ const joinOpt = lift.join !== undefined
1103
+ ? opts([
1104
+ ['id', lift.join.id !== `${gw.id}_join` ? str(lift.join.id) : undefined],
1105
+ ['name', lift.join.name !== undefined ? str(lift.join.name) : undefined],
1106
+ ])
1107
+ : undefined;
1108
+ const optsText = opts([
1109
+ ['name', gw.name !== undefined ? str(gw.name) : undefined],
1110
+ ['inclusive', lift.inclusive ? 'true' : undefined],
1111
+ ['join', joinOpt !== undefined && joinOpt !== '' && joinOpt !== '{}' ? joinOpt : undefined],
1112
+ ]);
1113
+ const armTexts = lift.arms.map(({ flow, arm, otherwise }) => {
1114
+ const body = renderArmBody(plan, arm, depth + 2, indent);
1115
+ if (lift.construct !== 'choose')
1116
+ return body ?? '() => {}';
1117
+ return opts([
1118
+ otherwise ? ['otherwise', 'true'] : ['when', str(flow.conditionExpression?.body)],
1119
+ ['label', flow.name !== undefined ? str(flow.name) : undefined],
1120
+ ['body', body],
1121
+ ]);
1122
+ });
1123
+ const arms = `[\n${pad}${armTexts.join(`,\n${pad}`)},\n${indent.repeat(depth + 1)}]`;
1124
+ return `.${lift.construct}(${str(gw.id)}, ${arms}${optionalArg(optsText)})`;
1125
+ }
1126
+ /**
1127
+ * An event sub-process as `.eventSubProcess()`, or nothing.
1128
+ *
1129
+ * Its body is a `sequence` scope, so the whole body has to be one path from the
1130
+ * synthesized start — a chain, optionally ending in a construct whose arms all
1131
+ * end — with nothing left over.
1132
+ */
1133
+ function renderEventSubProcess(plan, sp, depth, indent) {
1134
+ // A rejected body is written flat under its own flow ids, so the renames its
1135
+ // attempt recorded are withdrawn.
1136
+ const renamesBefore = plan.summary.renamedFlows.length;
1137
+ const reject = (reason) => {
1138
+ plan.summary.leftFlat.push({ id: sp.id, reason });
1139
+ plan.summary.renamedFlows.length = renamesBefore;
1140
+ return undefined;
1141
+ };
1142
+ if (retryOf(sp) || errorMappingOf(sp) || sp.loopCharacteristics)
1143
+ return reject('carries retry, error mapping or a loop');
1144
+ const g = scopeGraph(sp);
1145
+ const starts = g.elements.filter((el) => localName(el.$type) === 'startEvent');
1146
+ if (starts.length !== 1)
1147
+ return reject(`has ${starts.length} start events`);
1148
+ const start = starts[0];
1149
+ const def = singleDefinition(start);
1150
+ if (!def)
1151
+ return reject('its start carries no single error, timer or message definition');
1152
+ if (def === 'error' && start.isInterrupting === false)
1153
+ return reject('a non-interrupting error start is not a thing');
1154
+ const outs = g.outs.get(start.id) ?? [];
1155
+ if (outs.length !== 1)
1156
+ return reject(`its start has ${outs.length} outgoing flows`);
1157
+ // Shares the outer plan's pin list: a flow id is unique across the document, so every
1158
+ // pin can be declared once, in the one `.flowIds()` the top-level chain emits.
1159
+ const inner = {
1160
+ g,
1161
+ taken: new Set([start.id]),
1162
+ impliedFlows: new Set(),
1163
+ handlers: new Map(),
1164
+ summary: plan.summary,
1165
+ relabel: plan.relabel,
1166
+ pinned: plan.pinned,
1167
+ };
1168
+ planHandlers(inner);
1169
+ const arm = walkArm(inner, outs[0], inner.taken, (el) => LIFT_GATEWAYS.has(localName(el.$type)), new Set());
1170
+ if (!arm.ok)
1171
+ return reject(`body: ${arm.reason}`);
1172
+ const labelled = labelledImpliedFlow(arm.flows, false, arm.labelKept);
1173
+ if (labelled)
1174
+ return reject(`body: flow "${labelled.id}" carries the label "${labelled.name}", which an implied edge cannot`);
1175
+ for (const f of arm.flows)
1176
+ implyFlow(inner, f);
1177
+ const lines = renderArmLines(inner, arm, depth + 1, indent);
1178
+ if (arm.end === 'join') {
1179
+ // The chain reached a gateway: it may close the body as a construct whose arms all end.
1180
+ const gw = arm.join;
1181
+ if ((g.ins.get(gw.id) ?? []).length !== 1)
1182
+ return reject(`gateway "${gw.id}" in the body is reached from more than one place`);
1183
+ const lift = planBranch(inner, gw, new Set(), true);
1184
+ if (!lift)
1185
+ return reject(`gateway "${gw.id}" in the body could not be lifted`);
1186
+ for (const id of ownedByLift(lift))
1187
+ inner.taken.add(id);
1188
+ for (const f of lift.consumedFlows)
1189
+ implyFlow(inner, f);
1190
+ lines.push(renderBranch(inner, lift, depth + 1, indent));
1191
+ }
1192
+ else if (arm.end === 'goto') {
1193
+ return reject(`body jumps to "${arm.gotoTarget}", which a sequence body cannot`);
1194
+ }
1195
+ const leftover = g.elements.filter((el) => !inner.taken.has(el.id));
1196
+ if (leftover.length)
1197
+ return reject(`elements outside the one path: ${leftover.map((el) => `"${el.id}"`).join(', ')}`);
1198
+ if (g.flows.some((f) => !inner.impliedFlows.has(f.id)))
1199
+ return reject('flows outside the one path');
1200
+ // The start event's payload: the product's capture row is `errorVar`; anything else is spelled out.
1201
+ const payload = payloadOf(start);
1202
+ const type = extensionTypeOf(payload);
1203
+ const rows = payload?.output ?? [];
1204
+ const capture = def === 'error' && type === 'BPMN.Variables' && rows.length === 1 && rows[0].name === 'Error' && rows[0].source === '=Error' &&
1205
+ rows[0].type === 'jsonSchema' && rows[0].var !== undefined && (payload?.input ?? []).length === 0
1206
+ ? rows[0].var
1207
+ : undefined;
1208
+ const defExpr = eventOptions(start).find(([k]) => k === def)?.[1];
1209
+ const spPayload = payloadOf(sp);
1210
+ const spType = extensionTypeOf(spPayload);
1211
+ const optsText = opts([
1212
+ ['name', sp.name !== undefined ? str(sp.name) : undefined],
1213
+ [def, defExpr],
1214
+ ['interrupting', def !== 'error' && start.isInterrupting === false ? 'false' : undefined],
1215
+ // `` `${id}_Error` `` is compile's default and `false` its opt-out, so neither is
1216
+ // left implicit the wrong way round: an error start with no capture row recompiles
1217
+ // without one, and a capture the designer named something else is spelled out.
1218
+ [
1219
+ 'errorVar',
1220
+ capture === `${sp.id}_Error` ? undefined : capture !== undefined ? str(capture) : def === 'error' ? 'false' : undefined,
1221
+ ],
1222
+ ['startId', start.id !== `${sp.id}_start` ? str(start.id) : undefined],
1223
+ ['startName', start.name !== undefined ? str(start.name) : undefined],
1224
+ ['startPayload', payload && type && capture === undefined ? opts([['type', str(type)], ['outputRows', emitOutputRows(payload) ?? '[]']]) : undefined],
1225
+ ['tags', tagsOf(sp)],
1226
+ ['payload', spPayload && spType ? opts([['type', str(spType)], ['outputRows', emitOutputRows(spPayload) ?? '[]']]) : undefined],
1227
+ ]);
1228
+ const vars = new Chain('');
1229
+ emitVariablesInto(vars, sp, sp.id, capture !== undefined && captureDeclLooksDerived(sp, capture, start.id) ? new Set([capture]) : undefined);
1230
+ const bodyLines = [...vars.all, ...lines];
1231
+ const pad = indent.repeat(depth + 2);
1232
+ const body = bodyLines.length ? `(h) => h\n${pad}${bodyLines.join(`\n${pad}`)}` : '(h) => h';
1233
+ plan.summary.nets++;
1234
+ return `.eventSubProcess(${str(sp.id)}, ${optsText}, ${body})`;
1235
+ }
1236
+ /** `emitScope`, with the liftable regions written as constructs. */
1237
+ function emitScopeNested(chain, container, depth, indent, summary, relabel, sequence) {
1238
+ const g = scopeGraph(container);
1239
+ const plan = { g, taken: new Set(), impliedFlows: new Set(), handlers: new Map(), summary, relabel, pinned: [] };
1240
+ planHandlers(plan);
1241
+ const emitted = new Set();
1242
+ const written = new Set();
1243
+ if (sequence)
1244
+ chain.add(".flowMode('sequence')");
1245
+ const renderFlow = (f) => `.sequenceFlow(${str(f.sourceRef?.id)}, ${str(f.targetRef?.id)}${optionalArg(opts([
1246
+ ['id', f.id !== derivedFlowId(f) ? str(f.id) : undefined],
1247
+ ['name', f.name !== undefined ? str(f.name) : undefined],
1248
+ ['condition', f.conditionExpression?.body !== undefined ? str(f.conditionExpression.body) : undefined],
1249
+ ]))})`;
1250
+ /** Write an element's remaining flows now, before the next statement — so "explicit wins" applies. */
1251
+ const writeNow = (flows) => {
1252
+ for (const f of flows) {
1253
+ if (plan.impliedFlows.has(f.id) || written.has(f.id))
1254
+ continue;
1255
+ written.add(f.id);
1256
+ chain.add(renderFlow(f));
1257
+ }
1258
+ };
1259
+ /**
1260
+ * Write one element and say where the path continues: a lifted construct's exit, or
1261
+ * the one element a single outgoing flow leads to. `undefined` ends the chain.
1262
+ */
1263
+ const emitOne = (el) => {
1264
+ emitted.add(el.id);
1265
+ if (LIFT_GATEWAYS.has(localName(el.$type))) {
1266
+ const lift = planBranch(plan, el, emitted, false);
1267
+ if (lift) {
1268
+ for (const id of ownedByLift(lift))
1269
+ plan.taken.add(id);
1270
+ for (const f of lift.consumedFlows)
1271
+ implyFlow(plan, f);
1272
+ chain.add(renderBranch(plan, lift, depth, indent));
1273
+ pending.push(...gotoTargetsOf(lift));
1274
+ return lift.continuation;
1275
+ }
1276
+ }
1277
+ if (isEventSubProcess(el)) {
1278
+ const call = renderEventSubProcess(plan, el, depth, indent);
1279
+ if (call) {
1280
+ chain.add(call);
1281
+ return undefined;
1282
+ }
1283
+ }
1284
+ chain.add(renderElementNested(plan, el, depth, indent));
1285
+ for (const h of plan.handlers.get(el.id) ?? [])
1286
+ if (h.arm.end === 'goto' && h.arm.gotoTarget)
1287
+ pending.push(h.arm.gotoTarget);
1288
+ const outs = g.outs.get(el.id) ?? [];
1289
+ if (outs.length === 1) {
1290
+ const f = outs[0];
1291
+ const next = g.byId.get(f.targetRef?.id);
1292
+ const fresh = next !== undefined && !emitted.has(next.id) && !plan.taken.has(next.id);
1293
+ // In sequence mode the edge to the statement written next is implied — when that
1294
+ // statement IS the successor, the edge may be relabelled (or already carries the
1295
+ // derived id), and it carries no label. Otherwise it is written here, before the
1296
+ // next statement, so the builder does not imply a second one beside it.
1297
+ if (sequence && fresh && (relabel || derivableFlow(f)) && !labelledImpliedFlow([f], false))
1298
+ implyFlow(plan, f);
1299
+ else if (sequence)
1300
+ writeNow([f]);
1301
+ return fresh ? next : undefined;
1302
+ }
1303
+ // A fan-out: its edges are explicit, written now, and each target is a place the
1304
+ // process continues from.
1305
+ if (sequence)
1306
+ writeNow(outs);
1307
+ for (const f of outs)
1308
+ pending.push(f.targetRef?.id);
1309
+ return undefined;
1310
+ };
1311
+ // Emit in CONTROL-FLOW order, not document order: from each source, follow the
1312
+ // path while it is a path. An import lists elements in the order they were drawn,
1313
+ // which put a gateway after the elements its arms should own and had the arms jump
1314
+ // to them instead; walking from the start writes the gateway first, the arm owns
1315
+ // its chain, and what remains explicit is a merge or a back edge — the same
1316
+ // ordering an author would type. Manual starts first, then other starts, then
1317
+ // anything nothing flows into, then whatever a cycle left unreached.
1318
+ // The worklist: where the process continues after a construct's arms jumped out or
1319
+ // an element fanned out. Drained before the next source, so the top level follows
1320
+ // the graph's reach rather than the document's order.
1321
+ const pending = [];
1322
+ const emitFrom = (start) => {
1323
+ let el = start;
1324
+ while (el && !emitted.has(el.id) && !plan.taken.has(el.id))
1325
+ el = emitOne(el);
1326
+ while (pending.length) {
1327
+ const next = g.byId.get(pending.shift() ?? '');
1328
+ if (next && !emitted.has(next.id) && !plan.taken.has(next.id))
1329
+ emitFrom(next);
1330
+ }
1331
+ };
1332
+ const rank = (el) => {
1333
+ const kind = localName(el.$type);
1334
+ if (kind === 'startEvent')
1335
+ return (el.eventDefinitions ?? []).length === 0 && payloadOf(el)?.$type !== 'uipath:Event' ? 0 : 1;
1336
+ return (g.ins.get(el.id) ?? []).length === 0 ? 2 : 3;
1337
+ };
1338
+ const sources = [...g.elements].sort((a, b) => rank(a) - rank(b) || g.elements.indexOf(a) - g.elements.indexOf(b));
1339
+ for (const el of sources)
1340
+ emitFrom(el);
1341
+ for (const f of g.flows) {
1342
+ if (plan.impliedFlows.has(f.id) || written.has(f.id))
1343
+ continue;
1344
+ const auto = `Flow_${f.sourceRef?.id}_${f.targetRef?.id}`;
1345
+ chain.add(`.sequenceFlow(${str(f.sourceRef?.id)}, ${str(f.targetRef?.id)}${optionalArg(opts([
1346
+ ['id', f.id !== auto ? str(f.id) : undefined],
1347
+ ['name', f.name !== undefined ? str(f.name) : undefined],
1348
+ ['condition', f.conditionExpression?.body !== undefined ? str(f.conditionExpression.body) : undefined],
1349
+ ]))})`);
1350
+ }
1351
+ // Last, so it is first: the implied edges whose id is not the one the construct
1352
+ // derives. Emitted at the front of the chain because it is a declaration about the
1353
+ // whole scope, and only knowable once the walk has decided what to imply.
1354
+ if (plan.pinned.length) {
1355
+ const rows = plan.pinned.map((e) => `{ source: ${str(e.source)}, target: ${str(e.target)}, id: ${str(e.id)} }`);
1356
+ chain.addFirst(`.flowIds([${rows.join(', ')}])`);
1357
+ }
1358
+ }
597
1359
  /**
598
1360
  * Whether a start event's event definition carries configuration a registry spec
599
1361
  * cannot supply — today, a timer's duration/date/cycle.
@@ -715,6 +1477,9 @@ function emitEvent(chain, el, kind, payload, type) {
715
1477
  chain.add(`.${kind}(${str(el.id)}${optionalArg(opts([
716
1478
  ['name', el.name !== undefined ? str(el.name) : undefined],
717
1479
  ...eventOptions(el),
1480
+ // Only a start event can be non-interrupting, and only the exception is
1481
+ // spelled — the builder's default is BPMN's.
1482
+ ['interrupting', kind === 'startEvent' && el.isInterrupting === false ? 'false' : undefined],
718
1483
  ['payload', eventPayload],
719
1484
  ]))})`);
720
1485
  }
@@ -812,8 +1577,10 @@ function emitElement(chain, el, depth, indent) {
812
1577
  return;
813
1578
  case 'subProcess': {
814
1579
  const inner = new Chain(indent.repeat(depth + 2));
1580
+ if (pinExplicitInBodies)
1581
+ inner.add(".flowMode('explicit')");
815
1582
  emitScope(inner, el, depth + 1, indent);
816
- emitVariablesInto(inner, el);
1583
+ emitVariablesInto(inner, el, el.id);
817
1584
  const body = inner.all.length
818
1585
  ? `(sp) => {\n${inner.all.map((l, i) => (i === 0 ? `${l.replace(/^(\s*)\./, '$1sp.')}` : l)).join('\n')};\n${indent.repeat(depth + 1)}}`
819
1586
  : '(sp) => {}';
@@ -859,15 +1626,47 @@ function varType(wire) {
859
1626
  return wire ?? 'string';
860
1627
  }
861
1628
  }
862
- /** Emit `.var()` / `.input()` / `.output()` for one scope's declarations. */
863
- function emitVariablesInto(chain, container, skip = new Set(),
1629
+ /** How a declaration's `elementId` is spelled in source — see `emitVariablesInto`. */
1630
+ function emitElementId(elementId, ownerId, rehome) {
1631
+ if (elementId === undefined)
1632
+ return 'null';
1633
+ const effective = rehome && elementId === rehome.from ? rehome.to : elementId;
1634
+ return effective === ownerId ? undefined : str(effective);
1635
+ }
1636
+ /**
1637
+ * Whether an error net's capture variable is declared exactly as `errorVar` would
1638
+ * declare it — `jsonSchema`, named `Error`, scoped to the start event, no default — so
1639
+ * the nested form can leave it to the default instead of echoing it.
1640
+ */
1641
+ function captureDeclLooksDerived(sp, varId, startId) {
1642
+ const vars = (sp.extensionElements?.values ?? []).find((x) => x.$type === 'uipath:Variables');
1643
+ const v = (vars?.inputOutput ?? []).find((x) => x.id === varId);
1644
+ return v !== undefined && v.name === 'Error' && v.type === 'jsonSchema' && v.elementId === startId && v.default === undefined;
1645
+ }
1646
+ /**
1647
+ * Emit `.var()` / `.input()` / `.output()` for one scope's declarations.
1648
+ *
1649
+ * `elementId` is written the way `compile` reads it: a declaration belonging to the
1650
+ * scope itself (`ownerId` — the process or sub-process id, which is the compile-time
1651
+ * default) is emitted with no `elementId`; one belonging to another element names it;
1652
+ * and one with NO `elementId` on the wire is emitted as `elementId: null`, because the
1653
+ * default would otherwise add one on recompile and the round trip would not be byte
1654
+ * for byte. Older exports have that shape on every variable.
1655
+ */
1656
+ function emitVariablesInto(chain, container, ownerId, skip = new Set(),
864
1657
  /**
865
1658
  * Rewrite one `elementId` as another. Needed when the declarations are read off a
866
1659
  * COLLABORATION, whose id will not exist after a recompile — see `variablesHost`.
867
1660
  */
868
1661
  rehome,
869
1662
  /** How each schema body should be written — see {@link planSchemas}. */
870
- plan) {
1663
+ plan,
1664
+ /**
1665
+ * The scope the serializer would give an unscoped declaration of this kind — see
1666
+ * `publicDeclarationScope` in serialize.ts. A declaration already there is emitted
1667
+ * without `elementId`; the owner when absent.
1668
+ */
1669
+ defaultScope = () => ownerId) {
871
1670
  const vars = (container.extensionElements?.values ?? []).find((x) => x.$type === 'uipath:Variables');
872
1671
  if (!vars)
873
1672
  return;
@@ -887,12 +1686,7 @@ plan) {
887
1686
  ['default', def],
888
1687
  ['schema', v.body !== undefined ? (plan?.ref.get(String(v.body)) ?? jsonLiteral(v.body)) : undefined],
889
1688
  ['required', v.required !== undefined ? 'true' : undefined],
890
- [
891
- 'elementId',
892
- v.elementId === undefined
893
- ? undefined
894
- : str(rehome && v.elementId === rehome.from ? rehome.to : v.elementId),
895
- ],
1689
+ ['elementId', emitElementId(v.elementId, defaultScope(prop, v), rehome)],
896
1690
  ]))})`);
897
1691
  }
898
1692
  }
@@ -1034,8 +1828,22 @@ export async function decompile(xml, options = {}) {
1034
1828
  ]);
1035
1829
  for (const [id, body] of schemaPlan.declare)
1036
1830
  chain.add(`.schema(${str(id)}, ${body})`);
1037
- emitVariablesInto(chain, host.container, derived, host.rehome, schemaPlan);
1038
- emitScope(chain, process, 0, indent);
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);
1838
+ }
1839
+ finally {
1840
+ pinExplicitInBodies = false;
1841
+ }
1842
+ options.onLift?.(summary);
1843
+ }
1844
+ else {
1845
+ emitScope(chain, process, 0, indent);
1846
+ }
1039
1847
  const importFrom = options.importSpecifier ?? './bpmn-sdk.js';
1040
1848
  const imported = ['bpmn', ...[...schemaPlan.imports].sort()].join(', ');
1041
1849
  const head = `import { ${imported} } from ${str(importFrom)};\n\nexport default bpmn(${str(process.id)})`;
@@ -1069,17 +1877,21 @@ export async function decompile(xml, options = {}) {
1069
1877
  * `decision` while its declared variable is `o`.
1070
1878
  */
1071
1879
  function derivedVariableIds(process) {
1072
- const candidates = new Set();
1880
+ // var id -> [owner element id, wire type the derived declaration carries]
1881
+ const candidates = new Map();
1073
1882
  const walk = (container) => {
1074
1883
  for (const el of container.flowElements ?? []) {
1075
1884
  const payload = payloadOf(el);
1076
1885
  const type = extensionTypeOf(payload);
1077
- // A mapping node's rows write AUTHORED variables (a script task's outputs, a
1078
- // variable task's assignments), so they are not derived.
1079
- if (payload && type && type !== 'BPMN.Variables' && type !== 'BPMN.ScriptTask') {
1886
+ // A variable task's rows write AUTHORED variables (its assignments), so they are
1887
+ // not derived. A script task's `outputs` rows ARE — serialize declares each as a
1888
+ // `string` owned by the task, because the validator does not read the row as a
1889
+ // 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';
1080
1892
  for (const row of payload.output ?? [])
1081
1893
  if (row.var)
1082
- candidates.add(row.var);
1894
+ candidates.set(row.var, [el.id, wire]);
1083
1895
  }
1084
1896
  if (el.$type === 'bpmn:SubProcess')
1085
1897
  walk(el);
@@ -1087,20 +1899,22 @@ function derivedVariableIds(process) {
1087
1899
  };
1088
1900
  walk(process);
1089
1901
  // Keep only the candidates whose declaration matches what `implicitNodeVars` would
1090
- // write: `{ id, name: id, type: 'object', direction: 'inputOutput' }`, i.e. an
1091
- // `inputOutput` row, `name` equal to `id`, wire type `json`, and no default or
1092
- // element scope. A declaration that differs in ANY of those cannot have been
1093
- // derived, so it is the author's and has to be echoed.
1902
+ // write: an `inputOutput` row, `name` equal to `id`, the wire type above, no default,
1903
+ // and `elementId` naming the node whose row fills it. A declaration that differs in
1904
+ // ANY of those cannot have been derived, so it is the author's and has to be echoed
1905
+ // — including the older shape with no `elementId`, which recompiles identically only
1906
+ // because `emitVariablesInto` then writes it as `elementId: null`.
1094
1907
  const out = new Set();
1095
1908
  const vars = (process.extensionElements?.values ?? []).find((x) => x.$type === 'uipath:Variables');
1096
1909
  for (const v of vars?.inputOutput ?? []) {
1097
- if (!candidates.has(v.id))
1910
+ const c = candidates.get(v.id);
1911
+ if (!c)
1098
1912
  continue;
1099
1913
  if (v.name !== undefined && v.name !== v.id)
1100
1914
  continue;
1101
- if (v.type !== 'json')
1915
+ if (v.type !== c[1])
1102
1916
  continue;
1103
- if (v.default !== undefined || v.elementId !== undefined)
1917
+ if (v.default !== undefined || v.elementId !== c[0])
1104
1918
  continue;
1105
1919
  out.add(v.id);
1106
1920
  }