@bpmnkit/core 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -1
- package/dist/bpmn/agentic.d.ts +121 -0
- package/dist/bpmn/agentic.js +97 -0
- package/dist/bpmn/auto-layout.d.ts +5 -5
- package/dist/bpmn/auto-layout.js +592 -36
- package/dist/bpmn/bpmn-builder.d.ts +265 -3
- package/dist/bpmn/bpmn-builder.js +603 -197
- package/dist/bpmn/bpmn-model.d.ts +114 -0
- package/dist/bpmn/bpmn-parser.js +1414 -521
- package/dist/bpmn/bpmn-serializer.js +107 -19
- package/dist/bpmn/compact.d.ts +17 -2
- package/dist/bpmn/compact.js +3 -3
- package/dist/bpmn/full-operations.d.ts +89 -0
- package/dist/bpmn/full-operations.js +478 -0
- package/dist/bpmn/index.d.ts +19 -0
- package/dist/bpmn/index.js +21 -0
- package/dist/bpmn/optimize/agentic.d.ts +10 -0
- package/dist/bpmn/optimize/agentic.js +88 -0
- package/dist/bpmn/optimize/deploy.d.ts +16 -0
- package/dist/bpmn/optimize/deploy.js +143 -0
- package/dist/bpmn/optimize/feel-syntax.d.ts +12 -0
- package/dist/bpmn/optimize/feel-syntax.js +87 -0
- package/dist/bpmn/optimize/feel.js +7 -4
- package/dist/bpmn/optimize/flow.js +22 -2
- package/dist/bpmn/optimize/index.js +20 -9
- package/dist/bpmn/optimize/patterns.js +23 -16
- package/dist/bpmn/optimize/tasks.js +30 -7
- package/dist/bpmn/optimize/types.d.ts +10 -1
- package/dist/bpmn/optimize/utils.js +2 -4
- package/dist/bpmn/optimize/variable-flow.js +58 -67
- package/dist/bpmn/semantic-hash.d.ts +93 -0
- package/dist/bpmn/semantic-hash.js +155 -0
- package/dist/bpmn/sha256.d.ts +17 -0
- package/dist/bpmn/sha256.js +95 -0
- package/dist/bpmn/zeebe-extensions.d.ts +83 -0
- package/dist/bpmn/zeebe-extensions.js +117 -0
- package/dist/bpmn/zeebe-placement.d.ts +12 -0
- package/dist/bpmn/zeebe-placement.js +140 -0
- package/dist/errors.d.ts +40 -1
- package/dist/errors.js +41 -0
- package/dist/index.d.ts +16 -5
- package/dist/index.js +9 -3
- package/dist/layout/annotations.js +36 -1
- package/dist/layout/collaboration/alignment.d.ts +26 -0
- package/dist/layout/collaboration/alignment.js +66 -0
- package/dist/layout/collaboration/ordering.d.ts +21 -0
- package/dist/layout/collaboration/ordering.js +102 -0
- package/dist/layout/index.d.ts +1 -0
- package/dist/layout/layout-engine.d.ts +13 -3
- package/dist/layout/layout-engine.js +9 -4
- package/dist/layout/semantic/bands.d.ts +19 -0
- package/dist/layout/semantic/bands.js +324 -0
- package/dist/layout/semantic/graph.d.ts +37 -0
- package/dist/layout/semantic/graph.js +242 -0
- package/dist/layout/semantic/index.d.ts +13 -0
- package/dist/layout/semantic/index.js +181 -0
- package/dist/layout/semantic/place.d.ts +40 -0
- package/dist/layout/semantic/place.js +271 -0
- package/dist/layout/semantic/route.d.ts +14 -0
- package/dist/layout/semantic/route.js +514 -0
- package/dist/layout/types.d.ts +17 -0
- package/dist/node/index.d.ts +10 -0
- package/dist/node/index.js +9 -0
- package/dist/node/write.d.ts +81 -0
- package/dist/node/write.js +167 -0
- package/dist/plan/compile.d.ts +39 -0
- package/dist/plan/compile.js +380 -0
- package/dist/plan/extract.d.ts +31 -0
- package/dist/plan/extract.js +248 -0
- package/dist/plan/index.d.ts +6 -0
- package/dist/plan/index.js +5 -0
- package/dist/plan/merge.d.ts +13 -0
- package/dist/plan/merge.js +80 -0
- package/dist/plan/slug.d.ts +5 -0
- package/dist/plan/slug.js +22 -0
- package/dist/plan/types.d.ts +225 -0
- package/dist/plan/types.js +13 -0
- package/dist/types/id-generator.js +11 -3
- package/dist/xml/index.d.ts +3 -1
- package/dist/xml/index.js +2 -1
- package/dist/xml/xml-parser.d.ts +32 -0
- package/dist/xml/xml-parser.js +394 -143
- package/package.json +9 -2
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { readZeebeIoMapping, readZeebeTaskHeaders, readZeebeTaskType } from "./utils.js";
|
|
2
|
+
function findExt(el, name) {
|
|
3
|
+
return el.extensionElements.find((e) => e.name === name);
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Zeebe/Reebe deploy-parity checks — mirrors what Camunda 8 rejects at
|
|
7
|
+
* deployment time (`apps/reebe/crates/reebe-bpmn/src/validator.rs`), so a
|
|
8
|
+
* process that passes this profile deploys without a validation error.
|
|
9
|
+
*/
|
|
10
|
+
export function analyzeDeploy(p, resolveConnectorRequirements) {
|
|
11
|
+
const findings = [];
|
|
12
|
+
const processId = p.id;
|
|
13
|
+
if (p.isExecutable !== true) {
|
|
14
|
+
findings.push({
|
|
15
|
+
id: "deploy/process-not-executable",
|
|
16
|
+
category: "deploy",
|
|
17
|
+
severity: "error",
|
|
18
|
+
message: `Process "${processId}" is not marked executable.`,
|
|
19
|
+
suggestion: 'Set isExecutable="true" — Camunda 8 refuses to deploy a non-executable process.',
|
|
20
|
+
processId,
|
|
21
|
+
elementIds: [],
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
for (const el of p.flowElements) {
|
|
25
|
+
if (el.type === "serviceTask" || el.type === "sendTask") {
|
|
26
|
+
const type = readZeebeTaskType(el.extensionElements);
|
|
27
|
+
if (!type) {
|
|
28
|
+
findings.push({
|
|
29
|
+
id: "deploy/service-task-no-type",
|
|
30
|
+
category: "deploy",
|
|
31
|
+
severity: "error",
|
|
32
|
+
message: `"${el.name ?? el.id}" (${el.type}) has no zeebe:taskDefinition type.`,
|
|
33
|
+
suggestion: "Set a job type — Camunda 8 refuses to deploy a task with no task definition.",
|
|
34
|
+
processId,
|
|
35
|
+
elementIds: [el.id],
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (el.type === "businessRuleTask") {
|
|
40
|
+
const hasType = readZeebeTaskType(el.extensionElements) !== null;
|
|
41
|
+
const hasDecision = findExt(el, "zeebe:calledDecision") !== undefined;
|
|
42
|
+
if (!hasType && !hasDecision) {
|
|
43
|
+
findings.push({
|
|
44
|
+
id: "deploy/service-task-no-type",
|
|
45
|
+
category: "deploy",
|
|
46
|
+
severity: "error",
|
|
47
|
+
message: `"${el.name ?? el.id}" (businessRuleTask) has neither a zeebe:taskDefinition type nor a zeebe:calledDecision.`,
|
|
48
|
+
suggestion: "Set a decisionId or a job type.",
|
|
49
|
+
processId,
|
|
50
|
+
elementIds: [el.id],
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (el.type === "callActivity") {
|
|
55
|
+
const called = findExt(el, "zeebe:calledElement");
|
|
56
|
+
if (!called?.attributes.processId) {
|
|
57
|
+
findings.push({
|
|
58
|
+
id: "deploy/call-activity-no-process",
|
|
59
|
+
category: "deploy",
|
|
60
|
+
severity: "error",
|
|
61
|
+
message: `Call activity "${el.name ?? el.id}" has no zeebe:calledElement processId.`,
|
|
62
|
+
suggestion: "Set the process id to call.",
|
|
63
|
+
processId,
|
|
64
|
+
elementIds: [el.id],
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (el.type === "startEvent") {
|
|
69
|
+
const messageDef = el.eventDefinitions.find((d) => d.type === "message");
|
|
70
|
+
if (messageDef && !messageDef.messageRef) {
|
|
71
|
+
findings.push({
|
|
72
|
+
id: "deploy/message-start-no-name",
|
|
73
|
+
category: "deploy",
|
|
74
|
+
severity: "error",
|
|
75
|
+
message: `Message start event "${el.name ?? el.id}" has no message name.`,
|
|
76
|
+
suggestion: "Set a message name — Camunda 8 refuses to deploy an unnamed message reference.",
|
|
77
|
+
processId,
|
|
78
|
+
elementIds: [el.id],
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (el.type === "intermediateCatchEvent" ||
|
|
83
|
+
el.type === "boundaryEvent" ||
|
|
84
|
+
el.type === "receiveTask") {
|
|
85
|
+
const messageDef = el.type === "receiveTask"
|
|
86
|
+
? undefined
|
|
87
|
+
: el.eventDefinitions.find((d) => d.type === "message");
|
|
88
|
+
const isMessageCatch = el.type === "receiveTask" ? el.messageRef !== undefined : messageDef !== undefined;
|
|
89
|
+
if (isMessageCatch) {
|
|
90
|
+
const subscription = findExt(el, "zeebe:subscription");
|
|
91
|
+
if (!subscription?.attributes.correlationKey) {
|
|
92
|
+
findings.push({
|
|
93
|
+
id: "deploy/message-catch-no-correlation",
|
|
94
|
+
category: "deploy",
|
|
95
|
+
severity: "error",
|
|
96
|
+
message: `Message catch "${el.name ?? el.id}" (${el.type}) has no zeebe:subscription correlationKey.`,
|
|
97
|
+
suggestion: "Set a correlation key — required for every message catch in Camunda 8.",
|
|
98
|
+
processId,
|
|
99
|
+
elementIds: [el.id],
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (resolveConnectorRequirements) {
|
|
105
|
+
const templateId = el.unknownAttributes["zeebe:modelerTemplate"];
|
|
106
|
+
if (templateId) {
|
|
107
|
+
const boundKeys = new Set();
|
|
108
|
+
const io = readZeebeIoMapping(el.extensionElements);
|
|
109
|
+
if (io) {
|
|
110
|
+
for (const i of io.inputs)
|
|
111
|
+
boundKeys.add(i.target);
|
|
112
|
+
for (const o of io.outputs)
|
|
113
|
+
boundKeys.add(o.target);
|
|
114
|
+
}
|
|
115
|
+
const headers = readZeebeTaskHeaders(el.extensionElements);
|
|
116
|
+
if (headers)
|
|
117
|
+
for (const h of headers.headers)
|
|
118
|
+
boundKeys.add(h.key);
|
|
119
|
+
const propsExt = findExt(el, "zeebe:properties");
|
|
120
|
+
if (propsExt) {
|
|
121
|
+
for (const child of propsExt.children ?? []) {
|
|
122
|
+
if (child.attributes.name)
|
|
123
|
+
boundKeys.add(child.attributes.name);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
const missing = resolveConnectorRequirements(templateId, [...boundKeys]);
|
|
127
|
+
for (const key of missing) {
|
|
128
|
+
findings.push({
|
|
129
|
+
id: "connector/missing-required",
|
|
130
|
+
category: "connector",
|
|
131
|
+
severity: "error",
|
|
132
|
+
message: `"${el.name ?? el.id}" is missing required connector value "${key}" for template "${templateId}".`,
|
|
133
|
+
suggestion: `Set a value for "${key}".`,
|
|
134
|
+
processId,
|
|
135
|
+
elementIds: [el.id],
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return findings;
|
|
142
|
+
}
|
|
143
|
+
//# sourceMappingURL=deploy.js.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { BpmnProcess } from "../bpmn-model.js";
|
|
2
|
+
import type { OptimizationFinding } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Parse-validates every FEEL-looking expression (leading "=") in the process:
|
|
5
|
+
* sequence-flow conditions, zeebe:input/output sources, script task
|
|
6
|
+
* expressions, and ad-hoc sub-process completion conditions/outputElement —
|
|
7
|
+
* including inside nested sub-processes. Unlike `feel.ts` (heuristic
|
|
8
|
+
* complexity scoring), this uses the real `@bpmnkit/feel` parser and reports
|
|
9
|
+
* genuine syntax errors, not style suggestions.
|
|
10
|
+
*/
|
|
11
|
+
export declare function analyzeFeelSyntax(p: BpmnProcess): OptimizationFinding[];
|
|
12
|
+
//# sourceMappingURL=feel-syntax.d.ts.map
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { parseExpression } from "@bpmnkit/feel";
|
|
2
|
+
import { readZeebeIoMapping } from "./utils.js";
|
|
3
|
+
/** Recursively yields every flow element in a process, including inside sub-processes/ad-hoc sub-processes. */
|
|
4
|
+
function* walkElements(elements) {
|
|
5
|
+
for (const el of elements) {
|
|
6
|
+
yield el;
|
|
7
|
+
if (el.type === "subProcess" ||
|
|
8
|
+
el.type === "adHocSubProcess" ||
|
|
9
|
+
el.type === "eventSubProcess" ||
|
|
10
|
+
el.type === "transaction") {
|
|
11
|
+
yield* walkElements(el.flowElements);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
/** Recursively yields every sequence flow, including inside sub-processes/ad-hoc sub-processes. */
|
|
16
|
+
function* walkFlows(elements, topLevel) {
|
|
17
|
+
yield* topLevel;
|
|
18
|
+
for (const el of elements) {
|
|
19
|
+
if (el.type === "subProcess" ||
|
|
20
|
+
el.type === "adHocSubProcess" ||
|
|
21
|
+
el.type === "eventSubProcess" ||
|
|
22
|
+
el.type === "transaction") {
|
|
23
|
+
yield* walkFlows(el.flowElements, el.sequenceFlows);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function checkFeel(text, elementId, processId, surface, findings) {
|
|
28
|
+
const trimmed = text.trim();
|
|
29
|
+
if (!trimmed.startsWith("="))
|
|
30
|
+
return;
|
|
31
|
+
const { errors } = parseExpression(trimmed.slice(1));
|
|
32
|
+
for (const err of errors) {
|
|
33
|
+
findings.push({
|
|
34
|
+
id: "feel-syntax/parse-error",
|
|
35
|
+
category: "feel-syntax",
|
|
36
|
+
severity: "error",
|
|
37
|
+
message: `Invalid FEEL expression on ${surface} of "${elementId}": ${err.message}`,
|
|
38
|
+
suggestion: "Fix the FEEL syntax — see the error position for the offending token.",
|
|
39
|
+
processId,
|
|
40
|
+
elementIds: [elementId],
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Parse-validates every FEEL-looking expression (leading "=") in the process:
|
|
46
|
+
* sequence-flow conditions, zeebe:input/output sources, script task
|
|
47
|
+
* expressions, and ad-hoc sub-process completion conditions/outputElement —
|
|
48
|
+
* including inside nested sub-processes. Unlike `feel.ts` (heuristic
|
|
49
|
+
* complexity scoring), this uses the real `@bpmnkit/feel` parser and reports
|
|
50
|
+
* genuine syntax errors, not style suggestions.
|
|
51
|
+
*/
|
|
52
|
+
export function analyzeFeelSyntax(p) {
|
|
53
|
+
const findings = [];
|
|
54
|
+
const processId = p.id;
|
|
55
|
+
for (const flow of walkFlows(p.flowElements, p.sequenceFlows)) {
|
|
56
|
+
if (flow.conditionExpression?.text) {
|
|
57
|
+
checkFeel(flow.conditionExpression.text, flow.id, processId, "condition", findings);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
for (const el of walkElements(p.flowElements)) {
|
|
61
|
+
const io = readZeebeIoMapping(el.extensionElements);
|
|
62
|
+
if (io) {
|
|
63
|
+
for (const input of io.inputs)
|
|
64
|
+
checkFeel(input.source, el.id, processId, `input "${input.target}"`, findings);
|
|
65
|
+
for (const output of io.outputs)
|
|
66
|
+
checkFeel(output.source, el.id, processId, `output "${output.target}"`, findings);
|
|
67
|
+
}
|
|
68
|
+
const scriptExt = el.extensionElements.find((e) => e.name === "zeebe:script");
|
|
69
|
+
if (scriptExt?.attributes.expression) {
|
|
70
|
+
checkFeel(scriptExt.attributes.expression, el.id, processId, "script expression", findings);
|
|
71
|
+
}
|
|
72
|
+
if (el.type === "adHocSubProcess") {
|
|
73
|
+
if (el.completionCondition?.text) {
|
|
74
|
+
checkFeel(el.completionCondition.text, el.id, processId, "completion condition", findings);
|
|
75
|
+
}
|
|
76
|
+
const adHocExt = el.extensionElements.find((e) => e.name === "zeebe:adHoc");
|
|
77
|
+
if (adHocExt?.attributes.outputElement) {
|
|
78
|
+
checkFeel(adHocExt.attributes.outputElement, el.id, processId, "outputElement", findings);
|
|
79
|
+
}
|
|
80
|
+
if (adHocExt?.attributes.activeElementsCollection) {
|
|
81
|
+
checkFeel(adHocExt.attributes.activeElementsCollection, el.id, processId, "activeElementsCollection", findings);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return findings;
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=feel-syntax.js.map
|
|
@@ -58,15 +58,18 @@ function scoreFeelExpression(expr, opts) {
|
|
|
58
58
|
// ---------------------------------------------------------------------------
|
|
59
59
|
export function analyzeFeel(p, opts) {
|
|
60
60
|
const findings = [];
|
|
61
|
-
const { bySource } = buildFlowIndex(p);
|
|
61
|
+
const { byId, bySource } = buildFlowIndex(p);
|
|
62
62
|
const processId = p.id;
|
|
63
63
|
// Track FEEL expressions across all sequence flows for duplicate detection
|
|
64
64
|
const exprToFlowIds = new Map();
|
|
65
65
|
for (const flow of p.sequenceFlows) {
|
|
66
|
-
// Determine if source is an exclusive/inclusive gateway
|
|
67
|
-
|
|
66
|
+
// Determine if source is an exclusive/inclusive gateway with more than one
|
|
67
|
+
// outgoing flow — a join (single outgoing flow) makes no decision, so its
|
|
68
|
+
// one outgoing flow needs neither a condition nor a default marker.
|
|
69
|
+
const srcEl = byId.get(flow.sourceRef);
|
|
68
70
|
const srcIsDecisionGateway = srcEl !== undefined &&
|
|
69
|
-
(srcEl.type === "exclusiveGateway" || srcEl.type === "inclusiveGateway")
|
|
71
|
+
(srcEl.type === "exclusiveGateway" || srcEl.type === "inclusiveGateway") &&
|
|
72
|
+
(bySource.get(srcEl.id)?.length ?? 0) > 1;
|
|
70
73
|
if (srcIsDecisionGateway && srcEl !== undefined) {
|
|
71
74
|
const defaultFlowId = srcEl.type === "exclusiveGateway" || srcEl.type === "inclusiveGateway"
|
|
72
75
|
? srcEl.default
|
|
@@ -32,8 +32,28 @@ export function analyzeFlow(p, _opts) {
|
|
|
32
32
|
elementIds: [],
|
|
33
33
|
});
|
|
34
34
|
}
|
|
35
|
-
// BFS reachability from start events
|
|
36
|
-
|
|
35
|
+
// BFS reachability from start events. Boundary events have no incoming
|
|
36
|
+
// sequence flow by design (they attach via `attachedToRef`), so add a
|
|
37
|
+
// synthetic edge from each host element to its boundary event(s) —
|
|
38
|
+
// otherwise every boundary event (and everything chained after it) would
|
|
39
|
+
// always be flagged as unreachable.
|
|
40
|
+
const reachabilityEdges = new Map(bySource);
|
|
41
|
+
for (const el of p.flowElements) {
|
|
42
|
+
if (el.type !== "boundaryEvent")
|
|
43
|
+
continue;
|
|
44
|
+
const hostEdges = reachabilityEdges.get(el.attachedToRef) ?? [];
|
|
45
|
+
reachabilityEdges.set(el.attachedToRef, [
|
|
46
|
+
...hostEdges,
|
|
47
|
+
{
|
|
48
|
+
id: `synthetic-attachment-${el.id}`,
|
|
49
|
+
sourceRef: el.attachedToRef,
|
|
50
|
+
targetRef: el.id,
|
|
51
|
+
extensionElements: [],
|
|
52
|
+
unknownAttributes: {},
|
|
53
|
+
},
|
|
54
|
+
]);
|
|
55
|
+
}
|
|
56
|
+
const reachable = reachableFrom(startIds, reachabilityEdges);
|
|
37
57
|
for (const el of p.flowElements) {
|
|
38
58
|
// flow/unreachable
|
|
39
59
|
if (!reachable.has(el.id)) {
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { analyzeAgentic } from "./agentic.js";
|
|
2
|
+
import { analyzeDeploy } from "./deploy.js";
|
|
3
|
+
import { analyzeFeelSyntax } from "./feel-syntax.js";
|
|
1
4
|
import { analyzeFeel } from "./feel.js";
|
|
2
5
|
import { analyzeFlow } from "./flow.js";
|
|
3
6
|
import { analyzeNaming } from "./naming.js";
|
|
@@ -6,12 +9,16 @@ import { analyzeTasks } from "./tasks.js";
|
|
|
6
9
|
import { analyzeVariableFlow } from "./variable-flow.js";
|
|
7
10
|
const ALL_CATEGORIES = [
|
|
8
11
|
"feel",
|
|
12
|
+
"feel-syntax",
|
|
9
13
|
"flow",
|
|
10
14
|
"naming",
|
|
11
15
|
"task-reuse",
|
|
12
16
|
"extract",
|
|
13
17
|
"pattern",
|
|
14
18
|
"data-flow",
|
|
19
|
+
"deploy",
|
|
20
|
+
"agentic",
|
|
21
|
+
"connector",
|
|
15
22
|
];
|
|
16
23
|
function resolveOptions(opts) {
|
|
17
24
|
return {
|
|
@@ -21,6 +28,7 @@ function resolveOptions(opts) {
|
|
|
21
28
|
feelVariableThreshold: opts?.feelVariableThreshold ?? 4,
|
|
22
29
|
reuseThreshold: opts?.reuseThreshold ?? 2,
|
|
23
30
|
categories: opts?.categories ?? [...ALL_CATEGORIES],
|
|
31
|
+
resolveConnectorRequirements: opts?.resolveConnectorRequirements,
|
|
24
32
|
};
|
|
25
33
|
}
|
|
26
34
|
/** Run static analysis on a BPMN definitions object. */
|
|
@@ -31,6 +39,9 @@ export function optimize(defs, options) {
|
|
|
31
39
|
if (resolved.categories.includes("feel")) {
|
|
32
40
|
findings.push(...analyzeFeel(process, resolved));
|
|
33
41
|
}
|
|
42
|
+
if (resolved.categories.includes("feel-syntax")) {
|
|
43
|
+
findings.push(...analyzeFeelSyntax(process));
|
|
44
|
+
}
|
|
34
45
|
if (resolved.categories.includes("flow")) {
|
|
35
46
|
findings.push(...analyzeFlow(process, resolved));
|
|
36
47
|
}
|
|
@@ -46,16 +57,16 @@ export function optimize(defs, options) {
|
|
|
46
57
|
if (resolved.categories.includes("data-flow")) {
|
|
47
58
|
findings.push(...analyzeVariableFlow(process));
|
|
48
59
|
}
|
|
60
|
+
if (resolved.categories.includes("deploy") || resolved.categories.includes("connector")) {
|
|
61
|
+
const deployFindings = analyzeDeploy(process, resolved.resolveConnectorRequirements);
|
|
62
|
+
findings.push(...deployFindings.filter((f) => (f.category === "deploy" && resolved.categories.includes("deploy")) ||
|
|
63
|
+
(f.category === "connector" && resolved.categories.includes("connector"))));
|
|
64
|
+
}
|
|
65
|
+
if (resolved.categories.includes("agentic")) {
|
|
66
|
+
findings.push(...analyzeAgentic(process));
|
|
67
|
+
}
|
|
49
68
|
}
|
|
50
|
-
const byCategory = Object.fromEntries([
|
|
51
|
-
"feel",
|
|
52
|
-
"flow",
|
|
53
|
-
"naming",
|
|
54
|
-
"task-reuse",
|
|
55
|
-
"extract",
|
|
56
|
-
"pattern",
|
|
57
|
-
"data-flow",
|
|
58
|
-
].map((c) => [c, findings.filter((f) => f.category === c).length]));
|
|
69
|
+
const byCategory = Object.fromEntries(ALL_CATEGORIES.map((c) => [c, findings.filter((f) => f.category === c).length]));
|
|
59
70
|
const bySeverity = Object.fromEntries(["info", "warning", "error"].map((s) => [
|
|
60
71
|
s,
|
|
61
72
|
findings.filter((f) => f.severity === s).length,
|
|
@@ -2,19 +2,25 @@ import { buildFlowIndex, readZeebeIoMapping, readZeebeTaskType } from "./utils.j
|
|
|
2
2
|
// ---------------------------------------------------------------------------
|
|
3
3
|
// Helpers
|
|
4
4
|
// ---------------------------------------------------------------------------
|
|
5
|
-
/**
|
|
6
|
-
function
|
|
5
|
+
/** hostId → event-definition types of the boundary events attached to it. */
|
|
6
|
+
function indexBoundaryTypes(p) {
|
|
7
|
+
const index = new Map();
|
|
7
8
|
for (const el of p.flowElements) {
|
|
8
9
|
if (el.type !== "boundaryEvent")
|
|
9
10
|
continue;
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
return true;
|
|
11
|
+
let types = index.get(el.attachedToRef);
|
|
12
|
+
if (!types) {
|
|
13
|
+
types = new Set();
|
|
14
|
+
index.set(el.attachedToRef, types);
|
|
15
15
|
}
|
|
16
|
+
for (const def of el.eventDefinitions)
|
|
17
|
+
types.add(def.type);
|
|
16
18
|
}
|
|
17
|
-
return
|
|
19
|
+
return index;
|
|
20
|
+
}
|
|
21
|
+
/** Returns true if the element has a boundary event of the given type attached. */
|
|
22
|
+
function hasBoundaryOf(elementId, eventType, boundaryTypes) {
|
|
23
|
+
return boundaryTypes.get(elementId)?.has(eventType) ?? false;
|
|
18
24
|
}
|
|
19
25
|
/** Returns true if the condition expression text appears to contain only literals (no variable names). */
|
|
20
26
|
function isLiteralOnlyCondition(text) {
|
|
@@ -36,7 +42,8 @@ function isLiteralOnlyCondition(text) {
|
|
|
36
42
|
export function analyzePatterns(p) {
|
|
37
43
|
const findings = [];
|
|
38
44
|
const processId = p.id;
|
|
39
|
-
const { bySource, byTarget } = buildFlowIndex(p);
|
|
45
|
+
const { byId, bySource, byTarget } = buildFlowIndex(p);
|
|
46
|
+
const boundaryTypes = indexBoundaryTypes(p);
|
|
40
47
|
// ── Rule 1: HTTP/REST service task without error boundary ───────────────
|
|
41
48
|
for (const el of p.flowElements) {
|
|
42
49
|
if (el.type !== "serviceTask")
|
|
@@ -47,7 +54,7 @@ export function analyzePatterns(p) {
|
|
|
47
54
|
jobType === "io.camunda.connector.HttpJson:1";
|
|
48
55
|
if (!isHttp)
|
|
49
56
|
continue;
|
|
50
|
-
if (!hasBoundaryOf(el.id, "error",
|
|
57
|
+
if (!hasBoundaryOf(el.id, "error", boundaryTypes)) {
|
|
51
58
|
findings.push({
|
|
52
59
|
id: "pattern/http-no-error-boundary",
|
|
53
60
|
category: "pattern",
|
|
@@ -82,7 +89,7 @@ export function analyzePatterns(p) {
|
|
|
82
89
|
for (const el of p.flowElements) {
|
|
83
90
|
if (el.type !== "subProcess" && el.type !== "adHocSubProcess" && el.type !== "transaction")
|
|
84
91
|
continue;
|
|
85
|
-
if (!hasBoundaryOf(el.id, "error",
|
|
92
|
+
if (!hasBoundaryOf(el.id, "error", boundaryTypes)) {
|
|
86
93
|
findings.push({
|
|
87
94
|
id: "pattern/subprocess-no-error-boundary",
|
|
88
95
|
category: "pattern",
|
|
@@ -98,7 +105,7 @@ export function analyzePatterns(p) {
|
|
|
98
105
|
for (const el of p.flowElements) {
|
|
99
106
|
if (el.type !== "callActivity")
|
|
100
107
|
continue;
|
|
101
|
-
if (!hasBoundaryOf(el.id, "error",
|
|
108
|
+
if (!hasBoundaryOf(el.id, "error", boundaryTypes)) {
|
|
102
109
|
findings.push({
|
|
103
110
|
id: "pattern/call-activity-no-error-boundary",
|
|
104
111
|
category: "pattern",
|
|
@@ -121,7 +128,7 @@ export function analyzePatterns(p) {
|
|
|
121
128
|
const branchTargets = [];
|
|
122
129
|
for (const flow of outflows) {
|
|
123
130
|
const targets = [];
|
|
124
|
-
const branchEl =
|
|
131
|
+
const branchEl = byId.get(flow.targetRef);
|
|
125
132
|
if (branchEl !== undefined) {
|
|
126
133
|
const io = readZeebeIoMapping(branchEl.extensionElements);
|
|
127
134
|
if (io !== null) {
|
|
@@ -158,7 +165,7 @@ export function analyzePatterns(p) {
|
|
|
158
165
|
for (const el of p.flowElements) {
|
|
159
166
|
if (el.type !== "userTask")
|
|
160
167
|
continue;
|
|
161
|
-
if (!hasBoundaryOf(el.id, "timer",
|
|
168
|
+
if (!hasBoundaryOf(el.id, "timer", boundaryTypes)) {
|
|
162
169
|
findings.push({
|
|
163
170
|
id: "pattern/user-task-no-timer",
|
|
164
171
|
category: "pattern",
|
|
@@ -200,7 +207,7 @@ export function analyzePatterns(p) {
|
|
|
200
207
|
continue;
|
|
201
208
|
const outflows = bySource.get(el.id) ?? [];
|
|
202
209
|
for (const flow of outflows) {
|
|
203
|
-
const target =
|
|
210
|
+
const target = byId.get(flow.targetRef);
|
|
204
211
|
if (target !== undefined && target.type === "endEvent") {
|
|
205
212
|
findings.push({
|
|
206
213
|
id: "pattern/catch-and-swallow",
|
|
@@ -348,7 +355,7 @@ export function analyzePatterns(p) {
|
|
|
348
355
|
if (cond === undefined || cond === "")
|
|
349
356
|
continue;
|
|
350
357
|
if (isLiteralOnlyCondition(cond)) {
|
|
351
|
-
const sourceEl =
|
|
358
|
+
const sourceEl = byId.get(flow.sourceRef);
|
|
352
359
|
findings.push({
|
|
353
360
|
id: "pattern/literal-condition",
|
|
354
361
|
category: "pattern",
|
|
@@ -53,21 +53,45 @@ function computeSimilarity(a, b) {
|
|
|
53
53
|
score += 0.15;
|
|
54
54
|
return score;
|
|
55
55
|
}
|
|
56
|
-
/**
|
|
56
|
+
/**
|
|
57
|
+
* Cluster service tasks into connected components above the similarity
|
|
58
|
+
* threshold. Similarity requires an identical job type, so tasks are bucketed
|
|
59
|
+
* by type first and the pairwise comparison only runs within a bucket.
|
|
60
|
+
*/
|
|
57
61
|
function cluster(tasks, threshold) {
|
|
62
|
+
const byType = new Map();
|
|
63
|
+
const clusters = [];
|
|
64
|
+
for (const task of tasks) {
|
|
65
|
+
const type = readZeebeTaskType(task.extensionElements);
|
|
66
|
+
// Without a job type nothing is similar to this task; it stands alone.
|
|
67
|
+
if (type === null) {
|
|
68
|
+
clusters.push([task]);
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
const bucket = byType.get(type);
|
|
72
|
+
if (bucket)
|
|
73
|
+
bucket.push(task);
|
|
74
|
+
else
|
|
75
|
+
byType.set(type, [task]);
|
|
76
|
+
}
|
|
77
|
+
for (const bucket of byType.values())
|
|
78
|
+
clusterBucket(bucket, threshold, clusters);
|
|
79
|
+
// Report clusters in declaration order of their first member.
|
|
80
|
+
const order = new Map(tasks.map((task, index) => [task, index]));
|
|
81
|
+
clusters.sort((a, b) => (order.get(a[0]) ?? 0) - (order.get(b[0]) ?? 0));
|
|
82
|
+
return clusters;
|
|
83
|
+
}
|
|
84
|
+
function clusterBucket(tasks, threshold, clusters) {
|
|
58
85
|
const n = tasks.length;
|
|
59
86
|
const visited = new Array(n).fill(false);
|
|
60
|
-
const clusters = [];
|
|
61
87
|
for (let i = 0; i < n; i++) {
|
|
62
88
|
if (visited[i])
|
|
63
89
|
continue;
|
|
64
90
|
const group = [];
|
|
65
91
|
const queue = [i];
|
|
66
92
|
visited[i] = true;
|
|
67
|
-
|
|
68
|
-
const curr = queue
|
|
69
|
-
if (curr === undefined)
|
|
70
|
-
break;
|
|
93
|
+
for (let head = 0; head < queue.length; head++) {
|
|
94
|
+
const curr = queue[head];
|
|
71
95
|
const task = tasks[curr];
|
|
72
96
|
if (task === undefined)
|
|
73
97
|
break;
|
|
@@ -87,7 +111,6 @@ function cluster(tasks, threshold) {
|
|
|
87
111
|
}
|
|
88
112
|
clusters.push(group);
|
|
89
113
|
}
|
|
90
|
-
return clusters;
|
|
91
114
|
}
|
|
92
115
|
// ---------------------------------------------------------------------------
|
|
93
116
|
// Build the extracted BpmnDefinitions
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { BpmnDefinitions } from "../bpmn-model.js";
|
|
2
2
|
export type OptimizationSeverity = "info" | "warning" | "error";
|
|
3
|
-
export type OptimizationCategory = "feel" | "flow" | "naming" | "task-reuse" | "extract" | "pattern" | "data-flow";
|
|
3
|
+
export type OptimizationCategory = "feel" | "feel-syntax" | "flow" | "naming" | "task-reuse" | "extract" | "pattern" | "data-flow" | "deploy" | "agentic" | "connector";
|
|
4
4
|
export interface ApplyFixResult {
|
|
5
5
|
description: string;
|
|
6
6
|
/** New BpmnDefinitions generated by the fix (e.g. extracted reusable sub-process). */
|
|
@@ -36,6 +36,14 @@ export interface OptimizeOptions {
|
|
|
36
36
|
feelVariableThreshold?: number;
|
|
37
37
|
reuseThreshold?: number;
|
|
38
38
|
categories?: OptimizationCategory[];
|
|
39
|
+
/**
|
|
40
|
+
* Resolves a bundled connector template's missing required value keys —
|
|
41
|
+
* pass `(templateId, boundKeys) => applyConnectorTemplate(templateId,
|
|
42
|
+
* Object.fromEntries(boundKeys.map(k => [k, "x"]))).problems...` or an
|
|
43
|
+
* equivalent from `@bpmnkit/connectors`. Without it, `connector/*` findings
|
|
44
|
+
* are skipped (core has no dependency on the connector catalog).
|
|
45
|
+
*/
|
|
46
|
+
resolveConnectorRequirements?: (templateId: string, boundKeys: string[]) => string[];
|
|
39
47
|
}
|
|
40
48
|
export interface ResolvedOptions {
|
|
41
49
|
feelLengthThreshold: number;
|
|
@@ -44,5 +52,6 @@ export interface ResolvedOptions {
|
|
|
44
52
|
feelVariableThreshold: number;
|
|
45
53
|
reuseThreshold: number;
|
|
46
54
|
categories: OptimizationCategory[];
|
|
55
|
+
resolveConnectorRequirements?: (templateId: string, boundKeys: string[]) => string[];
|
|
47
56
|
}
|
|
48
57
|
//# sourceMappingURL=types.d.ts.map
|
|
@@ -70,10 +70,8 @@ export function buildFlowIndex(p) {
|
|
|
70
70
|
export function reachableFrom(startIds, bySource) {
|
|
71
71
|
const visited = new Set(startIds);
|
|
72
72
|
const queue = [...startIds];
|
|
73
|
-
|
|
74
|
-
const current = queue
|
|
75
|
-
if (current === undefined)
|
|
76
|
-
break;
|
|
73
|
+
for (let head = 0; head < queue.length; head++) {
|
|
74
|
+
const current = queue[head];
|
|
77
75
|
const outflows = bySource.get(current) ?? [];
|
|
78
76
|
for (const flow of outflows) {
|
|
79
77
|
if (!visited.has(flow.targetRef)) {
|