@bpmnkit/core 0.0.16 → 0.0.20

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 CHANGED
@@ -6,6 +6,8 @@
6
6
  [![npm](https://img.shields.io/npm/v/@bpmnkit/core?style=flat-square&color=6244d7)](https://www.npmjs.com/package/@bpmnkit/core)
7
7
  [![license](https://img.shields.io/npm/l/@bpmnkit/core?style=flat-square)](https://github.com/bpmnkit/monorepo/blob/main/LICENSE)
8
8
  [![typescript](https://img.shields.io/badge/TypeScript-strict-6244d7?style=flat-square&logo=typescript&logoColor=white)](https://github.com/bpmnkit/monorepo)
9
+ [![ai-assisted](https://img.shields.io/badge/AI--assisted-claude-8b5cf6?style=flat-square)](https://github.com/bpmnkit/monorepo)
10
+ [![experimental](https://img.shields.io/badge/status-experimental-f59e0b?style=flat-square)](https://github.com/bpmnkit/monorepo)
9
11
 
10
12
  [Website](https://bpmnkit.com) · [Documentation](https://docs.bpmnkit.com) · [GitHub](https://github.com/bpmnkit/monorepo) · [Changelog](https://github.com/bpmnkit/monorepo/blob/main/packages/core/CHANGELOG.md)
11
13
  </div>
@@ -133,7 +133,7 @@ function buildMultiInstance(options) {
133
133
  children: [],
134
134
  });
135
135
  }
136
- return { extensionElements: extChildren };
136
+ return { isSequential: options.isSequential || undefined, extensionElements: extChildren };
137
137
  }
138
138
  function buildAdHocLoopCharacteristics(lc) {
139
139
  const attrs = {
@@ -88,6 +88,8 @@ export interface BpmnCompensateEventDefinition {
88
88
  export type BpmnEventDefinition = BpmnTimerEventDefinition | BpmnErrorEventDefinition | BpmnEscalationEventDefinition | BpmnMessageEventDefinition | BpmnSignalEventDefinition | BpmnConditionalEventDefinition | BpmnLinkEventDefinition | BpmnCancelEventDefinition | BpmnTerminateEventDefinition | BpmnCompensateEventDefinition;
89
89
  /** Multi-instance loop configuration attached to a task or sub-process. */
90
90
  export interface BpmnMultiInstanceLoopCharacteristics {
91
+ /** When true, iterations run one at a time (sequential). When false or absent, runs in parallel. */
92
+ isSequential?: boolean;
91
93
  extensionElements: XmlElement[];
92
94
  }
93
95
  /** A FEEL condition expression on a sequence flow outgoing from a gateway. */
@@ -230,7 +230,8 @@ function parseLoopCharacteristics(element) {
230
230
  const loopEl = findChild(element, "multiInstanceLoopCharacteristics");
231
231
  if (!loopEl)
232
232
  return undefined;
233
- return { extensionElements: parseExtensionElements(loopEl) };
233
+ const isSequential = loopEl.attributes.isSequential === "true" ? true : undefined;
234
+ return { isSequential, extensionElements: parseExtensionElements(loopEl) };
234
235
  }
235
236
  // ---------------------------------------------------------------------------
236
237
  // Flow elements
@@ -137,8 +137,11 @@ function serializeExtensionElements(extensions, bp) {
137
137
  function serializeLoopCharacteristics(lc, bp) {
138
138
  if (!lc)
139
139
  return [];
140
+ const attrs = {};
141
+ if (lc.isSequential)
142
+ attrs.isSequential = "true";
140
143
  return [
141
- el(`${bp}:multiInstanceLoopCharacteristics`, {}, [
144
+ el(`${bp}:multiInstanceLoopCharacteristics`, attrs, [
142
145
  ...serializeExtensionElements(lc.extensionElements, bp),
143
146
  ]),
144
147
  ];
@@ -0,0 +1,85 @@
1
+ import type { BpmnDefinitions } from "./bpmn-model.js";
2
+ /** Supported variable types for input validation. */
3
+ export type ValidationVariableType = "string" | "number" | "boolean" | "context" | "list" | "any";
4
+ /** A single input variable definition used to build a validation DMN. */
5
+ export interface InputVariableDef {
6
+ /** Variable name (must be a valid FEEL identifier). */
7
+ name: string;
8
+ /** Expected type. */
9
+ type: ValidationVariableType;
10
+ /** When true, generates a required check row. */
11
+ required: boolean;
12
+ /** Minimum numeric value (number type only). */
13
+ min?: number;
14
+ /** Maximum numeric value (number type only). */
15
+ max?: number;
16
+ /** Minimum string length (string type only). */
17
+ minLength?: number;
18
+ /** Maximum string length (string type only). */
19
+ maxLength?: number;
20
+ /** Regex pattern the value must match (string type only). */
21
+ pattern?: string;
22
+ }
23
+ /** Location of a validation structure wired after a start event. */
24
+ export interface ValidationStructure {
25
+ businessRuleTaskId: string;
26
+ decisionId: string;
27
+ }
28
+ /**
29
+ * Returns the deterministic DMN decision ID for the validation decision linked
30
+ * to a given start event.
31
+ */
32
+ export declare function validationDecisionId(startEventId: string): string;
33
+ /**
34
+ * Builds a validation DMN XML string from a list of variable definitions.
35
+ *
36
+ * Uses the Collect hit policy so every violated rule contributes an error
37
+ * string to the `validationErrors` list. An empty list means valid input.
38
+ */
39
+ export declare function buildValidationDmn(startEventId: string, variables: InputVariableDef[]): string;
40
+ /**
41
+ * Detects whether a validation structure (Business Rule Task → XOR gateway →
42
+ * Error End Event) is already wired immediately after the given start event.
43
+ *
44
+ * Returns `{ businessRuleTaskId, decisionId }` on success, or `null` if no
45
+ * validation structure is found.
46
+ */
47
+ export declare function findValidationStructure(defs: BpmnDefinitions, startEventId: string): ValidationStructure | null;
48
+ /**
49
+ * Inserts the standard validation structure after a start event:
50
+ *
51
+ * ```
52
+ * [Start Event] → [Business Rule Task] → [XOR Gateway]
53
+ * ├── = count(validationErrors) = 0 → [original next]
54
+ * └── (default) → [Error End Event: VALIDATION_FAILED]
55
+ * ```
56
+ *
57
+ * If the start event has an existing outgoing flow, that flow is redirected to
58
+ * originate from the XOR gateway (valid path) with the condition
59
+ * `= count(validationErrors) = 0`. The start event now connects to the Business
60
+ * Rule Task instead.
61
+ *
62
+ * If the start event has no outgoing flow the validation elements are inserted
63
+ * with only the error path connected; the caller can wire the valid path later.
64
+ *
65
+ * @returns Updated `BpmnDefinitions` — the original is not mutated.
66
+ */
67
+ export declare function insertValidationStructure(defs: BpmnDefinitions, startEventId: string, decisionId: string): BpmnDefinitions;
68
+ /**
69
+ * Removes the validation structure wired after the given start event, if any.
70
+ *
71
+ * Restores the start event's outgoing connection to the element that the XOR
72
+ * gateway's valid path was pointing to. The generated error definition is also
73
+ * removed from `defs.errors`.
74
+ *
75
+ * @returns Updated `BpmnDefinitions` — the original is not mutated.
76
+ */
77
+ export declare function removeValidationStructure(defs: BpmnDefinitions, startEventId: string): BpmnDefinitions;
78
+ /**
79
+ * Parses a validation DMN XML and returns the input column expression names.
80
+ * Used by the Process Runner to display expected variable hints.
81
+ *
82
+ * Returns an empty array on any parse error.
83
+ */
84
+ export declare function getValidationInputNames(dmnXml: string): string[];
85
+ //# sourceMappingURL=input-validation.d.ts.map
@@ -0,0 +1,497 @@
1
+ import { DecisionTableBuilder } from "../dmn/dmn-builder.js";
2
+ import { parseDmn } from "../dmn/dmn-parser.js";
3
+ import { generateId } from "../types/id-generator.js";
4
+ import { getZeebeExtensions } from "./utils.js";
5
+ import { zeebeExtensionsToXmlElements } from "./zeebe-extensions.js";
6
+ // ── Decision ID ───────────────────────────────────────────────────────────────
7
+ /**
8
+ * Returns the deterministic DMN decision ID for the validation decision linked
9
+ * to a given start event.
10
+ */
11
+ export function validationDecisionId(startEventId) {
12
+ return `${startEventId}_inputValidation`;
13
+ }
14
+ // ── DMN factory ───────────────────────────────────────────────────────────────
15
+ /**
16
+ * Builds a validation DMN XML string from a list of variable definitions.
17
+ *
18
+ * Uses the Collect hit policy so every violated rule contributes an error
19
+ * string to the `validationErrors` list. An empty list means valid input.
20
+ */
21
+ export function buildValidationDmn(startEventId, variables) {
22
+ const decisionId = validationDecisionId(startEventId);
23
+ const builder = new DecisionTableBuilder(decisionId).name("Input Validation").hitPolicy("COLLECT");
24
+ for (const v of variables) {
25
+ builder.input({
26
+ label: v.name,
27
+ expression: v.name,
28
+ });
29
+ }
30
+ builder.output({ label: "Error", name: "error", typeRef: "string" });
31
+ for (let i = 0; i < variables.length; i++) {
32
+ const v = variables[i];
33
+ if (!v)
34
+ continue;
35
+ /** Build a rule that fires (on violation) for variable at index i. */
36
+ const addRow = (entry, message) => {
37
+ const inputs = variables.map((_, j) => (j === i ? entry : ""));
38
+ builder.rule({ inputs, outputs: [`"${message}"`] });
39
+ };
40
+ if (v.required) {
41
+ addRow("null", `${v.name} is required`);
42
+ }
43
+ if (v.type === "string") {
44
+ addRow("not(instance of string)", `${v.name} must be a string`);
45
+ }
46
+ else if (v.type === "number") {
47
+ addRow("not(instance of number)", `${v.name} must be a number`);
48
+ }
49
+ else if (v.type === "boolean") {
50
+ addRow("not(instance of boolean)", `${v.name} must be a boolean`);
51
+ }
52
+ else if (v.type === "context") {
53
+ addRow("not(instance of context)", `${v.name} must be a context`);
54
+ }
55
+ else if (v.type === "list") {
56
+ addRow("not(instance of list)", `${v.name} must be a list`);
57
+ }
58
+ // "any" accepts all types — no type-check rule generated
59
+ if (v.type === "string") {
60
+ if (v.minLength !== undefined) {
61
+ addRow(`string length(?) < ${v.minLength}`, `${v.name} must be at least ${v.minLength} characters`);
62
+ }
63
+ if (v.maxLength !== undefined) {
64
+ addRow(`string length(?) > ${v.maxLength}`, `${v.name} must be at most ${v.maxLength} characters`);
65
+ }
66
+ if (v.pattern) {
67
+ addRow(`not(matches(?, "${v.pattern}"))`, `${v.name} has invalid format`);
68
+ }
69
+ }
70
+ else if (v.type === "number") {
71
+ if (v.min !== undefined) {
72
+ addRow(`< ${v.min}`, `${v.name} must be >= ${v.min}`);
73
+ }
74
+ if (v.max !== undefined) {
75
+ addRow(`> ${v.max}`, `${v.name} must be <= ${v.max}`);
76
+ }
77
+ }
78
+ }
79
+ return builder.toXml();
80
+ }
81
+ // ── BPMN structure detection ──────────────────────────────────────────────────
82
+ /** Returns the process that contains the given element id, or undefined. */
83
+ function findProcessFor(defs, elementId) {
84
+ return defs.processes.find((p) => p.flowElements.some((e) => e.id === elementId));
85
+ }
86
+ /**
87
+ * Detects whether a validation structure (Business Rule Task → XOR gateway →
88
+ * Error End Event) is already wired immediately after the given start event.
89
+ *
90
+ * Returns `{ businessRuleTaskId, decisionId }` on success, or `null` if no
91
+ * validation structure is found.
92
+ */
93
+ export function findValidationStructure(defs, startEventId) {
94
+ const process = findProcessFor(defs, startEventId);
95
+ if (!process)
96
+ return null;
97
+ for (const flow of process.sequenceFlows) {
98
+ if (flow.sourceRef !== startEventId)
99
+ continue;
100
+ const target = process.flowElements.find((e) => e.id === flow.targetRef);
101
+ if (!target || target.type !== "businessRuleTask")
102
+ continue;
103
+ const ext = getZeebeExtensions(target.extensionElements);
104
+ if (ext.calledDecision?.resultVariable !== "validationErrors")
105
+ continue;
106
+ return { businessRuleTaskId: target.id, decisionId: ext.calledDecision.decisionId };
107
+ }
108
+ return null;
109
+ }
110
+ // ── BPMN structure insertion ──────────────────────────────────────────────────
111
+ /** Standard element dimensions used when placing new validation elements. */
112
+ const SIZES = {
113
+ event: { w: 36, h: 36 },
114
+ task: { w: 100, h: 80 },
115
+ gateway: { w: 50, h: 50 },
116
+ };
117
+ const H_GAP = 80;
118
+ const V_GAP = 80;
119
+ /**
120
+ * Inserts the standard validation structure after a start event:
121
+ *
122
+ * ```
123
+ * [Start Event] → [Business Rule Task] → [XOR Gateway]
124
+ * ├── = count(validationErrors) = 0 → [original next]
125
+ * └── (default) → [Error End Event: VALIDATION_FAILED]
126
+ * ```
127
+ *
128
+ * If the start event has an existing outgoing flow, that flow is redirected to
129
+ * originate from the XOR gateway (valid path) with the condition
130
+ * `= count(validationErrors) = 0`. The start event now connects to the Business
131
+ * Rule Task instead.
132
+ *
133
+ * If the start event has no outgoing flow the validation elements are inserted
134
+ * with only the error path connected; the caller can wire the valid path later.
135
+ *
136
+ * @returns Updated `BpmnDefinitions` — the original is not mutated.
137
+ */
138
+ export function insertValidationStructure(defs, startEventId, decisionId) {
139
+ const processIdx = defs.processes.findIndex((p) => p.flowElements.some((e) => e.id === startEventId));
140
+ const process = defs.processes[processIdx];
141
+ if (processIdx < 0 || !process)
142
+ return defs;
143
+ // ── Generate IDs ─────────────────────────────────────────────────────────
144
+ const brtId = generateId("Activity");
145
+ const gwId = generateId("Gateway");
146
+ const errEndId = generateId("Event");
147
+ const errorId = generateId("Error");
148
+ const flowStartToBrt = generateId("Flow");
149
+ const flowBrtToGw = generateId("Flow");
150
+ const flowGwToError = generateId("Flow");
151
+ // ── Find existing outgoing flow from start event ──────────────────────────
152
+ const originalOutgoingFlowId = process.sequenceFlows.find((f) => f.sourceRef === startEventId)?.id;
153
+ // ── Build new flow elements ───────────────────────────────────────────────
154
+ const brt = {
155
+ type: "businessRuleTask",
156
+ id: brtId,
157
+ name: "Validate Input",
158
+ incoming: [flowStartToBrt],
159
+ outgoing: [flowBrtToGw],
160
+ extensionElements: zeebeExtensionsToXmlElements({
161
+ calledDecision: { decisionId, resultVariable: "validationErrors" },
162
+ }),
163
+ unknownAttributes: {},
164
+ };
165
+ const gwOutgoing = [flowGwToError];
166
+ if (originalOutgoingFlowId)
167
+ gwOutgoing.unshift(originalOutgoingFlowId);
168
+ const gateway = {
169
+ type: "exclusiveGateway",
170
+ id: gwId,
171
+ name: "Input valid?",
172
+ default: flowGwToError,
173
+ incoming: [flowBrtToGw],
174
+ outgoing: gwOutgoing,
175
+ extensionElements: [],
176
+ unknownAttributes: {},
177
+ };
178
+ const errEnd = {
179
+ type: "endEvent",
180
+ id: errEndId,
181
+ name: "Invalid Input",
182
+ incoming: [flowGwToError],
183
+ outgoing: [],
184
+ eventDefinitions: [{ type: "error", errorRef: errorId }],
185
+ extensionElements: [],
186
+ unknownAttributes: {},
187
+ };
188
+ const errorDef = {
189
+ id: errorId,
190
+ name: "VALIDATION_FAILED",
191
+ errorCode: "VALIDATION_FAILED",
192
+ };
193
+ // ── New sequence flows ────────────────────────────────────────────────────
194
+ const newFlows = [
195
+ {
196
+ id: flowStartToBrt,
197
+ sourceRef: startEventId,
198
+ targetRef: brtId,
199
+ extensionElements: [],
200
+ unknownAttributes: {},
201
+ },
202
+ {
203
+ id: flowBrtToGw,
204
+ sourceRef: brtId,
205
+ targetRef: gwId,
206
+ extensionElements: [],
207
+ unknownAttributes: {},
208
+ },
209
+ {
210
+ id: flowGwToError,
211
+ sourceRef: gwId,
212
+ targetRef: errEndId,
213
+ extensionElements: [],
214
+ unknownAttributes: {},
215
+ },
216
+ ];
217
+ // ── Update start event outgoing list ──────────────────────────────────────
218
+ const updatedElements = process.flowElements.map((el) => {
219
+ if (el.id !== startEventId)
220
+ return el;
221
+ const without = el.outgoing.filter((o) => o !== originalOutgoingFlowId);
222
+ return { ...el, outgoing: [...without, flowStartToBrt] };
223
+ });
224
+ // ── Redirect original outgoing flow ───────────────────────────────────────
225
+ const updatedFlows = process.sequenceFlows.map((f) => {
226
+ if (f.id !== originalOutgoingFlowId)
227
+ return f;
228
+ return {
229
+ ...f,
230
+ sourceRef: gwId,
231
+ conditionExpression: {
232
+ text: "= count(validationErrors) = 0",
233
+ attributes: {},
234
+ },
235
+ };
236
+ });
237
+ const newProcess = {
238
+ ...process,
239
+ flowElements: [...updatedElements, brt, gateway, errEnd],
240
+ sequenceFlows: [...updatedFlows, ...newFlows],
241
+ };
242
+ // ── DI: compute positions ─────────────────────────────────────────────────
243
+ let updatedDefs = {
244
+ ...defs,
245
+ errors: [...defs.errors, errorDef],
246
+ processes: defs.processes.map((p, i) => (i === processIdx ? newProcess : p)),
247
+ };
248
+ updatedDefs = _insertValidationDi(updatedDefs, startEventId, {
249
+ brtId,
250
+ gwId,
251
+ errEndId,
252
+ flowStartToBrt,
253
+ flowBrtToGw,
254
+ flowGwToError,
255
+ originalOutgoingFlowId,
256
+ });
257
+ return updatedDefs;
258
+ }
259
+ /** Adds DI shapes and edges for the newly inserted validation elements. */
260
+ function _insertValidationDi(defs, startEventId, ids) {
261
+ if (!defs.diagrams[0])
262
+ return defs;
263
+ const plane = defs.diagrams[0].plane;
264
+ const startShape = plane.shapes.find((s) => s.bpmnElement === startEventId);
265
+ // Default position if no DI exists for the start event
266
+ const sx = startShape?.bounds.x ?? 152;
267
+ const sy = startShape?.bounds.y ?? 202;
268
+ const sw = SIZES.event.w;
269
+ const sh = SIZES.event.h;
270
+ // Vertical center of start event
271
+ const centerY = sy + sh / 2;
272
+ // Positions (top-left corner of each element)
273
+ const brtX = sx + sw + H_GAP;
274
+ const brtY = centerY - SIZES.task.h / 2;
275
+ const gwX = brtX + SIZES.task.w + H_GAP;
276
+ const gwY = centerY - SIZES.gateway.h / 2;
277
+ const errX = gwX + SIZES.gateway.w / 2 - SIZES.event.w / 2;
278
+ const errY = gwY + SIZES.gateway.h + V_GAP;
279
+ // Centers for waypoint computation
280
+ const startCx = sx + sw / 2;
281
+ const startCy = centerY;
282
+ const brtCx = brtX + SIZES.task.w / 2;
283
+ const brtCy = centerY;
284
+ const gwCx = gwX + SIZES.gateway.w / 2;
285
+ const gwCy = centerY;
286
+ const errCx = errX + SIZES.event.w / 2;
287
+ const errCy = errY + SIZES.event.h / 2;
288
+ const newShapes = [
289
+ {
290
+ id: generateId("Shape"),
291
+ bpmnElement: ids.brtId,
292
+ bounds: { x: brtX, y: brtY, width: SIZES.task.w, height: SIZES.task.h },
293
+ unknownAttributes: {},
294
+ },
295
+ {
296
+ id: generateId("Shape"),
297
+ bpmnElement: ids.gwId,
298
+ isMarkerVisible: true,
299
+ bounds: { x: gwX, y: gwY, width: SIZES.gateway.w, height: SIZES.gateway.h },
300
+ unknownAttributes: {},
301
+ },
302
+ {
303
+ id: generateId("Shape"),
304
+ bpmnElement: ids.errEndId,
305
+ bounds: { x: errX, y: errY, width: SIZES.event.w, height: SIZES.event.h },
306
+ unknownAttributes: {},
307
+ },
308
+ ];
309
+ const newEdges = [
310
+ {
311
+ id: generateId("Edge"),
312
+ bpmnElement: ids.flowStartToBrt,
313
+ waypoints: [
314
+ { x: startCx + sw / 2, y: startCy },
315
+ { x: brtX, y: brtCy },
316
+ ],
317
+ unknownAttributes: {},
318
+ },
319
+ {
320
+ id: generateId("Edge"),
321
+ bpmnElement: ids.flowBrtToGw,
322
+ waypoints: [
323
+ { x: brtX + SIZES.task.w, y: brtCy },
324
+ { x: gwX, y: gwCy },
325
+ ],
326
+ unknownAttributes: {},
327
+ },
328
+ {
329
+ id: generateId("Edge"),
330
+ bpmnElement: ids.flowGwToError,
331
+ waypoints: [
332
+ { x: gwCx, y: gwY + SIZES.gateway.h },
333
+ { x: errCx, y: errY },
334
+ ],
335
+ unknownAttributes: {},
336
+ },
337
+ ];
338
+ // Update the redirected outgoing flow edge to start from the gateway
339
+ const updatedEdges = plane.edges.map((e) => {
340
+ if (e.bpmnElement !== ids.originalOutgoingFlowId)
341
+ return e;
342
+ const firstWp = e.waypoints[0];
343
+ const rest = e.waypoints.slice(1);
344
+ return {
345
+ ...e,
346
+ waypoints: [
347
+ // Replace first waypoint with gateway right-edge
348
+ { x: gwX + SIZES.gateway.w, y: gwCy },
349
+ ...(firstWp ? rest : []),
350
+ ],
351
+ };
352
+ });
353
+ const updatedPlane = {
354
+ ...plane,
355
+ shapes: [...plane.shapes, ...newShapes],
356
+ edges: [...updatedEdges, ...newEdges],
357
+ };
358
+ return {
359
+ ...defs,
360
+ diagrams: [
361
+ {
362
+ ...defs.diagrams[0],
363
+ plane: updatedPlane,
364
+ },
365
+ ...defs.diagrams.slice(1),
366
+ ],
367
+ };
368
+ }
369
+ // ── BPMN structure removal ────────────────────────────────────────────────────
370
+ /**
371
+ * Removes the validation structure wired after the given start event, if any.
372
+ *
373
+ * Restores the start event's outgoing connection to the element that the XOR
374
+ * gateway's valid path was pointing to. The generated error definition is also
375
+ * removed from `defs.errors`.
376
+ *
377
+ * @returns Updated `BpmnDefinitions` — the original is not mutated.
378
+ */
379
+ export function removeValidationStructure(defs, startEventId) {
380
+ const processIdx = defs.processes.findIndex((p) => p.flowElements.some((e) => e.id === startEventId));
381
+ const process = defs.processes[processIdx];
382
+ if (processIdx < 0 || !process)
383
+ return defs;
384
+ // Locate the BRT immediately after the start event
385
+ const startToBrtFlow = process.sequenceFlows.find((f) => f.sourceRef === startEventId);
386
+ if (!startToBrtFlow)
387
+ return defs;
388
+ const brt = process.flowElements.find((e) => e.id === startToBrtFlow.targetRef && e.type === "businessRuleTask");
389
+ if (!brt)
390
+ return defs;
391
+ const ext = getZeebeExtensions(brt.extensionElements);
392
+ if (ext.calledDecision?.resultVariable !== "validationErrors")
393
+ return defs;
394
+ // BRT → gateway
395
+ const brtToGwFlow = process.sequenceFlows.find((f) => f.sourceRef === brt.id);
396
+ const gateway = brtToGwFlow
397
+ ? process.flowElements.find((e) => e.id === brtToGwFlow.targetRef && e.type === "exclusiveGateway")
398
+ : undefined;
399
+ if (!gateway || gateway.type !== "exclusiveGateway")
400
+ return defs;
401
+ // Gateway → error end event (default/no-condition flow)
402
+ const gwFlows = process.sequenceFlows.filter((f) => f.sourceRef === gateway.id);
403
+ const errorFlow = gwFlows.find((f) => !f.conditionExpression);
404
+ const validFlow = gwFlows.find((f) => f.conditionExpression);
405
+ const errEnd = errorFlow
406
+ ? process.flowElements.find((e) => e.id === errorFlow.targetRef && e.type === "endEvent")
407
+ : undefined;
408
+ // Collect IDs to remove
409
+ const removeElementIds = new Set([brt.id, gateway.id, ...(errEnd ? [errEnd.id] : [])]);
410
+ const removeFlowIds = new Set([
411
+ startToBrtFlow.id,
412
+ ...(brtToGwFlow ? [brtToGwFlow.id] : []),
413
+ ...(errorFlow ? [errorFlow.id] : []),
414
+ ]);
415
+ // Find error ref to remove from defs.errors
416
+ const errEndEl = errEnd?.type === "endEvent" ? errEnd : undefined;
417
+ const errorRefId = errEndEl?.eventDefinitions[0]?.type === "error"
418
+ ? errEndEl.eventDefinitions[0].errorRef
419
+ : undefined;
420
+ // Reconnect: restore the valid-path flow to come from the start event
421
+ const updatedFlows = process.sequenceFlows
422
+ .filter((f) => !removeFlowIds.has(f.id))
423
+ .map((f) => {
424
+ if (f.id !== validFlow?.id)
425
+ return f;
426
+ return { ...f, sourceRef: startEventId, conditionExpression: undefined };
427
+ });
428
+ // Restore start event outgoing list
429
+ const updatedElements = process.flowElements
430
+ .filter((e) => !removeElementIds.has(e.id))
431
+ .map((el) => {
432
+ if (el.id !== startEventId)
433
+ return el;
434
+ const without = el.outgoing.filter((o) => o !== startToBrtFlow.id);
435
+ const restored = validFlow ? [validFlow.id, ...without] : without;
436
+ return { ...el, outgoing: restored };
437
+ });
438
+ const newProcess = {
439
+ ...process,
440
+ flowElements: updatedElements,
441
+ sequenceFlows: updatedFlows,
442
+ };
443
+ // Remove DI shapes and edges for removed elements
444
+ let updatedDefs = {
445
+ ...defs,
446
+ errors: errorRefId ? defs.errors.filter((e) => e.id !== errorRefId) : defs.errors,
447
+ processes: defs.processes.map((p, i) => (i === processIdx ? newProcess : p)),
448
+ };
449
+ updatedDefs = _removeValidationDi(updatedDefs, removeElementIds, removeFlowIds, validFlow?.id);
450
+ return updatedDefs;
451
+ }
452
+ function _removeValidationDi(defs, removeElementIds, removeFlowIds, restoredFlowId) {
453
+ if (!defs.diagrams[0])
454
+ return defs;
455
+ const plane = defs.diagrams[0].plane;
456
+ const updatedShapes = plane.shapes.filter((s) => !removeElementIds.has(s.bpmnElement));
457
+ const updatedEdges = plane.edges
458
+ .filter((e) => !removeFlowIds.has(e.bpmnElement))
459
+ .map((e) => {
460
+ // The restored flow's edge now starts from the start event again.
461
+ // We don't know the start event's exact right-edge position here, but
462
+ // leaving the waypoints as-is is acceptable — auto-layout can fix them.
463
+ if (e.bpmnElement !== restoredFlowId)
464
+ return e;
465
+ return e; // leave waypoints unchanged; user can re-layout
466
+ });
467
+ return {
468
+ ...defs,
469
+ diagrams: [
470
+ {
471
+ ...defs.diagrams[0],
472
+ plane: { ...plane, shapes: updatedShapes, edges: updatedEdges },
473
+ },
474
+ ...defs.diagrams.slice(1),
475
+ ],
476
+ };
477
+ }
478
+ // ── DMN variable name extraction ──────────────────────────────────────────────
479
+ /**
480
+ * Parses a validation DMN XML and returns the input column expression names.
481
+ * Used by the Process Runner to display expected variable hints.
482
+ *
483
+ * Returns an empty array on any parse error.
484
+ */
485
+ export function getValidationInputNames(dmnXml) {
486
+ try {
487
+ const defs = parseDmn(dmnXml);
488
+ const decision = defs.decisions[0];
489
+ if (!decision?.decisionTable)
490
+ return [];
491
+ return decision.decisionTable.inputs.map((i) => i.inputExpression.text ?? "").filter(Boolean);
492
+ }
493
+ catch {
494
+ return [];
495
+ }
496
+ }
497
+ //# sourceMappingURL=input-validation.js.map
@@ -426,6 +426,84 @@ export function analyzeVariableFlow(p) {
426
426
  produces: scopeVars,
427
427
  });
428
428
  }
429
+ // ── Multi-instance sub-process inner scopes ──────────────────────────────────
430
+ // For each multi-instance sub-process, emit edge-scope findings for inner
431
+ // sequence flows seeded with the iteration variable (inputElement).
432
+ for (const el of p.flowElements) {
433
+ if (el.type !== "subProcess")
434
+ continue;
435
+ const sp = el;
436
+ if (!sp.loopCharacteristics)
437
+ continue;
438
+ // Extract inputElement from zeebe:loopCharacteristics extension element
439
+ const loopExt = sp.loopCharacteristics.extensionElements.find((e) => e.name === "zeebe:loopCharacteristics");
440
+ const inputElement = loopExt?.attributes.inputElement?.trim();
441
+ if (!inputElement)
442
+ continue;
443
+ // Collect variables produced by inner elements (from IO mapping outputs)
444
+ const innerProduces = new Map();
445
+ for (const inner of sp.flowElements) {
446
+ const io = readZeebeIoMapping(inner.extensionElements);
447
+ if (io) {
448
+ for (const out of io.outputs) {
449
+ if (out.target.trim() === "")
450
+ continue;
451
+ const list = innerProduces.get(inner.id) ?? [];
452
+ if (!list.includes(out.target.trim()))
453
+ list.push(out.target.trim());
454
+ innerProduces.set(inner.id, list);
455
+ }
456
+ }
457
+ }
458
+ // Build inner reverse adjacency for BFS
459
+ const innerReverseAdj = new Map();
460
+ for (const flow of sp.sequenceFlows) {
461
+ const set = innerReverseAdj.get(flow.targetRef) ?? new Set();
462
+ set.add(flow.sourceRef);
463
+ innerReverseAdj.set(flow.targetRef, set);
464
+ }
465
+ function innerAllPredecessors(elementId) {
466
+ const visited = new Set();
467
+ const queue = [elementId];
468
+ while (queue.length > 0) {
469
+ const current = queue.shift();
470
+ if (current === undefined)
471
+ break;
472
+ const preds = innerReverseAdj.get(current);
473
+ if (preds === undefined)
474
+ continue;
475
+ for (const pred of preds) {
476
+ if (!visited.has(pred)) {
477
+ visited.add(pred);
478
+ queue.push(pred);
479
+ }
480
+ }
481
+ }
482
+ return visited;
483
+ }
484
+ // Emit edge-scope findings for inner sequence flows
485
+ for (const flow of sp.sequenceFlows) {
486
+ const predIds = innerAllPredecessors(flow.sourceRef);
487
+ predIds.add(flow.sourceRef);
488
+ // Scope always includes the iteration variable, plus anything inner elements produce
489
+ const inScope = new Set([inputElement]);
490
+ for (const predId of predIds) {
491
+ for (const v of innerProduces.get(predId) ?? [])
492
+ inScope.add(v);
493
+ }
494
+ const scopeVars = [...inScope].sort();
495
+ findings.push({
496
+ id: `data-flow/edge-scope:${flow.id}`,
497
+ category: "data-flow",
498
+ severity: "info",
499
+ message: `Variables in scope at flow "${flow.id}": ${scopeVars.join(", ")}.`,
500
+ suggestion: "",
501
+ processId,
502
+ elementIds: [flow.id],
503
+ produces: scopeVars,
504
+ });
505
+ }
506
+ }
429
507
  return findings;
430
508
  }
431
509
  //# sourceMappingURL=variable-flow.js.map
package/dist/index.d.ts CHANGED
@@ -36,6 +36,8 @@ export { ELEMENT_SIZES, GRID_CELL_HEIGHT } from "./layout/index.js";
36
36
  export { compactify, expand } from "./bpmn/compact.js";
37
37
  export { applyOperations } from "./bpmn/operations.js";
38
38
  export type { BpmnOperation } from "./bpmn/operations.js";
39
+ export { buildValidationDmn, findValidationStructure, getValidationInputNames, insertValidationStructure, removeValidationStructure, validationDecisionId, } from "./bpmn/input-validation.js";
40
+ export type { InputVariableDef, ValidationStructure, ValidationVariableType, } from "./bpmn/input-validation.js";
39
41
  export { exportSvg } from "./bpmn/svg.js";
40
42
  export type { SvgExportOptions } from "./bpmn/svg.js";
41
43
  export type { CompactDiagram, CompactElement, CompactFlow, CompactProcess, } from "./bpmn/compact.js";
package/dist/index.js CHANGED
@@ -18,5 +18,6 @@ export { benchmarkLayout, compareLayouts, formatBenchmarkResult, generateAutoLay
18
18
  export { ELEMENT_SIZES, GRID_CELL_HEIGHT } from "./layout/index.js";
19
19
  export { compactify, expand } from "./bpmn/compact.js";
20
20
  export { applyOperations } from "./bpmn/operations.js";
21
+ export { buildValidationDmn, findValidationStructure, getValidationInputNames, insertValidationStructure, removeValidationStructure, validationDecisionId, } from "./bpmn/input-validation.js";
21
22
  export { exportSvg } from "./bpmn/svg.js";
22
23
  //# sourceMappingURL=index.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bpmnkit/core",
3
- "version": "0.0.16",
3
+ "version": "0.0.20",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -17,7 +17,7 @@
17
17
  "dist/**/*.d.ts"
18
18
  ],
19
19
  "dependencies": {
20
- "@bpmnkit/feel": "0.0.13"
20
+ "@bpmnkit/feel": "0.0.16"
21
21
  },
22
22
  "description": "TypeScript-first BPMN 2.0 SDK — parse, build, layout, and optimize diagrams",
23
23
  "keywords": [