@bpmnkit/core 0.0.8

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 (104) hide show
  1. package/README.md +185 -0
  2. package/dist/bpmn/auto-layout.d.ts +10 -0
  3. package/dist/bpmn/auto-layout.js +203 -0
  4. package/dist/bpmn/bpmn-builder.d.ts +381 -0
  5. package/dist/bpmn/bpmn-builder.js +1223 -0
  6. package/dist/bpmn/bpmn-model.d.ts +403 -0
  7. package/dist/bpmn/bpmn-model.js +2 -0
  8. package/dist/bpmn/bpmn-parser.d.ts +4 -0
  9. package/dist/bpmn/bpmn-parser.js +642 -0
  10. package/dist/bpmn/bpmn-serializer.d.ts +4 -0
  11. package/dist/bpmn/bpmn-serializer.js +515 -0
  12. package/dist/bpmn/compact.d.ts +91 -0
  13. package/dist/bpmn/compact.js +349 -0
  14. package/dist/bpmn/di-color.d.ts +9 -0
  15. package/dist/bpmn/di-color.js +32 -0
  16. package/dist/bpmn/index.d.ts +105 -0
  17. package/dist/bpmn/index.js +176 -0
  18. package/dist/bpmn/optimize/feel.d.ts +4 -0
  19. package/dist/bpmn/optimize/feel.js +186 -0
  20. package/dist/bpmn/optimize/flow.d.ts +4 -0
  21. package/dist/bpmn/optimize/flow.js +323 -0
  22. package/dist/bpmn/optimize/index.d.ts +5 -0
  23. package/dist/bpmn/optimize/index.js +51 -0
  24. package/dist/bpmn/optimize/naming.d.ts +8 -0
  25. package/dist/bpmn/optimize/naming.js +145 -0
  26. package/dist/bpmn/optimize/tasks.d.ts +4 -0
  27. package/dist/bpmn/optimize/tasks.js +273 -0
  28. package/dist/bpmn/optimize/types.d.ts +44 -0
  29. package/dist/bpmn/optimize/types.js +2 -0
  30. package/dist/bpmn/optimize/utils.d.ts +23 -0
  31. package/dist/bpmn/optimize/utils.js +129 -0
  32. package/dist/bpmn/rest-connector.d.ts +48 -0
  33. package/dist/bpmn/rest-connector.js +69 -0
  34. package/dist/bpmn/svg.d.ts +35 -0
  35. package/dist/bpmn/svg.js +557 -0
  36. package/dist/bpmn/type-guards.d.ts +136 -0
  37. package/dist/bpmn/type-guards.js +211 -0
  38. package/dist/bpmn/utils.d.ts +88 -0
  39. package/dist/bpmn/utils.js +207 -0
  40. package/dist/bpmn/zeebe-extensions.d.ts +72 -0
  41. package/dist/bpmn/zeebe-extensions.js +96 -0
  42. package/dist/dmn/compact.d.ts +44 -0
  43. package/dist/dmn/compact.js +125 -0
  44. package/dist/dmn/dmn-builder.d.ts +59 -0
  45. package/dist/dmn/dmn-builder.js +144 -0
  46. package/dist/dmn/dmn-layout.d.ts +57 -0
  47. package/dist/dmn/dmn-layout.js +256 -0
  48. package/dist/dmn/dmn-model.d.ts +158 -0
  49. package/dist/dmn/dmn-model.js +2 -0
  50. package/dist/dmn/dmn-parser.d.ts +4 -0
  51. package/dist/dmn/dmn-parser.js +275 -0
  52. package/dist/dmn/dmn-serializer.d.ts +4 -0
  53. package/dist/dmn/dmn-serializer.js +296 -0
  54. package/dist/dmn/index.d.ts +28 -0
  55. package/dist/dmn/index.js +51 -0
  56. package/dist/errors.d.ts +77 -0
  57. package/dist/errors.js +66 -0
  58. package/dist/form/compact.d.ts +20 -0
  59. package/dist/form/compact.js +146 -0
  60. package/dist/form/form-builder.d.ts +111 -0
  61. package/dist/form/form-builder.js +347 -0
  62. package/dist/form/form-model.d.ts +221 -0
  63. package/dist/form/form-model.js +2 -0
  64. package/dist/form/form-parser.d.ts +4 -0
  65. package/dist/form/form-parser.js +514 -0
  66. package/dist/form/form-serializer.d.ts +4 -0
  67. package/dist/form/form-serializer.js +240 -0
  68. package/dist/form/index.d.ts +27 -0
  69. package/dist/form/index.js +43 -0
  70. package/dist/index.d.ts +37 -0
  71. package/dist/index.js +19 -0
  72. package/dist/layout/bench.d.ts +86 -0
  73. package/dist/layout/bench.js +225 -0
  74. package/dist/layout/coordinates.d.ts +62 -0
  75. package/dist/layout/coordinates.js +868 -0
  76. package/dist/layout/crossing.d.ts +8 -0
  77. package/dist/layout/crossing.js +60 -0
  78. package/dist/layout/graph.d.ts +28 -0
  79. package/dist/layout/graph.js +126 -0
  80. package/dist/layout/index.d.ts +7 -0
  81. package/dist/layout/index.js +5 -0
  82. package/dist/layout/layers.d.ts +13 -0
  83. package/dist/layout/layers.js +49 -0
  84. package/dist/layout/layout-engine.d.ts +21 -0
  85. package/dist/layout/layout-engine.js +191 -0
  86. package/dist/layout/overlap.d.ts +10 -0
  87. package/dist/layout/overlap.js +70 -0
  88. package/dist/layout/routing.d.ts +26 -0
  89. package/dist/layout/routing.js +439 -0
  90. package/dist/layout/subprocess.d.ts +14 -0
  91. package/dist/layout/subprocess.js +77 -0
  92. package/dist/layout/types.d.ts +72 -0
  93. package/dist/layout/types.js +37 -0
  94. package/dist/types/id-generator.d.ts +8 -0
  95. package/dist/types/id-generator.js +31 -0
  96. package/dist/types/index.d.ts +3 -0
  97. package/dist/types/index.js +2 -0
  98. package/dist/types/xml-element.d.ts +12 -0
  99. package/dist/types/xml-element.js +2 -0
  100. package/dist/xml/index.d.ts +2 -0
  101. package/dist/xml/index.js +2 -0
  102. package/dist/xml/xml-parser.d.ts +13 -0
  103. package/dist/xml/xml-parser.js +271 -0
  104. package/package.json +38 -0
@@ -0,0 +1,186 @@
1
+ import { buildFlowIndex, findProcess, readZeebeIoMapping, readZeebeTaskType } from "./utils.js";
2
+ // ---------------------------------------------------------------------------
3
+ // FEEL complexity scoring
4
+ // ---------------------------------------------------------------------------
5
+ const FEEL_KEYWORD_OPERATORS = new Set([
6
+ "and",
7
+ "or",
8
+ "not",
9
+ "if",
10
+ "then",
11
+ "else",
12
+ "instance",
13
+ "of",
14
+ "some",
15
+ "every",
16
+ "satisfies",
17
+ "function",
18
+ ]);
19
+ function scoreFeelExpression(expr, opts) {
20
+ const length = expr.length;
21
+ // Nesting depth: track max depth of parentheses/brackets
22
+ let depth = 0;
23
+ let nestingDepth = 0;
24
+ for (const ch of expr) {
25
+ if (ch === "(" || ch === "[") {
26
+ depth++;
27
+ if (depth > nestingDepth)
28
+ nestingDepth = depth;
29
+ }
30
+ else if (ch === ")" || ch === "]") {
31
+ depth--;
32
+ }
33
+ }
34
+ // Tokenize on non-word boundaries; collect keywords and identifiers
35
+ const wordTokens = expr.split(/\W+/).filter(Boolean);
36
+ let operatorCount = 0;
37
+ const variables = new Set();
38
+ for (const token of wordTokens) {
39
+ if (FEEL_KEYWORD_OPERATORS.has(token)) {
40
+ operatorCount++;
41
+ }
42
+ else if (!/^\d/.test(token)) {
43
+ variables.add(token);
44
+ }
45
+ }
46
+ // Count symbol operators (order matters: longer patterns first)
47
+ const symbolMatches = expr.match(/!=|<=|>=|[+\-*/=<>]/g) ?? [];
48
+ operatorCount += symbolMatches.length;
49
+ const variableCount = variables.size;
50
+ const isComplex = length > opts.feelLengthThreshold ||
51
+ nestingDepth > opts.feelNestingThreshold ||
52
+ operatorCount > opts.feelOperatorThreshold ||
53
+ variableCount > opts.feelVariableThreshold;
54
+ return { length, nestingDepth, operatorCount, variableCount, isComplex };
55
+ }
56
+ // ---------------------------------------------------------------------------
57
+ // FEEL analyzer
58
+ // ---------------------------------------------------------------------------
59
+ export function analyzeFeel(p, opts) {
60
+ const findings = [];
61
+ const { bySource } = buildFlowIndex(p);
62
+ const processId = p.id;
63
+ // Track FEEL expressions across all sequence flows for duplicate detection
64
+ const exprToFlowIds = new Map();
65
+ for (const flow of p.sequenceFlows) {
66
+ // Determine if source is an exclusive/inclusive gateway
67
+ const srcEl = p.flowElements.find((e) => e.id === flow.sourceRef);
68
+ const srcIsDecisionGateway = srcEl !== undefined &&
69
+ (srcEl.type === "exclusiveGateway" || srcEl.type === "inclusiveGateway");
70
+ if (srcIsDecisionGateway && srcEl !== undefined) {
71
+ const defaultFlowId = srcEl.type === "exclusiveGateway" || srcEl.type === "inclusiveGateway"
72
+ ? srcEl.default
73
+ : undefined;
74
+ if (!flow.conditionExpression && flow.id !== defaultFlowId) {
75
+ findings.push({
76
+ id: "feel/empty-condition",
77
+ category: "feel",
78
+ severity: "error",
79
+ message: `Sequence flow "${flow.id}" exits a decision gateway without a condition expression.`,
80
+ suggestion: "Add a FEEL condition expression or mark this flow as the default.",
81
+ processId,
82
+ elementIds: [flow.id],
83
+ });
84
+ }
85
+ }
86
+ // Track FEEL expression for duplicate detection
87
+ if (flow.conditionExpression) {
88
+ const expr = flow.conditionExpression.text.trim();
89
+ if (expr.length > 0) {
90
+ const list = exprToFlowIds.get(expr) ?? [];
91
+ list.push(flow.id);
92
+ exprToFlowIds.set(expr, list);
93
+ }
94
+ // Check complexity
95
+ const score = scoreFeelExpression(flow.conditionExpression.text, opts);
96
+ if (score.isComplex) {
97
+ findings.push({
98
+ id: "feel/complex-condition",
99
+ category: "feel",
100
+ severity: "warning",
101
+ message: `Condition on flow "${flow.id}" is complex (length=${score.length}, nesting=${score.nestingDepth}, operators=${score.operatorCount}, variables=${score.variableCount}).`,
102
+ suggestion: "Consider extracting complex FEEL expressions into named variables or decision tables.",
103
+ processId,
104
+ elementIds: [flow.id],
105
+ });
106
+ }
107
+ }
108
+ }
109
+ // Check gateways: missing default flow
110
+ for (const el of p.flowElements) {
111
+ if (el.type !== "exclusiveGateway" && el.type !== "inclusiveGateway")
112
+ continue;
113
+ const outflows = bySource.get(el.id) ?? [];
114
+ if (outflows.length < 2)
115
+ continue;
116
+ if (!el.default) {
117
+ const lastFlowId = outflows[outflows.length - 1]?.id ?? "";
118
+ const gwId = el.id;
119
+ findings.push({
120
+ id: "feel/missing-default-flow",
121
+ category: "feel",
122
+ severity: "warning",
123
+ message: `Gateway "${el.id}" has ${outflows.length} outgoing flows but no default flow set.`,
124
+ suggestion: "Set a default flow to handle unmatched conditions.",
125
+ processId,
126
+ elementIds: [el.id],
127
+ applyFix: (defs) => {
128
+ const proc = findProcess(defs, processId);
129
+ if (!proc)
130
+ return { description: "Process not found" };
131
+ const gw = proc.flowElements.find((e) => e.id === gwId);
132
+ if (!gw)
133
+ return { description: "Gateway not found" };
134
+ if (gw.type === "exclusiveGateway" || gw.type === "inclusiveGateway") {
135
+ gw.default = lastFlowId;
136
+ }
137
+ return { description: `Set default flow to "${lastFlowId}" on gateway "${gwId}"` };
138
+ },
139
+ });
140
+ }
141
+ }
142
+ // Check service tasks: complex IO mapping
143
+ // Skip connector tasks — their ioMapping inputs are structured connector parameters
144
+ // (url, method, headers, body), not user-authored FEEL expressions.
145
+ const REST_CONNECTOR_TYPE = "io.camunda:http-json:1";
146
+ for (const el of p.flowElements) {
147
+ if (el.type !== "serviceTask")
148
+ continue;
149
+ if (readZeebeTaskType(el.extensionElements) === REST_CONNECTOR_TYPE)
150
+ continue;
151
+ const ioMapping = readZeebeIoMapping(el.extensionElements);
152
+ if (!ioMapping)
153
+ continue;
154
+ for (const input of ioMapping.inputs) {
155
+ const score = scoreFeelExpression(input.source, opts);
156
+ if (score.isComplex) {
157
+ findings.push({
158
+ id: "feel/complex-io-mapping",
159
+ category: "feel",
160
+ severity: "info",
161
+ message: `IO mapping input source on task "${el.id}" is complex.`,
162
+ suggestion: "Consider extracting complex FEEL expressions into intermediate variables.",
163
+ processId,
164
+ elementIds: [el.id],
165
+ });
166
+ break; // One finding per task
167
+ }
168
+ }
169
+ }
170
+ // Duplicate FEEL expressions
171
+ for (const [expr, flowIds] of exprToFlowIds) {
172
+ if (flowIds.length >= 2) {
173
+ findings.push({
174
+ id: "feel/duplicate-expression",
175
+ category: "feel",
176
+ severity: "info",
177
+ message: `FEEL expression "${expr.slice(0, 40)}" appears on ${flowIds.length} sequence flows.`,
178
+ suggestion: "Consider extracting repeated FEEL expressions into a shared variable or decision table.",
179
+ processId,
180
+ elementIds: flowIds,
181
+ });
182
+ }
183
+ }
184
+ return findings;
185
+ }
186
+ //# sourceMappingURL=feel.js.map
@@ -0,0 +1,4 @@
1
+ import type { BpmnProcess } from "../bpmn-model.js";
2
+ import type { OptimizationFinding, ResolvedOptions } from "./types.js";
3
+ export declare function analyzeFlow(p: BpmnProcess, _opts: ResolvedOptions): OptimizationFinding[];
4
+ //# sourceMappingURL=flow.d.ts.map
@@ -0,0 +1,323 @@
1
+ import { generateId } from "../../types/id-generator.js";
2
+ import { buildFlowIndex, findDiPlane, findProcess, insertElement, insertFlow, isEndEvent, isGateway, isStartEvent, reachableFrom, removeElement, removeFlow, } from "./utils.js";
3
+ export function analyzeFlow(p, _opts) {
4
+ const findings = [];
5
+ const processId = p.id;
6
+ const { bySource, byTarget } = buildFlowIndex(p);
7
+ // Start IDs for BFS reachability
8
+ const startIds = p.flowElements.filter((e) => isStartEvent(e.type)).map((e) => e.id);
9
+ // flow/no-start-event
10
+ const startEvents = p.flowElements.filter((e) => isStartEvent(e.type));
11
+ if (startEvents.length === 0) {
12
+ findings.push({
13
+ id: "flow/no-start-event",
14
+ category: "flow",
15
+ severity: "error",
16
+ message: `Process "${processId}" has no start event.`,
17
+ suggestion: "Add a start event to define where the process begins.",
18
+ processId,
19
+ elementIds: [],
20
+ });
21
+ }
22
+ // flow/no-end-event
23
+ const endEvents = p.flowElements.filter((e) => isEndEvent(e.type));
24
+ if (endEvents.length === 0) {
25
+ findings.push({
26
+ id: "flow/no-end-event",
27
+ category: "flow",
28
+ severity: "warning",
29
+ message: `Process "${processId}" has no end event.`,
30
+ suggestion: "Add an end event to properly terminate the process.",
31
+ processId,
32
+ elementIds: [],
33
+ });
34
+ }
35
+ // BFS reachability from start events
36
+ const reachable = reachableFrom(startIds, bySource);
37
+ for (const el of p.flowElements) {
38
+ // flow/unreachable
39
+ if (!reachable.has(el.id)) {
40
+ findings.push({
41
+ id: "flow/unreachable",
42
+ category: "flow",
43
+ severity: "error",
44
+ message: `Element "${el.id}" (${el.type}) is not reachable from any start event.`,
45
+ suggestion: "Connect this element to the process flow or remove it.",
46
+ processId,
47
+ elementIds: [el.id],
48
+ });
49
+ }
50
+ // flow/dead-end: non-end-event with 0 outgoing flows
51
+ const outflows = bySource.get(el.id) ?? [];
52
+ if (!isEndEvent(el.type) && outflows.length === 0) {
53
+ const deadEndId = el.id;
54
+ findings.push({
55
+ id: "flow/dead-end",
56
+ category: "flow",
57
+ severity: "warning",
58
+ message: `Element "${el.id}" (${el.type}) has no outgoing sequence flows.`,
59
+ suggestion: "Connect this element to a downstream element or an end event.",
60
+ processId,
61
+ elementIds: [el.id],
62
+ applyFix: (defs) => {
63
+ const proc = findProcess(defs, processId);
64
+ if (!proc)
65
+ return { description: "Process not found" };
66
+ const endEventId = generateId("EndEvent");
67
+ const flowId = generateId("Flow");
68
+ const endEvent = {
69
+ type: "endEvent",
70
+ id: endEventId,
71
+ incoming: [flowId],
72
+ outgoing: [],
73
+ extensionElements: [],
74
+ unknownAttributes: {},
75
+ eventDefinitions: [],
76
+ };
77
+ const newFlow = {
78
+ id: flowId,
79
+ sourceRef: deadEndId,
80
+ targetRef: endEventId,
81
+ extensionElements: [],
82
+ unknownAttributes: {},
83
+ };
84
+ // Update outgoing on dead-end element
85
+ const deadEl = proc.flowElements.find((e) => e.id === deadEndId);
86
+ if (deadEl) {
87
+ deadEl.outgoing.push(flowId);
88
+ }
89
+ insertElement(proc, endEvent);
90
+ insertFlow(proc, newFlow);
91
+ // Update diagram if plane exists
92
+ const plane = findDiPlane(defs, processId);
93
+ if (plane) {
94
+ const srcShape = plane.shapes.find((s) => s.bpmnElement === deadEndId);
95
+ const srcRight = srcShape ? srcShape.bounds.x + srcShape.bounds.width : 300;
96
+ const srcMidY = srcShape ? srcShape.bounds.y + srcShape.bounds.height / 2 : 220;
97
+ const endX = srcShape ? srcShape.bounds.x + srcShape.bounds.width + 150 : 450;
98
+ const endY = srcMidY - 18;
99
+ plane.shapes.push({
100
+ id: `${endEventId}_di`,
101
+ bpmnElement: endEventId,
102
+ bounds: { x: endX, y: endY, width: 36, height: 36 },
103
+ unknownAttributes: {},
104
+ });
105
+ plane.edges.push({
106
+ id: `${flowId}_di`,
107
+ bpmnElement: flowId,
108
+ waypoints: [
109
+ { x: srcRight, y: srcMidY },
110
+ { x: endX, y: srcMidY },
111
+ ],
112
+ unknownAttributes: {},
113
+ });
114
+ }
115
+ return { description: `Added end event "${endEventId}" connected from "${deadEndId}"` };
116
+ },
117
+ });
118
+ }
119
+ // flow/multi-incoming-task: non-gateway element with more than 1 incoming flow
120
+ const inflows = byTarget.get(el.id) ?? [];
121
+ if (!isGateway(el.type) && !isStartEvent(el.type) && inflows.length > 1) {
122
+ const elId = el.id;
123
+ const inflowCount = inflows.length;
124
+ findings.push({
125
+ id: "flow/multi-incoming-task",
126
+ category: "flow",
127
+ severity: "error",
128
+ message: `Element "${el.id}" (${el.type}) has ${inflows.length} incoming flows. Non-gateway elements must have at most one incoming flow.`,
129
+ suggestion: "Add an exclusive gateway before this element to join the incoming flows.",
130
+ processId,
131
+ elementIds: [el.id],
132
+ applyFix: (defs) => {
133
+ const proc = findProcess(defs, processId);
134
+ if (!proc)
135
+ return { description: "Process not found" };
136
+ const targetEl = proc.flowElements.find((e) => e.id === elId);
137
+ if (!targetEl)
138
+ return { description: "Element not found" };
139
+ const oldIncomingIds = [...targetEl.incoming];
140
+ const gatewayId = generateId("Gateway");
141
+ const joinFlowId = generateId("Flow");
142
+ // Redirect all existing incoming flows to the new gateway
143
+ for (const flowId of oldIncomingIds) {
144
+ const f = proc.sequenceFlows.find((sf) => sf.id === flowId);
145
+ if (f)
146
+ f.targetRef = gatewayId;
147
+ }
148
+ const gateway = {
149
+ type: "exclusiveGateway",
150
+ id: gatewayId,
151
+ incoming: oldIncomingIds,
152
+ outgoing: [joinFlowId],
153
+ extensionElements: [],
154
+ unknownAttributes: {},
155
+ };
156
+ const joinFlow = {
157
+ id: joinFlowId,
158
+ sourceRef: gatewayId,
159
+ targetRef: elId,
160
+ extensionElements: [],
161
+ unknownAttributes: {},
162
+ };
163
+ insertElement(proc, gateway);
164
+ insertFlow(proc, joinFlow);
165
+ targetEl.incoming = [joinFlowId];
166
+ // Add DI shape for the new gateway, positioned just before the target
167
+ const plane = findDiPlane(defs, processId);
168
+ if (plane) {
169
+ const tgtShape = plane.shapes.find((s) => s.bpmnElement === elId);
170
+ if (tgtShape) {
171
+ const gwWidth = 50;
172
+ const gwHeight = 50;
173
+ const gwX = tgtShape.bounds.x - 100;
174
+ const gwY = tgtShape.bounds.y + (tgtShape.bounds.height - gwHeight) / 2;
175
+ plane.shapes.push({
176
+ id: `${gatewayId}_di`,
177
+ bpmnElement: gatewayId,
178
+ bounds: { x: gwX, y: gwY, width: gwWidth, height: gwHeight },
179
+ unknownAttributes: {},
180
+ });
181
+ for (const inFlowId of oldIncomingIds) {
182
+ const edge = plane.edges.find((e) => e.bpmnElement === inFlowId);
183
+ if (edge && edge.waypoints.length > 0) {
184
+ const last = edge.waypoints[edge.waypoints.length - 1];
185
+ if (last) {
186
+ last.x = gwX + gwWidth / 2;
187
+ last.y = gwY + gwHeight / 2;
188
+ }
189
+ }
190
+ }
191
+ plane.edges.push({
192
+ id: `${joinFlowId}_di`,
193
+ bpmnElement: joinFlowId,
194
+ waypoints: [
195
+ { x: gwX + gwWidth, y: gwY + gwHeight / 2 },
196
+ { x: tgtShape.bounds.x, y: tgtShape.bounds.y + tgtShape.bounds.height / 2 },
197
+ ],
198
+ unknownAttributes: {},
199
+ });
200
+ }
201
+ }
202
+ return {
203
+ description: `Inserted exclusive gateway "${gatewayId}" before "${elId}" to join ${inflowCount} incoming flows`,
204
+ };
205
+ },
206
+ });
207
+ }
208
+ // flow/redundant-gateway: gateway with exactly 1 incoming AND 1 outgoing
209
+ if (isGateway(el.type)) {
210
+ const inflows = byTarget.get(el.id) ?? [];
211
+ if (inflows.length === 1 && outflows.length === 1) {
212
+ const gwId = el.id;
213
+ const inFlowId = inflows[0]?.id ?? "";
214
+ const outFlowId = outflows[0]?.id ?? "";
215
+ findings.push({
216
+ id: "flow/redundant-gateway",
217
+ category: "flow",
218
+ severity: "info",
219
+ message: `Gateway "${el.id}" (${el.type}) has only 1 incoming and 1 outgoing flow — it is redundant.`,
220
+ suggestion: "Remove this gateway and connect its source directly to its target.",
221
+ processId,
222
+ elementIds: [el.id],
223
+ applyFix: (defs) => {
224
+ const proc = findProcess(defs, processId);
225
+ if (!proc)
226
+ return { description: "Process not found" };
227
+ // Find the two flows
228
+ const inFlow = proc.sequenceFlows.find((f) => f.id === inFlowId);
229
+ const outFlow = proc.sequenceFlows.find((f) => f.id === outFlowId);
230
+ if (!inFlow || !outFlow)
231
+ return { description: "Flows not found" };
232
+ const srcId = inFlow.sourceRef;
233
+ const tgtId = outFlow.targetRef;
234
+ const newFlowId = generateId("Flow");
235
+ // Remove gateway and its two flows
236
+ removeElement(proc, gwId);
237
+ removeFlow(proc, inFlowId);
238
+ removeFlow(proc, outFlowId);
239
+ // Update outgoing/incoming on connected elements
240
+ const srcEl = proc.flowElements.find((e) => e.id === srcId);
241
+ if (srcEl) {
242
+ srcEl.outgoing = srcEl.outgoing.filter((id) => id !== inFlowId);
243
+ srcEl.outgoing.push(newFlowId);
244
+ }
245
+ const tgtEl = proc.flowElements.find((e) => e.id === tgtId);
246
+ if (tgtEl) {
247
+ tgtEl.incoming = tgtEl.incoming.filter((id) => id !== outFlowId);
248
+ tgtEl.incoming.push(newFlowId);
249
+ }
250
+ const newFlow = {
251
+ id: newFlowId,
252
+ sourceRef: srcId,
253
+ targetRef: tgtId,
254
+ extensionElements: [],
255
+ unknownAttributes: {},
256
+ };
257
+ insertFlow(proc, newFlow);
258
+ // Update diagram
259
+ const plane = findDiPlane(defs, processId);
260
+ if (plane) {
261
+ plane.shapes = plane.shapes.filter((s) => s.bpmnElement !== gwId);
262
+ plane.edges = plane.edges.filter((e) => e.bpmnElement !== inFlowId && e.bpmnElement !== outFlowId);
263
+ const srcShape = plane.shapes.find((s) => s.bpmnElement === srcId);
264
+ const tgtShape = plane.shapes.find((s) => s.bpmnElement === tgtId);
265
+ if (srcShape && tgtShape) {
266
+ plane.edges.push({
267
+ id: `${newFlowId}_di`,
268
+ bpmnElement: newFlowId,
269
+ waypoints: [
270
+ {
271
+ x: srcShape.bounds.x + srcShape.bounds.width,
272
+ y: srcShape.bounds.y + srcShape.bounds.height / 2,
273
+ },
274
+ {
275
+ x: tgtShape.bounds.x,
276
+ y: tgtShape.bounds.y + tgtShape.bounds.height / 2,
277
+ },
278
+ ],
279
+ unknownAttributes: {},
280
+ });
281
+ }
282
+ }
283
+ return {
284
+ description: `Removed redundant gateway "${gwId}" and connected "${srcId}" → "${tgtId}"`,
285
+ };
286
+ },
287
+ });
288
+ }
289
+ }
290
+ // flow/mixed-gateway: gateway acting as both split AND join simultaneously
291
+ if (isGateway(el.type)) {
292
+ const gwInflows = byTarget.get(el.id) ?? [];
293
+ const gwOutflows = bySource.get(el.id) ?? [];
294
+ if (gwInflows.length >= 2 && gwOutflows.length >= 2) {
295
+ findings.push({
296
+ id: "flow/mixed-gateway",
297
+ category: "flow",
298
+ severity: "warning",
299
+ message: `Gateway "${el.id}" (${el.type}) has ${gwInflows.length} incoming and ${gwOutflows.length} outgoing flows — it acts as both a join and a split simultaneously.`,
300
+ suggestion: "Separate into two gateways: one join (merging incoming flows) followed by one split (branching outgoing flows).",
301
+ processId,
302
+ elementIds: [el.id],
303
+ });
304
+ }
305
+ }
306
+ // flow/empty-subprocess
307
+ if (el.type === "subProcess" || el.type === "adHocSubProcess" || el.type === "transaction") {
308
+ if (el.flowElements.length === 0) {
309
+ findings.push({
310
+ id: "flow/empty-subprocess",
311
+ category: "flow",
312
+ severity: "warning",
313
+ message: `Sub-process "${el.id}" (${el.type}) contains no flow elements.`,
314
+ suggestion: "Add flow elements inside the sub-process or remove it.",
315
+ processId,
316
+ elementIds: [el.id],
317
+ });
318
+ }
319
+ }
320
+ }
321
+ return findings;
322
+ }
323
+ //# sourceMappingURL=flow.js.map
@@ -0,0 +1,5 @@
1
+ import type { BpmnDefinitions } from "../bpmn-model.js";
2
+ import type { OptimizationReport, OptimizeOptions } from "./types.js";
3
+ /** Run static analysis on a BPMN definitions object. */
4
+ export declare function optimize(defs: BpmnDefinitions, options?: OptimizeOptions): OptimizationReport;
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,51 @@
1
+ import { analyzeFeel } from "./feel.js";
2
+ import { analyzeFlow } from "./flow.js";
3
+ import { analyzeNaming } from "./naming.js";
4
+ import { analyzeTasks } from "./tasks.js";
5
+ const ALL_CATEGORIES = ["feel", "flow", "naming", "task-reuse", "extract"];
6
+ function resolveOptions(opts) {
7
+ return {
8
+ feelLengthThreshold: opts?.feelLengthThreshold ?? 80,
9
+ feelNestingThreshold: opts?.feelNestingThreshold ?? 3,
10
+ feelOperatorThreshold: opts?.feelOperatorThreshold ?? 5,
11
+ feelVariableThreshold: opts?.feelVariableThreshold ?? 4,
12
+ reuseThreshold: opts?.reuseThreshold ?? 2,
13
+ categories: opts?.categories ?? [...ALL_CATEGORIES],
14
+ };
15
+ }
16
+ /** Run static analysis on a BPMN definitions object. */
17
+ export function optimize(defs, options) {
18
+ const resolved = resolveOptions(options);
19
+ const findings = [];
20
+ for (const process of defs.processes) {
21
+ if (resolved.categories.includes("feel")) {
22
+ findings.push(...analyzeFeel(process, resolved));
23
+ }
24
+ if (resolved.categories.includes("flow")) {
25
+ findings.push(...analyzeFlow(process, resolved));
26
+ }
27
+ if (resolved.categories.includes("naming")) {
28
+ findings.push(...analyzeNaming(process, resolved));
29
+ }
30
+ if (resolved.categories.includes("task-reuse")) {
31
+ findings.push(...analyzeTasks(process, resolved));
32
+ }
33
+ }
34
+ const byCategory = Object.fromEntries(["feel", "flow", "naming", "task-reuse", "extract"].map((c) => [
35
+ c,
36
+ findings.filter((f) => f.category === c).length,
37
+ ]));
38
+ const bySeverity = Object.fromEntries(["info", "warning", "error"].map((s) => [
39
+ s,
40
+ findings.filter((f) => f.severity === s).length,
41
+ ]));
42
+ return {
43
+ findings,
44
+ summary: {
45
+ total: findings.length,
46
+ byCategory,
47
+ bySeverity,
48
+ },
49
+ };
50
+ }
51
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,8 @@
1
+ import type { BpmnProcess } from "../bpmn-model.js";
2
+ import type { OptimizationFinding, ResolvedOptions } from "./types.js";
3
+ /**
4
+ * Analyze BPMN naming conventions based on Camunda best practices:
5
+ * https://docs.camunda.io/docs/components/best-practices/modeling/naming-bpmnkit-elements/
6
+ */
7
+ export declare function analyzeNaming(p: BpmnProcess, _opts: ResolvedOptions): OptimizationFinding[];
8
+ //# sourceMappingURL=naming.d.ts.map