@bpmnkit/core 0.1.1 → 0.2.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.
Files changed (83) hide show
  1. package/README.md +32 -1
  2. package/dist/bpmn/agentic.d.ts +121 -0
  3. package/dist/bpmn/agentic.js +97 -0
  4. package/dist/bpmn/auto-layout.d.ts +5 -5
  5. package/dist/bpmn/auto-layout.js +592 -36
  6. package/dist/bpmn/bpmn-builder.d.ts +265 -3
  7. package/dist/bpmn/bpmn-builder.js +603 -197
  8. package/dist/bpmn/bpmn-model.d.ts +114 -0
  9. package/dist/bpmn/bpmn-parser.js +1414 -521
  10. package/dist/bpmn/bpmn-serializer.js +107 -19
  11. package/dist/bpmn/compact.d.ts +17 -2
  12. package/dist/bpmn/compact.js +3 -3
  13. package/dist/bpmn/full-operations.d.ts +89 -0
  14. package/dist/bpmn/full-operations.js +478 -0
  15. package/dist/bpmn/index.d.ts +19 -0
  16. package/dist/bpmn/index.js +21 -0
  17. package/dist/bpmn/optimize/agentic.d.ts +10 -0
  18. package/dist/bpmn/optimize/agentic.js +88 -0
  19. package/dist/bpmn/optimize/deploy.d.ts +16 -0
  20. package/dist/bpmn/optimize/deploy.js +143 -0
  21. package/dist/bpmn/optimize/feel-syntax.d.ts +12 -0
  22. package/dist/bpmn/optimize/feel-syntax.js +87 -0
  23. package/dist/bpmn/optimize/feel.js +7 -4
  24. package/dist/bpmn/optimize/flow.js +22 -2
  25. package/dist/bpmn/optimize/index.js +20 -9
  26. package/dist/bpmn/optimize/patterns.js +23 -16
  27. package/dist/bpmn/optimize/tasks.js +30 -7
  28. package/dist/bpmn/optimize/types.d.ts +10 -1
  29. package/dist/bpmn/optimize/utils.js +2 -4
  30. package/dist/bpmn/optimize/variable-flow.js +58 -67
  31. package/dist/bpmn/semantic-hash.d.ts +93 -0
  32. package/dist/bpmn/semantic-hash.js +155 -0
  33. package/dist/bpmn/sha256.d.ts +17 -0
  34. package/dist/bpmn/sha256.js +95 -0
  35. package/dist/bpmn/zeebe-extensions.d.ts +83 -0
  36. package/dist/bpmn/zeebe-extensions.js +117 -0
  37. package/dist/bpmn/zeebe-placement.d.ts +12 -0
  38. package/dist/bpmn/zeebe-placement.js +140 -0
  39. package/dist/errors.d.ts +40 -1
  40. package/dist/errors.js +41 -0
  41. package/dist/index.d.ts +16 -5
  42. package/dist/index.js +9 -3
  43. package/dist/layout/annotations.js +36 -1
  44. package/dist/layout/collaboration/alignment.d.ts +26 -0
  45. package/dist/layout/collaboration/alignment.js +66 -0
  46. package/dist/layout/collaboration/ordering.d.ts +21 -0
  47. package/dist/layout/collaboration/ordering.js +102 -0
  48. package/dist/layout/index.d.ts +1 -0
  49. package/dist/layout/layout-engine.d.ts +13 -3
  50. package/dist/layout/layout-engine.js +9 -4
  51. package/dist/layout/semantic/bands.d.ts +19 -0
  52. package/dist/layout/semantic/bands.js +324 -0
  53. package/dist/layout/semantic/graph.d.ts +37 -0
  54. package/dist/layout/semantic/graph.js +242 -0
  55. package/dist/layout/semantic/index.d.ts +13 -0
  56. package/dist/layout/semantic/index.js +181 -0
  57. package/dist/layout/semantic/place.d.ts +40 -0
  58. package/dist/layout/semantic/place.js +271 -0
  59. package/dist/layout/semantic/route.d.ts +14 -0
  60. package/dist/layout/semantic/route.js +514 -0
  61. package/dist/layout/types.d.ts +17 -0
  62. package/dist/node/index.d.ts +10 -0
  63. package/dist/node/index.js +9 -0
  64. package/dist/node/write.d.ts +81 -0
  65. package/dist/node/write.js +167 -0
  66. package/dist/plan/compile.d.ts +39 -0
  67. package/dist/plan/compile.js +380 -0
  68. package/dist/plan/extract.d.ts +31 -0
  69. package/dist/plan/extract.js +248 -0
  70. package/dist/plan/index.d.ts +6 -0
  71. package/dist/plan/index.js +5 -0
  72. package/dist/plan/merge.d.ts +13 -0
  73. package/dist/plan/merge.js +80 -0
  74. package/dist/plan/slug.d.ts +5 -0
  75. package/dist/plan/slug.js +22 -0
  76. package/dist/plan/types.d.ts +225 -0
  77. package/dist/plan/types.js +13 -0
  78. package/dist/types/id-generator.js +11 -3
  79. package/dist/xml/index.d.ts +3 -1
  80. package/dist/xml/index.js +2 -1
  81. package/dist/xml/xml-parser.d.ts +32 -0
  82. package/dist/xml/xml-parser.js +394 -143
  83. package/package.json +9 -2
@@ -1,7 +1,7 @@
1
1
  import { generateId } from "../types/id-generator.js";
2
2
  import { applyAutoLayout } from "./auto-layout.js";
3
3
  import { restConnectorRetries, restConnectorTaskType, restConnectorToIoMappingInputs, restConnectorToTaskHeaders, } from "./rest-connector.js";
4
- import { zeebeExtensionsToXmlElements } from "./zeebe-extensions.js";
4
+ import { ensureZeebeExtension, zeebeExtensionsToXmlElements, } from "./zeebe-extensions.js";
5
5
  // Keep in sync with packages/core/package.json version
6
6
  const EXPORTER_VERSION = "0.0.23";
7
7
  // ---------------------------------------------------------------------------
@@ -15,6 +15,12 @@ function resolveMessage(messageName, rootMessages) {
15
15
  }
16
16
  return existing.id;
17
17
  }
18
+ /** Builds the `zeebe:subscription` extension element for a correlated message catch. */
19
+ function buildMessageSubscriptionExt(correlationKey) {
20
+ if (!correlationKey)
21
+ return [];
22
+ return zeebeExtensionsToXmlElements({ subscription: { correlationKey } });
23
+ }
18
24
  function buildEventDefinitions(opts, rootErrors, rootMessages, rootSignals, rootEscalations) {
19
25
  const defs = [];
20
26
  if (opts.timerDuration || opts.timerDate || opts.timerCycle) {
@@ -86,6 +92,7 @@ function makeFlowElement(id, type, options) {
86
92
  name: options?.name,
87
93
  incoming: [],
88
94
  outgoing: [],
95
+ documentation: options?.documentation,
89
96
  extensionElements: options?.extensionElements ?? [],
90
97
  unknownAttributes: {},
91
98
  };
@@ -200,6 +207,17 @@ function buildAdHocLoopCharacteristics(lc) {
200
207
  ],
201
208
  };
202
209
  }
210
+ const GATEWAY_ELEMENT_TYPES = new Set([
211
+ "exclusiveGateway",
212
+ "parallelGateway",
213
+ "inclusiveGateway",
214
+ "eventBasedGateway",
215
+ "complexGateway",
216
+ ]);
217
+ /** Gateways are the elements for which several outgoing flows are the point. */
218
+ function isGatewayType(type) {
219
+ return GATEWAY_ELEMENT_TYPES.has(type);
220
+ }
203
221
  function recomputeIncomingOutgoing(elements, flows) {
204
222
  for (const el of elements) {
205
223
  el.incoming = [];
@@ -232,6 +250,9 @@ function buildServiceTaskExtensions(options) {
232
250
  })),
233
251
  };
234
252
  }
253
+ if (options.zeebeProperties) {
254
+ extensions.properties = { properties: options.zeebeProperties };
255
+ }
235
256
  return zeebeExtensionsToXmlElements(extensions);
236
257
  }
237
258
  function makeConditionExpression(expression) {
@@ -240,6 +261,72 @@ function makeConditionExpression(expression) {
240
261
  attributes: { "xsi:type": "bpmn:tFormalExpression" },
241
262
  };
242
263
  }
264
+ /** Builds the Zeebe extension elements + completion condition shared by all `adHocSubProcess()` overloads. */
265
+ function buildAdHocSubProcessExtensions(options) {
266
+ const zeebeExt = {};
267
+ if (options?.taskDefinition) {
268
+ zeebeExt.taskDefinition = options.taskDefinition;
269
+ }
270
+ if (options?.ioMapping) {
271
+ zeebeExt.ioMapping = {
272
+ inputs: options.ioMapping.inputs ?? [],
273
+ outputs: options.ioMapping.outputs ?? [],
274
+ };
275
+ }
276
+ if (options?.taskHeaders) {
277
+ zeebeExt.taskHeaders = {
278
+ headers: Object.entries(options.taskHeaders).map(([key, value]) => ({ key, value })),
279
+ };
280
+ }
281
+ if (options?.zeebeProperties) {
282
+ zeebeExt.properties = { properties: options.zeebeProperties };
283
+ }
284
+ const extensionElements = zeebeExtensionsToXmlElements(zeebeExt);
285
+ const adHocAttrs = {};
286
+ if (options?.activeElementsCollection) {
287
+ adHocAttrs.activeElementsCollection = options.activeElementsCollection;
288
+ }
289
+ if (options?.outputCollection)
290
+ adHocAttrs.outputCollection = options.outputCollection;
291
+ if (options?.outputElement)
292
+ adHocAttrs.outputElement = options.outputElement;
293
+ if (Object.keys(adHocAttrs).length > 0) {
294
+ extensionElements.push({ name: "zeebe:adHoc", attributes: adHocAttrs, children: [] });
295
+ }
296
+ return {
297
+ extensionElements,
298
+ completionCondition: options?.completionCondition
299
+ ? makeConditionExpression(options.completionCondition)
300
+ : undefined,
301
+ };
302
+ }
303
+ /** Applies modeler-template attrs, sub-process contents, loop characteristics, and completion condition. */
304
+ function finalizeAdHocSubProcessEl(element, options, sub, completionCondition) {
305
+ if (options?.modelerTemplate)
306
+ element.unknownAttributes["zeebe:modelerTemplate"] = options.modelerTemplate;
307
+ if (options?.modelerTemplateVersion) {
308
+ element.unknownAttributes["zeebe:modelerTemplateVersion"] = options.modelerTemplateVersion;
309
+ }
310
+ if (options?.modelerTemplateIcon) {
311
+ element.unknownAttributes["zeebe:modelerTemplateIcon"] = options.modelerTemplateIcon;
312
+ }
313
+ if (element.type !== "adHocSubProcess")
314
+ return;
315
+ element.flowElements = sub._elements;
316
+ element.sequenceFlows = sub._flows;
317
+ element.textAnnotations = sub._textAnnotations;
318
+ element.associations = sub._associations;
319
+ if (options?.loopCharacteristics) {
320
+ element.loopCharacteristics = buildAdHocLoopCharacteristics(options.loopCharacteristics);
321
+ }
322
+ else if (options?.multiInstance) {
323
+ element.loopCharacteristics = buildMultiInstance(options.multiInstance);
324
+ }
325
+ element.completionCondition = completionCondition;
326
+ if (options?.cancelRemainingInstances !== undefined) {
327
+ element.cancelRemainingInstances = options.cancelRemainingInstances;
328
+ }
329
+ }
243
330
  // ---------------------------------------------------------------------------
244
331
  // Element factory functions — shared by all three builder classes
245
332
  // ---------------------------------------------------------------------------
@@ -253,6 +340,7 @@ function makeServiceTaskEl(id, options) {
253
340
  unknownAttributes["zeebe:modelerTemplateIcon"] = options.modelerTemplateIcon;
254
341
  const el = makeFlowElement(id, "serviceTask", {
255
342
  name: options.name,
343
+ documentation: options.documentation,
256
344
  extensionElements: buildServiceTaskExtensions(options),
257
345
  });
258
346
  el.unknownAttributes = unknownAttributes;
@@ -263,6 +351,7 @@ function makeServiceTaskEl(id, options) {
263
351
  function makeScriptTaskEl(id, options) {
264
352
  const el = makeFlowElement(id, "scriptTask", {
265
353
  name: options.name,
354
+ documentation: options.documentation,
266
355
  extensionElements: zeebeExtensionsToXmlElements({
267
356
  unknownElements: [
268
357
  {
@@ -281,8 +370,27 @@ function makeUserTaskEl(id, options) {
281
370
  const ext = zeebeExtensionsToXmlElements({
282
371
  ...(options?.zeebeUserTask ? { userTask: true } : {}),
283
372
  ...(options?.formId ? { formDefinition: { formId: options.formId } } : {}),
373
+ ...(options?.assignee || options?.candidateGroups || options?.candidateUsers
374
+ ? {
375
+ assignmentDefinition: {
376
+ assignee: options?.assignee,
377
+ candidateGroups: options?.candidateGroups,
378
+ candidateUsers: options?.candidateUsers,
379
+ },
380
+ }
381
+ : {}),
382
+ ...(options?.dueDate || options?.followUpDate
383
+ ? { taskSchedule: { dueDate: options?.dueDate, followUpDate: options?.followUpDate } }
384
+ : {}),
385
+ ...(options?.priority !== undefined
386
+ ? { priorityDefinition: { priority: String(options.priority) } }
387
+ : {}),
388
+ });
389
+ const el = makeFlowElement(id, "userTask", {
390
+ name: options?.name,
391
+ documentation: options?.documentation,
392
+ extensionElements: ext,
284
393
  });
285
- const el = makeFlowElement(id, "userTask", { name: options?.name, extensionElements: ext });
286
394
  if (options?.isForCompensation)
287
395
  el.isForCompensation = true;
288
396
  return el;
@@ -302,6 +410,7 @@ function makeBusinessRuleTaskEl(id, options) {
302
410
  }
303
411
  const el = makeFlowElement(id, "businessRuleTask", {
304
412
  name: options?.name,
413
+ documentation: options?.documentation,
305
414
  extensionElements: ext,
306
415
  });
307
416
  if (options?.isForCompensation)
@@ -315,6 +424,7 @@ function makeCallActivityEl(id, options) {
315
424
  }
316
425
  const el = makeFlowElement(id, "callActivity", {
317
426
  name: options.name,
427
+ documentation: options.documentation,
318
428
  extensionElements: zeebeExtensionsToXmlElements({
319
429
  unknownElements: [{ name: "zeebe:calledElement", attributes: attrs, children: [] }],
320
430
  }),
@@ -471,6 +581,8 @@ function traceBackToSplit(nodeId, splitGateways, flows) {
471
581
  export class BranchBuilder {
472
582
  /** @internal */
473
583
  _elements = [];
584
+ /** Ids in `_elements`, for O(1) duplicate checks. */
585
+ _ids = new Set();
474
586
  /** @internal */
475
587
  _flows = [];
476
588
  /** @internal */
@@ -518,6 +630,7 @@ export class BranchBuilder {
518
630
  }
519
631
  addElement(element) {
520
632
  this._elements.push(element);
633
+ this._ids.add(element.id);
521
634
  if (this.lastNodeId) {
522
635
  const flowId = generateId("Flow");
523
636
  const flow = {
@@ -667,7 +780,10 @@ export class BranchBuilder {
667
780
  return this.addElement(el);
668
781
  }
669
782
  intermediateCatchEvent(id, options) {
670
- const el = makeFlowElement(id ?? generateId("IntermediateCatchEvent"), "intermediateCatchEvent", options);
783
+ const el = makeFlowElement(id ?? generateId("IntermediateCatchEvent"), "intermediateCatchEvent", {
784
+ ...options,
785
+ extensionElements: buildMessageSubscriptionExt(options?.correlationKey),
786
+ });
671
787
  if (el.type === "intermediateCatchEvent" && options) {
672
788
  el.eventDefinitions = buildEventDefinitions(options, this.rootErrors, this.rootMessages, this.rootSignals, this.rootEscalations);
673
789
  }
@@ -698,7 +814,10 @@ export class BranchBuilder {
698
814
  * cursor to return to the task afterward.
699
815
  */
700
816
  boundaryEvent(id, options) {
701
- const element = makeFlowElement(id, "boundaryEvent", options);
817
+ const element = makeFlowElement(id, "boundaryEvent", {
818
+ ...options,
819
+ extensionElements: buildMessageSubscriptionExt(options.correlationKey),
820
+ });
702
821
  if (element.type === "boundaryEvent") {
703
822
  element.attachedToRef = options.attachedTo;
704
823
  element.cancelActivity = options.cancelActivity;
@@ -706,6 +825,7 @@ export class BranchBuilder {
706
825
  }
707
826
  // Push directly — no sequence flow, boundary events attach via attachedToRef
708
827
  this._elements.push(element);
828
+ this._ids.add(element.id);
709
829
  this.lastNodeId = element.id;
710
830
  this.isFirstElement = false;
711
831
  return this;
@@ -770,68 +890,13 @@ export class BranchBuilder {
770
890
  resolved.content(sub);
771
891
  insertJoinGateways(sub._elements, sub._flows);
772
892
  recomputeIncomingOutgoing(sub._elements, sub._flows);
773
- const zeebeExt = {};
774
- if (resolved.options?.taskDefinition) {
775
- zeebeExt.taskDefinition = resolved.options.taskDefinition;
776
- }
777
- if (resolved.options?.ioMapping) {
778
- zeebeExt.ioMapping = {
779
- inputs: resolved.options.ioMapping.inputs ?? [],
780
- outputs: resolved.options.ioMapping.outputs ?? [],
781
- };
782
- }
783
- if (resolved.options?.taskHeaders) {
784
- zeebeExt.taskHeaders = {
785
- headers: Object.entries(resolved.options.taskHeaders).map(([key, value]) => ({
786
- key,
787
- value,
788
- })),
789
- };
790
- }
791
- const extensionElements = zeebeExtensionsToXmlElements(zeebeExt);
792
- const adHocAttrs = {};
793
- if (resolved.options?.activeElementsCollection) {
794
- adHocAttrs.activeElementsCollection = resolved.options.activeElementsCollection;
795
- }
796
- if (resolved.options?.outputCollection) {
797
- adHocAttrs.outputCollection = resolved.options.outputCollection;
798
- }
799
- if (resolved.options?.outputElement) {
800
- adHocAttrs.outputElement = resolved.options.outputElement;
801
- }
802
- if (Object.keys(adHocAttrs).length > 0) {
803
- extensionElements.push({
804
- name: "zeebe:adHoc",
805
- attributes: adHocAttrs,
806
- children: [],
807
- });
808
- }
893
+ const { extensionElements, completionCondition } = buildAdHocSubProcessExtensions(resolved.options);
809
894
  const element = makeFlowElement(id, "adHocSubProcess", {
810
895
  name: resolved.options?.name,
896
+ documentation: resolved.options?.documentation,
811
897
  extensionElements,
812
898
  });
813
- if (resolved.options?.modelerTemplate) {
814
- element.unknownAttributes["zeebe:modelerTemplate"] = resolved.options.modelerTemplate;
815
- }
816
- if (resolved.options?.modelerTemplateVersion) {
817
- element.unknownAttributes["zeebe:modelerTemplateVersion"] =
818
- resolved.options.modelerTemplateVersion;
819
- }
820
- if (resolved.options?.modelerTemplateIcon) {
821
- element.unknownAttributes["zeebe:modelerTemplateIcon"] = resolved.options.modelerTemplateIcon;
822
- }
823
- if (element.type === "adHocSubProcess") {
824
- element.flowElements = sub._elements;
825
- element.sequenceFlows = sub._flows;
826
- element.textAnnotations = sub._textAnnotations;
827
- element.associations = sub._associations;
828
- if (resolved.options?.loopCharacteristics) {
829
- element.loopCharacteristics = buildAdHocLoopCharacteristics(resolved.options.loopCharacteristics);
830
- }
831
- else if (resolved.options?.multiInstance) {
832
- element.loopCharacteristics = buildMultiInstance(resolved.options.multiInstance);
833
- }
834
- }
899
+ finalizeAdHocSubProcessEl(element, resolved.options, sub, completionCondition);
835
900
  return this.addElement(element);
836
901
  }
837
902
  /** Add an event sub-process. Triggered by its start event — no incoming or outgoing sequence flows. */
@@ -851,10 +916,11 @@ export class BranchBuilder {
851
916
  }
852
917
  // Event sub-processes have no incoming/outgoing sequence flows and must not
853
918
  // advance the branch cursor — mirrors ProcessBuilder.eventSubProcess.
854
- if (this._elements.some((n) => n.id === element.id)) {
919
+ if (this._ids.has(element.id)) {
855
920
  throw new Error(`Duplicate element ID "${element.id}"`);
856
921
  }
857
922
  this._elements.push(element);
923
+ this._ids.add(element.id);
858
924
  return this;
859
925
  }
860
926
  /**
@@ -871,10 +937,11 @@ export class BranchBuilder {
871
937
  const b = new BranchBuilder(this.currentGatewayId, name, this.rootErrors, this.rootMessages, this.rootSignals, this.rootEscalations);
872
938
  callback(b);
873
939
  for (const el of b._elements) {
874
- if (this._elements.some((n) => n.id === el.id)) {
940
+ if (this._ids.has(el.id)) {
875
941
  throw new Error(`Duplicate element ID "${el.id}"`);
876
942
  }
877
943
  this._elements.push(el);
944
+ this._ids.add(el.id);
878
945
  }
879
946
  for (const fl of b._flows)
880
947
  this._flows.push(fl);
@@ -911,6 +978,8 @@ export class BranchBuilder {
911
978
  export class SubProcessContentBuilder {
912
979
  /** @internal */
913
980
  _elements = [];
981
+ /** Ids in `_elements`, for O(1) duplicate checks. */
982
+ _ids = new Set();
914
983
  /** @internal */
915
984
  _flows = [];
916
985
  /** @internal */
@@ -927,10 +996,11 @@ export class SubProcessContentBuilder {
927
996
  this.rootMessages = rootMessages;
928
997
  }
929
998
  addElement(element) {
930
- if (this._elements.some((n) => n.id === element.id)) {
999
+ if (this._ids.has(element.id)) {
931
1000
  throw new Error(`Duplicate element ID "${element.id}" in sub-process`);
932
1001
  }
933
1002
  this._elements.push(element);
1003
+ this._ids.add(element.id);
934
1004
  if (this.lastNodeId) {
935
1005
  this._flows.push({
936
1006
  id: generateId("Flow"),
@@ -953,6 +1023,22 @@ export class SubProcessContentBuilder {
953
1023
  this.lastNodeId = element.id;
954
1024
  return this;
955
1025
  }
1026
+ /**
1027
+ * Add an element with no sequence-flow wiring and no cursor movement.
1028
+ *
1029
+ * Used for ad-hoc sub-process tool activities, which the AI Agent connector
1030
+ * requires to be root nodes (no incoming sequence flow, not a boundary event).
1031
+ * Unlike `serviceTask()`/`userTask()`/etc., this does not auto-connect from
1032
+ * the current cursor and does not move it.
1033
+ */
1034
+ addDisconnected(element) {
1035
+ if (this._ids.has(element.id)) {
1036
+ throw new Error(`Duplicate element ID "${element.id}" in sub-process`);
1037
+ }
1038
+ this._elements.push(element);
1039
+ this._ids.add(element.id);
1040
+ return this;
1041
+ }
956
1042
  // ---- Events ----
957
1043
  startEvent(id, options) {
958
1044
  const el = makeFlowElement(id ?? generateId("StartEvent"), "startEvent", options);
@@ -976,7 +1062,10 @@ export class SubProcessContentBuilder {
976
1062
  return this.addElement(el);
977
1063
  }
978
1064
  intermediateCatchEvent(id, options) {
979
- const el = makeFlowElement(id ?? generateId("IntermediateCatchEvent"), "intermediateCatchEvent", options);
1065
+ const el = makeFlowElement(id ?? generateId("IntermediateCatchEvent"), "intermediateCatchEvent", {
1066
+ ...options,
1067
+ extensionElements: buildMessageSubscriptionExt(options?.correlationKey),
1068
+ });
980
1069
  if (el.type === "intermediateCatchEvent" && options)
981
1070
  el.eventDefinitions = buildEventDefinitions(options);
982
1071
  return this.addElement(el);
@@ -1068,10 +1157,11 @@ export class SubProcessContentBuilder {
1068
1157
  const b = new BranchBuilder(this.currentGatewayId, name);
1069
1158
  callback(b);
1070
1159
  for (const el of b._elements) {
1071
- if (this._elements.some((n) => n.id === el.id)) {
1160
+ if (this._ids.has(el.id)) {
1072
1161
  throw new Error(`Duplicate element ID "${el.id}"`);
1073
1162
  }
1074
1163
  this._elements.push(el);
1164
+ this._ids.add(el.id);
1075
1165
  }
1076
1166
  for (const fl of b._flows)
1077
1167
  this._flows.push(fl);
@@ -1114,7 +1204,7 @@ export class SubProcessContentBuilder {
1114
1204
  return this;
1115
1205
  }
1116
1206
  element(elementId) {
1117
- if (!this._elements.some((n) => n.id === elementId)) {
1207
+ if (!this._ids.has(elementId)) {
1118
1208
  throw new Error(`Element "${elementId}" not found in sub-process`);
1119
1209
  }
1120
1210
  this.lastNodeId = elementId;
@@ -1131,7 +1221,10 @@ export class SubProcessContentBuilder {
1131
1221
  * cursor to return to the task afterward.
1132
1222
  */
1133
1223
  boundaryEvent(id, options) {
1134
- const element = makeFlowElement(id, "boundaryEvent", options);
1224
+ const element = makeFlowElement(id, "boundaryEvent", {
1225
+ ...options,
1226
+ extensionElements: buildMessageSubscriptionExt(options.correlationKey),
1227
+ });
1135
1228
  if (element.type === "boundaryEvent") {
1136
1229
  element.attachedToRef = options.attachedTo;
1137
1230
  element.cancelActivity = options.cancelActivity;
@@ -1139,6 +1232,7 @@ export class SubProcessContentBuilder {
1139
1232
  }
1140
1233
  // Push directly — no sequence flow, boundary events attach via attachedToRef
1141
1234
  this._elements.push(element);
1235
+ this._ids.add(element.id);
1142
1236
  this.lastNodeId = element.id;
1143
1237
  return this;
1144
1238
  }
@@ -1200,68 +1294,13 @@ export class SubProcessContentBuilder {
1200
1294
  resolved.content(sub);
1201
1295
  insertJoinGateways(sub._elements, sub._flows);
1202
1296
  recomputeIncomingOutgoing(sub._elements, sub._flows);
1203
- const zeebeExt = {};
1204
- if (resolved.options?.taskDefinition) {
1205
- zeebeExt.taskDefinition = resolved.options.taskDefinition;
1206
- }
1207
- if (resolved.options?.ioMapping) {
1208
- zeebeExt.ioMapping = {
1209
- inputs: resolved.options.ioMapping.inputs ?? [],
1210
- outputs: resolved.options.ioMapping.outputs ?? [],
1211
- };
1212
- }
1213
- if (resolved.options?.taskHeaders) {
1214
- zeebeExt.taskHeaders = {
1215
- headers: Object.entries(resolved.options.taskHeaders).map(([key, value]) => ({
1216
- key,
1217
- value,
1218
- })),
1219
- };
1220
- }
1221
- const extensionElements = zeebeExtensionsToXmlElements(zeebeExt);
1222
- const adHocAttrs = {};
1223
- if (resolved.options?.activeElementsCollection) {
1224
- adHocAttrs.activeElementsCollection = resolved.options.activeElementsCollection;
1225
- }
1226
- if (resolved.options?.outputCollection) {
1227
- adHocAttrs.outputCollection = resolved.options.outputCollection;
1228
- }
1229
- if (resolved.options?.outputElement) {
1230
- adHocAttrs.outputElement = resolved.options.outputElement;
1231
- }
1232
- if (Object.keys(adHocAttrs).length > 0) {
1233
- extensionElements.push({
1234
- name: "zeebe:adHoc",
1235
- attributes: adHocAttrs,
1236
- children: [],
1237
- });
1238
- }
1297
+ const { extensionElements, completionCondition } = buildAdHocSubProcessExtensions(resolved.options);
1239
1298
  const element = makeFlowElement(id, "adHocSubProcess", {
1240
1299
  name: resolved.options?.name,
1300
+ documentation: resolved.options?.documentation,
1241
1301
  extensionElements,
1242
1302
  });
1243
- if (resolved.options?.modelerTemplate) {
1244
- element.unknownAttributes["zeebe:modelerTemplate"] = resolved.options.modelerTemplate;
1245
- }
1246
- if (resolved.options?.modelerTemplateVersion) {
1247
- element.unknownAttributes["zeebe:modelerTemplateVersion"] =
1248
- resolved.options.modelerTemplateVersion;
1249
- }
1250
- if (resolved.options?.modelerTemplateIcon) {
1251
- element.unknownAttributes["zeebe:modelerTemplateIcon"] = resolved.options.modelerTemplateIcon;
1252
- }
1253
- if (element.type === "adHocSubProcess") {
1254
- element.flowElements = sub._elements;
1255
- element.sequenceFlows = sub._flows;
1256
- element.textAnnotations = sub._textAnnotations;
1257
- element.associations = sub._associations;
1258
- if (resolved.options?.loopCharacteristics) {
1259
- element.loopCharacteristics = buildAdHocLoopCharacteristics(resolved.options.loopCharacteristics);
1260
- }
1261
- else if (resolved.options?.multiInstance) {
1262
- element.loopCharacteristics = buildMultiInstance(resolved.options.multiInstance);
1263
- }
1264
- }
1303
+ finalizeAdHocSubProcessEl(element, resolved.options, sub, completionCondition);
1265
1304
  return this.addElement(element);
1266
1305
  }
1267
1306
  /** Add an event sub-process. Triggered by its start event — no incoming or outgoing sequence flows. */
@@ -1281,10 +1320,11 @@ export class SubProcessContentBuilder {
1281
1320
  }
1282
1321
  // Event sub-processes have no incoming/outgoing sequence flows and must not
1283
1322
  // advance the cursor — mirrors ProcessBuilder.eventSubProcess.
1284
- if (this._elements.some((n) => n.id === element.id)) {
1323
+ if (this._ids.has(element.id)) {
1285
1324
  throw new Error(`Duplicate element ID "${element.id}" in sub-process`);
1286
1325
  }
1287
1326
  this._elements.push(element);
1327
+ this._ids.add(element.id);
1288
1328
  return this;
1289
1329
  }
1290
1330
  }
@@ -1296,8 +1336,12 @@ export class ProcessBuilder {
1296
1336
  processId;
1297
1337
  processName;
1298
1338
  _isExecutable = true;
1339
+ /** Whether `executable()` was called, so continue-mode leaves it alone if not. */
1340
+ _executableSet = false;
1299
1341
  _versionTag;
1300
1342
  flowElements = [];
1343
+ /** Ids in `flowElements`, for O(1) duplicate and existence checks. */
1344
+ elementIds = new Set();
1301
1345
  sequenceFlows = [];
1302
1346
  rootErrors = [];
1303
1347
  rootMessages = [];
@@ -1313,9 +1357,140 @@ export class ProcessBuilder {
1313
1357
  _executionPlatformVersion = "8.9.0";
1314
1358
  _serviceTaskDefaults = {};
1315
1359
  _savedMainFlowId = undefined;
1360
+ /** Set by {@link ProcessBuilder.from}; makes `build()` update rather than generate. */
1361
+ _source;
1362
+ /** Pre-existing flow endpoints, so `build()` can prove it did not rewire them. */
1363
+ _sourceFlowTargets = new Map();
1364
+ /** An `insertAfter` flow waiting to be reattached to what gets built next. */
1365
+ _pendingSpliceFlowId;
1366
+ /** Flows the caller deliberately spliced, exempt from the rewiring guard. */
1367
+ _splicedFlowIds = new Set();
1316
1368
  constructor(processId) {
1317
1369
  this.processId = processId;
1318
1370
  }
1371
+ /**
1372
+ * Continues an existing model rather than generating a new one.
1373
+ *
1374
+ * `build()` then returns *that document* with this process's contents
1375
+ * replaced, so everything the builder has no opinion about — other processes,
1376
+ * the collaboration, lanes, diagram interchange, root elements, imports,
1377
+ * unmodelled content — is still there afterwards. Generating a replacement
1378
+ * from scratch is what loses those.
1379
+ *
1380
+ * The input is not mutated; the builder works on a copy.
1381
+ *
1382
+ * @param definitions - The parsed model to continue.
1383
+ * @param processId - Which process to continue. Named explicitly, because
1384
+ * "the first process" is a guess that goes wrong on a collaboration.
1385
+ * @example
1386
+ * ```typescript
1387
+ * const updated = ProcessBuilder.from(Bpmn.parse(xml), "order")
1388
+ * .at("validate")
1389
+ * .serviceTask("notify", { name: "Notify", taskType: "notify" })
1390
+ * .build()
1391
+ * ```
1392
+ */
1393
+ static from(definitions, processId) {
1394
+ const copy = structuredClone(definitions);
1395
+ const process = copy.processes.find((candidate) => candidate.id === processId);
1396
+ if (process === undefined) {
1397
+ const available = copy.processes.map((candidate) => candidate.id);
1398
+ throw new Error(`Process "${processId}" is not in this document. It contains: ${available.length > 0 ? available.map((id) => `"${id}"`).join(", ") : "no processes"}`);
1399
+ }
1400
+ const builder = new ProcessBuilder(processId);
1401
+ builder._source = { definitions: copy, process };
1402
+ builder.processName = process.name;
1403
+ builder.flowElements.push(...process.flowElements);
1404
+ for (const element of process.flowElements)
1405
+ builder.elementIds.add(element.id);
1406
+ builder.sequenceFlows.push(...process.sequenceFlows);
1407
+ builder._textAnnotations.push(...process.textAnnotations);
1408
+ builder._associations.push(...process.associations);
1409
+ // Seeded so a message event reuses the document's existing message rather
1410
+ // than declaring a second one with the same name.
1411
+ builder.rootErrors.push(...copy.errors);
1412
+ builder.rootMessages.push(...copy.messages);
1413
+ builder.rootSignals.push(...copy.signals);
1414
+ builder.rootEscalations.push(...copy.escalations);
1415
+ builder._sourceFlowTargets = new Map(process.sequenceFlows.map((flow) => [flow.id, `${flow.sourceRef}→${flow.targetRef}`]));
1416
+ return builder;
1417
+ }
1418
+ /**
1419
+ * Moves the cursor to an existing flow node, so the next call chains from it.
1420
+ *
1421
+ * @param nodeId - A flow node directly contained by this process. Nodes inside
1422
+ * a sub-process are not reachable: continuing into one means building that
1423
+ * sub-process, not this one.
1424
+ */
1425
+ at(nodeId) {
1426
+ const node = this.flowElements.find((element) => element.id === nodeId);
1427
+ if (node === undefined) {
1428
+ throw new Error(`"${nodeId}" is not a flow node in process "${this.processId}". Nodes inside a sub-process cannot be continued from here.`);
1429
+ }
1430
+ if (node.type === "endEvent") {
1431
+ throw new Error(`Cannot continue from end event "${nodeId}": an end event has no outgoing sequence flow.`);
1432
+ }
1433
+ const hasOutgoing = this.sequenceFlows.some((flow) => flow.sourceRef === nodeId);
1434
+ if (hasOutgoing && !isGatewayType(node.type)) {
1435
+ throw new Error(`"${nodeId}" already has an outgoing sequence flow. Continuing from it would give a ${node.type} two outgoing flows, which is an uncontrolled split. Use insertAfter("${nodeId}") to splice into that path instead.`);
1436
+ }
1437
+ this.moveCursor(node);
1438
+ return this;
1439
+ }
1440
+ /**
1441
+ * Splices what you build next into the path leaving an existing node.
1442
+ *
1443
+ * `insertAfter("validate")` followed by `.serviceTask("notify", …)` turns
1444
+ * `validate → end` into `validate → notify → end`. The existing flow keeps its
1445
+ * id and its target and only changes where it starts, so an edge nobody asked
1446
+ * to move keeps its identity in the diagram and in a diff.
1447
+ *
1448
+ * This is the counterpart to {@link at}, which continues from a node whose
1449
+ * path is open. Which one you mean is not guessable, so it is not guessed.
1450
+ *
1451
+ * @param nodeId - A flow node with exactly one outgoing sequence flow.
1452
+ */
1453
+ insertAfter(nodeId) {
1454
+ const node = this.flowElements.find((element) => element.id === nodeId);
1455
+ if (node === undefined) {
1456
+ throw new Error(`"${nodeId}" is not a flow node in process "${this.processId}".`);
1457
+ }
1458
+ const outgoing = this.sequenceFlows.filter((flow) => flow.sourceRef === nodeId);
1459
+ if (outgoing.length === 0) {
1460
+ throw new Error(`"${nodeId}" has no outgoing sequence flow, so there is nothing to insert into. Use at("${nodeId}") to continue from it.`);
1461
+ }
1462
+ if (outgoing.length > 1) {
1463
+ throw new Error(`"${nodeId}" has ${outgoing.length} outgoing sequence flows, so "after" is ambiguous. Name the flow's target and insert before that instead.`);
1464
+ }
1465
+ this.resolvePendingSplice();
1466
+ const flow = outgoing[0];
1467
+ this._pendingSpliceFlowId = flow.id;
1468
+ this._splicedFlowIds.add(flow.id);
1469
+ this.moveCursor(node);
1470
+ return this;
1471
+ }
1472
+ /** Points the cursor at an existing node and forgets any branch state. */
1473
+ moveCursor(node) {
1474
+ this.lastNodeId = node.id;
1475
+ this.currentGatewayId = isGatewayType(node.type) ? node.id : undefined;
1476
+ this.openBranchEnds = [];
1477
+ this._savedMainFlowId = undefined;
1478
+ }
1479
+ /**
1480
+ * Reattaches the flow a pending `insertAfter` detached, to whatever the cursor
1481
+ * has reached. With nothing built in between the cursor has not moved and this
1482
+ * is a no-op, which is the right answer for `insertAfter(x)` followed by
1483
+ * nothing.
1484
+ */
1485
+ resolvePendingSplice() {
1486
+ const flowId = this._pendingSpliceFlowId;
1487
+ if (flowId === undefined)
1488
+ return;
1489
+ this._pendingSpliceFlowId = undefined;
1490
+ const flow = this.sequenceFlows.find((candidate) => candidate.id === flowId);
1491
+ if (flow !== undefined && this.lastNodeId !== undefined)
1492
+ flow.sourceRef = this.lastNodeId;
1493
+ }
1319
1494
  /** Enable auto-layout: `build()` will run the layout engine and populate diagram interchange data. */
1320
1495
  withAutoLayout() {
1321
1496
  this._autoLayout = true;
@@ -1340,6 +1515,7 @@ export class ProcessBuilder {
1340
1515
  /** Set whether this process is executable. */
1341
1516
  executable(value) {
1342
1517
  this._isExecutable = value;
1518
+ this._executableSet = true;
1343
1519
  return this;
1344
1520
  }
1345
1521
  /** Set the process version tag. */
@@ -1421,7 +1597,10 @@ export class ProcessBuilder {
1421
1597
  /** Add an intermediate catch event (timer, message, signal). */
1422
1598
  intermediateCatchEvent(id, options) {
1423
1599
  const nodeId = id ?? generateId("IntermediateCatchEvent");
1424
- const element = makeFlowElement(nodeId, "intermediateCatchEvent", options);
1600
+ const element = makeFlowElement(nodeId, "intermediateCatchEvent", {
1601
+ ...options,
1602
+ extensionElements: buildMessageSubscriptionExt(options?.correlationKey),
1603
+ });
1425
1604
  if (element.type === "intermediateCatchEvent" && options) {
1426
1605
  element.eventDefinitions = buildEventDefinitions(options, this.rootErrors, this.rootMessages, this.rootSignals, this.rootEscalations);
1427
1606
  }
@@ -1435,7 +1614,10 @@ export class ProcessBuilder {
1435
1614
  * They start a new outgoing chain from the boundary event itself.
1436
1615
  */
1437
1616
  boundaryEvent(id, options) {
1438
- const element = makeFlowElement(id, "boundaryEvent", options);
1617
+ const element = makeFlowElement(id, "boundaryEvent", {
1618
+ ...options,
1619
+ extensionElements: buildMessageSubscriptionExt(options.correlationKey),
1620
+ });
1439
1621
  if (element.type === "boundaryEvent") {
1440
1622
  element.attachedToRef = options.attachedTo;
1441
1623
  element.cancelActivity = options.cancelActivity;
@@ -1616,10 +1798,11 @@ export class ProcessBuilder {
1616
1798
  const b = new BranchBuilder(this.currentGatewayId, name, this.rootErrors, this.rootMessages, this.rootSignals, this.rootEscalations);
1617
1799
  callback(b);
1618
1800
  for (const el of b._elements) {
1619
- if (this.flowElements.some((n) => n.id === el.id)) {
1801
+ if (this.elementIds.has(el.id)) {
1620
1802
  throw new Error(`Duplicate element ID "${el.id}"`);
1621
1803
  }
1622
1804
  this.flowElements.push(el);
1805
+ this.elementIds.add(el.id);
1623
1806
  }
1624
1807
  for (const fl of b._flows) {
1625
1808
  this.sequenceFlows.push(fl);
@@ -1674,7 +1857,7 @@ export class ProcessBuilder {
1674
1857
  * @throws If no element with the given ID exists.
1675
1858
  */
1676
1859
  element(elementId) {
1677
- const found = this.flowElements.some((n) => n.id === elementId);
1860
+ const found = this.elementIds.has(elementId);
1678
1861
  if (!found) {
1679
1862
  throw new Error(`Element "${elementId}" not found in process "${this.processId}"`);
1680
1863
  }
@@ -1690,69 +1873,13 @@ export class ProcessBuilder {
1690
1873
  resolved.content(sub);
1691
1874
  insertJoinGateways(sub._elements, sub._flows);
1692
1875
  recomputeIncomingOutgoing(sub._elements, sub._flows);
1693
- const zeebeExt = {};
1694
- if (resolved.options?.taskDefinition) {
1695
- zeebeExt.taskDefinition = resolved.options.taskDefinition;
1696
- }
1697
- if (resolved.options?.ioMapping) {
1698
- zeebeExt.ioMapping = {
1699
- inputs: resolved.options.ioMapping.inputs ?? [],
1700
- outputs: resolved.options.ioMapping.outputs ?? [],
1701
- };
1702
- }
1703
- if (resolved.options?.taskHeaders) {
1704
- zeebeExt.taskHeaders = {
1705
- headers: Object.entries(resolved.options.taskHeaders).map(([key, value]) => ({
1706
- key,
1707
- value,
1708
- })),
1709
- };
1710
- }
1711
- const extensionElements = zeebeExtensionsToXmlElements(zeebeExt);
1712
- // zeebe:adHoc element
1713
- const adHocAttrs = {};
1714
- if (resolved.options?.activeElementsCollection) {
1715
- adHocAttrs.activeElementsCollection = resolved.options.activeElementsCollection;
1716
- }
1717
- if (resolved.options?.outputCollection) {
1718
- adHocAttrs.outputCollection = resolved.options.outputCollection;
1719
- }
1720
- if (resolved.options?.outputElement) {
1721
- adHocAttrs.outputElement = resolved.options.outputElement;
1722
- }
1723
- if (Object.keys(adHocAttrs).length > 0) {
1724
- extensionElements.push({
1725
- name: "zeebe:adHoc",
1726
- attributes: adHocAttrs,
1727
- children: [],
1728
- });
1729
- }
1876
+ const { extensionElements, completionCondition } = buildAdHocSubProcessExtensions(resolved.options);
1730
1877
  const element = makeFlowElement(id, "adHocSubProcess", {
1731
1878
  name: resolved.options?.name,
1879
+ documentation: resolved.options?.documentation,
1732
1880
  extensionElements,
1733
1881
  });
1734
- if (resolved.options?.modelerTemplate) {
1735
- element.unknownAttributes["zeebe:modelerTemplate"] = resolved.options.modelerTemplate;
1736
- }
1737
- if (resolved.options?.modelerTemplateVersion) {
1738
- element.unknownAttributes["zeebe:modelerTemplateVersion"] =
1739
- resolved.options.modelerTemplateVersion;
1740
- }
1741
- if (resolved.options?.modelerTemplateIcon) {
1742
- element.unknownAttributes["zeebe:modelerTemplateIcon"] = resolved.options.modelerTemplateIcon;
1743
- }
1744
- if (element.type === "adHocSubProcess") {
1745
- element.flowElements = sub._elements;
1746
- element.sequenceFlows = sub._flows;
1747
- element.textAnnotations = sub._textAnnotations;
1748
- element.associations = sub._associations;
1749
- if (resolved.options?.loopCharacteristics) {
1750
- element.loopCharacteristics = buildAdHocLoopCharacteristics(resolved.options.loopCharacteristics);
1751
- }
1752
- else if (resolved.options?.multiInstance) {
1753
- element.loopCharacteristics = buildMultiInstance(resolved.options.multiInstance);
1754
- }
1755
- }
1882
+ finalizeAdHocSubProcessEl(element, resolved.options, sub, completionCondition);
1756
1883
  this.addFlowElement(element);
1757
1884
  return this;
1758
1885
  }
@@ -1795,11 +1922,12 @@ export class ProcessBuilder {
1795
1922
  // advance the flow cursor — the surrounding process wires around them.
1796
1923
  // openBranchEnds is intentionally NOT drained here; the next normal
1797
1924
  // addFlowElement call will drain it and connect branch ends to that element.
1798
- if (this.flowElements.some((n) => n.id === element.id)) {
1925
+ if (this.elementIds.has(element.id)) {
1799
1926
  throw new Error(`Duplicate element ID "${element.id}" in process "${this.processId}"`);
1800
1927
  }
1801
1928
  this._savedMainFlowId = undefined;
1802
1929
  this.flowElements.push(element);
1930
+ this.elementIds.add(element.id);
1803
1931
  return this;
1804
1932
  }
1805
1933
  // ---- Annotations ----
@@ -1831,19 +1959,50 @@ export class ProcessBuilder {
1831
1959
  *
1832
1960
  * Resolves all forward-referenced `incoming` / `outgoing` arrays and wraps
1833
1961
  * the process in a {@link BpmnDefinitions} ready for XML serialization.
1962
+ *
1963
+ * **The join contract.** Branches built with `.branch()` converge implicitly:
1964
+ * where several paths from one gateway reach the same element, a matching join
1965
+ * gateway is inserted for you. That is convenient by hand and a trap for
1966
+ * generated code, which cannot see the element it did not write. Pass
1967
+ * `{ explicitJoins: true }` to be told instead of helped — the build throws,
1968
+ * naming the gateways it would have inserted, and you declare them yourself
1969
+ * with `.connectTo(joinId)`.
1970
+ *
1971
+ * A join you declare only counts if it *matches the split*: an exclusive split
1972
+ * converging on a parallel gateway is not the gateway inference would have
1973
+ * added, so it is still inferred — and with `explicitJoins` that refusal is the
1974
+ * only thing that tells you.
1975
+ *
1976
+ * {@link ProcessBuilder.from} never infers joins at all, whatever this option
1977
+ * says: inference reads the whole topology, and on a document you were handed
1978
+ * that means rewriting edges you never touched.
1979
+ *
1980
+ * @param options - `explicitJoins` refuses inferred join gateways. `strict` is
1981
+ * the former name for it and still works.
1834
1982
  */
1835
1983
  build(options) {
1984
+ this.resolvePendingSplice();
1836
1985
  const beforeCount = this.flowElements.length;
1837
- insertJoinGateways(this.flowElements, this.sequenceFlows);
1838
- if (options?.strict && this.flowElements.length > beforeCount) {
1986
+ // Continuing a document never infers joins. `insertJoinGateways` reads the
1987
+ // whole topology, so on a parsed model it retargets edges the caller never
1988
+ // touched — the corpus has a document where a no-op continue would have
1989
+ // invented a join gateway and rerouted two flows into it. Building a branch
1990
+ // that needs a join here means saying so with `.connectTo(joinId)`.
1991
+ if (this._source === undefined)
1992
+ insertJoinGateways(this.flowElements, this.sequenceFlows);
1993
+ const explicitJoins = options?.explicitJoins ?? options?.strict ?? false;
1994
+ if (explicitJoins && this.flowElements.length > beforeCount) {
1839
1995
  const inserted = this.flowElements
1840
1996
  .slice(beforeCount)
1841
1997
  .map((e) => e.id)
1842
1998
  .join(", ");
1843
- throw new Error(`auto-join gateways were inserted: ${inserted}. Use explicit .connectTo(joinId) to make gateway topology explicit, or remove { strict: true }.`);
1999
+ throw new Error(`Inferred join gateways: ${inserted}. Declare them with .connectTo(joinId), or drop { explicitJoins: true } to keep the inference.`);
1844
2000
  }
1845
2001
  this.validate();
2002
+ this.assertSourceTopologyIntact();
1846
2003
  recomputeIncomingOutgoing(this.flowElements, this.sequenceFlows);
2004
+ if (this._source !== undefined)
2005
+ return this.buildOntoSource(this._source);
1847
2006
  const extensionElements = [];
1848
2007
  if (this._versionTag) {
1849
2008
  extensionElements.push({
@@ -1905,6 +2064,78 @@ export class ProcessBuilder {
1905
2064
  };
1906
2065
  return this._autoLayout ? applyAutoLayout(defs) : defs;
1907
2066
  }
2067
+ /**
2068
+ * Writes this process's contents back into the document it came from.
2069
+ *
2070
+ * Everything not listed here is kept by identity — other processes, the
2071
+ * collaboration, diagram interchange, root elements, the process's own lanes,
2072
+ * documentation, extensions and unmodelled content. That is the whole point of
2073
+ * continuing rather than regenerating.
2074
+ *
2075
+ * Diagram interchange is *not* regenerated: existing shapes keep their
2076
+ * positions, and elements added here have none until `withAutoLayout()` or a
2077
+ * later `applyAutoLayout()` gives them one.
2078
+ */
2079
+ buildOntoSource(source) {
2080
+ const { definitions, process } = source;
2081
+ process.flowElements = this.flowElements;
2082
+ process.sequenceFlows = this.sequenceFlows;
2083
+ process.textAnnotations = this._textAnnotations;
2084
+ process.associations = this._associations;
2085
+ if (this.processName !== undefined)
2086
+ process.name = this.processName;
2087
+ // Only when asked. BPMN reads an absent `isExecutable` as false, so writing
2088
+ // the builder's `true` default onto a process that never carried it makes a
2089
+ // non-executable process executable — a change nobody requested.
2090
+ if (this._executableSet)
2091
+ process.isExecutable = this._isExecutable;
2092
+ if (this._versionTag !== undefined) {
2093
+ // Set the attribute on the existing element rather than replacing the
2094
+ // bag: a real process carries other extensions next to it.
2095
+ ensureZeebeExtension({ type: "process", extensionElements: process.extensionElements }, "zeebe:versionTag").attributes.value = this._versionTag;
2096
+ }
2097
+ definitions.errors = this.rootErrors;
2098
+ definitions.messages = this.rootMessages;
2099
+ definitions.signals = this.rootSignals;
2100
+ definitions.escalations = this.rootEscalations;
2101
+ return this._autoLayout ? applyAutoLayout(definitions) : definitions;
2102
+ }
2103
+ /**
2104
+ * Refuses to rewire a flow that was already in the document.
2105
+ *
2106
+ * `insertJoinGateways` retargets converging flows, which is right for a
2107
+ * topology this builder just created and wrong for one it was handed: a
2108
+ * document would come back with edges the caller never touched pointing
2109
+ * somewhere else. Continuing a model has to leave the model alone.
2110
+ */
2111
+ assertSourceTopologyIntact() {
2112
+ if (this._source === undefined)
2113
+ return;
2114
+ const rewired = [];
2115
+ const byId = new Map(this.sequenceFlows.map((flow) => [flow.id, flow]));
2116
+ for (const [id, endpoints] of this._sourceFlowTargets) {
2117
+ const flow = byId.get(id);
2118
+ if (flow === undefined) {
2119
+ rewired.push(`${id} (removed)`);
2120
+ continue;
2121
+ }
2122
+ // A spliced flow was moved on purpose, but only its source may move:
2123
+ // `insertAfter` inserts into a path, it does not redirect one.
2124
+ const [, target] = endpoints.split("→");
2125
+ if (this._splicedFlowIds.has(id)) {
2126
+ if (flow.targetRef !== target) {
2127
+ rewired.push(`${id} (spliced flow now targets ${flow.targetRef}, not ${target})`);
2128
+ }
2129
+ continue;
2130
+ }
2131
+ const now = `${flow.sourceRef}→${flow.targetRef}`;
2132
+ if (now !== endpoints)
2133
+ rewired.push(`${id} (${endpoints} became ${now})`);
2134
+ }
2135
+ if (rewired.length > 0) {
2136
+ throw new Error(`Building would rewire sequence flows this document already had: ${rewired.join(", ")}. Continuing a model must not change the part you did not touch.`);
2137
+ }
2138
+ }
1908
2139
  validate() {
1909
2140
  const elementIds = new Set(this.flowElements.map((el) => el.id));
1910
2141
  for (const flow of this.sequenceFlows) {
@@ -1919,10 +2150,11 @@ export class ProcessBuilder {
1919
2150
  }
1920
2151
  }
1921
2152
  addFlowElement(element) {
1922
- if (this.flowElements.some((n) => n.id === element.id)) {
2153
+ if (this.elementIds.has(element.id)) {
1923
2154
  throw new Error(`Duplicate element ID "${element.id}" in process "${this.processId}"`);
1924
2155
  }
1925
2156
  this.flowElements.push(element);
2157
+ this.elementIds.add(element.id);
1926
2158
  // Compensation handlers are outside the normal token flow: link via association
1927
2159
  // from the preceding compensation boundary event, then restore the main-flow cursor.
1928
2160
  if (element.isForCompensation) {
@@ -1981,6 +2213,9 @@ export class DiagramBuilder {
1981
2213
  _processes = [];
1982
2214
  _errors = [];
1983
2215
  _messages = [];
2216
+ _participants = [];
2217
+ _messageFlows = [];
2218
+ _collaborationId = "Collaboration_1";
1984
2219
  _executionPlatformVersion = "8.9.0";
1985
2220
  constructor(id) {
1986
2221
  this._id = id;
@@ -1999,6 +2234,152 @@ export class DiagramBuilder {
1999
2234
  this._messages.push(...defs.messages);
2000
2235
  return this;
2001
2236
  }
2237
+ /** Renames the collaboration element. Defaults to `"Collaboration_1"`. */
2238
+ collaborationId(id) {
2239
+ this._collaborationId = id;
2240
+ return this;
2241
+ }
2242
+ /**
2243
+ * Adds a pool.
2244
+ *
2245
+ * Omit `processId` for a black box — a participant whose internals are not
2246
+ * modelled. That is a real BPMN construct, not an incomplete one: it is how
2247
+ * you draw the counterparty you exchange messages with but do not execute.
2248
+ *
2249
+ * @param id - The participant's element id, used verbatim.
2250
+ */
2251
+ participant(id, options = {}) {
2252
+ const participant = { id, unknownAttributes: {} };
2253
+ if (options.name !== undefined)
2254
+ participant.name = options.name;
2255
+ if (options.processId !== undefined)
2256
+ participant.processRef = options.processId;
2257
+ this._participants.push(participant);
2258
+ return this;
2259
+ }
2260
+ /**
2261
+ * Declares a root-level message, which a message flow may name and a Camunda 8
2262
+ * message subscription correlates on.
2263
+ *
2264
+ * `ProcessBuilder` already creates messages by name for message events, so
2265
+ * only call this for a message no event declared — typically one carried by a
2266
+ * message flow between pools.
2267
+ *
2268
+ * @param id - The message's element id, used verbatim.
2269
+ */
2270
+ message(id, options = {}) {
2271
+ const message = { id, unknownAttributes: {} };
2272
+ if (options.name !== undefined)
2273
+ message.name = options.name;
2274
+ const extensions = zeebeExtensionsToXmlElements(options.correlationKey === undefined
2275
+ ? {}
2276
+ : { subscription: { correlationKey: options.correlationKey } });
2277
+ if (extensions.length > 0)
2278
+ message.extensionElements = extensions;
2279
+ this._messages.push(message);
2280
+ return this;
2281
+ }
2282
+ /**
2283
+ * Connects two pools.
2284
+ *
2285
+ * `source` and `target` name either participants or flow nodes inside them.
2286
+ * Both forms are valid BPMN and the layout engine reads either, but they must
2287
+ * be in *different* pools — a message flow is what crosses a pool boundary,
2288
+ * and one that does not is the error this catches.
2289
+ *
2290
+ * @param id - The message flow's element id, used verbatim.
2291
+ */
2292
+ messageFlow(id, options) {
2293
+ const flow = {
2294
+ id,
2295
+ sourceRef: options.source,
2296
+ targetRef: options.target,
2297
+ unknownAttributes: {},
2298
+ };
2299
+ if (options.name !== undefined)
2300
+ flow.name = options.name;
2301
+ if (options.messageRef !== undefined)
2302
+ flow.messageRef = options.messageRef;
2303
+ this._messageFlows.push(flow);
2304
+ return this;
2305
+ }
2306
+ /**
2307
+ * Reports every way the declared collaboration would not survive contact with
2308
+ * a modeler, so `build()` can refuse rather than emit a file that opens broken.
2309
+ */
2310
+ collaborationProblems() {
2311
+ const problems = [];
2312
+ const seen = new Set();
2313
+ for (const id of [
2314
+ ...this._participants.map((p) => p.id),
2315
+ ...this._messageFlows.map((f) => f.id),
2316
+ ]) {
2317
+ if (seen.has(id))
2318
+ problems.push(`Duplicate element ID "${id}"`);
2319
+ seen.add(id);
2320
+ }
2321
+ const processIds = new Set(this._processes.map((process) => process.id));
2322
+ const claimed = new Map();
2323
+ for (const participant of this._participants) {
2324
+ const processId = participant.processRef;
2325
+ if (processId === undefined)
2326
+ continue;
2327
+ if (!processIds.has(processId)) {
2328
+ problems.push(`Participant "${participant.id}" references process "${processId}", which this diagram does not contain`);
2329
+ continue;
2330
+ }
2331
+ const owner = claimed.get(processId);
2332
+ if (owner !== undefined) {
2333
+ problems.push(`Participants "${owner}" and "${participant.id}" both reference process "${processId}"; a process belongs to one pool`);
2334
+ }
2335
+ claimed.set(processId, participant.id);
2336
+ }
2337
+ const owningParticipant = this.participantIndex();
2338
+ const messageIds = new Set(this._messages.map((message) => message.id));
2339
+ for (const flow of this._messageFlows) {
2340
+ for (const [role, ref] of [
2341
+ ["source", flow.sourceRef],
2342
+ ["target", flow.targetRef],
2343
+ ]) {
2344
+ if (!owningParticipant.has(ref)) {
2345
+ problems.push(`Message flow "${flow.id}" names ${role} "${ref}", which is not a participant or a flow node in one`);
2346
+ }
2347
+ }
2348
+ const from = owningParticipant.get(flow.sourceRef);
2349
+ const to = owningParticipant.get(flow.targetRef);
2350
+ if (from !== undefined && from === to) {
2351
+ problems.push(`Message flow "${flow.id}" starts and ends in participant "${from}"; a message flow crosses pools, a sequence flow stays inside one`);
2352
+ }
2353
+ if (flow.messageRef !== undefined && !messageIds.has(flow.messageRef)) {
2354
+ problems.push(`Message flow "${flow.id}" references message "${flow.messageRef}", which this diagram does not declare`);
2355
+ }
2356
+ }
2357
+ return problems;
2358
+ }
2359
+ /** Maps every participant id and every flow node id to its owning participant. */
2360
+ participantIndex() {
2361
+ const index = new Map();
2362
+ const byProcess = new Map();
2363
+ for (const participant of this._participants) {
2364
+ index.set(participant.id, participant.id);
2365
+ if (participant.processRef !== undefined)
2366
+ byProcess.set(participant.processRef, participant.id);
2367
+ }
2368
+ const walk = (elements, owner) => {
2369
+ for (const element of elements) {
2370
+ index.set(element.id, owner);
2371
+ if ("flowElements" in element && Array.isArray(element.flowElements)) {
2372
+ walk(element.flowElements, owner);
2373
+ }
2374
+ }
2375
+ };
2376
+ for (const process of this._processes) {
2377
+ const owner = byProcess.get(process.id);
2378
+ if (owner !== undefined)
2379
+ walk(process.flowElements, owner);
2380
+ }
2381
+ return index;
2382
+ }
2002
2383
  build() {
2003
2384
  return {
2004
2385
  id: this._id,
@@ -2022,10 +2403,35 @@ export class DiagramBuilder {
2022
2403
  escalations: [],
2023
2404
  messages: this._messages,
2024
2405
  signals: [],
2025
- collaborations: [],
2406
+ collaborations: this.buildCollaborations(),
2026
2407
  processes: this._processes,
2027
2408
  diagrams: [],
2028
2409
  };
2029
2410
  }
2411
+ /**
2412
+ * A document with no participants has no collaboration — an empty
2413
+ * `<bpmn:collaboration/>` is not a neutral addition, it makes every process a
2414
+ * pool-less participant in a modeler.
2415
+ */
2416
+ buildCollaborations() {
2417
+ if (this._participants.length === 0 && this._messageFlows.length === 0)
2418
+ return [];
2419
+ const problems = this.collaborationProblems();
2420
+ if (problems.length > 0) {
2421
+ throw new Error(`Invalid collaboration:\n ${problems.join("\n ")}`);
2422
+ }
2423
+ return [
2424
+ {
2425
+ id: this._collaborationId,
2426
+ participants: this._participants,
2427
+ messageFlows: this._messageFlows,
2428
+ textAnnotations: [],
2429
+ associations: [],
2430
+ groups: [],
2431
+ extensionElements: [],
2432
+ unknownAttributes: {},
2433
+ },
2434
+ ];
2435
+ }
2030
2436
  }
2031
2437
  //# sourceMappingURL=bpmn-builder.js.map