@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.
@@ -14,6 +14,7 @@
14
14
 
15
15
  import { stringify as yamlStringify } from 'yaml';
16
16
  import { pad, toSlug } from './pbProject.js';
17
+ import { SCRIPT_TEMPLATE } from './pbScriptTemplate.js';
17
18
 
18
19
  // Fixed element sizes (measured across all fixtures). Containers are sized by
19
20
  // the layout engine; we seed a small default so a stubbed clone stays valid.
@@ -27,6 +28,12 @@ const SIZE = {
27
28
  userTask: [84, 84],
28
29
  subProcess: [350, 200],
29
30
  transaction: [350, 200],
31
+ // LRP-only (measured across the real LRP corpus):
32
+ intermediateCatchEvent: [36, 36],
33
+ intermediateThrowEvent: [36, 36],
34
+ callActivity: [84, 84],
35
+ parallelGateway: [50, 50],
36
+ eventBasedGateway: [50, 50],
30
37
  };
31
38
 
32
39
  // BPMN id prefixes by node type (mirrors the upstream naming convention).
@@ -40,10 +47,29 @@ const PREFIX = {
40
47
  userTask: 'UserTask',
41
48
  subProcess: 'SubProcess',
42
49
  transaction: 'Transaction',
50
+ intermediateCatchEvent: 'IntermediateCatchEvent',
51
+ intermediateThrowEvent: 'IntermediateThrowEvent',
52
+ callActivity: 'CallActivity',
53
+ parallelGateway: 'ParallelGateway',
54
+ eventBasedGateway: 'EventBasedGateway',
43
55
  };
44
56
 
45
57
  const CONTAINER = new Set(['subProcess', 'transaction']);
46
58
 
59
+ // Default attrs injected at node creation so the editor accepts them without manual edits.
60
+ const DEFAULT_ATTRS = {
61
+ scriptTask: {
62
+ scriptFormat: 'javascript',
63
+ 'activiti:async': 'false',
64
+ 'activiti:exclusive': 'false',
65
+ 'activiti:autoStoreVariables': 'false',
66
+ },
67
+ serviceTask: {
68
+ 'activiti:async': 'false',
69
+ 'activiti:exclusive': 'false',
70
+ },
71
+ };
72
+
47
73
  // Depth-first visit of every node in the nested graph, with its parent node.
48
74
  function eachNode(nodes, parent, fn) {
49
75
  for (const n of nodes || []) {
@@ -112,7 +138,7 @@ function nextScriptSeq(existingScriptFiles) {
112
138
 
113
139
  // ---------- add ----------
114
140
 
115
- // Add a node. For scriptTask, scaffolds an empty script file and refs it. For
141
+ // Add a node. For scriptTask, scaffolds a templated script file and refs it. For
116
142
  // userTask, scaffolds a minimal page file + a manifest page entry (guid=null →
117
143
  // push will create it upstream). Container nodes start empty + expanded.
118
144
  function addNode(structure, manifest, opts, ctx = {}) {
@@ -130,7 +156,7 @@ function addNode(structure, manifest, opts, ctx = {}) {
130
156
  if (type === 'scriptTask') {
131
157
  const seq = nextScriptSeq(ctx.existingScripts);
132
158
  const file = `scripts/${pad(seq)}_${toSlug(name || id)}.js`;
133
- writes[file] = '';
159
+ writes[file] = SCRIPT_TEMPLATE;
134
160
  node.script = file;
135
161
  } else if (type === 'userTask') {
136
162
  const formKey = toSlug(name || id);
@@ -161,6 +187,8 @@ function addNode(structure, manifest, opts, ctx = {}) {
161
187
  node.nodes = [];
162
188
  }
163
189
 
190
+ if (DEFAULT_ATTRS[type]) node.attrs = { ...DEFAULT_ATTRS[type] };
191
+
164
192
  if (parentId) {
165
193
  const f = findNode(structure, parentId);
166
194
  if (!f) throw new Error(`parent not found: ${parentId}`);
@@ -334,10 +362,9 @@ function connect(structure, { from, to, name, condition, conditionType, makeDefa
334
362
  warnings.push(`replaced previous default flow ${prev} on ${from}.`);
335
363
  }
336
364
  }
337
- // Re-check the gateway's invariant after the edit.
338
- if (sf.node.type === 'exclusiveGateway') {
339
- warnings.push(...lintGateways(structure).filter((w) => w.startsWith(from)));
340
- }
365
+ // Re-check all structural invariants after the edit, scoped to affected nodes.
366
+ const allIssues = lintAll(structure);
367
+ warnings.push(...allIssues.filter((w) => w.startsWith(from) || w.startsWith(to)));
341
368
 
342
369
  return { writes: {}, deletes: [], result: { id, from, to, warnings } };
343
370
  }
@@ -407,6 +434,73 @@ function lintGateways(structure) {
407
434
  return issues;
408
435
  }
409
436
 
437
+ // Flag non-default outgoing edges that are missing a name when a node has 2+
438
+ // outgoing flows. In Activiti diagrams these labels are mandatory so operators
439
+ // can distinguish branches at runtime.
440
+ function lintEdgeNames(structure) {
441
+ const issues = [];
442
+ eachNode(structure.nodes, null, (n) => {
443
+ const outs = n.edges || [];
444
+ if (outs.length < 2) return;
445
+ const def = n.attrs?.default;
446
+ for (const e of outs) {
447
+ if (e.id === def) continue;
448
+ if (!e.name) {
449
+ issues.push(`${n.id} → ${e.target} (${e.id}): non-default flow has no name.`);
450
+ }
451
+ }
452
+ });
453
+ return issues;
454
+ }
455
+
456
+ // Gateway types that legitimately fan multiple flows into one node (a "join").
457
+ // parallelGateway/eventBasedGateway joins are common in the real LRP corpus
458
+ // (61 occurrences) — only exclusiveGateway's merge semantics need a lint rule.
459
+ const JOIN_CAPABLE_GATEWAYS = new Set(['exclusiveGateway', 'parallelGateway', 'eventBasedGateway']);
460
+
461
+ // Flag incoming-edge violations:
462
+ // - Only a gateway may have >1 incoming flows (plain tasks/events are 1-in).
463
+ // - An exclusiveGateway may not have both >1 incoming AND >1 outgoing (pick one
464
+ // direction for merging or splitting, not both at once) — parallel/event-based
465
+ // gateways may legitimately fork-then-join across the diagram, so this
466
+ // specific rule stays scoped to exclusiveGateway.
467
+ function lintIncomingEdges(structure) {
468
+ const inCount = {};
469
+ const outCount = {};
470
+ eachNode(structure.nodes, null, (n) => {
471
+ for (const e of n.edges || []) {
472
+ inCount[e.target] = (inCount[e.target] || 0) + 1;
473
+ outCount[n.id] = (outCount[n.id] || 0) + 1;
474
+ }
475
+ });
476
+
477
+ const issues = [];
478
+ eachNode(structure.nodes, null, (n) => {
479
+ const inc = inCount[n.id] || 0;
480
+ const out = outCount[n.id] || 0;
481
+ if (inc > 1 && !JOIN_CAPABLE_GATEWAYS.has(n.type)) {
482
+ issues.push(
483
+ `${n.id} (${n.name || n.type}): only a gateway may have multiple incoming flows (has ${inc}).`
484
+ );
485
+ }
486
+ if (n.type === 'exclusiveGateway' && inc > 1 && out > 1) {
487
+ issues.push(
488
+ `${n.id} (${n.name || 'gateway'}): exclusiveGateway may not have both multiple incoming (${inc}) and multiple outgoing (${out}) flows.`
489
+ );
490
+ }
491
+ });
492
+ return issues;
493
+ }
494
+
495
+ // Run all lint rules and return combined issues.
496
+ function lintAll(structure) {
497
+ return [
498
+ ...lintGateways(structure),
499
+ ...lintEdgeNames(structure),
500
+ ...lintIncomingEdges(structure),
501
+ ];
502
+ }
503
+
410
504
  // Remove an edge by id, or by --from/--to pair.
411
505
  function disconnect(structure, { id, from, to }) {
412
506
  let removed = 0;
@@ -464,4 +558,7 @@ export {
464
558
  setDefault,
465
559
  listGraph,
466
560
  lintGateways,
561
+ lintEdgeNames,
562
+ lintIncomingEdges,
563
+ lintAll,
467
564
  };
@@ -37,7 +37,13 @@ const ROOT_OPTS = {
37
37
  'elk.layered.spacing.edgeEdgeBetweenLayers': '20',
38
38
  'elk.layered.spacing.edgeNodeBetweenLayers': '20',
39
39
  };
40
- const CONTAINER_OPTS = { 'elk.padding': '[top=40.0,left=20.0,bottom=20.0,right=20.0]' };
40
+ const CONTAINER_OPTS = {
41
+ 'elk.padding': '[top=50.0,left=30.0,bottom=50.0,right=30.0]',
42
+ 'elk.layered.spacing.nodeNodeBetweenLayers': '60',
43
+ 'elk.spacing.nodeNode': '80',
44
+ 'elk.spacing.edgeNode': '20',
45
+ 'elk.layered.spacing.edgeNodeBetweenLayers': '20',
46
+ };
41
47
 
42
48
  function sizeOf(n) {
43
49
  if (SIZE[n.type]) return SIZE[n.type];
@@ -147,6 +153,9 @@ async function autoLayout(structure) {
147
153
  sources: [source],
148
154
  targets: [e.target],
149
155
  sourcePort,
156
+ labels: e.name
157
+ ? [{ id: `${e.id}__lbl`, text: e.name, width: Math.min(e.name.length * 6, 140), height: 14 }]
158
+ : [],
150
159
  layoutOptions: {
151
160
  'elk.layered.priority.straightness': isHappy ? '10' : 1,
152
161
  'elk.layered.priority.shortness': isHappy ? '10' : 1,
@@ -237,8 +246,20 @@ async function autoLayout(structure) {
237
246
  height: round(p.height),
238
247
  };
239
248
  for (const e of n.edges || []) {
240
- const wp = waypointsFor(e.id, lcaOffset(n.id, e.target));
249
+ const off = lcaOffset(n.id, e.target);
250
+ const wp = waypointsFor(e.id, off);
241
251
  if (wp) e.waypoints = wp;
252
+ if (e.name) {
253
+ const lbl = elkEdges[e.id]?.labels?.[0];
254
+ if (lbl) {
255
+ e.labelPos = {
256
+ x: round(lbl.x + off.x),
257
+ y: round(lbl.y + off.y),
258
+ width: round(lbl.width),
259
+ height: round(lbl.height),
260
+ };
261
+ }
262
+ }
242
263
  }
243
264
  });
244
265