@elabs-ai/components-process 4.1.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 (87) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +73 -0
  3. package/dist/core/index.d.ts +1029 -0
  4. package/dist/core/index.js +1553 -0
  5. package/dist/core/index.js.map +1 -0
  6. package/dist/core/process-worker.js +462 -0
  7. package/dist/core/process-worker.js.map +1 -0
  8. package/dist/index.d.ts +1153 -0
  9. package/dist/index.js +3146 -0
  10. package/dist/index.js.map +1 -0
  11. package/dist/test/index.d.ts +196 -0
  12. package/dist/test/index.js +527 -0
  13. package/dist/test/index.js.map +1 -0
  14. package/package.json +80 -0
  15. package/src/abstraction-controls/abstraction-controls-fixtures.ts +86 -0
  16. package/src/abstraction-controls/abstraction-controls.stories.tsx +188 -0
  17. package/src/abstraction-controls/abstraction-controls.test.tsx +226 -0
  18. package/src/abstraction-controls/abstraction-controls.tsx +288 -0
  19. package/src/abstraction-controls/auto-abstraction.test.ts +196 -0
  20. package/src/abstraction-controls/auto-abstraction.ts +128 -0
  21. package/src/abstraction-controls/index.ts +4 -0
  22. package/src/core/abstract-graph.test.ts +209 -0
  23. package/src/core/abstract-graph.ts +407 -0
  24. package/src/core/adapters/csv.test.ts +131 -0
  25. package/src/core/adapters/csv.ts +146 -0
  26. package/src/core/adapters/flat.test.ts +149 -0
  27. package/src/core/adapters/flat.ts +168 -0
  28. package/src/core/aggregate-performance.test.ts +208 -0
  29. package/src/core/aggregate-performance.ts +200 -0
  30. package/src/core/detect-rework.test.ts +134 -0
  31. package/src/core/detect-rework.ts +100 -0
  32. package/src/core/discover-graph.test.ts +378 -0
  33. package/src/core/discover-graph.ts +202 -0
  34. package/src/core/duration-stats.test.ts +116 -0
  35. package/src/core/duration-stats.ts +162 -0
  36. package/src/core/event-log.test.ts +224 -0
  37. package/src/core/event-log.ts +244 -0
  38. package/src/core/extract-variants.test.ts +126 -0
  39. package/src/core/extract-variants.ts +140 -0
  40. package/src/core/filter-log.test.ts +193 -0
  41. package/src/core/filter-log.ts +215 -0
  42. package/src/core/fixtures/generate-bpi-2012-subset.test.ts +50 -0
  43. package/src/core/fixtures/generate-bpi-2012-subset.ts +216 -0
  44. package/src/core/fixtures/generate-bpi-2012-subset.write.ts +40 -0
  45. package/src/core/fixtures/order-to-cash-small.json +200 -0
  46. package/src/core/fixtures/synthetic-log.test.ts +109 -0
  47. package/src/core/fixtures/synthetic-log.ts +167 -0
  48. package/src/core/index.ts +118 -0
  49. package/src/core/reconcile-graph.test.ts +175 -0
  50. package/src/core/reconcile-graph.ts +107 -0
  51. package/src/core/scale.test.ts +80 -0
  52. package/src/core/scale.ts +100 -0
  53. package/src/core/types.ts +151 -0
  54. package/src/core/worker/create-process-worker.test.ts +255 -0
  55. package/src/core/worker/create-process-worker.ts +211 -0
  56. package/src/core/worker/process-worker.ts +80 -0
  57. package/src/index.ts +29 -0
  58. package/src/metric-layer-switch/index.ts +6 -0
  59. package/src/metric-layer-switch/metric-layer-switch.stories.tsx +131 -0
  60. package/src/metric-layer-switch/metric-layer-switch.test.tsx +102 -0
  61. package/src/metric-layer-switch/metric-layer-switch.tsx +276 -0
  62. package/src/process-explorer.stories.tsx +392 -0
  63. package/src/process-kpi-strip/index.ts +6 -0
  64. package/src/process-kpi-strip/process-kpi-strip.stories.tsx +128 -0
  65. package/src/process-kpi-strip/process-kpi-strip.test.tsx +106 -0
  66. package/src/process-kpi-strip/process-kpi-strip.tsx +237 -0
  67. package/src/process-map/index.ts +13 -0
  68. package/src/process-map/map-model.test.ts +326 -0
  69. package/src/process-map/map-model.ts +873 -0
  70. package/src/process-map/process-activity-node.tsx +200 -0
  71. package/src/process-map/process-map-context.ts +71 -0
  72. package/src/process-map/process-map.stories.tsx +673 -0
  73. package/src/process-map/process-map.test.tsx +523 -0
  74. package/src/process-map/process-map.tsx +979 -0
  75. package/src/process-map/process-transition-edge.test.tsx +160 -0
  76. package/src/process-map/process-transition-edge.tsx +151 -0
  77. package/src/process-map/use-process-layout.test.tsx +265 -0
  78. package/src/process-map/use-process-layout.ts +315 -0
  79. package/src/test/contract.test.ts +99 -0
  80. package/src/test/contract.ts +118 -0
  81. package/src/test/doubles.test.tsx +51 -0
  82. package/src/test/doubles.tsx +82 -0
  83. package/src/test/index.ts +34 -0
  84. package/src/test/primitives.tsx +35 -0
  85. package/src/use-process-explorer/index.ts +8 -0
  86. package/src/use-process-explorer/use-process-explorer.test.ts +564 -0
  87. package/src/use-process-explorer/use-process-explorer.ts +540 -0
package/dist/index.js ADDED
@@ -0,0 +1,3146 @@
1
+ "use client";
2
+
3
+ // src/core/scale.ts
4
+ function ascending(a, b) {
5
+ return a - b;
6
+ }
7
+ function minMax(values) {
8
+ let min = Number.POSITIVE_INFINITY;
9
+ let max = Number.NEGATIVE_INFINITY;
10
+ let seen = false;
11
+ for (const v of values) {
12
+ if (!Number.isFinite(v)) continue;
13
+ seen = true;
14
+ if (v < min) min = v;
15
+ if (v > max) max = v;
16
+ }
17
+ return seen ? [min, max] : [0, 0];
18
+ }
19
+ function quantileSorted(sorted, q) {
20
+ const n = sorted.length;
21
+ if (n === 0) return 0;
22
+ if (n === 1) return sorted[0];
23
+ const clamped = q < 0 ? 0 : q > 1 ? 1 : q;
24
+ const pos = clamped * (n - 1);
25
+ const lo = Math.floor(pos);
26
+ const hi = Math.ceil(pos);
27
+ const loValue = sorted[lo];
28
+ if (lo === hi) return loValue;
29
+ const hiValue = sorted[hi];
30
+ return loValue + (hiValue - loValue) * (pos - lo);
31
+ }
32
+
33
+ // src/core/duration-stats.ts
34
+ var TRIM_FRACTION = 0.1;
35
+ var EMPTY_DURATION_STATS = Object.freeze({
36
+ min: 0,
37
+ max: 0,
38
+ mean: 0,
39
+ median: 0,
40
+ p90: 0,
41
+ sum: 0,
42
+ trimmedMean: 0
43
+ });
44
+ function emptyDurationStats() {
45
+ return { ...EMPTY_DURATION_STATS };
46
+ }
47
+ function durationStats(samples) {
48
+ const sorted = [];
49
+ for (const s of samples) if (Number.isFinite(s)) sorted.push(s);
50
+ const n = sorted.length;
51
+ if (n === 0) return emptyDurationStats();
52
+ sorted.sort(ascending);
53
+ let sum = 0;
54
+ for (const s of sorted) sum += s;
55
+ const trim = Math.floor(n * TRIM_FRACTION);
56
+ const lo = trim;
57
+ const hi = n - trim;
58
+ let trimmedMean;
59
+ if (hi - lo <= 0) {
60
+ trimmedMean = sum / n;
61
+ } else {
62
+ let trimmedSum = 0;
63
+ for (let i = lo; i < hi; i += 1) trimmedSum += sorted[i];
64
+ trimmedMean = trimmedSum / (hi - lo);
65
+ }
66
+ return {
67
+ min: sorted[0],
68
+ max: sorted[n - 1],
69
+ mean: sum / n,
70
+ median: quantileSorted(sorted, 0.5),
71
+ p90: quantileSorted(sorted, 0.9),
72
+ sum,
73
+ trimmedMean
74
+ };
75
+ }
76
+ var DURATION_SAMPLE_CAP = 4096;
77
+ function mulberry32(seed) {
78
+ let a = seed >>> 0;
79
+ return () => {
80
+ a = a + 1831565813 >>> 0;
81
+ let t = a;
82
+ t = Math.imul(t ^ t >>> 15, t | 1);
83
+ t ^= t + Math.imul(t ^ t >>> 7, t | 61);
84
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
85
+ };
86
+ }
87
+ var DurationSampler = class {
88
+ capacity;
89
+ random;
90
+ reservoir = [];
91
+ /** Number of finite samples offered, including those the reservoir discarded. */
92
+ seen = 0;
93
+ total = 0;
94
+ lowest = Number.POSITIVE_INFINITY;
95
+ highest = Number.NEGATIVE_INFINITY;
96
+ constructor(seed = 2654435769, capacity = DURATION_SAMPLE_CAP) {
97
+ this.capacity = capacity > 0 ? capacity : 1;
98
+ this.random = mulberry32(seed);
99
+ }
100
+ /** Offer one sample. Non-finite values are ignored. */
101
+ add(sample) {
102
+ if (!Number.isFinite(sample)) return;
103
+ this.total += sample;
104
+ if (sample < this.lowest) this.lowest = sample;
105
+ if (sample > this.highest) this.highest = sample;
106
+ if (this.reservoir.length < this.capacity) {
107
+ this.reservoir.push(sample);
108
+ } else {
109
+ const j = Math.floor(this.random() * (this.seen + 1));
110
+ if (j < this.capacity) this.reservoir[j] = sample;
111
+ }
112
+ this.seen += 1;
113
+ }
114
+ /** True number of finite samples offered. */
115
+ get size() {
116
+ return this.seen;
117
+ }
118
+ /**
119
+ * Summarize what was collected. `sum`, `mean`, `min` and `max` are EXACT even past the
120
+ * cap (they are accumulated, not read off the reservoir); the remaining three are
121
+ * computed from the retained sample.
122
+ */
123
+ stats() {
124
+ if (this.seen === 0) return emptyDurationStats();
125
+ const fromReservoir = durationStats(this.reservoir);
126
+ return {
127
+ ...fromReservoir,
128
+ min: this.lowest,
129
+ max: this.highest,
130
+ sum: this.total,
131
+ mean: this.total / this.seen
132
+ };
133
+ }
134
+ };
135
+
136
+ // src/core/event-log.ts
137
+ function isNormalizedLog(log) {
138
+ return Array.isArray(log.cases);
139
+ }
140
+ function asNormalizedLog(log) {
141
+ return isNormalizedLog(log) ? log : normalizeLog(log);
142
+ }
143
+ function toEpochMs(value) {
144
+ if (value === void 0 || value === null) return Number.NaN;
145
+ if (typeof value === "number") return Number.isFinite(value) ? value : Number.NaN;
146
+ if (value instanceof Date) return value.getTime();
147
+ const parsed = Date.parse(value);
148
+ if (!Number.isNaN(parsed)) return parsed;
149
+ const asNumber = Number(value);
150
+ return Number.isFinite(asNumber) && value.trim() !== "" ? asNumber : Number.NaN;
151
+ }
152
+ function byTimestamp(a, b) {
153
+ if (Number.isNaN(a.at) || Number.isNaN(b.at)) return 0;
154
+ return a.at - b.at;
155
+ }
156
+ function normalizeLog(log) {
157
+ const staged = /* @__PURE__ */ new Map();
158
+ const order = [];
159
+ for (const row of log.events) {
160
+ if (!row || typeof row.caseId !== "string" || row.caseId === "") continue;
161
+ if (typeof row.activity !== "string" || row.activity === "") continue;
162
+ let bucket = staged.get(row.caseId);
163
+ if (bucket === void 0) {
164
+ bucket = [];
165
+ staged.set(row.caseId, bucket);
166
+ order.push(row.caseId);
167
+ }
168
+ bucket.push({ at: toEpochMs(row.timestamp), row });
169
+ }
170
+ const cases = [];
171
+ let events = 0;
172
+ for (const caseId of order) {
173
+ const bucket = staged.get(caseId);
174
+ bucket.sort(byTimestamp);
175
+ const instances = [];
176
+ const open = /* @__PURE__ */ new Map();
177
+ for (const { at, row } of bucket) {
178
+ if (row.lifecycle === "start") {
179
+ const index2 = instances.length;
180
+ instances.push(makeInstance(row, at, at, true));
181
+ const queue = open.get(row.activity);
182
+ if (queue === void 0) open.set(row.activity, [index2]);
183
+ else queue.push(index2);
184
+ continue;
185
+ }
186
+ if (row.lifecycle === "complete") {
187
+ const queue = open.get(row.activity);
188
+ const index2 = queue?.shift();
189
+ if (index2 !== void 0) {
190
+ const instance = instances[index2];
191
+ instance.end = at;
192
+ instance.duration = durationOf(instance.start, at);
193
+ instance.isOpen = false;
194
+ if (instance.resource === void 0 && row.resource !== void 0) {
195
+ instance.resource = row.resource;
196
+ }
197
+ if (instance.attributes === void 0 && row.attributes !== void 0) {
198
+ instance.attributes = row.attributes;
199
+ }
200
+ continue;
201
+ }
202
+ }
203
+ const end = at;
204
+ const explicitStart = toEpochMs(row.startTimestamp);
205
+ const start = Number.isNaN(explicitStart) ? end : explicitStart;
206
+ instances.push(makeInstance(row, start, end, false));
207
+ }
208
+ if (instances.length === 0) continue;
209
+ instances.sort(byStart);
210
+ let caseStart = Number.POSITIVE_INFINITY;
211
+ let caseEnd = Number.NEGATIVE_INFINITY;
212
+ for (const instance of instances) {
213
+ if (Number.isFinite(instance.start) && instance.start < caseStart) caseStart = instance.start;
214
+ if (Number.isFinite(instance.end) && instance.end > caseEnd) caseEnd = instance.end;
215
+ }
216
+ const hasExtent = caseStart !== Number.POSITIVE_INFINITY && caseEnd !== Number.NEGATIVE_INFINITY;
217
+ const normalizedCase = {
218
+ caseId,
219
+ events: instances,
220
+ start: hasExtent ? caseStart : Number.NaN,
221
+ end: hasExtent ? caseEnd : Number.NaN,
222
+ duration: hasExtent ? durationOf(caseStart, caseEnd) : Number.NaN
223
+ };
224
+ const caseAttributes = log.caseAttributes?.[caseId];
225
+ if (caseAttributes !== void 0) normalizedCase.attributes = caseAttributes;
226
+ cases.push(normalizedCase);
227
+ events += instances.length;
228
+ }
229
+ return { cases, totals: { cases: cases.length, events } };
230
+ }
231
+ function byStart(a, b) {
232
+ if (Number.isNaN(a.start) || Number.isNaN(b.start)) return 0;
233
+ return a.start - b.start;
234
+ }
235
+ function durationOf(start, end) {
236
+ const delta = end - start;
237
+ if (!Number.isFinite(delta)) return Number.NaN;
238
+ return delta < 0 ? 0 : delta;
239
+ }
240
+ function makeInstance(row, start, end, isOpen) {
241
+ const instance = {
242
+ activity: row.activity,
243
+ start,
244
+ end,
245
+ duration: durationOf(start, end),
246
+ isOpen
247
+ };
248
+ if (row.resource !== void 0) instance.resource = row.resource;
249
+ if (row.attributes !== void 0) instance.attributes = row.attributes;
250
+ return instance;
251
+ }
252
+
253
+ // src/core/extract-variants.ts
254
+ var VARIANT_KEY_SEPARATOR = "";
255
+ function variantKey(sequence) {
256
+ return sequence.join(VARIANT_KEY_SEPARATOR);
257
+ }
258
+ function fnv1a32(text, basis) {
259
+ let hash = basis >>> 0;
260
+ for (let i = 0; i < text.length; i += 1) {
261
+ hash ^= text.charCodeAt(i);
262
+ hash = Math.imul(hash, 16777619) >>> 0;
263
+ }
264
+ return hash >>> 0;
265
+ }
266
+ function hex8(value) {
267
+ return (value >>> 0).toString(16).padStart(8, "0");
268
+ }
269
+ function variantId(sequence) {
270
+ const key = variantKey(sequence);
271
+ return `v${hex8(fnv1a32(key, 2166136261))}${hex8(fnv1a32(key, 2216829733))}`;
272
+ }
273
+ function extractVariants(log) {
274
+ const normalized = asNormalizedLog(log);
275
+ const totalCases = normalized.cases.length;
276
+ if (totalCases === 0) return [];
277
+ const groups = /* @__PURE__ */ new Map();
278
+ for (const kase of normalized.cases) {
279
+ const sequence = sequenceOf(kase);
280
+ const key = variantKey(sequence);
281
+ let group = groups.get(key);
282
+ if (group === void 0) {
283
+ group = { key, sequence, caseIds: [], durations: [] };
284
+ groups.set(key, group);
285
+ }
286
+ group.caseIds.push(kase.caseId);
287
+ group.durations.push(kase.duration);
288
+ }
289
+ const ranked = [...groups.values()].sort(
290
+ (a, b) => b.caseIds.length - a.caseIds.length || (a.key < b.key ? -1 : a.key > b.key ? 1 : 0)
291
+ );
292
+ const taken = /* @__PURE__ */ new Set();
293
+ const variants = [];
294
+ let cumulativeCount = 0;
295
+ for (const group of ranked) {
296
+ const count = group.caseIds.length;
297
+ cumulativeCount += count;
298
+ let id = variantId(group.sequence);
299
+ if (taken.has(id)) {
300
+ let suffix = 1;
301
+ while (taken.has(`${id}-${suffix}`)) suffix += 1;
302
+ id = `${id}-${suffix}`;
303
+ }
304
+ taken.add(id);
305
+ variants.push({
306
+ id,
307
+ sequence: group.sequence,
308
+ count,
309
+ share: count / totalCases,
310
+ cumulativeShare: cumulativeCount / totalCases,
311
+ caseIds: group.caseIds,
312
+ duration: durationStats(group.durations)
313
+ });
314
+ }
315
+ return variants;
316
+ }
317
+ function sequenceOf(kase) {
318
+ const sequence = new Array(kase.events.length);
319
+ for (let i = 0; i < kase.events.length; i += 1) {
320
+ sequence[i] = kase.events[i].activity;
321
+ }
322
+ return sequence;
323
+ }
324
+
325
+ // src/core/discover-graph.ts
326
+ var EDGE_KEY_SEPARATOR = VARIANT_KEY_SEPARATOR;
327
+ function discoverGraph(log, options = {}) {
328
+ const flowTime = options.flowTime ?? "idle_time";
329
+ const capacity = options.maxDurationSamples ?? DURATION_SAMPLE_CAP;
330
+ const normalized = asNormalizedLog(log);
331
+ const activities = /* @__PURE__ */ new Map();
332
+ const transitions = /* @__PURE__ */ new Map();
333
+ const startActivities = /* @__PURE__ */ new Map();
334
+ const endActivities = /* @__PURE__ */ new Map();
335
+ const variantKeys = /* @__PURE__ */ new Set();
336
+ let samplerIndex = 0;
337
+ const nextSeed = () => {
338
+ samplerIndex += 1;
339
+ return (2654435769 ^ Math.imul(samplerIndex, 2654435761)) >>> 0;
340
+ };
341
+ let events = 0;
342
+ const seenActivities = /* @__PURE__ */ new Set();
343
+ const seenEdges = /* @__PURE__ */ new Set();
344
+ for (const kase of normalized.cases) {
345
+ const trace = kase.events;
346
+ if (trace.length === 0) continue;
347
+ events += trace.length;
348
+ seenActivities.clear();
349
+ seenEdges.clear();
350
+ const sequence = new Array(trace.length);
351
+ for (let i = 0; i < trace.length; i += 1) {
352
+ const event = trace[i];
353
+ const name = event.activity;
354
+ sequence[i] = name;
355
+ let activity = activities.get(name);
356
+ if (activity === void 0) {
357
+ activity = { instances: 0, cases: 0, duration: new DurationSampler(nextSeed(), capacity) };
358
+ activities.set(name, activity);
359
+ }
360
+ activity.instances += 1;
361
+ activity.duration.add(event.duration);
362
+ if (!seenActivities.has(name)) {
363
+ seenActivities.add(name);
364
+ activity.cases += 1;
365
+ }
366
+ if (i === 0) continue;
367
+ const previous = trace[i - 1];
368
+ const source = previous.activity;
369
+ const key = `${source}${EDGE_KEY_SEPARATOR}${name}`;
370
+ let edge = transitions.get(key);
371
+ if (edge === void 0) {
372
+ edge = {
373
+ source,
374
+ target: name,
375
+ count: 0,
376
+ caseCount: 0,
377
+ duration: new DurationSampler(nextSeed(), capacity)
378
+ };
379
+ transitions.set(key, edge);
380
+ }
381
+ edge.count += 1;
382
+ edge.duration.add(
383
+ flowTime === "inter_start_time" ? event.start - previous.start : event.start - previous.end
384
+ );
385
+ if (!seenEdges.has(key)) {
386
+ seenEdges.add(key);
387
+ edge.caseCount += 1;
388
+ }
389
+ }
390
+ const first = trace[0];
391
+ const last = trace[trace.length - 1];
392
+ startActivities.set(first.activity, (startActivities.get(first.activity) ?? 0) + 1);
393
+ endActivities.set(last.activity, (endActivities.get(last.activity) ?? 0) + 1);
394
+ variantKeys.add(sequence.join(VARIANT_KEY_SEPARATOR));
395
+ }
396
+ const activityList = [];
397
+ for (const [id, accumulator] of activities) {
398
+ activityList.push({
399
+ id,
400
+ label: id,
401
+ instances: accumulator.instances,
402
+ cases: accumulator.cases,
403
+ isStart: startActivities.has(id),
404
+ isEnd: endActivities.has(id),
405
+ duration: accumulator.duration.stats()
406
+ });
407
+ }
408
+ activityList.sort((a, b) => b.instances - a.instances || compareStrings(a.id, b.id));
409
+ const transitionList = [];
410
+ for (const accumulator of transitions.values()) {
411
+ transitionList.push({
412
+ source: accumulator.source,
413
+ target: accumulator.target,
414
+ count: accumulator.count,
415
+ caseCount: accumulator.caseCount,
416
+ duration: accumulator.duration.stats(),
417
+ isSelfLoop: accumulator.source === accumulator.target,
418
+ isBackEdge: false
419
+ });
420
+ }
421
+ transitionList.sort(
422
+ (a, b) => b.count - a.count || compareStrings(a.source, b.source) || compareStrings(a.target, b.target)
423
+ );
424
+ return {
425
+ activities: activityList,
426
+ transitions: transitionList,
427
+ startActivities: toSortedRecord(startActivities),
428
+ endActivities: toSortedRecord(endActivities),
429
+ totals: { cases: normalized.cases.length, events, variants: variantKeys.size }
430
+ };
431
+ }
432
+ function compareStrings(a, b) {
433
+ return a < b ? -1 : a > b ? 1 : 0;
434
+ }
435
+ function toSortedRecord(counts) {
436
+ const out = {};
437
+ for (const key of [...counts.keys()].sort()) out[key] = counts.get(key);
438
+ return out;
439
+ }
440
+
441
+ // src/core/aggregate-performance.ts
442
+ var DURATION_UNIT_MS = Object.freeze({
443
+ ms: 1,
444
+ s: 1e3,
445
+ min: 6e4,
446
+ h: 36e5,
447
+ d: 864e5
448
+ });
449
+ function performanceValue(stats, agg) {
450
+ switch (agg) {
451
+ case "mean":
452
+ return stats.mean;
453
+ case "min":
454
+ return stats.min;
455
+ case "max":
456
+ return stats.max;
457
+ case "sum":
458
+ return stats.sum;
459
+ case "p90":
460
+ return stats.p90;
461
+ case "trimmed_mean":
462
+ return stats.trimmedMean;
463
+ case "median":
464
+ default:
465
+ return stats.median;
466
+ }
467
+ }
468
+
469
+ // src/process-map/map-model.ts
470
+ var PROCESS_FILTER_INTENT_KINDS = ["with", "without", "startsWith", "endsWith"];
471
+ var PROCESS_FILTER_INTENT_LABELS = Object.freeze({
472
+ with: "Keep cases containing",
473
+ without: "Keep cases without",
474
+ startsWith: "Keep cases starting with",
475
+ endsWith: "Keep cases ending with"
476
+ });
477
+ var PROCESS_FILTER_INTENT_MESSAGE_KEYS = Object.freeze({
478
+ with: "process.map.filterIntentWith",
479
+ without: "process.map.filterIntentWithout",
480
+ startsWith: "process.map.filterIntentStartsWith",
481
+ endsWith: "process.map.filterIntentEndsWith"
482
+ });
483
+ var GHOST_OPACITY = 0.35;
484
+ function resolveActivityFrequencyMode(mode) {
485
+ switch (mode) {
486
+ case "absolute_case":
487
+ return "absolute_case";
488
+ case "relative":
489
+ return "relative";
490
+ case "relative_case":
491
+ case "relative_antecedent":
492
+ case "relative_consequent":
493
+ return "relative_case";
494
+ case "max_repetitions":
495
+ case "absolute":
496
+ default:
497
+ return "absolute";
498
+ }
499
+ }
500
+ function resolveTransitionFrequencyMode(mode) {
501
+ return mode === "max_repetitions" ? "absolute" : mode;
502
+ }
503
+ var PERFORMANCE_AGGS = /* @__PURE__ */ new Set([
504
+ "median",
505
+ "mean",
506
+ "min",
507
+ "max",
508
+ "sum",
509
+ "p90",
510
+ "trimmed_mean"
511
+ ]);
512
+ function isPerformanceMetric(metric) {
513
+ return PERFORMANCE_AGGS.has(metric);
514
+ }
515
+ var PERFORMANCE_AGG_LABELS = Object.freeze({
516
+ median: "Median duration",
517
+ mean: "Mean duration",
518
+ min: "Fastest",
519
+ max: "Slowest",
520
+ sum: "Total duration",
521
+ p90: "90th percentile duration",
522
+ trimmed_mean: "Trimmed mean duration"
523
+ });
524
+ var ACTIVITY_FREQUENCY_LABELS = Object.freeze({
525
+ absolute: "Occurrences",
526
+ absolute_case: "Cases",
527
+ relative: "Share of events",
528
+ relative_case: "Share of cases"
529
+ });
530
+ var TRANSITION_FREQUENCY_LABELS = Object.freeze({
531
+ absolute: "Transitions",
532
+ absolute_case: "Cases",
533
+ relative: "Share of transitions",
534
+ relative_case: "Share of cases",
535
+ relative_antecedent: "Share of source traffic",
536
+ relative_consequent: "Share of target traffic"
537
+ });
538
+ function nodeMetricLabel(metric) {
539
+ return isPerformanceMetric(metric) ? PERFORMANCE_AGG_LABELS[metric] : ACTIVITY_FREQUENCY_LABELS[resolveActivityFrequencyMode(metric)];
540
+ }
541
+ function edgeMetricLabel(metric) {
542
+ return isPerformanceMetric(metric) ? PERFORMANCE_AGG_LABELS[metric] : TRANSITION_FREQUENCY_LABELS[resolveTransitionFrequencyMode(metric)];
543
+ }
544
+ function isShare(metric) {
545
+ if (isPerformanceMetric(metric)) return false;
546
+ const resolved = resolveTransitionFrequencyMode(metric);
547
+ return resolved !== "absolute" && resolved !== "absolute_case";
548
+ }
549
+ function activityMetricValue(activity, metric, totals) {
550
+ if (isPerformanceMetric(metric)) return performanceValue(activity.duration, metric);
551
+ switch (resolveActivityFrequencyMode(metric)) {
552
+ case "absolute_case":
553
+ return activity.cases;
554
+ case "relative":
555
+ return totals.events === 0 ? 0 : activity.instances / totals.events;
556
+ case "relative_case":
557
+ return totals.cases === 0 ? 0 : activity.cases / totals.cases;
558
+ case "absolute":
559
+ default:
560
+ return activity.instances;
561
+ }
562
+ }
563
+ function processEdgeDenominators(transitions) {
564
+ const outgoing = /* @__PURE__ */ new Map();
565
+ const incoming = /* @__PURE__ */ new Map();
566
+ let total = 0;
567
+ for (const t of transitions) {
568
+ total += t.count;
569
+ outgoing.set(t.source, (outgoing.get(t.source) ?? 0) + t.count);
570
+ incoming.set(t.target, (incoming.get(t.target) ?? 0) + t.count);
571
+ }
572
+ return { total, outgoing, incoming };
573
+ }
574
+ function ratio(numerator, denominator) {
575
+ return denominator === 0 ? 0 : numerator / denominator;
576
+ }
577
+ function transitionMetricValue(transition, metric, totals, denominators) {
578
+ if (isPerformanceMetric(metric)) return performanceValue(transition.duration, metric);
579
+ switch (resolveTransitionFrequencyMode(metric)) {
580
+ case "absolute_case":
581
+ return transition.caseCount;
582
+ case "relative":
583
+ return ratio(transition.count, denominators.total);
584
+ case "relative_case":
585
+ return ratio(transition.caseCount, totals.cases);
586
+ case "relative_antecedent":
587
+ return ratio(transition.count, denominators.outgoing.get(transition.source) ?? 0);
588
+ case "relative_consequent":
589
+ return ratio(transition.count, denominators.incoming.get(transition.target) ?? 0);
590
+ case "absolute":
591
+ default:
592
+ return transition.count;
593
+ }
594
+ }
595
+ var DURATION_LADDER = [
596
+ [864e5, "d"],
597
+ [36e5, "h"],
598
+ [6e4, "min"],
599
+ [1e3, "s"]
600
+ ];
601
+ function formatDurationMs(ms) {
602
+ if (!Number.isFinite(ms) || ms < 0) return "\u2014";
603
+ if (ms === 0) return "0 s";
604
+ for (const [size, unit] of DURATION_LADDER) {
605
+ if (ms >= size) {
606
+ const scaled = ms / size;
607
+ return `${scaled < 10 ? scaled.toFixed(1) : Math.round(scaled)} ${unit}`;
608
+ }
609
+ }
610
+ return `${Math.round(ms)} ms`;
611
+ }
612
+ function formatMetricValue(value, metric) {
613
+ if (isPerformanceMetric(metric)) return formatDurationMs(value);
614
+ if (!Number.isFinite(value)) return "\u2014";
615
+ if (isShare(metric)) return `${(value * 100).toFixed(1)}%`;
616
+ return value.toLocaleString();
617
+ }
618
+ function processEdgeId(source, target) {
619
+ return `${source}${EDGE_KEY_SEPARATOR}${target}`;
620
+ }
621
+ function processGraphStructureKey(graph) {
622
+ const activities = graph.activities.map((a) => a.id).sort();
623
+ const transitions = graph.transitions.map((t) => processEdgeId(t.source, t.target)).sort();
624
+ return `${activities.join(EDGE_KEY_SEPARATOR)}|${transitions.join(EDGE_KEY_SEPARATOR)}`;
625
+ }
626
+ function selectionNeighbourhood(graph, selection) {
627
+ if (!selection) return null;
628
+ const activities = /* @__PURE__ */ new Set();
629
+ const transitions = /* @__PURE__ */ new Set();
630
+ if (selection.kind === "activity") {
631
+ activities.add(selection.id);
632
+ for (const t of graph.transitions) {
633
+ if (t.source !== selection.id && t.target !== selection.id) continue;
634
+ transitions.add(processEdgeId(t.source, t.target));
635
+ activities.add(t.source);
636
+ activities.add(t.target);
637
+ }
638
+ return { activities, transitions };
639
+ }
640
+ for (const t of graph.transitions) {
641
+ if (processEdgeId(t.source, t.target) !== selection.id) continue;
642
+ transitions.add(selection.id);
643
+ activities.add(t.source);
644
+ activities.add(t.target);
645
+ }
646
+ return { activities, transitions };
647
+ }
648
+ function resolveSelectionState(id, kind, selection, neighbourhood, states) {
649
+ if (selection && selection.kind === kind && selection.id === id) return "selected";
650
+ const namespace = kind === "activity" ? states?.activities : states?.transitions;
651
+ const filterState = namespace?.[id];
652
+ if (filterState === "excluded") return "excluded";
653
+ if (selection && neighbourhood) {
654
+ const kept = kind === "activity" ? neighbourhood.activities : neighbourhood.transitions;
655
+ if (!kept.has(id)) return "excluded";
656
+ }
657
+ if (filterState !== void 0) return filterState;
658
+ return "associated";
659
+ }
660
+ function buildProcessMapModel({
661
+ graph,
662
+ metric,
663
+ rework,
664
+ selection,
665
+ selectionStates,
666
+ backEdgeIds
667
+ }) {
668
+ const neighbourhood = selectionNeighbourhood(graph, selection);
669
+ const denominators = processEdgeDenominators(graph.transitions);
670
+ const nodeValues = graph.activities.map((a) => activityMetricValue(a, metric.node, graph.totals));
671
+ const nodeDomain = minMax(nodeValues);
672
+ const edgeValues = graph.transitions.map(
673
+ (t) => transitionMetricValue(t, metric.edge, graph.totals, denominators)
674
+ );
675
+ const edgeDomain = minMax(edgeValues);
676
+ const resolvedNodeMetricLabel = nodeMetricLabel(metric.node);
677
+ const resolvedEdgeMetricLabel = edgeMetricLabel(metric.edge);
678
+ const nodeSpan = nodeDomain[1] - nodeDomain[0];
679
+ const nodes = graph.activities.map((activity, index2) => {
680
+ const primaryValue = nodeValues[index2];
681
+ const secondaryLabel = metric.secondary === void 0 ? void 0 : formatMetricValue(
682
+ activityMetricValue(activity, metric.secondary, graph.totals),
683
+ metric.secondary
684
+ );
685
+ const reworkEntry = rework?.perActivity[activity.id];
686
+ const reworkCount = reworkEntry === void 0 ? void 0 : reworkEntry.selfLoops + reworkEntry.loops;
687
+ const selectionState = resolveSelectionState(
688
+ activity.id,
689
+ "activity",
690
+ selection,
691
+ neighbourhood,
692
+ selectionStates
693
+ );
694
+ const data = {
695
+ title: activity.label || activity.id,
696
+ metricLabel: resolvedNodeMetricLabel,
697
+ primaryLabel: formatMetricValue(primaryValue, metric.node),
698
+ primaryValue,
699
+ secondaryLabel,
700
+ // A single-activity graph (or a flat metric) has no domain to sit in; half
701
+ // saturation is the honest answer — "no comparison available" — rather than a
702
+ // full-strength fill claiming this is the busiest node in a set of one.
703
+ saturation: nodeSpan === 0 ? 0.5 : (primaryValue - nodeDomain[0]) / nodeSpan,
704
+ isStart: activity.isStart,
705
+ isEnd: activity.isEnd,
706
+ reworkCount,
707
+ selectionState
708
+ };
709
+ return {
710
+ id: activity.id,
711
+ type: "process-activity",
712
+ position: { x: 0, y: 0 },
713
+ data,
714
+ draggable: false,
715
+ // React Flow reads a node's accessible name from the node OBJECT, not from the
716
+ // component — the same seam `withWeightedEdgeAria` exists for on the edge side
717
+ // (#285). Without this the node announces only its id and the metric the map
718
+ // exists to show reaches no assistive technology.
719
+ ariaLabel: activityAriaLabel(data),
720
+ // No `aria-disabled` here even when `selectionState === "excluded"` — an excluded
721
+ // node stays fully operable (clicking it is how a reader filters it back in), and
722
+ // `activityAriaLabel` already appends the word "excluded" to its accessible name, so
723
+ // assistive technology gets the state as real text rather than a lie about
724
+ // disablement. See `ProcessSelectionState`'s own doc comment.
725
+ domAttributes: {
726
+ "data-selection": selectionState,
727
+ "data-activity": activity.id
728
+ }
729
+ };
730
+ });
731
+ const edges = graph.transitions.map((transition, index2) => {
732
+ const value = edgeValues[index2];
733
+ const id = processEdgeId(transition.source, transition.target);
734
+ const secondaryLabel = metric.secondary === void 0 ? void 0 : formatMetricValue(
735
+ transitionMetricValue(transition, metric.secondary, graph.totals, denominators),
736
+ metric.secondary
737
+ );
738
+ const selectionState = resolveSelectionState(
739
+ id,
740
+ "transition",
741
+ selection,
742
+ neighbourhood,
743
+ selectionStates
744
+ );
745
+ const data = {
746
+ source: transition.source,
747
+ target: transition.target,
748
+ weight: value,
749
+ value,
750
+ valueDomain: edgeDomain,
751
+ label: formatMetricValue(value, metric.edge),
752
+ secondaryLabel,
753
+ isSelfLoop: transition.isSelfLoop,
754
+ isBackEdge: backEdgeIds?.has(id) ?? transition.isBackEdge,
755
+ selectionState
756
+ };
757
+ return {
758
+ id,
759
+ source: transition.source,
760
+ target: transition.target,
761
+ type: "process-transition",
762
+ data,
763
+ ariaLabel: transitionAriaLabel(data, resolvedEdgeMetricLabel)
764
+ };
765
+ });
766
+ const activityRows = nodes.map((node) => ({
767
+ id: node.id,
768
+ title: node.data.title,
769
+ primaryLabel: node.data.primaryLabel,
770
+ secondaryLabel: node.data.secondaryLabel,
771
+ reworkCount: node.data.reworkCount,
772
+ role: activityRole(node.data),
773
+ selectionState: node.data.selectionState
774
+ }));
775
+ const transitionRows = edges.map((edge) => {
776
+ const data = edge.data;
777
+ return {
778
+ id: edge.id,
779
+ source: data.source,
780
+ target: data.target,
781
+ primaryLabel: data.label,
782
+ secondaryLabel: data.secondaryLabel,
783
+ shape: transitionShape(data),
784
+ selectionState: data.selectionState
785
+ };
786
+ });
787
+ const excludedCounts = {
788
+ activities: nodes.reduce((n, node) => n + (node.data.selectionState === "excluded" ? 1 : 0), 0),
789
+ totalActivities: nodes.length,
790
+ transitions: edges.reduce(
791
+ (n, edge) => n + (edge.data.selectionState === "excluded" ? 1 : 0),
792
+ 0
793
+ ),
794
+ totalTransitions: edges.length
795
+ };
796
+ return {
797
+ nodes,
798
+ edges,
799
+ nodeDomain,
800
+ edgeDomain,
801
+ nodeMetricLabel: resolvedNodeMetricLabel,
802
+ edgeMetricLabel: resolvedEdgeMetricLabel,
803
+ activityRows,
804
+ transitionRows,
805
+ formatEdgeValue: (value) => formatMetricValue(value, metric.edge),
806
+ excludedCounts
807
+ };
808
+ }
809
+ function activityRole(data) {
810
+ if (data.isStart && data.isEnd) return "Start and end";
811
+ if (data.isStart) return "Start";
812
+ if (data.isEnd) return "End";
813
+ return "Step";
814
+ }
815
+ function transitionShape(data) {
816
+ if (data.isSelfLoop) return "Self-loop";
817
+ if (data.isBackEdge) return "Back edge";
818
+ return "Forward";
819
+ }
820
+ function selectionStateLabel(state) {
821
+ if (state === "selected") return "Selected";
822
+ if (state === "excluded") return "Excluded";
823
+ return "";
824
+ }
825
+ var PROCESS_SELECTION_STATE_MESSAGE_KEYS = Object.freeze({
826
+ selected: "process.map.stateSelected",
827
+ excluded: "process.map.stateExcluded"
828
+ });
829
+ function activityAriaLabel(data) {
830
+ const parts = [`${data.title} \u2014 ${activityRole(data).toLowerCase()}`];
831
+ parts.push(`${data.metricLabel} ${data.primaryLabel}`);
832
+ if (data.secondaryLabel) parts.push(data.secondaryLabel);
833
+ if (data.reworkCount) parts.push(`${data.reworkCount} repeated executions`);
834
+ if (data.selectionState !== "associated") parts.push(data.selectionState);
835
+ return parts.join(", ");
836
+ }
837
+ function transitionAriaLabel(data, metricLabel) {
838
+ const parts = [
839
+ data.isSelfLoop ? `Self-loop on ${data.source}` : `${data.isBackEdge ? "Back edge" : "Transition"} from ${data.source} to ${data.target}`,
840
+ `${metricLabel} ${data.label}`
841
+ ];
842
+ if (data.secondaryLabel) parts.push(data.secondaryLabel);
843
+ if (data.selectionState !== "associated") parts.push(data.selectionState);
844
+ return parts.join(", ");
845
+ }
846
+
847
+ // src/process-map/process-map.tsx
848
+ import {
849
+ useCallback as useCallback2,
850
+ useEffect as useEffect2,
851
+ useMemo as useMemo4,
852
+ useRef as useRef2,
853
+ useState as useState2
854
+ } from "react";
855
+ import { Filter } from "lucide-react";
856
+ import {
857
+ Button,
858
+ DropdownMenu,
859
+ DropdownMenuContent,
860
+ DropdownMenuItem,
861
+ DropdownMenuLabel,
862
+ DropdownMenuSeparator,
863
+ DropdownMenuTrigger,
864
+ StatePanel,
865
+ Table,
866
+ TableBody,
867
+ TableCaption,
868
+ TableCell,
869
+ TableHead,
870
+ TableHeader,
871
+ TableRow,
872
+ useLocale
873
+ } from "@elabs-ai/components-ui";
874
+ import { cn as cn2 } from "@elabs-ai/components-ui/lib/cn";
875
+ import {
876
+ CanvasShell,
877
+ FlowMiniMap,
878
+ Legend,
879
+ ZoomControls
880
+ } from "@elabs-ai/components-flow";
881
+
882
+ // src/core/abstract-graph.ts
883
+ function edgeKey(source, target) {
884
+ return `${source}${EDGE_KEY_SEPARATOR}${target}`;
885
+ }
886
+ function compareStrings2(a, b) {
887
+ return a < b ? -1 : a > b ? 1 : 0;
888
+ }
889
+ function clampFraction(value) {
890
+ if (!Number.isFinite(value)) return 1;
891
+ return value < 0 ? 0 : value > 1 ? 1 : value;
892
+ }
893
+ function countToKeep(total, fraction, atLeast) {
894
+ if (total === 0) return 0;
895
+ const kept = Math.round(total * fraction);
896
+ return kept < atLeast ? Math.min(atLeast, total) : kept;
897
+ }
898
+ var MinHeap = class {
899
+ nodes = [];
900
+ costs = [];
901
+ get size() {
902
+ return this.nodes.length;
903
+ }
904
+ push(node, cost) {
905
+ this.nodes.push(node);
906
+ this.costs.push(cost);
907
+ let i = this.nodes.length - 1;
908
+ while (i > 0) {
909
+ const parent = i - 1 >> 1;
910
+ if (this.costs[parent] <= this.costs[i]) break;
911
+ this.swap(i, parent);
912
+ i = parent;
913
+ }
914
+ }
915
+ pop() {
916
+ if (this.nodes.length === 0) return void 0;
917
+ const node = this.nodes[0];
918
+ const cost = this.costs[0];
919
+ const lastNode = this.nodes.pop();
920
+ const lastCost = this.costs.pop();
921
+ if (this.nodes.length > 0) {
922
+ this.nodes[0] = lastNode;
923
+ this.costs[0] = lastCost;
924
+ let i = 0;
925
+ for (; ; ) {
926
+ const left = i * 2 + 1;
927
+ const right = left + 1;
928
+ let smallest = i;
929
+ const size = this.nodes.length;
930
+ if (left < size && this.costs[left] < this.costs[smallest]) {
931
+ smallest = left;
932
+ }
933
+ if (right < size && this.costs[right] < this.costs[smallest]) {
934
+ smallest = right;
935
+ }
936
+ if (smallest === i) break;
937
+ this.swap(i, smallest);
938
+ i = smallest;
939
+ }
940
+ }
941
+ return { node, cost };
942
+ }
943
+ swap(a, b) {
944
+ const node = this.nodes[a];
945
+ this.nodes[a] = this.nodes[b];
946
+ this.nodes[b] = node;
947
+ const cost = this.costs[a];
948
+ this.costs[a] = this.costs[b];
949
+ this.costs[b] = cost;
950
+ }
951
+ };
952
+ function abstractGraph(graph, opts) {
953
+ const keepConnected = opts.keepConnected ?? true;
954
+ const invert = opts.invert ?? false;
955
+ const activityFraction = clampFraction(opts.activities);
956
+ const pathFraction = clampFraction(opts.paths);
957
+ const rankedActivities = [...graph.activities].sort(
958
+ (a, b) => b.cases - a.cases || b.instances - a.instances || compareStrings2(a.id, b.id)
959
+ );
960
+ const activityKeepCount = countToKeep(rankedActivities.length, activityFraction, 1);
961
+ const keptActivities = new Set(
962
+ (invert ? rankedActivities.slice(rankedActivities.length - activityKeepCount) : rankedActivities.slice(0, activityKeepCount)).map((activity) => activity.id)
963
+ );
964
+ const candidateEdges = graph.transitions.filter(
965
+ (edge) => keptActivities.has(edge.source) && keptActivities.has(edge.target)
966
+ );
967
+ const rankedEdges = [...candidateEdges].sort(
968
+ (a, b) => b.count - a.count || b.caseCount - a.caseCount || compareStrings2(a.source, b.source) || compareStrings2(a.target, b.target)
969
+ );
970
+ const edgeKeepCount = countToKeep(rankedEdges.length, pathFraction, 0);
971
+ const keptEdges = new Set(
972
+ (invert ? rankedEdges.slice(rankedEdges.length - edgeKeepCount) : rankedEdges.slice(0, edgeKeepCount)).map((edge) => edgeKey(edge.source, edge.target))
973
+ );
974
+ if (keepConnected && keptActivities.size > 0) {
975
+ reconnect(graph, keptActivities, keptEdges);
976
+ }
977
+ const activities = graph.activities.filter(
978
+ (activity) => keptActivities.has(activity.id)
979
+ );
980
+ const transitions = graph.transitions.filter(
981
+ (edge) => keptEdges.has(edgeKey(edge.source, edge.target))
982
+ );
983
+ return {
984
+ activities,
985
+ transitions,
986
+ startActivities: pick(graph.startActivities, keptActivities),
987
+ endActivities: pick(graph.endActivities, keptActivities),
988
+ // Passed through by reference: totals describe the LOG, and abstraction is a view.
989
+ totals: graph.totals,
990
+ hidden: {
991
+ activities: graph.activities.length - activities.length,
992
+ paths: graph.transitions.length - transitions.length
993
+ }
994
+ };
995
+ }
996
+ function pick(record, keep) {
997
+ const out = {};
998
+ for (const name of Object.keys(record)) {
999
+ if (keep.has(name)) out[name] = record[name];
1000
+ }
1001
+ return out;
1002
+ }
1003
+ function reconnect(graph, keptActivities, keptEdges) {
1004
+ const forward = /* @__PURE__ */ new Map();
1005
+ const backward = /* @__PURE__ */ new Map();
1006
+ let maxCount = 1;
1007
+ for (const edge of graph.transitions) {
1008
+ if (edge.count > maxCount) maxCount = edge.count;
1009
+ index(forward, edge.source, edge);
1010
+ index(backward, edge.target, edge);
1011
+ }
1012
+ const logMax = Math.log(maxCount);
1013
+ const cost = (count) => count > 0 ? logMax - Math.log(count) : logMax;
1014
+ const bound = graph.activities.length + graph.transitions.length + 2;
1015
+ for (let round = 0; round < bound; round += 1) {
1016
+ const before = keptActivities.size + keptEdges.size;
1017
+ repairDirection(graph.startActivities, forward, true);
1018
+ repairDirection(graph.endActivities, backward, false);
1019
+ if (keptActivities.size + keptEdges.size === before) break;
1020
+ }
1021
+ function repairDirection(seedCounts, adjacency, downstream) {
1022
+ const allSeeds = Object.keys(seedCounts);
1023
+ if (allSeeds.length === 0) return;
1024
+ if (!allSeeds.some((id) => keptActivities.has(id))) anchor(seedCounts, keptActivities);
1025
+ const keptSeeds = allSeeds.filter((id) => keptActivities.has(id));
1026
+ if (keptSeeds.length === 0) return;
1027
+ const reached = spread(keptSeeds, adjacency, downstream);
1028
+ for (const activity of graph.activities) {
1029
+ if (!keptActivities.has(activity.id) || reached.has(activity.id)) continue;
1030
+ const route = shortestRoute(keptSeeds, activity.id, adjacency, downstream, true) ?? shortestRoute(keptSeeds, activity.id, adjacency, downstream, false) ?? shortestRoute(allSeeds, activity.id, adjacency, downstream, false);
1031
+ if (route === void 0) continue;
1032
+ for (const id of route.activities) keptActivities.add(id);
1033
+ for (const key of route.edges) keptEdges.add(key);
1034
+ for (const id of spread([...route.activities], adjacency, downstream)) reached.add(id);
1035
+ }
1036
+ }
1037
+ function spread(seeds, adjacency, downstream) {
1038
+ const seen = new Set(seeds.filter((id) => keptActivities.has(id)));
1039
+ const queue = [...seen];
1040
+ for (let head = 0; head < queue.length; head += 1) {
1041
+ const node = queue[head];
1042
+ for (const edge of adjacency.get(node) ?? []) {
1043
+ const next = downstream ? edge.target : edge.source;
1044
+ if (!keptActivities.has(next)) continue;
1045
+ if (!keptEdges.has(edgeKey(edge.source, edge.target))) continue;
1046
+ if (seen.has(next)) continue;
1047
+ seen.add(next);
1048
+ queue.push(next);
1049
+ }
1050
+ }
1051
+ return seen;
1052
+ }
1053
+ function shortestRoute(seeds, target, adjacency, downstream, keptOnly) {
1054
+ const distance = /* @__PURE__ */ new Map();
1055
+ const previous = /* @__PURE__ */ new Map();
1056
+ const settled = /* @__PURE__ */ new Set();
1057
+ const heap = new MinHeap();
1058
+ for (const seed of seeds) {
1059
+ distance.set(seed, 0);
1060
+ heap.push(seed, 0);
1061
+ }
1062
+ while (heap.size > 0) {
1063
+ const top = heap.pop();
1064
+ if (settled.has(top.node)) continue;
1065
+ settled.add(top.node);
1066
+ if (top.node === target) break;
1067
+ for (const edge of adjacency.get(top.node) ?? []) {
1068
+ const next = downstream ? edge.target : edge.source;
1069
+ if (next === top.node) continue;
1070
+ if (keptOnly && !keptActivities.has(next)) continue;
1071
+ const candidate = top.cost + cost(edge.count);
1072
+ const known = distance.get(next);
1073
+ if (known !== void 0 && known <= candidate) continue;
1074
+ distance.set(next, candidate);
1075
+ previous.set(next, edge);
1076
+ heap.push(next, candidate);
1077
+ }
1078
+ }
1079
+ if (!settled.has(target)) return void 0;
1080
+ const route = { activities: /* @__PURE__ */ new Set([target]), edges: /* @__PURE__ */ new Set() };
1081
+ let cursor = target;
1082
+ for (; ; ) {
1083
+ const edge = previous.get(cursor);
1084
+ if (edge === void 0) break;
1085
+ route.edges.add(edgeKey(edge.source, edge.target));
1086
+ route.activities.add(edge.source);
1087
+ route.activities.add(edge.target);
1088
+ cursor = downstream ? edge.source : edge.target;
1089
+ }
1090
+ return route;
1091
+ }
1092
+ function anchor(counts, kept) {
1093
+ const names = Object.keys(counts);
1094
+ if (names.length === 0) return;
1095
+ if (names.some((name) => kept.has(name))) return;
1096
+ let best = names[0];
1097
+ for (const name of names) {
1098
+ const value = counts[name];
1099
+ const bestValue = counts[best];
1100
+ if (value > bestValue || value === bestValue && compareStrings2(name, best) < 0) best = name;
1101
+ }
1102
+ kept.add(best);
1103
+ }
1104
+ }
1105
+ function index(map, key, edge) {
1106
+ const bucket = map.get(key);
1107
+ if (bucket === void 0) map.set(key, [edge]);
1108
+ else bucket.push(edge);
1109
+ }
1110
+
1111
+ // src/core/detect-rework.ts
1112
+ function detectRework(log) {
1113
+ const normalized = asNormalizedLog(log);
1114
+ const tallies = /* @__PURE__ */ new Map();
1115
+ let selfLoops = 0;
1116
+ let loops = 0;
1117
+ let casesWithRework = 0;
1118
+ const seen = /* @__PURE__ */ new Set();
1119
+ for (const kase of normalized.cases) {
1120
+ seen.clear();
1121
+ let caseHasRework = false;
1122
+ let previous;
1123
+ for (const event of kase.events) {
1124
+ const name = event.activity;
1125
+ let tally = tallies.get(name);
1126
+ if (tally === void 0) {
1127
+ tally = { selfLoops: 0, loops: 0 };
1128
+ tallies.set(name, tally);
1129
+ }
1130
+ if (seen.has(name)) {
1131
+ caseHasRework = true;
1132
+ if (name === previous) {
1133
+ tally.selfLoops += 1;
1134
+ selfLoops += 1;
1135
+ } else {
1136
+ tally.loops += 1;
1137
+ loops += 1;
1138
+ }
1139
+ } else {
1140
+ seen.add(name);
1141
+ }
1142
+ previous = name;
1143
+ }
1144
+ if (caseHasRework) casesWithRework += 1;
1145
+ }
1146
+ const perActivity = {};
1147
+ for (const name of [...tallies.keys()].sort()) {
1148
+ perActivity[name] = tallies.get(name);
1149
+ }
1150
+ return {
1151
+ selfLoops,
1152
+ loops,
1153
+ caseReworkRate: normalized.cases.length === 0 ? 0 : casesWithRework / normalized.cases.length,
1154
+ perActivity
1155
+ };
1156
+ }
1157
+
1158
+ // src/process-map/process-activity-node.tsx
1159
+ import { useMemo } from "react";
1160
+ import { CircleDot, Flag, Play, RefreshCw } from "lucide-react";
1161
+ import { Badge } from "@elabs-ai/components-ui";
1162
+ import { cn } from "@elabs-ai/components-ui/lib/cn";
1163
+ import { FlowNode } from "@elabs-ai/components-flow";
1164
+
1165
+ // src/process-map/process-map-context.ts
1166
+ import { createContext, use } from "react";
1167
+ var EMPTY_PROCESS_MAP_HOVER = Object.freeze({
1168
+ activityId: null,
1169
+ incidentEdgeIds: /* @__PURE__ */ new Set()
1170
+ });
1171
+ var ProcessMapHoverContext = createContext(EMPTY_PROCESS_MAP_HOVER);
1172
+ function useProcessMapHover() {
1173
+ return use(ProcessMapHoverContext);
1174
+ }
1175
+ var NOOP_EDGE_KEY_HANDLER = () => {
1176
+ };
1177
+ var ProcessMapEdgeKeyContext = createContext(NOOP_EDGE_KEY_HANDLER);
1178
+ function useProcessMapEdgeKeys() {
1179
+ return use(ProcessMapEdgeKeyContext);
1180
+ }
1181
+
1182
+ // src/process-map/process-activity-node.tsx
1183
+ import { jsx, jsxs } from "react/jsx-runtime";
1184
+ var GHOST_FRAME_STYLE = {
1185
+ "--border": "var(--border-strong)",
1186
+ "--flow-node": "color-mix(in oklab, var(--card) 60%, var(--surface-muted) 40%)"
1187
+ };
1188
+ function meterFill(saturation) {
1189
+ const percent = Math.round(Math.min(1, Math.max(0, saturation)) * 100);
1190
+ return `color-mix(in oklab, var(--primary) ${percent}%, var(--surface-muted))`;
1191
+ }
1192
+ function roleIcon(isStart, isEnd) {
1193
+ if (isStart && isEnd) return CircleDot;
1194
+ if (isStart) return Play;
1195
+ if (isEnd) return Flag;
1196
+ return void 0;
1197
+ }
1198
+ function ProcessActivityNode(props) {
1199
+ const { data } = props;
1200
+ const hover = useProcessMapHover();
1201
+ const RoleIcon = roleIcon(data.isStart, data.isEnd);
1202
+ const isHovered = hover.activityId === props.id;
1203
+ const isDimmed = data.selectionState === "excluded";
1204
+ const percent = Math.round(Math.min(1, Math.max(0, data.saturation)) * 100);
1205
+ const flowData = useMemo(
1206
+ () => ({
1207
+ title: data.title,
1208
+ kind: data.metricLabel,
1209
+ subtitle: data.secondaryLabel ? `${data.primaryLabel} \xB7 ${data.secondaryLabel}` : data.primaryLabel,
1210
+ icon: RoleIcon ? /* @__PURE__ */ jsx(RoleIcon, { "aria-hidden": "true" }) : void 0,
1211
+ // `tone` is a COLOUR axis in FlowNode. The process map never uses it to carry a
1212
+ // metric — the fill would then be the only channel — so it stays default and the
1213
+ // role/rework signals are carried by the glyph, the badge and the accessible name.
1214
+ tone: "default",
1215
+ // The metric's second, colour-free channel: bar LENGTH. `aria-hidden` because the
1216
+ // same number is already printed in the subtitle above and repeated in the node's
1217
+ // accessible name — a third announcement would be noise, not access. The fill (not
1218
+ // the text) is the one thing here that still dims at the shared ghost rung.
1219
+ footer: /* @__PURE__ */ jsx(
1220
+ "div",
1221
+ {
1222
+ "aria-hidden": "true",
1223
+ "data-slot": "process-activity-node-meter",
1224
+ "data-percent": percent,
1225
+ className: "h-1.5 w-full overflow-hidden rounded-full bg-surface-muted transition-opacity duration-fast ease-standard motion-reduce:transition-none",
1226
+ style: isDimmed ? { opacity: GHOST_OPACITY } : void 0,
1227
+ children: /* @__PURE__ */ jsx(
1228
+ "div",
1229
+ {
1230
+ className: "h-full rounded-full transition-[width] duration-base ease-standard motion-reduce:transition-none",
1231
+ style: { width: `${percent}%`, background: meterFill(data.saturation) }
1232
+ }
1233
+ )
1234
+ }
1235
+ )
1236
+ }),
1237
+ [
1238
+ data.title,
1239
+ data.metricLabel,
1240
+ data.primaryLabel,
1241
+ data.secondaryLabel,
1242
+ data.saturation,
1243
+ RoleIcon,
1244
+ percent,
1245
+ isDimmed
1246
+ ]
1247
+ );
1248
+ return /* @__PURE__ */ jsxs(
1249
+ "div",
1250
+ {
1251
+ "data-slot": "process-activity-node",
1252
+ "data-selection": data.selectionState,
1253
+ "data-role": activityRole(data).toLowerCase(),
1254
+ "data-hover": isHovered ? "true" : void 0,
1255
+ className: cn("relative", isHovered && "z-10"),
1256
+ children: [
1257
+ data.reworkCount ? /* @__PURE__ */ jsxs(
1258
+ Badge,
1259
+ {
1260
+ variant: "warning",
1261
+ "data-slot": "process-activity-node-rework",
1262
+ className: "absolute -end-2 -top-2 z-10 gap-1 px-1.5 py-0 text-meta tabular-nums",
1263
+ children: [
1264
+ /* @__PURE__ */ jsx(RefreshCw, { "aria-hidden": "true", className: "size-3" }),
1265
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: data.reworkCount }),
1266
+ /* @__PURE__ */ jsxs("span", { className: "sr-only", children: [
1267
+ data.reworkCount,
1268
+ " repeated executions"
1269
+ ] })
1270
+ ]
1271
+ }
1272
+ ) : null,
1273
+ /* @__PURE__ */ jsx("div", { "data-slot": "process-activity-node-frame", style: isDimmed ? GHOST_FRAME_STYLE : void 0, children: /* @__PURE__ */ jsx(FlowNode, { ...props, type: "brand", data: flowData }) })
1274
+ ]
1275
+ }
1276
+ );
1277
+ }
1278
+
1279
+ // src/process-map/process-transition-edge.tsx
1280
+ import { useMemo as useMemo2 } from "react";
1281
+ import {
1282
+ FlowSelfLoopEdge,
1283
+ FlowWeightedEdge
1284
+ } from "@elabs-ai/components-flow";
1285
+ import { jsx as jsx2 } from "react/jsx-runtime";
1286
+ var PROCESS_MAP_EDGE_SCALE_GROUP = "process-map";
1287
+ var RESTING_OPACITY = 1;
1288
+ var UNRELATED_OPACITY = 0.25;
1289
+ var EXCLUDED_LABEL_PROPS = { className: "border-dashed", "data-selection": "excluded" };
1290
+ function ProcessTransitionEdge(props) {
1291
+ const { data } = props;
1292
+ const hover = useProcessMapHover();
1293
+ const onEdgeKey = useProcessMapEdgeKeys();
1294
+ const isExcluded = data?.selectionState === "excluded";
1295
+ const labelProps = isExcluded ? EXCLUDED_LABEL_PROPS : void 0;
1296
+ const weightedData = useMemo2(
1297
+ () => ({
1298
+ weight: data?.weight,
1299
+ scaleGroup: PROCESS_MAP_EDGE_SCALE_GROUP,
1300
+ value: data?.value,
1301
+ valueDomain: data?.valueDomain,
1302
+ label: data?.label,
1303
+ secondaryLabel: data?.secondaryLabel,
1304
+ variant: data?.isBackEdge ? "back" : "forward",
1305
+ labelProps
1306
+ }),
1307
+ [
1308
+ data?.weight,
1309
+ data?.value,
1310
+ data?.valueDomain,
1311
+ data?.label,
1312
+ data?.secondaryLabel,
1313
+ data?.isBackEdge,
1314
+ labelProps
1315
+ ]
1316
+ );
1317
+ const selfLoopData = useMemo2(
1318
+ () => ({
1319
+ weight: data?.weight,
1320
+ scaleGroup: PROCESS_MAP_EDGE_SCALE_GROUP,
1321
+ label: data?.label,
1322
+ secondaryLabel: data?.secondaryLabel,
1323
+ labelProps
1324
+ }),
1325
+ [data?.weight, data?.label, data?.secondaryLabel, labelProps]
1326
+ );
1327
+ const opacity = isExcluded ? GHOST_OPACITY : hover.activityId !== null && !hover.incidentEdgeIds.has(props.id) ? UNRELATED_OPACITY : RESTING_OPACITY;
1328
+ return /* @__PURE__ */ jsx2(
1329
+ "g",
1330
+ {
1331
+ "data-slot": "process-transition-edge",
1332
+ "data-shape": data?.isSelfLoop ? "self-loop" : data?.isBackEdge ? "back" : "forward",
1333
+ "data-selection": data?.selectionState,
1334
+ "data-incident": hover.incidentEdgeIds.has(props.id) ? "true" : void 0,
1335
+ className: "transition-opacity duration-fast ease-standard motion-reduce:transition-none",
1336
+ style: { opacity },
1337
+ onKeyDown: (event) => onEdgeKey(props.id, event),
1338
+ children: data?.isSelfLoop ? /* @__PURE__ */ jsx2(FlowSelfLoopEdge, { ...props, type: "self-loop", data: selfLoopData }) : /* @__PURE__ */ jsx2(FlowWeightedEdge, { ...props, type: "weighted", data: weightedData })
1339
+ }
1340
+ );
1341
+ }
1342
+
1343
+ // src/process-map/use-process-layout.ts
1344
+ import { useCallback, useEffect, useMemo as useMemo3, useRef, useState } from "react";
1345
+ import { layoutFlow } from "@elabs-ai/components-flow";
1346
+ var NODE_SPACING = 72;
1347
+ var RANK_SPACING = { TB: 72, BT: 72, LR: 120, RL: 120 };
1348
+ var DEFAULT_LAYOUT_DEBOUNCE_MS = 80;
1349
+ var PROCESS_MAP_NODE_MOTION_CLASS = "[&_div[data-id]:not([data-handlepos])]:transition-transform [&_div[data-id]:not([data-handlepos])]:duration-base [&_div[data-id]:not([data-handlepos])]:ease-standard motion-reduce:[&_div[data-id]:not([data-handlepos])]:transition-none";
1350
+ var EMPTY_SET = /* @__PURE__ */ new Set();
1351
+ function toSnapshot(result, durationMs) {
1352
+ const positions = {};
1353
+ const sourcePosition = {};
1354
+ const targetPosition = {};
1355
+ for (const node of result.nodes) {
1356
+ positions[node.id] = node.position;
1357
+ sourcePosition[node.id] = node.sourcePosition;
1358
+ targetPosition[node.id] = node.targetPosition;
1359
+ }
1360
+ return {
1361
+ positions,
1362
+ sourcePosition,
1363
+ targetPosition,
1364
+ backEdges: result.backEdges,
1365
+ selfLoops: result.selfLoops,
1366
+ durationMs
1367
+ };
1368
+ }
1369
+ function applyLayoutSnapshot(nodes, snapshot) {
1370
+ return nodes.map((node) => {
1371
+ const position = snapshot.positions[node.id];
1372
+ if (!position) return node;
1373
+ return {
1374
+ ...node,
1375
+ position,
1376
+ sourcePosition: snapshot.sourcePosition[node.id],
1377
+ targetPosition: snapshot.targetPosition[node.id]
1378
+ };
1379
+ });
1380
+ }
1381
+ function useProcessLayout({
1382
+ nodes,
1383
+ edges,
1384
+ structureKey,
1385
+ direction,
1386
+ debounceMs = DEFAULT_LAYOUT_DEBOUNCE_MS
1387
+ }) {
1388
+ const cacheKey = `${structureKey}::${direction}`;
1389
+ const cache = useRef(/* @__PURE__ */ new Map());
1390
+ const runs = useRef(0);
1391
+ const [applied, setApplied] = useState(
1392
+ null
1393
+ );
1394
+ const compute = useCallback(
1395
+ (key, currentNodes, currentEdges) => {
1396
+ const started = performance.now();
1397
+ const result = layoutFlow(currentNodes, currentEdges, {
1398
+ direction,
1399
+ nodeSpacing: NODE_SPACING,
1400
+ rankSpacing: RANK_SPACING[direction]
1401
+ });
1402
+ const snapshot2 = toSnapshot(result, performance.now() - started);
1403
+ runs.current += 1;
1404
+ cache.current.set(key, snapshot2);
1405
+ setApplied({ key, snapshot: snapshot2 });
1406
+ },
1407
+ [direction]
1408
+ );
1409
+ const latest = useRef({ nodes, edges });
1410
+ latest.current = { nodes, edges };
1411
+ const cachedForKey = cache.current.get(cacheKey);
1412
+ const hasLayout = applied?.key === cacheKey || cachedForKey !== void 0;
1413
+ useEffect(() => {
1414
+ const cached = cache.current.get(cacheKey);
1415
+ if (cached) {
1416
+ setApplied(
1417
+ (current) => current?.key === cacheKey ? current : { key: cacheKey, snapshot: cached }
1418
+ );
1419
+ return;
1420
+ }
1421
+ if (latest.current.nodes.length === 0) {
1422
+ setApplied({
1423
+ key: cacheKey,
1424
+ snapshot: {
1425
+ positions: {},
1426
+ sourcePosition: {},
1427
+ targetPosition: {},
1428
+ backEdges: [],
1429
+ selfLoops: [],
1430
+ durationMs: 0
1431
+ }
1432
+ });
1433
+ return;
1434
+ }
1435
+ if (runs.current === 0 || debounceMs <= 0) {
1436
+ compute(cacheKey, latest.current.nodes, latest.current.edges);
1437
+ return;
1438
+ }
1439
+ const timer = setTimeout(
1440
+ () => compute(cacheKey, latest.current.nodes, latest.current.edges),
1441
+ debounceMs
1442
+ );
1443
+ return () => clearTimeout(timer);
1444
+ }, [cacheKey, compute, debounceMs]);
1445
+ const snapshot = applied?.key === cacheKey ? applied.snapshot : cachedForKey;
1446
+ const positionedNodes = useMemo3(
1447
+ () => snapshot ? applyLayoutSnapshot(nodes, snapshot) : nodes,
1448
+ [nodes, snapshot]
1449
+ );
1450
+ const backEdgeIds = useMemo3(
1451
+ () => snapshot ? new Set(snapshot.backEdges) : EMPTY_SET,
1452
+ [snapshot]
1453
+ );
1454
+ const selfLoopIds = useMemo3(
1455
+ () => snapshot ? new Set(snapshot.selfLoops) : EMPTY_SET,
1456
+ [snapshot]
1457
+ );
1458
+ return {
1459
+ nodes: positionedNodes,
1460
+ backEdgeIds,
1461
+ selfLoopIds,
1462
+ layoutRuns: runs.current,
1463
+ lastLayoutMs: snapshot?.durationMs ?? 0,
1464
+ pending: !hasLayout && nodes.length > 0
1465
+ };
1466
+ }
1467
+
1468
+ // src/process-map/process-map.tsx
1469
+ import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
1470
+ var NODE_TYPES = { "process-activity": ProcessActivityNode };
1471
+ var EDGE_TYPES = { "process-transition": ProcessTransitionEdge };
1472
+ var PROCESS_MAP_LEGIBLE_ZOOM = 0.75;
1473
+ var FIT_VIEW_OPTIONS = {
1474
+ padding: 0.15,
1475
+ maxZoom: 1,
1476
+ minZoom: PROCESS_MAP_LEGIBLE_ZOOM
1477
+ };
1478
+ var MIN_ZOOM = 0.1;
1479
+ function useResolvedGraph(graph, log, abstraction) {
1480
+ const discovered = useMemo4(
1481
+ () => graph ? void 0 : log ? discoverGraph(log) : void 0,
1482
+ [graph, log]
1483
+ );
1484
+ const base = graph ?? discovered;
1485
+ const activities = abstraction?.activities;
1486
+ const paths = abstraction?.paths;
1487
+ return useMemo4(() => {
1488
+ if (!base) return void 0;
1489
+ if (activities === void 0 && paths === void 0) return base;
1490
+ return abstractGraph(base, { activities: activities ?? 1, paths: paths ?? 1 });
1491
+ }, [base, activities, paths]);
1492
+ }
1493
+ function ProcessMap({
1494
+ graph,
1495
+ log,
1496
+ abstraction,
1497
+ metric,
1498
+ rework,
1499
+ selection,
1500
+ selectionStates,
1501
+ onSelect,
1502
+ onFilterIntent,
1503
+ direction = "TB",
1504
+ showMiniMap = true,
1505
+ showLegend = true,
1506
+ tableView = false,
1507
+ loading = false,
1508
+ label,
1509
+ className,
1510
+ ...props
1511
+ }) {
1512
+ const { t } = useLocale();
1513
+ const mapLabel = label ?? t("process.map.label");
1514
+ const selectionStateText = useCallback2(
1515
+ (state) => {
1516
+ const key = PROCESS_SELECTION_STATE_MESSAGE_KEYS[state];
1517
+ return key ? t(key) : "";
1518
+ },
1519
+ [t]
1520
+ );
1521
+ const resolved = useResolvedGraph(graph, log, abstraction);
1522
+ const derivedRework = useMemo4(
1523
+ () => rework ? void 0 : log ? detectRework(log) : void 0,
1524
+ [rework, log]
1525
+ );
1526
+ const activeRework = rework ?? derivedRework;
1527
+ const isControlled = selection !== void 0;
1528
+ const [ownSelection, setOwnSelection] = useState2(null);
1529
+ const activeSelection = isControlled ? selection ?? null : ownSelection;
1530
+ const applySelection = useCallback2(
1531
+ (next) => {
1532
+ if (!isControlled) setOwnSelection(next);
1533
+ onSelect?.(next);
1534
+ },
1535
+ [isControlled, onSelect]
1536
+ );
1537
+ const structureKey = useMemo4(
1538
+ () => resolved ? processGraphStructureKey(resolved) : "",
1539
+ [resolved]
1540
+ );
1541
+ const firstPass = useMemo4(
1542
+ () => resolved ? buildProcessMapModel({
1543
+ graph: resolved,
1544
+ metric,
1545
+ rework: activeRework,
1546
+ selection: activeSelection,
1547
+ selectionStates
1548
+ }) : null,
1549
+ [resolved, metric, activeRework, activeSelection, selectionStates]
1550
+ );
1551
+ const [nodeSizes, setNodeSizes] = useState2({});
1552
+ const handleNodesChange = useCallback2((changes) => {
1553
+ setNodeSizes((current) => {
1554
+ let next = null;
1555
+ for (const change of changes) {
1556
+ if (change.type !== "dimensions" || !change.dimensions) continue;
1557
+ const previous = current[change.id];
1558
+ if (previous?.width === change.dimensions.width && previous.height === change.dimensions.height) {
1559
+ continue;
1560
+ }
1561
+ next ??= { ...current };
1562
+ next[change.id] = { width: change.dimensions.width, height: change.dimensions.height };
1563
+ }
1564
+ return next ?? current;
1565
+ });
1566
+ }, []);
1567
+ const layoutNodes = useMemo4(() => {
1568
+ const source = firstPass?.nodes ?? EMPTY_NODES;
1569
+ let changed = false;
1570
+ const next = source.map((node) => {
1571
+ const measured = nodeSizes[node.id];
1572
+ if (!measured) return node;
1573
+ changed = true;
1574
+ return { ...node, measured };
1575
+ });
1576
+ return changed ? next : source;
1577
+ }, [firstPass, nodeSizes]);
1578
+ const sizeKey = useMemo4(() => {
1579
+ let width = 0;
1580
+ let height = 0;
1581
+ for (const size of Object.values(nodeSizes)) {
1582
+ if (size.width > width) width = size.width;
1583
+ if (size.height > height) height = size.height;
1584
+ }
1585
+ if (width === 0 && height === 0) return "unmeasured";
1586
+ return `${Math.round(width / 8)}x${Math.round(height / 8)}`;
1587
+ }, [nodeSizes]);
1588
+ const layoutKey = `${structureKey}::${sizeKey}`;
1589
+ const layout = useProcessLayout({
1590
+ nodes: layoutNodes,
1591
+ edges: firstPass?.edges ?? EMPTY_EDGES,
1592
+ structureKey: layoutKey,
1593
+ direction
1594
+ });
1595
+ const model = useMemo4(
1596
+ () => resolved ? buildProcessMapModel({
1597
+ graph: resolved,
1598
+ metric,
1599
+ rework: activeRework,
1600
+ selection: activeSelection,
1601
+ selectionStates,
1602
+ backEdgeIds: layout.backEdgeIds
1603
+ }) : null,
1604
+ [resolved, metric, activeRework, activeSelection, selectionStates, layout.backEdgeIds]
1605
+ );
1606
+ const positionedNodes = useMemo4(
1607
+ () => model ? applyPositions(model, layout, direction) : EMPTY_NODES,
1608
+ [model, layout, direction]
1609
+ );
1610
+ const [hover, setHover] = useState2(EMPTY_PROCESS_MAP_HOVER);
1611
+ const edgesRef = useRef2([]);
1612
+ edgesRef.current = model?.edges ?? [];
1613
+ const handleNodeEnter = useCallback2((_event, node) => {
1614
+ const incident = /* @__PURE__ */ new Set();
1615
+ for (const edge of edgesRef.current) {
1616
+ if (edge.source === node.id || edge.target === node.id) incident.add(edge.id);
1617
+ }
1618
+ setHover({ activityId: node.id, incidentEdgeIds: incident });
1619
+ }, []);
1620
+ const handleNodeLeave = useCallback2(() => setHover(EMPTY_PROCESS_MAP_HOVER), []);
1621
+ const [menuTarget, setMenuTarget] = useState2(null);
1622
+ const menuOpen = menuTarget !== null;
1623
+ const returnFocusRef = useRef2(null);
1624
+ const openMenuFor = useCallback2(
1625
+ (target, restoreFocus = null) => {
1626
+ if (!target) return;
1627
+ returnFocusRef.current = restoreFocus;
1628
+ setMenuTarget(target);
1629
+ },
1630
+ []
1631
+ );
1632
+ const menuActivities = useMemo4(() => {
1633
+ if (!menuTarget) return [];
1634
+ if (menuTarget.kind === "activity") return [menuTarget.id];
1635
+ const edge = (model?.edges ?? []).find((e) => e.id === menuTarget.id);
1636
+ if (!edge) return [];
1637
+ return edge.source === edge.target ? [edge.source] : [edge.source, edge.target];
1638
+ }, [menuTarget, model]);
1639
+ const targetOfEvent = useCallback2(
1640
+ (element) => {
1641
+ const focusedId = element?.closest("[data-id]")?.dataset.id;
1642
+ if (!focusedId) return activeSelection;
1643
+ return model?.edges.some((edge) => edge.id === focusedId) ? { kind: "transition", id: focusedId } : { kind: "activity", id: focusedId };
1644
+ },
1645
+ [activeSelection, model]
1646
+ );
1647
+ const handleElementKey = useCallback2(
1648
+ (event, target) => {
1649
+ if (!target) return false;
1650
+ if (event.metaKey || event.ctrlKey || event.altKey) return false;
1651
+ if (event.key === "Enter" || event.key === " ") {
1652
+ applySelection(
1653
+ activeSelection?.kind === target.kind && activeSelection.id === target.id ? null : target
1654
+ );
1655
+ return true;
1656
+ }
1657
+ if (event.key !== "f" && event.key !== "F") return false;
1658
+ event.preventDefault();
1659
+ const active = event.target?.ownerDocument.activeElement;
1660
+ openMenuFor(target, active instanceof HTMLElement ? focusRestorer(active) : null);
1661
+ return true;
1662
+ },
1663
+ [activeSelection, applySelection, openMenuFor]
1664
+ );
1665
+ const handleKeyDown = useCallback2(
1666
+ (event) => {
1667
+ const element = event.target;
1668
+ if (element?.closest("input, textarea, [contenteditable='true']")) return;
1669
+ const onFlowElement = Boolean(element?.closest("[data-id]"));
1670
+ if ((event.key === "Enter" || event.key === " ") && !onFlowElement) return;
1671
+ handleElementKey(event, targetOfEvent(element));
1672
+ },
1673
+ [handleElementKey, targetOfEvent]
1674
+ );
1675
+ const handleEdgeKey = useCallback2(
1676
+ (edgeId, event) => {
1677
+ if (handleElementKey(event, { kind: "transition", id: edgeId })) event.stopPropagation();
1678
+ },
1679
+ [handleElementKey]
1680
+ );
1681
+ const emitIntent = useCallback2(
1682
+ (kind, activity) => {
1683
+ onFilterIntent?.({ kind, activity });
1684
+ setMenuTarget(null);
1685
+ },
1686
+ [onFilterIntent]
1687
+ );
1688
+ useEffect2(() => {
1689
+ if (!menuTarget || !model) return;
1690
+ const stillThere = menuTarget.kind === "activity" ? model.nodes.some((n) => n.id === menuTarget.id) : model.edges.some((e) => e.id === menuTarget.id);
1691
+ if (!stillThere) setMenuTarget(null);
1692
+ }, [menuTarget, model]);
1693
+ if (loading) {
1694
+ return /* @__PURE__ */ jsx3(
1695
+ "div",
1696
+ {
1697
+ "data-slot": "process-map",
1698
+ "data-state": "loading",
1699
+ className: cn2("relative size-full min-h-64", className),
1700
+ ...props,
1701
+ children: /* @__PURE__ */ jsx3(StatePanel, { kind: "loading", title: t("process.map.loading") })
1702
+ }
1703
+ );
1704
+ }
1705
+ if (!model || model.nodes.length === 0) {
1706
+ return /* @__PURE__ */ jsx3(
1707
+ "div",
1708
+ {
1709
+ "data-slot": "process-map",
1710
+ "data-state": "empty",
1711
+ className: cn2("relative size-full min-h-64", className),
1712
+ ...props,
1713
+ children: /* @__PURE__ */ jsx3(
1714
+ StatePanel,
1715
+ {
1716
+ kind: "empty",
1717
+ title: t("process.map.empty"),
1718
+ description: t("process.map.emptyBody")
1719
+ }
1720
+ )
1721
+ }
1722
+ );
1723
+ }
1724
+ const filterMenu = /* @__PURE__ */ jsxs2(
1725
+ DropdownMenu,
1726
+ {
1727
+ modal: false,
1728
+ open: menuOpen,
1729
+ onOpenChange: (open) => {
1730
+ if (!open) setMenuTarget(null);
1731
+ },
1732
+ children: [
1733
+ /* @__PURE__ */ jsx3(DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsxs2(
1734
+ Button,
1735
+ {
1736
+ type: "button",
1737
+ variant: "outline",
1738
+ size: "sm",
1739
+ "data-slot": "process-map-filter-trigger",
1740
+ "aria-keyshortcuts": "f",
1741
+ onClick: () => {
1742
+ if (menuOpen) return;
1743
+ openMenuFor(activeSelection ?? { kind: "activity", id: model.nodes[0].id });
1744
+ },
1745
+ children: [
1746
+ /* @__PURE__ */ jsx3(Filter, { "aria-hidden": "true", className: "size-4" }),
1747
+ t("process.map.filter")
1748
+ ]
1749
+ }
1750
+ ) }),
1751
+ /* @__PURE__ */ jsx3(
1752
+ DropdownMenuContent,
1753
+ {
1754
+ align: "end",
1755
+ "data-slot": "process-map-filter-menu",
1756
+ onCloseAutoFocus: (event) => {
1757
+ const restore = returnFocusRef.current;
1758
+ returnFocusRef.current = null;
1759
+ if (!restore) return;
1760
+ event.preventDefault();
1761
+ restore();
1762
+ },
1763
+ children: menuActivities.map((activity, index2) => /* @__PURE__ */ jsxs2("div", { children: [
1764
+ index2 > 0 ? /* @__PURE__ */ jsx3(DropdownMenuSeparator, {}) : null,
1765
+ /* @__PURE__ */ jsx3(DropdownMenuLabel, { children: activity }),
1766
+ PROCESS_FILTER_INTENT_KINDS.map((kind) => /* @__PURE__ */ jsx3(
1767
+ DropdownMenuItem,
1768
+ {
1769
+ "aria-label": t(PROCESS_FILTER_INTENT_MESSAGE_KEYS[kind], { activity }),
1770
+ onSelect: () => emitIntent(kind, activity),
1771
+ children: PROCESS_FILTER_INTENT_LABELS[kind]
1772
+ },
1773
+ kind
1774
+ ))
1775
+ ] }, activity))
1776
+ }
1777
+ )
1778
+ ]
1779
+ }
1780
+ );
1781
+ const excludedActivityNames = model.activityRows.filter((row) => row.selectionState === "excluded").map((row) => row.title).sort((a, b) => a.localeCompare(b));
1782
+ const excludedTransitionNames = model.transitionRows.filter((row) => row.selectionState === "excluded").map((row) => `${row.source} \u2192 ${row.target}`).sort((a, b) => a.localeCompare(b));
1783
+ const selectionSummary = [
1784
+ t("process.map.excludedActivities", {
1785
+ count: model.excludedCounts.activities,
1786
+ total: model.excludedCounts.totalActivities
1787
+ }),
1788
+ excludedActivityNames.length > 0 ? t("process.map.excludedActivityNames", { names: excludedActivityNames.join(", ") }) : null,
1789
+ t("process.map.excludedTransitions", {
1790
+ count: model.excludedCounts.transitions,
1791
+ total: model.excludedCounts.totalTransitions
1792
+ }),
1793
+ excludedTransitionNames.length > 0 ? t("process.map.excludedTransitionNames", { names: excludedTransitionNames.join(", ") }) : null
1794
+ ].filter((part) => Boolean(part)).join(" \xB7 ");
1795
+ if (tableView) {
1796
+ return /* @__PURE__ */ jsxs2(
1797
+ "div",
1798
+ {
1799
+ "data-slot": "process-map",
1800
+ "data-view": "table",
1801
+ className: cn2("flex size-full flex-col gap-4", className),
1802
+ ...props,
1803
+ children: [
1804
+ /* @__PURE__ */ jsx3(
1805
+ "p",
1806
+ {
1807
+ "data-slot": "process-map-selection-summary",
1808
+ role: "status",
1809
+ "aria-live": "polite",
1810
+ className: "sr-only",
1811
+ children: selectionSummary
1812
+ }
1813
+ ),
1814
+ /* @__PURE__ */ jsx3("div", { className: "flex items-center justify-end", children: filterMenu }),
1815
+ /* @__PURE__ */ jsxs2(Table, { "data-slot": "process-map-activity-table", children: [
1816
+ /* @__PURE__ */ jsx3(TableCaption, { children: t("process.map.activityCaption", { metric: model.nodeMetricLabel.toLowerCase() }) }),
1817
+ /* @__PURE__ */ jsx3(TableHeader, { children: /* @__PURE__ */ jsxs2(TableRow, { children: [
1818
+ /* @__PURE__ */ jsx3(TableHead, { scope: "col", children: t("process.map.columnActivity") }),
1819
+ /* @__PURE__ */ jsx3(TableHead, { scope: "col", children: t("process.map.columnRole") }),
1820
+ /* @__PURE__ */ jsx3(TableHead, { scope: "col", children: model.nodeMetricLabel }),
1821
+ /* @__PURE__ */ jsx3(TableHead, { scope: "col", children: t("process.map.columnRework") }),
1822
+ /* @__PURE__ */ jsx3(TableHead, { scope: "col", children: t("process.map.columnState") })
1823
+ ] }) }),
1824
+ /* @__PURE__ */ jsx3(TableBody, { children: model.activityRows.map((row) => /* @__PURE__ */ jsxs2(
1825
+ TableRow,
1826
+ {
1827
+ "data-selection": row.selectionState,
1828
+ "data-state": row.selectionState === "selected" ? "selected" : void 0,
1829
+ children: [
1830
+ /* @__PURE__ */ jsx3(TableCell, { children: row.title }),
1831
+ /* @__PURE__ */ jsx3(TableCell, { children: row.role }),
1832
+ /* @__PURE__ */ jsx3(TableCell, { className: "tabular-nums", children: row.secondaryLabel ? `${row.primaryLabel} \xB7 ${row.secondaryLabel}` : row.primaryLabel }),
1833
+ /* @__PURE__ */ jsx3(TableCell, { className: "tabular-nums", children: row.reworkCount ?? 0 }),
1834
+ /* @__PURE__ */ jsx3(TableCell, { children: selectionStateText(row.selectionState) })
1835
+ ]
1836
+ },
1837
+ row.id
1838
+ )) })
1839
+ ] }),
1840
+ /* @__PURE__ */ jsxs2(Table, { "data-slot": "process-map-transition-table", children: [
1841
+ /* @__PURE__ */ jsx3(TableCaption, { children: t("process.map.transitionCaption", {
1842
+ metric: model.edgeMetricLabel.toLowerCase()
1843
+ }) }),
1844
+ /* @__PURE__ */ jsx3(TableHeader, { children: /* @__PURE__ */ jsxs2(TableRow, { children: [
1845
+ /* @__PURE__ */ jsx3(TableHead, { scope: "col", children: t("process.map.columnFrom") }),
1846
+ /* @__PURE__ */ jsx3(TableHead, { scope: "col", children: t("process.map.columnTo") }),
1847
+ /* @__PURE__ */ jsx3(TableHead, { scope: "col", children: t("process.map.columnShape") }),
1848
+ /* @__PURE__ */ jsx3(TableHead, { scope: "col", children: model.edgeMetricLabel }),
1849
+ /* @__PURE__ */ jsx3(TableHead, { scope: "col", children: t("process.map.columnState") })
1850
+ ] }) }),
1851
+ /* @__PURE__ */ jsx3(TableBody, { children: model.transitionRows.map((row) => /* @__PURE__ */ jsxs2(
1852
+ TableRow,
1853
+ {
1854
+ "data-selection": row.selectionState,
1855
+ "data-state": row.selectionState === "selected" ? "selected" : void 0,
1856
+ children: [
1857
+ /* @__PURE__ */ jsx3(TableCell, { children: row.source }),
1858
+ /* @__PURE__ */ jsx3(TableCell, { children: row.target }),
1859
+ /* @__PURE__ */ jsx3(TableCell, { children: row.shape }),
1860
+ /* @__PURE__ */ jsx3(TableCell, { className: "tabular-nums", children: row.secondaryLabel ? `${row.primaryLabel} \xB7 ${row.secondaryLabel}` : row.primaryLabel }),
1861
+ /* @__PURE__ */ jsx3(TableCell, { children: selectionStateText(row.selectionState) })
1862
+ ]
1863
+ },
1864
+ row.id
1865
+ )) })
1866
+ ] })
1867
+ ]
1868
+ }
1869
+ );
1870
+ }
1871
+ return /* @__PURE__ */ jsxs2(
1872
+ "div",
1873
+ {
1874
+ "data-slot": "process-map",
1875
+ "data-view": "canvas",
1876
+ "data-direction": direction,
1877
+ className: cn2("relative size-full min-h-64", className),
1878
+ onKeyDown: handleKeyDown,
1879
+ ...props,
1880
+ children: [
1881
+ /* @__PURE__ */ jsx3(
1882
+ "p",
1883
+ {
1884
+ "data-slot": "process-map-selection-summary",
1885
+ role: "status",
1886
+ "aria-live": "polite",
1887
+ className: "sr-only",
1888
+ children: selectionSummary
1889
+ }
1890
+ ),
1891
+ /* @__PURE__ */ jsx3(ProcessMapHoverContext, { value: hover, children: /* @__PURE__ */ jsx3(ProcessMapEdgeKeyContext, { value: handleEdgeKey, children: /* @__PURE__ */ jsxs2(
1892
+ CanvasShell,
1893
+ {
1894
+ nodes: positionedNodes,
1895
+ edges: model.edges,
1896
+ nodeTypes: NODE_TYPES,
1897
+ edgeTypes: EDGE_TYPES,
1898
+ fitViewKey: `${layoutKey}::${direction}::${layout.layoutRuns}`,
1899
+ fitViewKeyOptions: FIT_VIEW_OPTIONS,
1900
+ minZoom: MIN_ZOOM,
1901
+ nodesDraggable: false,
1902
+ nodesConnectable: false,
1903
+ edgesFocusable: false,
1904
+ className: PROCESS_MAP_NODE_MOTION_CLASS,
1905
+ "aria-label": mapLabel,
1906
+ onNodeClick: (_event, node) => applySelection(
1907
+ activeSelection?.kind === "activity" && activeSelection.id === node.id ? null : { kind: "activity", id: node.id }
1908
+ ),
1909
+ onEdgeClick: (_event, edge) => applySelection(
1910
+ activeSelection?.kind === "transition" && activeSelection.id === edge.id ? null : { kind: "transition", id: edge.id }
1911
+ ),
1912
+ onNodeMouseEnter: handleNodeEnter,
1913
+ onNodeMouseLeave: handleNodeLeave,
1914
+ onNodeContextMenu: (event, node) => {
1915
+ event.preventDefault();
1916
+ openMenuFor({ kind: "activity", id: node.id });
1917
+ },
1918
+ onEdgeContextMenu: (event, edge) => {
1919
+ event.preventDefault();
1920
+ openMenuFor({ kind: "transition", id: edge.id });
1921
+ },
1922
+ onPaneClick: () => applySelection(null),
1923
+ onNodesChange: handleNodesChange,
1924
+ children: [
1925
+ /* @__PURE__ */ jsx3(ZoomControls, { position: "bottom-left" }),
1926
+ showMiniMap ? /* @__PURE__ */ jsx3(FlowMiniMap, { pannable: true, zoomable: true }) : null
1927
+ ]
1928
+ }
1929
+ ) }) }),
1930
+ /* @__PURE__ */ jsxs2(
1931
+ "div",
1932
+ {
1933
+ "data-slot": "process-map-top-rail",
1934
+ className: "pointer-events-none absolute inset-x-3 top-3 flex items-start justify-between gap-3",
1935
+ children: [
1936
+ showLegend ? /* @__PURE__ */ jsx3(
1937
+ Legend,
1938
+ {
1939
+ variant: "scale",
1940
+ kind: "width",
1941
+ domain: model.edgeDomain,
1942
+ format: model.formatEdgeValue,
1943
+ title: model.edgeMetricLabel,
1944
+ className: "pointer-events-auto"
1945
+ }
1946
+ ) : /* @__PURE__ */ jsx3("span", {}),
1947
+ /* @__PURE__ */ jsx3("div", { className: "pointer-events-auto", children: filterMenu })
1948
+ ]
1949
+ }
1950
+ )
1951
+ ]
1952
+ }
1953
+ );
1954
+ }
1955
+ function focusRestorer(element) {
1956
+ const flowId = element.closest("[data-id]")?.dataset.id;
1957
+ const slot = element.dataset.slot;
1958
+ const layer = element.closest(".react-flow__edgelabel-renderer");
1959
+ const peers = slot && layer ? [...layer.querySelectorAll(`[data-slot="${slot}"]`)] : [];
1960
+ const index2 = peers.indexOf(element);
1961
+ const root = element.closest('[data-slot="process-map"]');
1962
+ return () => {
1963
+ if (element.isConnected) {
1964
+ element.focus();
1965
+ return;
1966
+ }
1967
+ if (flowId && root) {
1968
+ root.querySelector(`[data-id="${CSS.escape(flowId)}"]`)?.focus();
1969
+ return;
1970
+ }
1971
+ if (slot && index2 >= 0 && layer?.isConnected) {
1972
+ layer.querySelectorAll(`[data-slot="${slot}"]`)[index2]?.focus();
1973
+ }
1974
+ };
1975
+ }
1976
+ var EMPTY_NODES = [];
1977
+ var EMPTY_EDGES = [];
1978
+ function applyPositions(model, layout, direction) {
1979
+ const byId = new Map(layout.nodes.map((node) => [node.id, node]));
1980
+ const positioned = model.nodes.map((node) => {
1981
+ const laidOut = byId.get(node.id);
1982
+ if (!laidOut) return node;
1983
+ return {
1984
+ ...node,
1985
+ position: laidOut.position,
1986
+ sourcePosition: laidOut.sourcePosition,
1987
+ targetPosition: laidOut.targetPosition
1988
+ };
1989
+ });
1990
+ const alongFlow = direction === "LR" || direction === "RL" ? "x" : "y";
1991
+ const acrossFlow = alongFlow === "x" ? "y" : "x";
1992
+ const sign = direction === "BT" || direction === "RL" ? -1 : 1;
1993
+ return positioned.sort((a, b) => {
1994
+ const along = sign * (a.position[alongFlow] - b.position[alongFlow]);
1995
+ if (along !== 0) return along;
1996
+ const across = a.position[acrossFlow] - b.position[acrossFlow];
1997
+ if (across !== 0) return across;
1998
+ return a.id.localeCompare(b.id);
1999
+ });
2000
+ }
2001
+
2002
+ // src/abstraction-controls/abstraction-controls.tsx
2003
+ import { forwardRef, useCallback as useCallback3, useId } from "react";
2004
+ import { Sparkles } from "lucide-react";
2005
+ import { Button as Button2, Label, Slider, Switch, useLocale as useLocale2 } from "@elabs-ai/components-ui";
2006
+ import { cn as cn3 } from "@elabs-ai/components-ui/lib/cn";
2007
+
2008
+ // src/abstraction-controls/auto-abstraction.ts
2009
+ var DEFAULT_MAX_ACTIVITIES = 25;
2010
+ var DEFAULT_MAX_STEPS = 8;
2011
+ var DEFAULT_MIN_FRACTION = 0.05;
2012
+ function keptActivityCount(total, fraction) {
2013
+ return Math.max(1, Math.round(total * fraction));
2014
+ }
2015
+ function computeAutoAbstraction(totalActivities, opts = {}) {
2016
+ const maxActivities = opts.maxActivities ?? DEFAULT_MAX_ACTIVITIES;
2017
+ const maxSteps = opts.maxSteps ?? DEFAULT_MAX_STEPS;
2018
+ const minFraction = opts.minFraction ?? DEFAULT_MIN_FRACTION;
2019
+ const total = totalActivities;
2020
+ if (total === 0 || keptActivityCount(total, 1) <= maxActivities) {
2021
+ return { activities: 1, steps: 0 };
2022
+ }
2023
+ if (keptActivityCount(total, minFraction) > maxActivities) {
2024
+ return { activities: minFraction, steps: 1 };
2025
+ }
2026
+ let lo = minFraction;
2027
+ let hi = 1;
2028
+ let best = minFraction;
2029
+ for (let step = 1; step <= maxSteps; step += 1) {
2030
+ const mid = (lo + hi) / 2;
2031
+ if (keptActivityCount(total, mid) <= maxActivities) {
2032
+ best = mid;
2033
+ lo = mid;
2034
+ } else {
2035
+ hi = mid;
2036
+ }
2037
+ if (step === maxSteps) {
2038
+ return { activities: best, steps: step };
2039
+ }
2040
+ }
2041
+ return { activities: best, steps: maxSteps };
2042
+ }
2043
+
2044
+ // src/abstraction-controls/abstraction-controls.tsx
2045
+ import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
2046
+ var TICKS = [25, 50, 75, 100];
2047
+ var SLIDER_MIN = 0;
2048
+ var SLIDER_MAX = 100;
2049
+ function tickOffsetPercent(percent) {
2050
+ return (percent - SLIDER_MIN) / (SLIDER_MAX - SLIDER_MIN) * 100;
2051
+ }
2052
+ var AUTO_PATHS_OFFSET = 0.2;
2053
+ function toPercent(fraction) {
2054
+ return Math.round(fraction * 100);
2055
+ }
2056
+ function fromPercent(percent) {
2057
+ return percent / 100;
2058
+ }
2059
+ function Tick({
2060
+ percent,
2061
+ active,
2062
+ onSelect
2063
+ }) {
2064
+ const offsetPercent = tickOffsetPercent(percent);
2065
+ return /* @__PURE__ */ jsxs3(
2066
+ "button",
2067
+ {
2068
+ type: "button",
2069
+ "data-slot": "abstraction-controls-tick",
2070
+ "aria-pressed": active,
2071
+ onClick: () => onSelect(percent),
2072
+ style: { left: `${offsetPercent}%` },
2073
+ className: cn3(
2074
+ "focus-ring absolute top-0 rounded-sm text-meta text-muted-foreground transition-colors duration-fast ease-standard hover:text-foreground",
2075
+ offsetPercent <= 0 ? "translate-x-0" : offsetPercent >= 100 ? "-translate-x-full" : "-translate-x-1/2",
2076
+ active && "font-semibold text-foreground"
2077
+ ),
2078
+ children: [
2079
+ percent,
2080
+ "%"
2081
+ ]
2082
+ }
2083
+ );
2084
+ }
2085
+ var AbstractionControls = forwardRef(
2086
+ function AbstractionControls2({
2087
+ abstraction,
2088
+ onAbstractionChange,
2089
+ graph,
2090
+ hiddenCounts,
2091
+ autoMaxActivities = 25,
2092
+ label,
2093
+ className,
2094
+ ...props
2095
+ }, ref) {
2096
+ const { t } = useLocale2();
2097
+ const activitiesId = useId();
2098
+ const pathsId = useId();
2099
+ const activitiesPercent = toPercent(abstraction.activities);
2100
+ const pathsPercent = toPercent(abstraction.paths);
2101
+ const setActivitiesPercent = useCallback3(
2102
+ (percent) => onAbstractionChange({ activities: fromPercent(percent) }),
2103
+ [onAbstractionChange]
2104
+ );
2105
+ const setPathsPercent = useCallback3(
2106
+ (percent) => onAbstractionChange({ paths: fromPercent(percent) }),
2107
+ [onAbstractionChange]
2108
+ );
2109
+ const handleAuto = useCallback3(() => {
2110
+ const totalActivities = graph.activities.length + hiddenCounts.activities;
2111
+ const result = computeAutoAbstraction(totalActivities, {
2112
+ maxActivities: autoMaxActivities
2113
+ });
2114
+ onAbstractionChange({
2115
+ activities: result.activities,
2116
+ paths: Math.min(1, result.activities + AUTO_PATHS_OFFSET)
2117
+ });
2118
+ }, [graph.activities.length, hiddenCounts.activities, autoMaxActivities, onAbstractionChange]);
2119
+ const hiddenSummary = `${t("process.abstractionControls.hiddenActivities", {
2120
+ count: hiddenCounts.activities
2121
+ })} \xB7 ${t("process.abstractionControls.hiddenPaths", { count: hiddenCounts.paths })}`;
2122
+ return /* @__PURE__ */ jsxs3(
2123
+ "div",
2124
+ {
2125
+ ref,
2126
+ "data-slot": "abstraction-controls",
2127
+ role: "group",
2128
+ "aria-label": label ?? t("process.abstractionControls.label"),
2129
+ className: cn3("flex flex-col gap-4", className),
2130
+ ...props,
2131
+ children: [
2132
+ /* @__PURE__ */ jsxs3(
2133
+ "div",
2134
+ {
2135
+ "data-slot": "abstraction-controls-activities",
2136
+ role: "group",
2137
+ "aria-labelledby": `${activitiesId}-label`,
2138
+ className: "flex flex-col gap-1.5",
2139
+ children: [
2140
+ /* @__PURE__ */ jsxs3("div", { className: "flex items-center justify-between gap-2", children: [
2141
+ /* @__PURE__ */ jsx4(Label, { id: `${activitiesId}-label`, htmlFor: activitiesId, className: "text-body", children: t("process.abstractionControls.activities") }),
2142
+ /* @__PURE__ */ jsxs3("span", { className: "text-meta text-muted-foreground tabular-nums", children: [
2143
+ activitiesPercent,
2144
+ "%"
2145
+ ] })
2146
+ ] }),
2147
+ /* @__PURE__ */ jsx4(
2148
+ Slider,
2149
+ {
2150
+ id: activitiesId,
2151
+ min: SLIDER_MIN,
2152
+ max: SLIDER_MAX,
2153
+ step: 1,
2154
+ value: [activitiesPercent],
2155
+ onValueChange: ([next]) => setActivitiesPercent(next ?? activitiesPercent),
2156
+ "aria-label": t("process.abstractionControls.activities")
2157
+ }
2158
+ ),
2159
+ /* @__PURE__ */ jsx4("div", { className: "relative h-4", children: TICKS.map((percent) => /* @__PURE__ */ jsx4(
2160
+ Tick,
2161
+ {
2162
+ percent,
2163
+ active: activitiesPercent === percent,
2164
+ onSelect: setActivitiesPercent
2165
+ },
2166
+ percent
2167
+ )) })
2168
+ ]
2169
+ }
2170
+ ),
2171
+ /* @__PURE__ */ jsxs3(
2172
+ "div",
2173
+ {
2174
+ "data-slot": "abstraction-controls-paths",
2175
+ role: "group",
2176
+ "aria-labelledby": `${pathsId}-label`,
2177
+ className: "flex flex-col gap-1.5",
2178
+ children: [
2179
+ /* @__PURE__ */ jsxs3("div", { className: "flex items-center justify-between gap-2", children: [
2180
+ /* @__PURE__ */ jsx4(Label, { id: `${pathsId}-label`, htmlFor: pathsId, className: "text-body", children: t("process.abstractionControls.paths") }),
2181
+ /* @__PURE__ */ jsxs3("span", { className: "text-meta text-muted-foreground tabular-nums", children: [
2182
+ pathsPercent,
2183
+ "%"
2184
+ ] })
2185
+ ] }),
2186
+ /* @__PURE__ */ jsx4(
2187
+ Slider,
2188
+ {
2189
+ id: pathsId,
2190
+ min: SLIDER_MIN,
2191
+ max: SLIDER_MAX,
2192
+ step: 1,
2193
+ value: [pathsPercent],
2194
+ onValueChange: ([next]) => setPathsPercent(next ?? pathsPercent),
2195
+ "aria-label": t("process.abstractionControls.paths")
2196
+ }
2197
+ ),
2198
+ /* @__PURE__ */ jsx4("div", { className: "relative h-4", children: TICKS.map((percent) => /* @__PURE__ */ jsx4(
2199
+ Tick,
2200
+ {
2201
+ percent,
2202
+ active: pathsPercent === percent,
2203
+ onSelect: setPathsPercent
2204
+ },
2205
+ percent
2206
+ )) })
2207
+ ]
2208
+ }
2209
+ ),
2210
+ /* @__PURE__ */ jsxs3(
2211
+ "div",
2212
+ {
2213
+ "data-slot": "abstraction-controls-footer",
2214
+ className: "flex items-center justify-between gap-3",
2215
+ children: [
2216
+ /* @__PURE__ */ jsxs3("div", { className: "flex items-center gap-2", children: [
2217
+ /* @__PURE__ */ jsx4(
2218
+ Switch,
2219
+ {
2220
+ id: `${activitiesId}-invert`,
2221
+ checked: abstraction.invert ?? false,
2222
+ onCheckedChange: (checked) => onAbstractionChange({ invert: checked })
2223
+ }
2224
+ ),
2225
+ /* @__PURE__ */ jsx4(Label, { htmlFor: `${activitiesId}-invert`, className: "text-body", children: t("process.abstractionControls.invert") })
2226
+ ] }),
2227
+ /* @__PURE__ */ jsxs3(
2228
+ Button2,
2229
+ {
2230
+ type: "button",
2231
+ variant: "outline",
2232
+ size: "sm",
2233
+ onClick: handleAuto,
2234
+ "data-slot": "abstraction-controls-auto",
2235
+ children: [
2236
+ /* @__PURE__ */ jsx4(Sparkles, { "aria-hidden": "true" }),
2237
+ t("process.abstractionControls.auto")
2238
+ ]
2239
+ }
2240
+ )
2241
+ ]
2242
+ }
2243
+ ),
2244
+ /* @__PURE__ */ jsx4(
2245
+ "p",
2246
+ {
2247
+ "data-slot": "abstraction-controls-hidden-summary",
2248
+ role: "status",
2249
+ "aria-live": "polite",
2250
+ className: "text-meta text-muted-foreground",
2251
+ children: hiddenSummary
2252
+ }
2253
+ )
2254
+ ]
2255
+ }
2256
+ );
2257
+ }
2258
+ );
2259
+
2260
+ // src/metric-layer-switch/metric-layer-switch.tsx
2261
+ import { forwardRef as forwardRef2, useCallback as useCallback4, useId as useId2, useState as useState3 } from "react";
2262
+ import { Lock, LockOpen } from "lucide-react";
2263
+ import {
2264
+ Select,
2265
+ SelectContent,
2266
+ SelectItem,
2267
+ SelectTrigger,
2268
+ SelectValue,
2269
+ Toggle,
2270
+ ToggleGroup,
2271
+ ToggleGroupItem,
2272
+ useLocale as useLocale3
2273
+ } from "@elabs-ai/components-ui";
2274
+ import { cn as cn4 } from "@elabs-ai/components-ui/lib/cn";
2275
+ import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
2276
+ var FREQUENCY_NODE_OPTIONS = [
2277
+ "absolute",
2278
+ "absolute_case",
2279
+ "relative",
2280
+ "relative_case"
2281
+ ];
2282
+ var FREQUENCY_EDGE_OPTIONS_LOCKED = FREQUENCY_NODE_OPTIONS;
2283
+ var FREQUENCY_EDGE_OPTIONS_UNLOCKED = [
2284
+ "absolute",
2285
+ "absolute_case",
2286
+ "relative",
2287
+ "relative_case",
2288
+ "relative_antecedent",
2289
+ "relative_consequent"
2290
+ ];
2291
+ var PERFORMANCE_OPTIONS = [
2292
+ "median",
2293
+ "mean",
2294
+ "min",
2295
+ "max",
2296
+ "sum",
2297
+ "p90",
2298
+ "trimmed_mean"
2299
+ ];
2300
+ var MetricLayerSwitch = forwardRef2(
2301
+ function MetricLayerSwitch2({
2302
+ layer,
2303
+ onLayerChange,
2304
+ metric,
2305
+ onMetricChange,
2306
+ locked: lockedProp,
2307
+ defaultLocked = true,
2308
+ onLockedChange,
2309
+ label,
2310
+ className,
2311
+ ...props
2312
+ }, ref) {
2313
+ const { t } = useLocale3();
2314
+ const nodeId = useId2();
2315
+ const edgeId = useId2();
2316
+ const [uncontrolledLocked, setUncontrolledLocked] = useState3(defaultLocked);
2317
+ const locked = lockedProp ?? uncontrolledLocked;
2318
+ const setLocked = useCallback4(
2319
+ (next) => {
2320
+ if (lockedProp === void 0) setUncontrolledLocked(next);
2321
+ onLockedChange?.(next);
2322
+ },
2323
+ [lockedProp, onLockedChange]
2324
+ );
2325
+ const isRework = layer === "rework";
2326
+ const isPerformance = layer === "performance";
2327
+ const nodeOptions = isPerformance ? PERFORMANCE_OPTIONS : FREQUENCY_NODE_OPTIONS;
2328
+ const edgeOptions = isPerformance ? PERFORMANCE_OPTIONS : locked ? FREQUENCY_EDGE_OPTIONS_LOCKED : FREQUENCY_EDGE_OPTIONS_UNLOCKED;
2329
+ const handleLayerChange = useCallback4(
2330
+ (next) => {
2331
+ if (!next) return;
2332
+ const nextLayer = next;
2333
+ onLayerChange(nextLayer);
2334
+ if (nextLayer === "performance" && !isPerformanceMetric(metric.node)) {
2335
+ onMetricChange({ node: "median", edge: "median" });
2336
+ } else if (nextLayer === "frequency" && isPerformanceMetric(metric.node)) {
2337
+ onMetricChange({ node: "absolute", edge: "absolute" });
2338
+ }
2339
+ },
2340
+ [onLayerChange, onMetricChange, metric.node]
2341
+ );
2342
+ const handleNodeChange = useCallback4(
2343
+ (value) => {
2344
+ const next = value;
2345
+ onMetricChange(locked ? { node: next, edge: next } : { node: next });
2346
+ },
2347
+ [onMetricChange, locked]
2348
+ );
2349
+ const handleEdgeChange = useCallback4(
2350
+ (value) => {
2351
+ const next = value;
2352
+ onMetricChange(locked ? { node: next, edge: next } : { edge: next });
2353
+ },
2354
+ [onMetricChange, locked]
2355
+ );
2356
+ const handleLockToggle = useCallback4(
2357
+ (pressed) => {
2358
+ setLocked(pressed);
2359
+ if (pressed && metric.edge !== metric.node) {
2360
+ onMetricChange({ edge: metric.node });
2361
+ }
2362
+ },
2363
+ [setLocked, metric.edge, metric.node, onMetricChange]
2364
+ );
2365
+ return /* @__PURE__ */ jsxs4(
2366
+ "div",
2367
+ {
2368
+ ref,
2369
+ "data-slot": "metric-layer-switch",
2370
+ role: "group",
2371
+ "aria-label": label ?? t("process.metricLayerSwitch.label"),
2372
+ className: cn4("flex flex-col gap-3", className),
2373
+ ...props,
2374
+ children: [
2375
+ /* @__PURE__ */ jsxs4(
2376
+ ToggleGroup,
2377
+ {
2378
+ type: "single",
2379
+ variant: "segmented",
2380
+ value: layer,
2381
+ onValueChange: handleLayerChange,
2382
+ "aria-label": t("process.metricLayerSwitch.layer"),
2383
+ children: [
2384
+ /* @__PURE__ */ jsx5(ToggleGroupItem, { value: "frequency", children: t("process.metricLayerSwitch.frequency") }),
2385
+ /* @__PURE__ */ jsx5(ToggleGroupItem, { value: "performance", children: t("process.metricLayerSwitch.performance") }),
2386
+ /* @__PURE__ */ jsx5(ToggleGroupItem, { value: "rework", children: t("process.metricLayerSwitch.rework") })
2387
+ ]
2388
+ }
2389
+ ),
2390
+ /* @__PURE__ */ jsxs4("div", { className: "flex items-end gap-2", children: [
2391
+ /* @__PURE__ */ jsxs4(
2392
+ "div",
2393
+ {
2394
+ "data-slot": "metric-layer-switch-node",
2395
+ className: "flex min-w-0 flex-1 flex-col gap-1.5",
2396
+ children: [
2397
+ /* @__PURE__ */ jsx5("label", { htmlFor: nodeId, className: "text-meta text-muted-foreground", children: t("process.metricLayerSwitch.node") }),
2398
+ /* @__PURE__ */ jsxs4(Select, { value: metric.node, onValueChange: handleNodeChange, disabled: isRework, children: [
2399
+ /* @__PURE__ */ jsx5(SelectTrigger, { id: nodeId, "aria-label": t("process.metricLayerSwitch.node"), children: /* @__PURE__ */ jsx5(SelectValue, {}) }),
2400
+ /* @__PURE__ */ jsx5(SelectContent, { children: nodeOptions.map((option) => /* @__PURE__ */ jsx5(SelectItem, { value: option, children: nodeMetricLabel(option) }, option)) })
2401
+ ] })
2402
+ ]
2403
+ }
2404
+ ),
2405
+ /* @__PURE__ */ jsx5(
2406
+ Toggle,
2407
+ {
2408
+ pressed: locked,
2409
+ onPressedChange: handleLockToggle,
2410
+ disabled: isRework,
2411
+ "aria-label": locked ? t("process.metricLayerSwitch.lockOn") : t("process.metricLayerSwitch.lockOff"),
2412
+ "data-slot": "metric-layer-switch-lock",
2413
+ children: locked ? /* @__PURE__ */ jsx5(Lock, { "aria-hidden": "true" }) : /* @__PURE__ */ jsx5(LockOpen, { "aria-hidden": "true" })
2414
+ }
2415
+ ),
2416
+ /* @__PURE__ */ jsxs4(
2417
+ "div",
2418
+ {
2419
+ "data-slot": "metric-layer-switch-edge",
2420
+ className: "flex min-w-0 flex-1 flex-col gap-1.5",
2421
+ children: [
2422
+ /* @__PURE__ */ jsx5("label", { htmlFor: edgeId, className: "text-meta text-muted-foreground", children: t("process.metricLayerSwitch.edge") }),
2423
+ /* @__PURE__ */ jsxs4(Select, { value: metric.edge, onValueChange: handleEdgeChange, disabled: isRework, children: [
2424
+ /* @__PURE__ */ jsx5(SelectTrigger, { id: edgeId, "aria-label": t("process.metricLayerSwitch.edge"), children: /* @__PURE__ */ jsx5(SelectValue, {}) }),
2425
+ /* @__PURE__ */ jsx5(SelectContent, { children: edgeOptions.map((option) => /* @__PURE__ */ jsx5(SelectItem, { value: option, children: edgeMetricLabel(option) }, option)) })
2426
+ ] })
2427
+ ]
2428
+ }
2429
+ )
2430
+ ] })
2431
+ ]
2432
+ }
2433
+ );
2434
+ }
2435
+ );
2436
+
2437
+ // src/process-kpi-strip/process-kpi-strip.tsx
2438
+ import "react";
2439
+ import { CircleSlash2 } from "lucide-react";
2440
+ import { MetricCard } from "@elabs-ai/components-ui";
2441
+ import { useLocale as useLocale4 } from "@elabs-ai/components-ui";
2442
+ import { MetricGrid } from "@elabs-ai/components-charts";
2443
+ import { Sparkline } from "@elabs-ai/components-charts";
2444
+ import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
2445
+ function trendDirectionKey(first, last) {
2446
+ if (last > first) return "process.kpiStrip.trendRising";
2447
+ if (last < first) return "process.kpiStrip.trendFalling";
2448
+ return "process.kpiStrip.trendSteady";
2449
+ }
2450
+ function formatTrendValue(formatNumber, value, format) {
2451
+ switch (format) {
2452
+ case "duration":
2453
+ return formatDurationMs(value);
2454
+ case "percent":
2455
+ return formatNumber(value, { style: "percent", maximumFractionDigits: 1 });
2456
+ case "count":
2457
+ default:
2458
+ return formatNumber(value);
2459
+ }
2460
+ }
2461
+ function trendVisual(values, subject, format, t, formatNumber) {
2462
+ if (!values || values.length === 0) return void 0;
2463
+ const first = values[0];
2464
+ const last = values[values.length - 1];
2465
+ const label = t("process.kpiStrip.trendAlt", {
2466
+ subject,
2467
+ periods: values.length,
2468
+ direction: t(trendDirectionKey(first, last)),
2469
+ first: formatTrendValue(formatNumber, first, format),
2470
+ last: formatTrendValue(formatNumber, last, format)
2471
+ });
2472
+ return /* @__PURE__ */ jsx6(Sparkline, { values, label });
2473
+ }
2474
+ function ProcessKpiStrip({
2475
+ kpis,
2476
+ conformance,
2477
+ trends,
2478
+ loading = false,
2479
+ className,
2480
+ ...props
2481
+ }) {
2482
+ const { t, formatNumber } = useLocale4();
2483
+ const hasConformance = conformance !== null && conformance !== void 0;
2484
+ const conformanceHint = t("process.kpiStrip.conformanceUnavailableHint");
2485
+ return /* @__PURE__ */ jsx6("div", { "data-slot": "process-kpi-strip", className, ...props, children: /* @__PURE__ */ jsxs5(MetricGrid, { columns: 3, loading, children: [
2486
+ /* @__PURE__ */ jsx6(
2487
+ MetricCard,
2488
+ {
2489
+ label: t("process.kpiStrip.cases"),
2490
+ value: kpis.cases,
2491
+ announceLoading: false,
2492
+ visual: trendVisual(trends?.cases, t("process.kpiStrip.cases"), "count", t, formatNumber)
2493
+ }
2494
+ ),
2495
+ /* @__PURE__ */ jsx6(
2496
+ MetricCard,
2497
+ {
2498
+ label: t("process.kpiStrip.events"),
2499
+ value: kpis.events,
2500
+ announceLoading: false,
2501
+ visual: trendVisual(
2502
+ trends?.events,
2503
+ t("process.kpiStrip.events"),
2504
+ "count",
2505
+ t,
2506
+ formatNumber
2507
+ )
2508
+ }
2509
+ ),
2510
+ /* @__PURE__ */ jsx6(
2511
+ MetricCard,
2512
+ {
2513
+ label: t("process.kpiStrip.variants"),
2514
+ value: kpis.variants,
2515
+ announceLoading: false,
2516
+ visual: trendVisual(
2517
+ trends?.variants,
2518
+ t("process.kpiStrip.variants"),
2519
+ "count",
2520
+ t,
2521
+ formatNumber
2522
+ )
2523
+ }
2524
+ ),
2525
+ /* @__PURE__ */ jsx6(
2526
+ MetricCard,
2527
+ {
2528
+ label: t("process.kpiStrip.medianThroughput"),
2529
+ value: formatDurationMs(kpis.medianThroughput),
2530
+ announceLoading: false,
2531
+ visual: trendVisual(
2532
+ trends?.medianThroughput,
2533
+ t("process.kpiStrip.medianThroughput"),
2534
+ "duration",
2535
+ t,
2536
+ formatNumber
2537
+ )
2538
+ }
2539
+ ),
2540
+ /* @__PURE__ */ jsx6(
2541
+ MetricCard,
2542
+ {
2543
+ label: t("process.kpiStrip.reworkRate"),
2544
+ value: kpis.reworkRate,
2545
+ valueFormat: "percent",
2546
+ announceLoading: false,
2547
+ visual: trendVisual(
2548
+ trends?.reworkRate,
2549
+ t("process.kpiStrip.reworkRate"),
2550
+ "percent",
2551
+ t,
2552
+ formatNumber
2553
+ )
2554
+ }
2555
+ ),
2556
+ /* @__PURE__ */ jsx6(
2557
+ MetricCard,
2558
+ {
2559
+ label: t("process.kpiStrip.conformance"),
2560
+ value: hasConformance ? conformance : /* @__PURE__ */ jsxs5(
2561
+ "span",
2562
+ {
2563
+ "data-slot": "process-kpi-strip-conformance-unavailable",
2564
+ className: "text-title inline-flex items-center gap-1.5 text-muted-foreground",
2565
+ children: [
2566
+ /* @__PURE__ */ jsx6(CircleSlash2, { "aria-hidden": "true", className: "size-5" }),
2567
+ t("process.kpiStrip.conformanceUnavailable")
2568
+ ]
2569
+ }
2570
+ ),
2571
+ valueFormat: hasConformance ? "percent" : void 0,
2572
+ description: hasConformance ? (
2573
+ // Same slot, same height, every state (F3) — invisible rather than absent, so
2574
+ // the tile's box never reflows depending on whether conformance was measured.
2575
+ /* @__PURE__ */ jsx6("span", { "aria-hidden": "true", className: "invisible", children: conformanceHint })
2576
+ ) : conformanceHint,
2577
+ announceLoading: false,
2578
+ visual: hasConformance ? trendVisual(
2579
+ trends?.conformance,
2580
+ t("process.kpiStrip.conformance"),
2581
+ "percent",
2582
+ t,
2583
+ formatNumber
2584
+ ) : void 0
2585
+ }
2586
+ )
2587
+ ] }) });
2588
+ }
2589
+
2590
+ // src/use-process-explorer/use-process-explorer.ts
2591
+ import { useCallback as useCallback5, useEffect as useEffect3, useMemo as useMemo5, useRef as useRef3, useState as useState4 } from "react";
2592
+
2593
+ // src/core/filter-log.ts
2594
+ function sequenceOf2(kase) {
2595
+ const sequence = new Array(kase.events.length);
2596
+ for (let i = 0; i < kase.events.length; i += 1) {
2597
+ sequence[i] = kase.events[i].activity;
2598
+ }
2599
+ return sequence;
2600
+ }
2601
+ function valuesFor(kase, key) {
2602
+ const attributes = kase.attributes;
2603
+ if (attributes !== void 0 && Object.hasOwn(attributes, key)) return [attributes[key]];
2604
+ const values = [];
2605
+ const seen = /* @__PURE__ */ new Set();
2606
+ for (const event of kase.events) {
2607
+ const value = key === "resource" ? event.resource : event.attributes?.[key];
2608
+ if (value === void 0) continue;
2609
+ if (seen.has(value)) continue;
2610
+ seen.add(value);
2611
+ values.push(value);
2612
+ }
2613
+ return values;
2614
+ }
2615
+ function ordered(left, right, wantGreater) {
2616
+ if (typeof left === "number" && typeof right === "number") {
2617
+ return wantGreater ? left > right : left < right;
2618
+ }
2619
+ if (typeof left === "string" && typeof right === "string") {
2620
+ return wantGreater ? left > right : left < right;
2621
+ }
2622
+ return false;
2623
+ }
2624
+ function matchesAttribute(kase, spec) {
2625
+ const values = valuesFor(kase, spec.key);
2626
+ switch (spec.op) {
2627
+ case "eq":
2628
+ return values.some((value) => Object.is(value, spec.value));
2629
+ // `ne` is the negation of `eq`, not "some value differs" — otherwise a case offering
2630
+ // two resources would satisfy both `eq` and `ne` against the same value.
2631
+ case "ne":
2632
+ return !values.some((value) => Object.is(value, spec.value));
2633
+ case "gt":
2634
+ return values.some((value) => ordered(value, spec.value, true));
2635
+ case "lt":
2636
+ return values.some((value) => ordered(value, spec.value, false));
2637
+ case "in": {
2638
+ if (!Array.isArray(spec.value)) return false;
2639
+ const allowed = spec.value;
2640
+ return values.some((value) => allowed.some((candidate) => Object.is(value, candidate)));
2641
+ }
2642
+ default:
2643
+ return false;
2644
+ }
2645
+ }
2646
+ function matchesOne(kase, sequence, spec) {
2647
+ switch (spec.kind) {
2648
+ case "with":
2649
+ return sequence.includes(spec.activity);
2650
+ case "without":
2651
+ return !sequence.includes(spec.activity);
2652
+ case "startsWith":
2653
+ return sequence[0] === spec.activity;
2654
+ case "endsWith":
2655
+ return sequence[sequence.length - 1] === spec.activity;
2656
+ case "follower": {
2657
+ if (spec.direct === true) {
2658
+ for (let i = 0; i + 1 < sequence.length; i += 1) {
2659
+ if (sequence[i] === spec.a && sequence[i + 1] === spec.b) return true;
2660
+ }
2661
+ return false;
2662
+ }
2663
+ const first = sequence.indexOf(spec.a);
2664
+ if (first < 0) return false;
2665
+ return sequence.indexOf(spec.b, first + 1) >= 0;
2666
+ }
2667
+ case "attribute":
2668
+ return matchesAttribute(kase, spec);
2669
+ case "duration": {
2670
+ if (spec.min === void 0 && spec.max === void 0) return true;
2671
+ if (!Number.isFinite(kase.duration)) return false;
2672
+ if (spec.min !== void 0 && kase.duration < spec.min) return false;
2673
+ if (spec.max !== void 0 && kase.duration > spec.max) return false;
2674
+ return true;
2675
+ }
2676
+ case "variant":
2677
+ return spec.ids.includes(variantId(sequence));
2678
+ case "cases":
2679
+ return spec.ids.includes(kase.caseId);
2680
+ default:
2681
+ return true;
2682
+ }
2683
+ }
2684
+ function caseMatchesFilters(kase, specs) {
2685
+ if (specs.length === 0) return true;
2686
+ const sequence = sequenceOf2(kase);
2687
+ for (const spec of specs) {
2688
+ if (!matchesOne(kase, sequence, spec)) return false;
2689
+ }
2690
+ return true;
2691
+ }
2692
+ function filterNormalizedLog(log, specs) {
2693
+ const normalized = asNormalizedLog(log);
2694
+ if (specs.length === 0) return normalized;
2695
+ const cases = [];
2696
+ let events = 0;
2697
+ for (const kase of normalized.cases) {
2698
+ if (!caseMatchesFilters(kase, specs)) continue;
2699
+ cases.push(kase);
2700
+ events += kase.events.length;
2701
+ }
2702
+ return { cases, totals: { cases: cases.length, events } };
2703
+ }
2704
+ function filterLog(log, specs) {
2705
+ if (specs.length === 0) return log;
2706
+ const kept = /* @__PURE__ */ new Set();
2707
+ for (const kase of filterNormalizedLog(log, specs).cases) kept.add(kase.caseId);
2708
+ const filtered = { events: log.events.filter((row) => kept.has(row?.caseId)) };
2709
+ if (log.caseAttributes !== void 0) {
2710
+ const caseAttributes = {};
2711
+ for (const caseId of Object.keys(log.caseAttributes)) {
2712
+ if (kept.has(caseId)) {
2713
+ caseAttributes[caseId] = log.caseAttributes[caseId];
2714
+ }
2715
+ }
2716
+ filtered.caseAttributes = caseAttributes;
2717
+ }
2718
+ return filtered;
2719
+ }
2720
+
2721
+ // src/core/reconcile-graph.ts
2722
+ function transitionKey(transition) {
2723
+ return `${transition.source}${EDGE_KEY_SEPARATOR}${transition.target}`;
2724
+ }
2725
+ function reconcileGraph(presented, filtered) {
2726
+ const filteredActivities = new Map(
2727
+ filtered.activities.map((activity) => [activity.id, activity])
2728
+ );
2729
+ const filteredTransitions = new Map(
2730
+ filtered.transitions.map((transition) => [transitionKey(transition), transition])
2731
+ );
2732
+ const excludedActivities = [];
2733
+ const activities = presented.activities.map((activity) => {
2734
+ const survivor = filteredActivities.get(activity.id);
2735
+ if (survivor) return survivor;
2736
+ excludedActivities.push(activity.id);
2737
+ return {
2738
+ ...activity,
2739
+ instances: 0,
2740
+ cases: 0,
2741
+ isStart: false,
2742
+ isEnd: false,
2743
+ duration: emptyDurationStats()
2744
+ };
2745
+ });
2746
+ const excludedTransitions = [];
2747
+ const transitions = presented.transitions.map((transition) => {
2748
+ const key = transitionKey(transition);
2749
+ const survivor = filteredTransitions.get(key);
2750
+ if (survivor) return survivor;
2751
+ excludedTransitions.push(key);
2752
+ return {
2753
+ ...transition,
2754
+ count: 0,
2755
+ caseCount: 0,
2756
+ duration: emptyDurationStats()
2757
+ };
2758
+ });
2759
+ const graph = {
2760
+ ...presented,
2761
+ activities,
2762
+ transitions,
2763
+ startActivities: filtered.startActivities,
2764
+ endActivities: filtered.endActivities,
2765
+ totals: filtered.totals
2766
+ };
2767
+ return { graph, excludedActivities, excludedTransitions };
2768
+ }
2769
+
2770
+ // src/core/worker/process-worker.ts
2771
+ function handleProcessRequest(request) {
2772
+ try {
2773
+ if (request.kind === "discover") {
2774
+ return {
2775
+ id: request.id,
2776
+ ok: true,
2777
+ kind: "discover",
2778
+ graph: discoverGraph(request.log, request.options ?? {})
2779
+ };
2780
+ }
2781
+ return { id: request.id, ok: true, kind: "variants", variants: extractVariants(request.log) };
2782
+ } catch (error) {
2783
+ return { id: request.id, ok: false, error: describe(error) };
2784
+ }
2785
+ }
2786
+ function describe(error) {
2787
+ if (error instanceof Error) return error.message;
2788
+ return String(error);
2789
+ }
2790
+ function inWorkerScope() {
2791
+ const scope = globalThis;
2792
+ const constructor = scope.WorkerGlobalScope;
2793
+ if (typeof constructor !== "function") return false;
2794
+ return scope.self instanceof constructor;
2795
+ }
2796
+ if (inWorkerScope()) {
2797
+ const scope = self;
2798
+ scope.addEventListener("message", (event) => {
2799
+ scope.postMessage(handleProcessRequest(event.data));
2800
+ });
2801
+ }
2802
+
2803
+ // src/core/worker/create-process-worker.ts
2804
+ function workerConstructible() {
2805
+ return typeof Worker !== "undefined" && typeof URL !== "undefined";
2806
+ }
2807
+ function createProcessWorker(options = {}) {
2808
+ const construct = options.createWorker;
2809
+ let inline = options.forceInline === true || construct === void 0 && !workerConstructible();
2810
+ let terminated = false;
2811
+ let worker;
2812
+ let nextId = 0;
2813
+ const pending = /* @__PURE__ */ new Map();
2814
+ function degrade() {
2815
+ inline = true;
2816
+ const stale = [...pending.values()];
2817
+ pending.clear();
2818
+ if (worker !== void 0) {
2819
+ const dying = worker;
2820
+ worker = void 0;
2821
+ try {
2822
+ dying.terminate();
2823
+ } catch {
2824
+ }
2825
+ }
2826
+ for (const entry of stale) entry.settle(handleProcessRequest(entry.request));
2827
+ }
2828
+ function onMessage(event) {
2829
+ const response = event.data;
2830
+ if (response === void 0 || typeof response.id !== "number") return;
2831
+ const entry = pending.get(response.id);
2832
+ if (entry === void 0) return;
2833
+ pending.delete(response.id);
2834
+ entry.settle(response);
2835
+ }
2836
+ function ensureWorker() {
2837
+ if (inline || terminated) return void 0;
2838
+ if (worker !== void 0) return worker;
2839
+ try {
2840
+ worker = construct === void 0 ? new Worker(new URL("./core/process-worker.js", import.meta.url), {
2841
+ type: "module"
2842
+ }) : construct();
2843
+ } catch {
2844
+ degrade();
2845
+ return void 0;
2846
+ }
2847
+ worker.addEventListener("message", onMessage);
2848
+ worker.addEventListener("error", degrade);
2849
+ worker.addEventListener("messageerror", degrade);
2850
+ return worker;
2851
+ }
2852
+ function send(build) {
2853
+ if (terminated) return Promise.reject(new Error("process worker terminated"));
2854
+ nextId += 1;
2855
+ const request = build(nextId);
2856
+ const active = ensureWorker();
2857
+ if (active === void 0) {
2858
+ return Promise.resolve().then(() => handleProcessRequest(request));
2859
+ }
2860
+ return new Promise((resolve, reject) => {
2861
+ pending.set(request.id, { request, settle: resolve });
2862
+ try {
2863
+ active.postMessage(request);
2864
+ } catch {
2865
+ pending.delete(request.id);
2866
+ degrade();
2867
+ try {
2868
+ resolve(handleProcessRequest(request));
2869
+ } catch (error) {
2870
+ reject(error instanceof Error ? error : new Error(String(error)));
2871
+ }
2872
+ }
2873
+ });
2874
+ }
2875
+ function unwrap(response, read) {
2876
+ if (!response.ok) throw new Error(response.error);
2877
+ return read(response);
2878
+ }
2879
+ return {
2880
+ async discover(log, discoverOptions) {
2881
+ const response = await send(
2882
+ (id) => discoverOptions === void 0 ? { id, kind: "discover", log } : { id, kind: "discover", log, options: discoverOptions }
2883
+ );
2884
+ return unwrap(response, (ok) => ok.graph);
2885
+ },
2886
+ async variants(log) {
2887
+ const response = await send((id) => ({ id, kind: "variants", log }));
2888
+ return unwrap(response, (ok) => ok.variants);
2889
+ },
2890
+ terminate() {
2891
+ terminated = true;
2892
+ const stale = [...pending.values()];
2893
+ pending.clear();
2894
+ for (const entry of stale) {
2895
+ entry.settle({ id: entry.request.id, ok: false, error: "process worker terminated" });
2896
+ }
2897
+ if (worker !== void 0) {
2898
+ const dying = worker;
2899
+ worker = void 0;
2900
+ try {
2901
+ dying.terminate();
2902
+ } catch {
2903
+ }
2904
+ }
2905
+ },
2906
+ get inline() {
2907
+ return inline;
2908
+ }
2909
+ };
2910
+ }
2911
+
2912
+ // src/use-process-explorer/use-process-explorer.ts
2913
+ var DEFAULT_ABSTRACTION = {
2914
+ activities: 1,
2915
+ paths: 1,
2916
+ invert: false,
2917
+ keepConnected: true
2918
+ };
2919
+ var DEFAULT_METRIC = { node: "absolute", edge: "absolute" };
2920
+ var DEFAULT_WORKER_THRESHOLD = 5e4;
2921
+ var EMPTY_GRAPH = {
2922
+ activities: [],
2923
+ transitions: [],
2924
+ startActivities: {},
2925
+ endActivities: {},
2926
+ totals: { cases: 0, events: 0, variants: 0 }
2927
+ };
2928
+ function discoverInline(log) {
2929
+ return { graph: discoverGraph(log), variants: extractVariants(log) };
2930
+ }
2931
+ function countDistinctVariants(normalized) {
2932
+ const keys = /* @__PURE__ */ new Set();
2933
+ for (const kase of normalized.cases) {
2934
+ keys.add(variantKey(kase.events.map((event) => event.activity)));
2935
+ }
2936
+ return keys.size;
2937
+ }
2938
+ function useLogDiscovery(targetLog, workerThreshold, getHandle) {
2939
+ const useWorkerPath = targetLog !== null && targetLog.events.length > workerThreshold;
2940
+ const syncResult = useMemo5(
2941
+ () => targetLog === null || useWorkerPath ? null : discoverInline(targetLog),
2942
+ [targetLog, useWorkerPath]
2943
+ );
2944
+ const [asyncState, setAsyncState] = useState4(
2945
+ null
2946
+ );
2947
+ const [loading, setLoading] = useState4(false);
2948
+ const requestIdRef = useRef3(0);
2949
+ useEffect3(() => {
2950
+ const requestId = requestIdRef.current += 1;
2951
+ if (targetLog === null || !useWorkerPath) {
2952
+ setLoading(false);
2953
+ return;
2954
+ }
2955
+ setLoading(true);
2956
+ const handle = getHandle();
2957
+ Promise.all([handle.discover(targetLog), handle.variants(targetLog)]).then(([graph, variants]) => {
2958
+ if (requestIdRef.current !== requestId) return;
2959
+ setAsyncState({ log: targetLog, result: { graph, variants } });
2960
+ setLoading(false);
2961
+ }).catch(() => {
2962
+ if (requestIdRef.current !== requestId) return;
2963
+ setAsyncState({ log: targetLog, result: discoverInline(targetLog) });
2964
+ setLoading(false);
2965
+ });
2966
+ }, [targetLog, useWorkerPath]);
2967
+ const asyncResult = asyncState !== null && asyncState.log === targetLog ? asyncState.result : null;
2968
+ const targetUnsettled = useWorkerPath && (asyncState === null || asyncState.log !== targetLog);
2969
+ return {
2970
+ result: syncResult ?? asyncResult ?? { graph: EMPTY_GRAPH, variants: [] },
2971
+ loading: loading || targetUnsettled,
2972
+ settled: syncResult !== null || asyncResult !== null
2973
+ };
2974
+ }
2975
+ function useProcessExplorer(log, opts = {}) {
2976
+ const workerThreshold = opts.workerThreshold ?? DEFAULT_WORKER_THRESHOLD;
2977
+ const workerOptionsRef = useRef3(opts.worker);
2978
+ workerOptionsRef.current = opts.worker;
2979
+ const handleRef = useRef3(null);
2980
+ function getHandle() {
2981
+ if (handleRef.current === null) {
2982
+ handleRef.current = createProcessWorker(workerOptionsRef.current);
2983
+ }
2984
+ return handleRef.current;
2985
+ }
2986
+ useEffect3(
2987
+ () => () => {
2988
+ handleRef.current?.terminate();
2989
+ },
2990
+ []
2991
+ );
2992
+ const [abstraction, setAbstractionState] = useState4(() => ({
2993
+ ...DEFAULT_ABSTRACTION,
2994
+ ...opts.abstraction
2995
+ }));
2996
+ const [metric, setMetricState] = useState4(() => ({
2997
+ ...DEFAULT_METRIC,
2998
+ ...opts.metric
2999
+ }));
3000
+ const [layer, setLayerState] = useState4(opts.layer ?? "frequency");
3001
+ const [selection, setSelection] = useState4(null);
3002
+ const [intents, setIntents] = useState4([]);
3003
+ const setAbstraction = useCallback5((next) => {
3004
+ setAbstractionState((prev) => ({ ...prev, ...next }));
3005
+ }, []);
3006
+ const setMetric = useCallback5((next) => {
3007
+ setMetricState((prev) => ({ ...prev, ...next }));
3008
+ }, []);
3009
+ const setLayer = useCallback5((next) => setLayerState(next), []);
3010
+ const onSelect = useCallback5((next) => setSelection(next), []);
3011
+ const applyIntent = useCallback5((intent) => {
3012
+ setIntents((prev) => [...prev, intent]);
3013
+ }, []);
3014
+ const clearIntent = useCallback5((index2) => {
3015
+ setIntents((prev) => prev.filter((_, i) => i !== index2));
3016
+ }, []);
3017
+ const filteredLog = useMemo5(
3018
+ () => intents.length === 0 ? log : filterLog(log, intents),
3019
+ [log, intents]
3020
+ );
3021
+ const sameLog = filteredLog === log;
3022
+ const fullDiscovery = useLogDiscovery(log, workerThreshold, getHandle);
3023
+ const filteredOwnDiscovery = useLogDiscovery(
3024
+ sameLog ? null : filteredLog,
3025
+ workerThreshold,
3026
+ getHandle
3027
+ );
3028
+ const filteredDiscovery = sameLog || !filteredOwnDiscovery.settled ? fullDiscovery : filteredOwnDiscovery;
3029
+ const loading = fullDiscovery.loading || filteredOwnDiscovery.loading;
3030
+ const presented = useMemo5(
3031
+ () => abstractGraph(fullDiscovery.result.graph, abstraction),
3032
+ [fullDiscovery.result.graph, abstraction]
3033
+ );
3034
+ const reconciled = useMemo5(
3035
+ () => reconcileGraph(presented, filteredDiscovery.result.graph),
3036
+ [presented, filteredDiscovery.result.graph]
3037
+ );
3038
+ const graph = reconciled.graph;
3039
+ const selectionStates = useMemo5(
3040
+ () => ({
3041
+ activities: Object.fromEntries(
3042
+ reconciled.excludedActivities.map((id) => [id, "excluded"])
3043
+ ),
3044
+ transitions: Object.fromEntries(
3045
+ reconciled.excludedTransitions.map((key) => [key, "excluded"])
3046
+ ),
3047
+ // Decision §1.4 step 5 (RM-052 round 3, #227, G2): there is no click channel for a
3048
+ // variant, so `"selected"` here is intent-derived — every id named by an active
3049
+ // `{ kind: "variant" }` intent, read by `VariantExplorer` (RM-054), not by
3050
+ // `ProcessMap`. This namespace was declared on `ProcessSelectionStates` from round 2
3051
+ // onward and never populated until this fix.
3052
+ variants: Object.fromEntries(
3053
+ intents.flatMap((intent) => intent.kind === "variant" ? intent.ids : []).map((id) => [id, "selected"])
3054
+ )
3055
+ }),
3056
+ [reconciled, intents]
3057
+ );
3058
+ const excludedCounts = useMemo5(
3059
+ () => ({
3060
+ activities: reconciled.excludedActivities.length,
3061
+ paths: reconciled.excludedTransitions.length
3062
+ }),
3063
+ [reconciled]
3064
+ );
3065
+ const rework = useMemo5(() => detectRework(filteredLog), [filteredLog]);
3066
+ const kpis = useMemo5(() => {
3067
+ const normalized = asNormalizedLog(filteredLog);
3068
+ const medianThroughput = durationStats(normalized.cases.map((kase) => kase.duration)).median;
3069
+ return {
3070
+ cases: normalized.totals.cases,
3071
+ events: normalized.totals.events,
3072
+ variants: countDistinctVariants(normalized),
3073
+ medianThroughput,
3074
+ reworkRate: rework.caseReworkRate
3075
+ };
3076
+ }, [filteredLog, rework]);
3077
+ return {
3078
+ graph,
3079
+ variants: filteredDiscovery.result.variants,
3080
+ abstraction,
3081
+ setAbstraction,
3082
+ metric,
3083
+ setMetric,
3084
+ layer,
3085
+ setLayer,
3086
+ selection,
3087
+ onSelect,
3088
+ applyIntent,
3089
+ clearIntent,
3090
+ intents,
3091
+ filteredLog,
3092
+ selectionStates,
3093
+ hiddenCounts: graph.hidden,
3094
+ excludedCounts,
3095
+ kpis,
3096
+ rework,
3097
+ loading
3098
+ };
3099
+ }
3100
+ export {
3101
+ AbstractionControls,
3102
+ DEFAULT_LAYOUT_DEBOUNCE_MS,
3103
+ EMPTY_PROCESS_MAP_HOVER,
3104
+ GHOST_OPACITY,
3105
+ MetricLayerSwitch,
3106
+ PROCESS_FILTER_INTENT_KINDS,
3107
+ PROCESS_FILTER_INTENT_LABELS,
3108
+ PROCESS_FILTER_INTENT_MESSAGE_KEYS,
3109
+ PROCESS_MAP_EDGE_SCALE_GROUP,
3110
+ PROCESS_MAP_LEGIBLE_ZOOM,
3111
+ PROCESS_MAP_NODE_MOTION_CLASS,
3112
+ PROCESS_SELECTION_STATE_MESSAGE_KEYS,
3113
+ ProcessActivityNode,
3114
+ ProcessKpiStrip,
3115
+ ProcessMap,
3116
+ ProcessMapEdgeKeyContext,
3117
+ ProcessMapHoverContext,
3118
+ ProcessTransitionEdge,
3119
+ activityAriaLabel,
3120
+ activityMetricValue,
3121
+ activityRole,
3122
+ applyLayoutSnapshot,
3123
+ buildProcessMapModel,
3124
+ computeAutoAbstraction,
3125
+ edgeMetricLabel,
3126
+ formatDurationMs,
3127
+ formatMetricValue,
3128
+ isPerformanceMetric,
3129
+ nodeMetricLabel,
3130
+ processEdgeDenominators,
3131
+ processEdgeId,
3132
+ processGraphStructureKey,
3133
+ resolveActivityFrequencyMode,
3134
+ resolveSelectionState,
3135
+ resolveTransitionFrequencyMode,
3136
+ selectionNeighbourhood,
3137
+ selectionStateLabel,
3138
+ transitionAriaLabel,
3139
+ transitionMetricValue,
3140
+ transitionShape,
3141
+ useProcessExplorer,
3142
+ useProcessLayout,
3143
+ useProcessMapEdgeKeys,
3144
+ useProcessMapHover
3145
+ };
3146
+ //# sourceMappingURL=index.js.map