@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.
Files changed (83) hide show
  1. package/README.md +32 -1
  2. package/dist/bpmn/agentic.d.ts +121 -0
  3. package/dist/bpmn/agentic.js +97 -0
  4. package/dist/bpmn/auto-layout.d.ts +5 -5
  5. package/dist/bpmn/auto-layout.js +592 -36
  6. package/dist/bpmn/bpmn-builder.d.ts +265 -3
  7. package/dist/bpmn/bpmn-builder.js +603 -197
  8. package/dist/bpmn/bpmn-model.d.ts +114 -0
  9. package/dist/bpmn/bpmn-parser.js +1414 -521
  10. package/dist/bpmn/bpmn-serializer.js +107 -19
  11. package/dist/bpmn/compact.d.ts +17 -2
  12. package/dist/bpmn/compact.js +3 -3
  13. package/dist/bpmn/full-operations.d.ts +89 -0
  14. package/dist/bpmn/full-operations.js +478 -0
  15. package/dist/bpmn/index.d.ts +19 -0
  16. package/dist/bpmn/index.js +21 -0
  17. package/dist/bpmn/optimize/agentic.d.ts +10 -0
  18. package/dist/bpmn/optimize/agentic.js +88 -0
  19. package/dist/bpmn/optimize/deploy.d.ts +16 -0
  20. package/dist/bpmn/optimize/deploy.js +143 -0
  21. package/dist/bpmn/optimize/feel-syntax.d.ts +12 -0
  22. package/dist/bpmn/optimize/feel-syntax.js +87 -0
  23. package/dist/bpmn/optimize/feel.js +7 -4
  24. package/dist/bpmn/optimize/flow.js +22 -2
  25. package/dist/bpmn/optimize/index.js +20 -9
  26. package/dist/bpmn/optimize/patterns.js +23 -16
  27. package/dist/bpmn/optimize/tasks.js +30 -7
  28. package/dist/bpmn/optimize/types.d.ts +10 -1
  29. package/dist/bpmn/optimize/utils.js +2 -4
  30. package/dist/bpmn/optimize/variable-flow.js +58 -67
  31. package/dist/bpmn/semantic-hash.d.ts +93 -0
  32. package/dist/bpmn/semantic-hash.js +155 -0
  33. package/dist/bpmn/sha256.d.ts +17 -0
  34. package/dist/bpmn/sha256.js +95 -0
  35. package/dist/bpmn/zeebe-extensions.d.ts +83 -0
  36. package/dist/bpmn/zeebe-extensions.js +117 -0
  37. package/dist/bpmn/zeebe-placement.d.ts +12 -0
  38. package/dist/bpmn/zeebe-placement.js +140 -0
  39. package/dist/errors.d.ts +40 -1
  40. package/dist/errors.js +41 -0
  41. package/dist/index.d.ts +16 -5
  42. package/dist/index.js +9 -3
  43. package/dist/layout/annotations.js +36 -1
  44. package/dist/layout/collaboration/alignment.d.ts +26 -0
  45. package/dist/layout/collaboration/alignment.js +66 -0
  46. package/dist/layout/collaboration/ordering.d.ts +21 -0
  47. package/dist/layout/collaboration/ordering.js +102 -0
  48. package/dist/layout/index.d.ts +1 -0
  49. package/dist/layout/layout-engine.d.ts +13 -3
  50. package/dist/layout/layout-engine.js +9 -4
  51. package/dist/layout/semantic/bands.d.ts +19 -0
  52. package/dist/layout/semantic/bands.js +324 -0
  53. package/dist/layout/semantic/graph.d.ts +37 -0
  54. package/dist/layout/semantic/graph.js +242 -0
  55. package/dist/layout/semantic/index.d.ts +13 -0
  56. package/dist/layout/semantic/index.js +181 -0
  57. package/dist/layout/semantic/place.d.ts +40 -0
  58. package/dist/layout/semantic/place.js +271 -0
  59. package/dist/layout/semantic/route.d.ts +14 -0
  60. package/dist/layout/semantic/route.js +514 -0
  61. package/dist/layout/types.d.ts +17 -0
  62. package/dist/node/index.d.ts +10 -0
  63. package/dist/node/index.js +9 -0
  64. package/dist/node/write.d.ts +81 -0
  65. package/dist/node/write.js +167 -0
  66. package/dist/plan/compile.d.ts +39 -0
  67. package/dist/plan/compile.js +380 -0
  68. package/dist/plan/extract.d.ts +31 -0
  69. package/dist/plan/extract.js +248 -0
  70. package/dist/plan/index.d.ts +6 -0
  71. package/dist/plan/index.js +5 -0
  72. package/dist/plan/merge.d.ts +13 -0
  73. package/dist/plan/merge.js +80 -0
  74. package/dist/plan/slug.d.ts +5 -0
  75. package/dist/plan/slug.js +22 -0
  76. package/dist/plan/types.d.ts +225 -0
  77. package/dist/plan/types.js +13 -0
  78. package/dist/types/id-generator.js +11 -3
  79. package/dist/xml/index.d.ts +3 -1
  80. package/dist/xml/index.js +2 -1
  81. package/dist/xml/xml-parser.d.ts +32 -0
  82. package/dist/xml/xml-parser.js +394 -143
  83. 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
- const srcEl = p.flowElements.find((e) => e.id === flow.sourceRef);
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
- const reachable = reachableFrom(startIds, bySource);
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
- /** Returns true if the element has a boundary event of the given type attached. */
6
- function hasBoundaryOf(elementId, eventType, p) {
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
- if (el.attachedToRef !== elementId)
11
- continue;
12
- for (const def of el.eventDefinitions) {
13
- if (def.type === eventType)
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 false;
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", p)) {
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", p)) {
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", p)) {
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 = p.flowElements.find((e) => e.id === flow.targetRef);
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", p)) {
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 = p.flowElements.find((e) => e.id === flow.targetRef);
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 = p.flowElements.find((e) => e.id === flow.sourceRef);
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
- /** Cluster service tasks into connected components above the similarity threshold. */
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
- while (queue.length > 0) {
68
- const curr = queue.shift();
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
- while (queue.length > 0) {
74
- const current = queue.shift();
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)) {