@waron97/prbot 3.2.0 → 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({
@@ -353,11 +525,13 @@ function boundsAttrs(b) {
353
525
  }
354
526
 
355
527
  // Rebuild <bpmndi> purely from structure.yaml geometry (model graph + geo maps).
356
- // DI element ids are derived (`<id>_di`); the manifest is never consulted. Labels
357
- // are omitted (renderers auto-place them); only positions, sizes, waypoints and
528
+ // DI element ids are derived (`<id>_di`); the manifest is never consulted. Node
529
+ // labels are omitted (renderers auto-place them); edge labels use ELK-computed
530
+ // bounds from geo.labelPos when present — only positions, sizes, waypoints and
358
531
  // subprocess expand-state are authoritative — see compareDiagram.
359
- function buildDiagram(model, geo) {
532
+ function buildDiagram(model, geo, ns) {
360
533
  if (!geo) return null;
534
+ const wpTag = diWaypointTag(ns);
361
535
  const shapeEls = [];
362
536
  const pushShape = (id) => {
363
537
  const b = geo.bounds[id];
@@ -368,15 +542,19 @@ function buildDiagram(model, geo) {
368
542
  };
369
543
  for (const n of model.nodes) pushShape(n.id);
370
544
  for (const a of model.annotations || []) pushShape(a.id);
545
+ for (const g of model.groups || []) pushShape(g.id);
371
546
 
372
547
  const edgeEls = [];
373
548
  const pushEdge = (id) => {
374
549
  const wps = geo.waypoints[id];
375
550
  if (!wps) return;
376
- const pts = wps.map(([x, y]) =>
377
- el('omgdi:waypoint', { '@_x': String(x), '@_y': String(y) }, [])
378
- );
379
- edgeEls.push(el('bpmndi:BPMNEdge', { '@_id': `${id}_di`, '@_bpmnElement': id }, pts));
551
+ const children = [];
552
+ const lp = geo.labelPos?.[id];
553
+ for (const [x, y] of wps)
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), [])]));
557
+ edgeEls.push(el('bpmndi:BPMNEdge', { '@_id': `${id}_di`, '@_bpmnElement': id }, children));
380
558
  };
381
559
  for (const e of model.edges) pushEdge(e.id);
382
560
  for (const a of model.associations || []) pushEdge(a.id);
@@ -399,18 +577,8 @@ function buildFlowChildren(model, parentId) {
399
577
  if (parentId === null && model.process.documentation != null) {
400
578
  children.push(el('documentation', {}, [textChild(model.process.documentation)]));
401
579
  }
402
- // errors (process root only)
403
- if (parentId === null) {
404
- for (const e of model.errors) {
405
- children.push(el('error', { '@_id': e.id, '@_name': e.name }, []));
406
- }
407
- }
408
-
409
- const restoreAttr = (extra) => {
410
- const out = {};
411
- for (const [k, v] of Object.entries(extra || {})) out[`@_${k}`] = v;
412
- return out;
413
- };
580
+ // message/error/signal declarations are <definitions>-root siblings of
581
+ // <process>, not process children — built separately in buildProcess().
414
582
 
415
583
  const incoming = (id) => model.edges.filter((e) => e.target === id).map((e) => e.id);
416
584
  const outgoing = (id) => model.edges.filter((e) => e.source === id).map((e) => e.id);
@@ -443,16 +611,15 @@ function buildFlowChildren(model, parentId) {
443
611
  if (n.class !== undefined) attrs['@_activiti:class'] = n.class;
444
612
  Object.assign(attrs, restoreAttr(n.attrs));
445
613
  const kids = [];
446
- if (n.fields && n.fields.length) {
614
+ if (n.fields !== undefined) {
447
615
  const fieldEls = n.fields.map((f) => {
448
- let inner = [];
449
- if (Object.prototype.hasOwnProperty.call(f, 'string')) {
450
- inner = [el('activiti:string', {}, [cdataChild(f.string)])];
451
- } else if (Object.prototype.hasOwnProperty.call(f, 'expression')) {
452
- inner = [el('activiti:expression', {}, [cdataChild(f.expression)])];
453
- }
616
+ const inner = (f.parts || []).map((p) =>
617
+ el(`activiti:${p.kind}`, {}, [cdataChild(p.value)])
618
+ );
454
619
  return el('activiti:field', { '@_name': f.name }, inner);
455
620
  });
621
+ // Emit the wrapper even with zero fields — some sources carry a
622
+ // bare <extensionElements/> (see parseServiceFields).
456
623
  kids.push(el('extensionElements', {}, fieldEls));
457
624
  }
458
625
  kids.push(...baseKids(n));
@@ -466,22 +633,35 @@ function buildFlowChildren(model, parentId) {
466
633
  children.push(el('exclusiveGateway', attrs, baseKids(n)));
467
634
  } else if (n.type === 'startEvent') {
468
635
  Object.assign(attrs, restoreAttr(n.attrs));
469
- 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));
470
639
  } else if (n.type === 'endEvent') {
471
640
  Object.assign(attrs, restoreAttr(n.attrs));
472
641
  const kids = baseKids(n);
473
- if (n.errorEventDefinition) {
474
- kids.push(el('errorEventDefinition', restoreAttr(n.errorEventDefinition), []));
475
- }
642
+ kids.push(...buildEventDefinitions(n.eventDefinitions));
476
643
  children.push(el('endEvent', attrs, kids));
477
644
  } else if (n.type === 'boundaryEvent') {
478
645
  if (n.attachedToRef !== undefined) attrs['@_attachedToRef'] = n.attachedToRef;
479
646
  Object.assign(attrs, restoreAttr(n.attrs));
480
647
  const kids = baseKids(n);
481
- if (n.errorEventDefinition) {
482
- kids.push(el('errorEventDefinition', restoreAttr(n.errorEventDefinition), []));
483
- }
648
+ kids.push(...buildEventDefinitions(n.eventDefinitions));
484
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)));
485
665
  } else if (CONTAINER_TAGS.has(n.type)) {
486
666
  Object.assign(attrs, restoreAttr(n.attrs));
487
667
  const kids = baseKids(n);
@@ -522,45 +702,67 @@ function buildFlowChildren(model, parentId) {
522
702
  const kids = [];
523
703
  if (e.documentation != null)
524
704
  kids.push(el('documentation', {}, [textChild(e.documentation)]));
525
- if (e.condition != null) {
526
- const cAttrs = e.conditionType ? { '@_xsi:type': e.conditionType } : {};
527
- 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));
528
712
  }
529
713
  children.push(el('sequenceFlow', attrs, kids));
530
714
  }
531
715
 
532
- // annotations + associations (process root only)
533
- if (parentId === null) {
534
- for (const a of model.annotations) {
535
- children.push(
536
- el('textAnnotation', { '@_id': a.id, ...restoreAttr(a.attrs) }, [
537
- el('text', {}, [textChild(a.text ?? '')]),
538
- ])
539
- );
540
- }
541
- for (const a of model.associations) {
542
- children.push(
543
- el(
544
- 'association',
545
- {
546
- '@_id': a.id,
547
- '@_sourceRef': a.sourceRef,
548
- '@_targetRef': a.targetRef,
549
- ...restoreAttr(a.attrs),
550
- },
551
- []
552
- )
553
- );
554
- }
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
+ );
555
747
  }
556
748
 
557
749
  return children;
558
750
  }
559
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
+
560
758
  // Build the full built_page string from { ns, model, geo }. The <bpmndi> block
561
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).
562
764
  function buildProcess({ ns, model, geo }) {
563
- const diagramXml = buildDiagram(model, geo);
765
+ const diagramXml = buildDiagram(model, geo, ns);
564
766
  const procAttrs = { '@_id': model.process.id };
565
767
  if (model.process.name !== undefined) procAttrs['@_name'] = model.process.name;
566
768
  if (model.process.isExecutable !== undefined)
@@ -570,6 +772,14 @@ function buildProcess({ ns, model, geo }) {
570
772
  const procNode = el('process', procAttrs, buildFlowChildren(model, null));
571
773
  const procXml = builder.build([procNode]).trim();
572
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
+
573
783
  const defAttrs = Object.entries(ns)
574
784
  .map(([k, v]) => `${k.replace(/^@_/, '')}="${v}"`)
575
785
  .join(' ');
@@ -577,6 +787,8 @@ function buildProcess({ ns, model, geo }) {
577
787
  // Whitespace between elements is insignificant in XML. The <process> is
578
788
  // compact (format:false) to keep CDATA exact; the diagram is regenerated.
579
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);
580
792
  if (diagramXml) parts.push(diagramXml);
581
793
  parts.push('</definitions>');
582
794
  return parts.join('\n');
@@ -603,7 +815,16 @@ function canon(node) {
603
815
  const attrs = {};
604
816
  for (const [k, v] of Object.entries(attrsOf(node))) attrs[k] = v;
605
817
  const rawKids = Array.isArray(node[tag]) ? node[tag] : [];
606
- 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);
607
828
  kids.sort((a, b) => keyOf(a).localeCompare(keyOf(b)));
608
829
  return { tag, attrs, kids };
609
830
  }
@@ -618,11 +839,24 @@ function keyOf(c) {
618
839
  return `${c.tag}|${id}|${childKey}`;
619
840
  }
620
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.
621
847
  function normalizeProcessTree(builtPage) {
622
848
  const tree = parser.parse(builtPage);
623
849
  const defNode = tree.find((n) => tagOf(n) === 'definitions');
624
850
  const procNode = defNode.definitions.find((n) => tagOf(n) === 'process');
625
- 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 };
626
860
  }
627
861
 
628
862
  // Deep structural comparison; returns first diff path or null if equal.
@@ -91,9 +91,11 @@ function toSvg(structure) {
91
91
  `<polyline points="${pts}" fill="none" stroke="#444" stroke-width="1.5" marker-end="url(#arrow)"/>`
92
92
  );
93
93
  if (e.name) {
94
- const m = e.waypoints[Math.floor(e.waypoints.length / 2)];
94
+ const lp = e.labelPos;
95
+ const lx = lp ? lp.x + lp.width / 2 : e.waypoints[Math.floor(e.waypoints.length / 2)][0];
96
+ const ly = lp ? lp.y + lp.height / 2 : e.waypoints[Math.floor(e.waypoints.length / 2)][1] - 4;
95
97
  out.push(
96
- `<text x="${m[0] + ox}" y="${m[1] + oy - 4}" text-anchor="middle" fill="#666" font-size="9">${esc(e.name)}</text>`
98
+ `<text x="${lx + ox}" y="${ly + oy}" text-anchor="middle" fill="#666" font-size="9">${esc(e.name)}</text>`
97
99
  );
98
100
  }
99
101
  }