@waron97/prbot 3.2.1 → 3.3.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.
@@ -84,6 +84,22 @@ const NODE_TAGS = new Set([
84
84
  'subProcess',
85
85
  'transaction',
86
86
  'boundaryEvent',
87
+ // LRP-only (also legal on PBs, just unused there so far):
88
+ 'intermediateCatchEvent',
89
+ 'intermediateThrowEvent',
90
+ 'callActivity',
91
+ 'parallelGateway',
92
+ 'eventBasedGateway',
93
+ ]);
94
+
95
+ // Flow-node types that can carry <xEventDefinition/> children (see
96
+ // parseEventDefinitions/buildEventDefinitions below).
97
+ const EVENT_HOST_TAGS = new Set([
98
+ 'startEvent',
99
+ 'endEvent',
100
+ 'boundaryEvent',
101
+ 'intermediateCatchEvent',
102
+ 'intermediateThrowEvent',
87
103
  ]);
88
104
 
89
105
  const PROMOTED_ATTRS = {
@@ -95,8 +111,14 @@ const PROMOTED_ATTRS = {
95
111
  boundaryEvent: ['@_id', '@_name', '@_attachedToRef'],
96
112
  startEvent: ['@_id', '@_name'],
97
113
  endEvent: ['@_id', '@_name'],
114
+ intermediateCatchEvent: ['@_id', '@_name'],
115
+ intermediateThrowEvent: ['@_id', '@_name'],
116
+ callActivity: ['@_id', '@_name', '@_calledElement', '@_activiti:class'],
117
+ parallelGateway: ['@_id', '@_name'],
118
+ eventBasedGateway: ['@_id', '@_name'],
98
119
  process_root: ['@_id', '@_name', '@_isExecutable'],
99
120
  association: ['@_id', '@_sourceRef', '@_targetRef'],
121
+ conditionExpression: ['@_xsi:type'],
100
122
  };
101
123
 
102
124
  // Collect the non-promoted attributes into a plain map (prefix stripped),
@@ -111,19 +133,28 @@ function extraAttrs(attrs, type) {
111
133
  return out;
112
134
  }
113
135
 
136
+ // Returns undefined if the node has no <extensionElements> at all (nothing to
137
+ // rebuild); an array (possibly empty) if it does — some real BPMNs carry a
138
+ // bare `<extensionElements/>` with zero activiti:field children, and that
139
+ // empty wrapper must still round-trip (see buildFlowChildren's serviceTask case).
114
140
  function parseServiceFields(node) {
115
141
  const ext = node.serviceTask?.find((k) => tagOf(k) === 'extensionElements');
116
- if (!ext) return [];
142
+ if (!ext) return undefined;
117
143
  const fields = [];
118
144
  for (const f of ext.extensionElements) {
119
145
  if (tagOf(f) !== 'activiti:field') continue;
120
146
  const name = attrsOf(f)['@_name'];
121
- const strChild = f['activiti:field'].find((k) => tagOf(k) === 'activiti:string');
122
- const exprChild = f['activiti:field'].find((k) => tagOf(k) === 'activiti:expression');
123
- if (strChild) {
124
- fields.push({ name, string: cdataOf(strChild, 'activiti:string') ?? '' });
125
- } else if (exprChild) {
126
- fields.push({ name, expression: cdataOf(exprChild, 'activiti:expression') ?? '' });
147
+ // Normally exactly one of activiti:string/activiti:expression, but the
148
+ // real corpus has fields carrying both (likely a copy-paste artifact
149
+ // upstream) — preserve every child, in order, rather than picking one.
150
+ const parts = f['activiti:field']
151
+ .filter((k) => tagOf(k) === 'activiti:string' || tagOf(k) === 'activiti:expression')
152
+ .map((k) => {
153
+ const kind = tagOf(k) === 'activiti:string' ? 'string' : 'expression';
154
+ return { kind, value: cdataOf(k, tagOf(k)) ?? '' };
155
+ });
156
+ if (parts.length) {
157
+ fields.push({ name, parts });
127
158
  } else {
128
159
  fields.push({ name }); // empty <activiti:field/>
129
160
  }
@@ -156,10 +187,83 @@ function parseMultiInstance(node, tag) {
156
187
  return out;
157
188
  }
158
189
 
159
- function parseErrorEventDef(node, type) {
160
- const eed = node[type]?.find((k) => tagOf(k) === 'errorEventDefinition');
161
- if (!eed) return undefined;
162
- return { ...extraAttrs(attrsOf(eed), type) };
190
+ // Restore a prefix-stripped extra-attrs map back to `@_`-prefixed XML attrs.
191
+ function restoreAttr(extra) {
192
+ const out = {};
193
+ for (const [k, v] of Object.entries(extra || {})) out[`@_${k}`] = v;
194
+ return out;
195
+ }
196
+
197
+ // Event-definition child tags legal on startEvent/endEvent/boundaryEvent/
198
+ // intermediateCatchEvent/intermediateThrowEvent. Each carries only reference
199
+ // attrs (messageRef/errorRef/signalRef) except timerEventDefinition, which
200
+ // nests one of timeDuration/timeDate/timeCycle (text + xsi:type attr).
201
+ const EVENT_DEF_TAGS = new Set([
202
+ 'messageEventDefinition',
203
+ 'errorEventDefinition',
204
+ 'timerEventDefinition',
205
+ 'terminateEventDefinition',
206
+ 'signalEventDefinition',
207
+ ]);
208
+ const TIMER_CHILD_TAGS = new Set(['timeDuration', 'timeDate', 'timeCycle']);
209
+
210
+ function parseTimerChildren(defNode, tag) {
211
+ const kids = defNode[tag];
212
+ if (!Array.isArray(kids)) return [];
213
+ return kids
214
+ .filter((k) => TIMER_CHILD_TAGS.has(tagOf(k)))
215
+ .map((k) => ({ tag: tagOf(k), value: textOf(k, tagOf(k)) ?? '', attrs: allAttrs(k) }));
216
+ }
217
+
218
+ // Parse every event-definition child of a node (order preserved; almost always
219
+ // exactly one, but modeled as an array — never observed >1 in the corpus).
220
+ function parseEventDefinitions(child, tag) {
221
+ const kids = child[tag];
222
+ if (!Array.isArray(kids)) return [];
223
+ const defs = [];
224
+ for (const k of kids) {
225
+ const dtag = tagOf(k);
226
+ if (!EVENT_DEF_TAGS.has(dtag)) continue;
227
+ const d = { type: dtag, attrs: allAttrs(k) };
228
+ if (dtag === 'timerEventDefinition') {
229
+ const timer = parseTimerChildren(k, dtag);
230
+ if (timer.length) d.timer = timer;
231
+ }
232
+ defs.push(d);
233
+ }
234
+ return defs;
235
+ }
236
+
237
+ function buildEventDefinitions(defs) {
238
+ return (defs || []).map((d) => {
239
+ const kids = (d.timer || []).map((t) =>
240
+ el(t.tag, restoreAttr(t.attrs), t.value != null ? [textChild(t.value)] : [])
241
+ );
242
+ return el(d.type, restoreAttr(d.attrs), kids);
243
+ });
244
+ }
245
+
246
+ // callActivity data mappings live in <extensionElements><activiti:in/out .../>
247
+ // (self-closing, source/target attrs only — same shape both ways).
248
+ function parseCallActivityIO(child) {
249
+ const ext = child.callActivity?.find((k) => tagOf(k) === 'extensionElements');
250
+ if (!ext) return [];
251
+ const io = [];
252
+ for (const f of ext.extensionElements) {
253
+ const t = tagOf(f);
254
+ if (t === 'activiti:in' || t === 'activiti:out') {
255
+ io.push({ dir: t === 'activiti:in' ? 'in' : 'out', ...allAttrs(f) });
256
+ }
257
+ }
258
+ return io;
259
+ }
260
+
261
+ function buildCallActivityIO(io) {
262
+ if (!io || !io.length) return [];
263
+ const ioEls = io.map((d) =>
264
+ el(`activiti:${d.dir}`, { '@_source': d.source, '@_target': d.target }, [])
265
+ );
266
+ return [el('extensionElements', {}, ioEls)];
163
267
  }
164
268
 
165
269
  // Parse the children of a <process> or <subProcess> element array into model.
@@ -173,26 +277,39 @@ function parseFlowChildren(children, parentId, model) {
173
277
  if (parentId === null) model.process.documentation = textOf(child, tag);
174
278
  continue;
175
279
  }
176
- if (tag === 'error') {
177
- model.errors.push({ id: attrs['@_id'], name: attrs['@_name'] });
178
- continue;
179
- }
180
280
  if (tag === 'textAnnotation') {
181
281
  const txtEl = child.textAnnotation.find((k) => tagOf(k) === 'text');
182
- model.annotations.push({
282
+ const a = {
183
283
  id: attrs['@_id'],
184
284
  attrs: extraAttrs(attrs, 'textAnnotation'),
185
285
  text: txtEl ? textOf(txtEl, 'text') : '',
186
- });
286
+ };
287
+ if (parentId) a.parent = parentId;
288
+ model.annotations.push(a);
187
289
  continue;
188
290
  }
189
291
  if (tag === 'association') {
190
- model.associations.push({
292
+ const a = {
191
293
  id: attrs['@_id'],
192
294
  sourceRef: attrs['@_sourceRef'],
193
295
  targetRef: attrs['@_targetRef'],
194
296
  attrs: extraAttrs({ ...attrs }, 'association'),
195
- });
297
+ };
298
+ if (parentId) a.parent = parentId;
299
+ model.associations.push(a);
300
+ continue;
301
+ }
302
+ if (tag === 'group') {
303
+ // Visual grouping box, references a <definitions>-root
304
+ // categoryValue by id. Has its own bpmndi:BPMNShape (geometry) but
305
+ // no flow semantics of its own — modeled like an annotation.
306
+ const g = {
307
+ id: attrs['@_id'],
308
+ categoryValueRef: attrs['@_categoryValueRef'],
309
+ attrs: extraAttrs({ ...attrs }, 'group'),
310
+ };
311
+ if (parentId) g.parent = parentId;
312
+ model.groups.push(g);
196
313
  continue;
197
314
  }
198
315
  if (tag === 'sequenceFlow') {
@@ -206,10 +323,23 @@ function parseFlowChildren(children, parentId, model) {
206
323
  if (attrs['@_name'] !== undefined) edge.name = attrs['@_name'];
207
324
  if (parentId) edge.parent = parentId;
208
325
  if (cond) {
209
- edge.condition =
210
- cdataOf(cond, 'conditionExpression') ?? textOf(cond, 'conditionExpression');
211
- const xsi = attrsOf(cond)['@_xsi:type'];
326
+ // A self-closing/childless <conditionExpression/> (xsi:type
327
+ // only, no text) is real in the corpus — don't synthesize an
328
+ // empty CDATA body for it on rebuild (see conditionType below).
329
+ const condKids = cond.conditionExpression;
330
+ if (Array.isArray(condKids) && condKids.length) {
331
+ edge.condition =
332
+ cdataOf(cond, 'conditionExpression') ??
333
+ textOf(cond, 'conditionExpression') ??
334
+ '';
335
+ }
336
+ const condAttrs = attrsOf(cond);
337
+ const xsi = condAttrs['@_xsi:type'];
212
338
  if (xsi) edge.conditionType = xsi;
339
+ // Rare non-standard attrs (e.g. a stray `language="..."` seen
340
+ // in the corpus) — preserve verbatim rather than drop.
341
+ const extra = extraAttrs(condAttrs, 'conditionExpression');
342
+ if (Object.keys(extra).length) edge.conditionAttrs = extra;
213
343
  }
214
344
  if (doc) edge.documentation = textOf(doc, 'documentation');
215
345
  model.edges.push(edge);
@@ -237,15 +367,23 @@ function parseFlowChildren(children, parentId, model) {
237
367
  } else if (tag === 'boundaryEvent') {
238
368
  if (attrs['@_attachedToRef'] !== undefined)
239
369
  n.attachedToRef = attrs['@_attachedToRef'];
240
- const eed = parseErrorEventDef(child, tag);
241
- if (eed) n.errorEventDefinition = eed;
242
- } else if (tag === 'endEvent') {
243
- const eed = parseErrorEventDef(child, tag);
244
- if (eed) n.errorEventDefinition = eed;
370
+ } else if (tag === 'callActivity') {
371
+ if (attrs['@_calledElement'] !== undefined)
372
+ n.calledElement = attrs['@_calledElement'];
373
+ if (attrs['@_activiti:class'] !== undefined) n.class = attrs['@_activiti:class'];
374
+ const io = parseCallActivityIO(child);
375
+ if (io.length) n.dataIO = io;
245
376
  } else if (CONTAINER_TAGS.has(tag)) {
246
377
  const mi = parseMultiInstance(child, tag);
247
378
  if (mi) n.multiInstance = mi;
248
379
  }
380
+ // Event-definition children apply across several event-host types
381
+ // (start/end/boundary/intermediate catch+throw) regardless of the
382
+ // per-type branch above.
383
+ if (EVENT_HOST_TAGS.has(tag)) {
384
+ const defs = parseEventDefinitions(child, tag);
385
+ if (defs.length) n.eventDefinitions = defs;
386
+ }
249
387
  model.nodes.push(n);
250
388
 
251
389
  if (CONTAINER_TAGS.has(tag)) {
@@ -278,12 +416,30 @@ function parseProcess(builtPage) {
278
416
  if (Object.keys(extra).length) p.attrs = extra;
279
417
  return p;
280
418
  })(),
419
+ // Declared at <definitions> root, as siblings of <process> — NOT nested
420
+ // inside it (verified against the real corpus). messageRef/errorRef/
421
+ // signalRef on event definitions resolve against these.
422
+ messages: [],
281
423
  errors: [],
424
+ signals: [],
425
+ // Any other <definitions>-level child we don't model explicitly
426
+ // (e.g. <category>/<categoryValue> — rare) — kept as raw preserveOrder
427
+ // nodes and re-emitted verbatim, so nothing at this level is ever lost.
428
+ extraDefs: [],
282
429
  nodes: [],
283
430
  edges: [],
284
431
  annotations: [],
285
432
  associations: [],
433
+ groups: [],
286
434
  };
435
+ for (const c of defNode.definitions) {
436
+ const t = tagOf(c);
437
+ if (t === 'process' || t === 'bpmndi:BPMNDiagram') continue;
438
+ if (t === 'message') model.messages.push({ attrs: allAttrs(c) });
439
+ else if (t === 'error') model.errors.push({ attrs: allAttrs(c) });
440
+ else if (t === 'signal') model.signals.push({ attrs: allAttrs(c) });
441
+ else model.extraDefs.push(c);
442
+ }
287
443
  parseFlowChildren(procNode.process, null, model);
288
444
 
289
445
  return { ns, model, diagram: parseDiagram(builtPage) };
@@ -299,6 +455,20 @@ function num(v) {
299
455
  return Number.isNaN(n) ? v : n;
300
456
  }
301
457
 
458
+ // The DD/DI package (waypoint's namespace) is bound to different prefixes
459
+ // across the corpus — usually `omgdi:` but some exporters use `di:` for the
460
+ // identical namespace URI. Resolve the real prefix from the <definitions>
461
+ // root's xmlns declarations instead of hardcoding one, so both the `<process>`
462
+ // diagram bpmnElement and its waypoint tag stay readable and re-emittable
463
+ // under whichever prefix the source actually declared.
464
+ const DI_NS_URI = 'http://www.omg.org/spec/DD/20100524/DI';
465
+ function diWaypointTag(ns) {
466
+ for (const [k, v] of Object.entries(ns || {})) {
467
+ if (v === DI_NS_URI) return `${k.replace(/^@_xmlns:/, '')}:waypoint`;
468
+ }
469
+ return 'omgdi:waypoint';
470
+ }
471
+
302
472
  function parseLabel(labelNode) {
303
473
  const b = labelNode['bpmndi:BPMNLabel'].find((k) => tagOf(k) === 'omgdc:Bounds');
304
474
  return { attrs: allAttrs(labelNode), bounds: b ? mapBounds(allAttrs(b)) : null };
@@ -312,6 +482,8 @@ function mapBounds(a) {
312
482
  function parseDiagram(builtPage) {
313
483
  const tree = parser.parse(builtPage);
314
484
  const defNode = tree.find((n) => tagOf(n) === 'definitions');
485
+ const ns = attrsOf(defNode);
486
+ const wpTag = diWaypointTag(ns);
315
487
  const dia = defNode.definitions.find((n) => tagOf(n) === 'bpmndi:BPMNDiagram');
316
488
  if (!dia) return null;
317
489
  const plane = dia['bpmndi:BPMNDiagram'].find((n) => tagOf(n) === 'bpmndi:BPMNPlane');
@@ -330,7 +502,7 @@ function parseDiagram(builtPage) {
330
502
  });
331
503
  } else if (tg === 'bpmndi:BPMNEdge') {
332
504
  const wps = c['bpmndi:BPMNEdge']
333
- .filter((x) => tagOf(x) === 'omgdi:waypoint')
505
+ .filter((x) => tagOf(x) === wpTag)
334
506
  .map((w) => mapBounds(allAttrs(w)));
335
507
  const label = c['bpmndi:BPMNEdge'].find((x) => tagOf(x) === 'bpmndi:BPMNLabel');
336
508
  edges.push({
@@ -357,8 +529,9 @@ function boundsAttrs(b) {
357
529
  // labels are omitted (renderers auto-place them); edge labels use ELK-computed
358
530
  // bounds from geo.labelPos when present — only positions, sizes, waypoints and
359
531
  // subprocess expand-state are authoritative — see compareDiagram.
360
- function buildDiagram(model, geo) {
532
+ function buildDiagram(model, geo, ns) {
361
533
  if (!geo) return null;
534
+ const wpTag = diWaypointTag(ns);
362
535
  const shapeEls = [];
363
536
  const pushShape = (id) => {
364
537
  const b = geo.bounds[id];
@@ -369,6 +542,7 @@ function buildDiagram(model, geo) {
369
542
  };
370
543
  for (const n of model.nodes) pushShape(n.id);
371
544
  for (const a of model.annotations || []) pushShape(a.id);
545
+ for (const g of model.groups || []) pushShape(g.id);
372
546
 
373
547
  const edgeEls = [];
374
548
  const pushEdge = (id) => {
@@ -377,8 +551,9 @@ function buildDiagram(model, geo) {
377
551
  const children = [];
378
552
  const lp = geo.labelPos?.[id];
379
553
  for (const [x, y] of wps)
380
- children.push(el('omgdi:waypoint', { '@_x': String(x), '@_y': String(y) }, []));
381
- if (lp) children.push(el('bpmndi:BPMNLabel', {}, [el('omgdc:Bounds', boundsAttrs(lp), [])]));
554
+ children.push(el(wpTag, { '@_x': String(x), '@_y': String(y) }, []));
555
+ if (lp)
556
+ children.push(el('bpmndi:BPMNLabel', {}, [el('omgdc:Bounds', boundsAttrs(lp), [])]));
382
557
  edgeEls.push(el('bpmndi:BPMNEdge', { '@_id': `${id}_di`, '@_bpmnElement': id }, children));
383
558
  };
384
559
  for (const e of model.edges) pushEdge(e.id);
@@ -402,18 +577,8 @@ function buildFlowChildren(model, parentId) {
402
577
  if (parentId === null && model.process.documentation != null) {
403
578
  children.push(el('documentation', {}, [textChild(model.process.documentation)]));
404
579
  }
405
- // errors (process root only)
406
- if (parentId === null) {
407
- for (const e of model.errors) {
408
- children.push(el('error', { '@_id': e.id, '@_name': e.name }, []));
409
- }
410
- }
411
-
412
- const restoreAttr = (extra) => {
413
- const out = {};
414
- for (const [k, v] of Object.entries(extra || {})) out[`@_${k}`] = v;
415
- return out;
416
- };
580
+ // message/error/signal declarations are <definitions>-root siblings of
581
+ // <process>, not process children — built separately in buildProcess().
417
582
 
418
583
  const incoming = (id) => model.edges.filter((e) => e.target === id).map((e) => e.id);
419
584
  const outgoing = (id) => model.edges.filter((e) => e.source === id).map((e) => e.id);
@@ -446,16 +611,15 @@ function buildFlowChildren(model, parentId) {
446
611
  if (n.class !== undefined) attrs['@_activiti:class'] = n.class;
447
612
  Object.assign(attrs, restoreAttr(n.attrs));
448
613
  const kids = [];
449
- if (n.fields && n.fields.length) {
614
+ if (n.fields !== undefined) {
450
615
  const fieldEls = n.fields.map((f) => {
451
- let inner = [];
452
- if (Object.prototype.hasOwnProperty.call(f, 'string')) {
453
- inner = [el('activiti:string', {}, [cdataChild(f.string)])];
454
- } else if (Object.prototype.hasOwnProperty.call(f, 'expression')) {
455
- inner = [el('activiti:expression', {}, [cdataChild(f.expression)])];
456
- }
616
+ const inner = (f.parts || []).map((p) =>
617
+ el(`activiti:${p.kind}`, {}, [cdataChild(p.value)])
618
+ );
457
619
  return el('activiti:field', { '@_name': f.name }, inner);
458
620
  });
621
+ // Emit the wrapper even with zero fields — some sources carry a
622
+ // bare <extensionElements/> (see parseServiceFields).
459
623
  kids.push(el('extensionElements', {}, fieldEls));
460
624
  }
461
625
  kids.push(...baseKids(n));
@@ -469,22 +633,35 @@ function buildFlowChildren(model, parentId) {
469
633
  children.push(el('exclusiveGateway', attrs, baseKids(n)));
470
634
  } else if (n.type === 'startEvent') {
471
635
  Object.assign(attrs, restoreAttr(n.attrs));
472
- children.push(el('startEvent', attrs, baseKids(n)));
636
+ const kids = baseKids(n);
637
+ kids.push(...buildEventDefinitions(n.eventDefinitions));
638
+ children.push(el('startEvent', attrs, kids));
473
639
  } else if (n.type === 'endEvent') {
474
640
  Object.assign(attrs, restoreAttr(n.attrs));
475
641
  const kids = baseKids(n);
476
- if (n.errorEventDefinition) {
477
- kids.push(el('errorEventDefinition', restoreAttr(n.errorEventDefinition), []));
478
- }
642
+ kids.push(...buildEventDefinitions(n.eventDefinitions));
479
643
  children.push(el('endEvent', attrs, kids));
480
644
  } else if (n.type === 'boundaryEvent') {
481
645
  if (n.attachedToRef !== undefined) attrs['@_attachedToRef'] = n.attachedToRef;
482
646
  Object.assign(attrs, restoreAttr(n.attrs));
483
647
  const kids = baseKids(n);
484
- if (n.errorEventDefinition) {
485
- kids.push(el('errorEventDefinition', restoreAttr(n.errorEventDefinition), []));
486
- }
648
+ kids.push(...buildEventDefinitions(n.eventDefinitions));
487
649
  children.push(el('boundaryEvent', attrs, kids));
650
+ } else if (n.type === 'intermediateCatchEvent' || n.type === 'intermediateThrowEvent') {
651
+ Object.assign(attrs, restoreAttr(n.attrs));
652
+ const kids = baseKids(n);
653
+ kids.push(...buildEventDefinitions(n.eventDefinitions));
654
+ children.push(el(n.type, attrs, kids));
655
+ } else if (n.type === 'callActivity') {
656
+ if (n.calledElement !== undefined) attrs['@_calledElement'] = n.calledElement;
657
+ if (n.class !== undefined) attrs['@_activiti:class'] = n.class;
658
+ Object.assign(attrs, restoreAttr(n.attrs));
659
+ const kids = buildCallActivityIO(n.dataIO);
660
+ kids.push(...baseKids(n));
661
+ children.push(el('callActivity', attrs, kids));
662
+ } else if (n.type === 'parallelGateway' || n.type === 'eventBasedGateway') {
663
+ Object.assign(attrs, restoreAttr(n.attrs));
664
+ children.push(el(n.type, attrs, baseKids(n)));
488
665
  } else if (CONTAINER_TAGS.has(n.type)) {
489
666
  Object.assign(attrs, restoreAttr(n.attrs));
490
667
  const kids = baseKids(n);
@@ -525,45 +702,67 @@ function buildFlowChildren(model, parentId) {
525
702
  const kids = [];
526
703
  if (e.documentation != null)
527
704
  kids.push(el('documentation', {}, [textChild(e.documentation)]));
528
- if (e.condition != null) {
529
- const cAttrs = e.conditionType ? { '@_xsi:type': e.conditionType } : {};
530
- kids.push(el('conditionExpression', cAttrs, [cdataChild(e.condition)]));
705
+ if (e.condition != null || e.conditionType !== undefined || e.conditionAttrs) {
706
+ const cAttrs = {
707
+ ...(e.conditionType ? { '@_xsi:type': e.conditionType } : {}),
708
+ ...restoreAttr(e.conditionAttrs),
709
+ };
710
+ const condKids = e.condition != null ? [cdataChild(e.condition)] : [];
711
+ kids.push(el('conditionExpression', cAttrs, condKids));
531
712
  }
532
713
  children.push(el('sequenceFlow', attrs, kids));
533
714
  }
534
715
 
535
- // annotations + associations (process root only)
536
- if (parentId === null) {
537
- for (const a of model.annotations) {
538
- children.push(
539
- el('textAnnotation', { '@_id': a.id, ...restoreAttr(a.attrs) }, [
540
- el('text', {}, [textChild(a.text ?? '')]),
541
- ])
542
- );
543
- }
544
- for (const a of model.associations) {
545
- children.push(
546
- el(
547
- 'association',
548
- {
549
- '@_id': a.id,
550
- '@_sourceRef': a.sourceRef,
551
- '@_targetRef': a.targetRef,
552
- ...restoreAttr(a.attrs),
553
- },
554
- []
555
- )
556
- );
557
- }
716
+ // annotations + associations + groups scoped to this parent (nested inside
717
+ // a subProcess/transaction in the original just as often as at root).
718
+ for (const a of model.annotations.filter((x) => (x.parent ?? null) === parentId)) {
719
+ children.push(
720
+ el('textAnnotation', { '@_id': a.id, ...restoreAttr(a.attrs) }, [
721
+ el('text', {}, [textChild(a.text ?? '')]),
722
+ ])
723
+ );
724
+ }
725
+ for (const a of model.associations.filter((x) => (x.parent ?? null) === parentId)) {
726
+ children.push(
727
+ el(
728
+ 'association',
729
+ {
730
+ '@_id': a.id,
731
+ '@_sourceRef': a.sourceRef,
732
+ '@_targetRef': a.targetRef,
733
+ ...restoreAttr(a.attrs),
734
+ },
735
+ []
736
+ )
737
+ );
738
+ }
739
+ for (const g of (model.groups || []).filter((x) => (x.parent ?? null) === parentId)) {
740
+ children.push(
741
+ el(
742
+ 'group',
743
+ { '@_id': g.id, '@_categoryValueRef': g.categoryValueRef, ...restoreAttr(g.attrs) },
744
+ []
745
+ )
746
+ );
558
747
  }
559
748
 
560
749
  return children;
561
750
  }
562
751
 
752
+ // Build one <definitions>-root declaration (<message>/<error>/<signal>), attrs
753
+ // passed through verbatim.
754
+ function buildDefDecl(tag, attrs) {
755
+ return el(tag, restoreAttr(attrs), []);
756
+ }
757
+
563
758
  // Build the full built_page string from { ns, model, geo }. The <bpmndi> block
564
759
  // is regenerated solely from `geo` (structure.yaml geometry) — never a manifest.
760
+ // message/error/signal declarations and any unmodeled <definitions>-level
761
+ // children (model.extraDefs, kept as raw preserveOrder nodes) are re-emitted as
762
+ // siblings of <process>, matching the real corpus layout (process, then decls,
763
+ // then bpmndi).
565
764
  function buildProcess({ ns, model, geo }) {
566
- const diagramXml = buildDiagram(model, geo);
765
+ const diagramXml = buildDiagram(model, geo, ns);
567
766
  const procAttrs = { '@_id': model.process.id };
568
767
  if (model.process.name !== undefined) procAttrs['@_name'] = model.process.name;
569
768
  if (model.process.isExecutable !== undefined)
@@ -573,6 +772,14 @@ function buildProcess({ ns, model, geo }) {
573
772
  const procNode = el('process', procAttrs, buildFlowChildren(model, null));
574
773
  const procXml = builder.build([procNode]).trim();
575
774
 
775
+ const declEls = [
776
+ ...(model.messages || []).map((m) => buildDefDecl('message', m.attrs)),
777
+ ...(model.errors || []).map((e) => buildDefDecl('error', e.attrs)),
778
+ ...(model.signals || []).map((s) => buildDefDecl('signal', s.attrs)),
779
+ ];
780
+ const declXml = declEls.length ? builder.build(declEls).trim() : '';
781
+ const extraXml = (model.extraDefs || []).length ? builder.build(model.extraDefs).trim() : '';
782
+
576
783
  const defAttrs = Object.entries(ns)
577
784
  .map(([k, v]) => `${k.replace(/^@_/, '')}="${v}"`)
578
785
  .join(' ');
@@ -580,6 +787,8 @@ function buildProcess({ ns, model, geo }) {
580
787
  // Whitespace between elements is insignificant in XML. The <process> is
581
788
  // compact (format:false) to keep CDATA exact; the diagram is regenerated.
582
789
  const parts = ['<?xml version="1.0" encoding="UTF-8"?>', `<definitions ${defAttrs}>`, procXml];
790
+ if (declXml) parts.push(declXml);
791
+ if (extraXml) parts.push(extraXml);
583
792
  if (diagramXml) parts.push(diagramXml);
584
793
  parts.push('</definitions>');
585
794
  return parts.join('\n');
@@ -606,7 +815,16 @@ function canon(node) {
606
815
  const attrs = {};
607
816
  for (const [k, v] of Object.entries(attrsOf(node))) attrs[k] = v;
608
817
  const rawKids = Array.isArray(node[tag]) ? node[tag] : [];
609
- const kids = rawKids.map(canon).filter((x) => x !== null);
818
+ // <incoming>/<outgoing> are a redundant echo of sequenceFlow sourceRef/
819
+ // targetRef (already compared via the sequenceFlow nodes themselves) —
820
+ // never consulted by the execution engine, and inconsistently omitted by
821
+ // some exporters/hand-edits in the real corpus. We always regenerate a
822
+ // complete, correct set from the edge list on rebuild, so comparing their
823
+ // raw presence/count here would flag a behaviorally meaningless "loss".
824
+ const kids = rawKids
825
+ .filter((k) => tagOf(k) !== 'incoming' && tagOf(k) !== 'outgoing')
826
+ .map(canon)
827
+ .filter((x) => x !== null);
610
828
  kids.sort((a, b) => keyOf(a).localeCompare(keyOf(b)));
611
829
  return { tag, attrs, kids };
612
830
  }
@@ -621,11 +839,24 @@ function keyOf(c) {
621
839
  return `${c.tag}|${id}|${childKey}`;
622
840
  }
623
841
 
842
+ // Canonicalize both the <process> subtree AND every other <definitions>-level
843
+ // declaration (message/error/signal/category/...) except <process> itself and
844
+ // <bpmndi:BPMNDiagram> (compared separately, by geometry, in compareDiagram).
845
+ // Declarations reference each other by id (messageRef/errorRef/signalRef), so
846
+ // they must be part of the semantic 0-loss gate, not just the flow graph.
624
847
  function normalizeProcessTree(builtPage) {
625
848
  const tree = parser.parse(builtPage);
626
849
  const defNode = tree.find((n) => tagOf(n) === 'definitions');
627
850
  const procNode = defNode.definitions.find((n) => tagOf(n) === 'process');
628
- return canon(procNode);
851
+ const decls = defNode.definitions
852
+ .filter((n) => {
853
+ const t = tagOf(n);
854
+ return t !== 'process' && t !== 'bpmndi:BPMNDiagram';
855
+ })
856
+ .map(canon)
857
+ .filter((x) => x !== null)
858
+ .sort((a, b) => keyOf(a).localeCompare(keyOf(b)));
859
+ return { process: canon(procNode), decls };
629
860
  }
630
861
 
631
862
  // Deep structural comparison; returns first diff path or null if equal.