@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.
- package/LICENSE +21 -0
- package/README.md +73 -0
- package/dist/core/index.d.ts +1029 -0
- package/dist/core/index.js +1553 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/process-worker.js +462 -0
- package/dist/core/process-worker.js.map +1 -0
- package/dist/index.d.ts +1153 -0
- package/dist/index.js +3146 -0
- package/dist/index.js.map +1 -0
- package/dist/test/index.d.ts +196 -0
- package/dist/test/index.js +527 -0
- package/dist/test/index.js.map +1 -0
- package/package.json +80 -0
- package/src/abstraction-controls/abstraction-controls-fixtures.ts +86 -0
- package/src/abstraction-controls/abstraction-controls.stories.tsx +188 -0
- package/src/abstraction-controls/abstraction-controls.test.tsx +226 -0
- package/src/abstraction-controls/abstraction-controls.tsx +288 -0
- package/src/abstraction-controls/auto-abstraction.test.ts +196 -0
- package/src/abstraction-controls/auto-abstraction.ts +128 -0
- package/src/abstraction-controls/index.ts +4 -0
- package/src/core/abstract-graph.test.ts +209 -0
- package/src/core/abstract-graph.ts +407 -0
- package/src/core/adapters/csv.test.ts +131 -0
- package/src/core/adapters/csv.ts +146 -0
- package/src/core/adapters/flat.test.ts +149 -0
- package/src/core/adapters/flat.ts +168 -0
- package/src/core/aggregate-performance.test.ts +208 -0
- package/src/core/aggregate-performance.ts +200 -0
- package/src/core/detect-rework.test.ts +134 -0
- package/src/core/detect-rework.ts +100 -0
- package/src/core/discover-graph.test.ts +378 -0
- package/src/core/discover-graph.ts +202 -0
- package/src/core/duration-stats.test.ts +116 -0
- package/src/core/duration-stats.ts +162 -0
- package/src/core/event-log.test.ts +224 -0
- package/src/core/event-log.ts +244 -0
- package/src/core/extract-variants.test.ts +126 -0
- package/src/core/extract-variants.ts +140 -0
- package/src/core/filter-log.test.ts +193 -0
- package/src/core/filter-log.ts +215 -0
- package/src/core/fixtures/generate-bpi-2012-subset.test.ts +50 -0
- package/src/core/fixtures/generate-bpi-2012-subset.ts +216 -0
- package/src/core/fixtures/generate-bpi-2012-subset.write.ts +40 -0
- package/src/core/fixtures/order-to-cash-small.json +200 -0
- package/src/core/fixtures/synthetic-log.test.ts +109 -0
- package/src/core/fixtures/synthetic-log.ts +167 -0
- package/src/core/index.ts +118 -0
- package/src/core/reconcile-graph.test.ts +175 -0
- package/src/core/reconcile-graph.ts +107 -0
- package/src/core/scale.test.ts +80 -0
- package/src/core/scale.ts +100 -0
- package/src/core/types.ts +151 -0
- package/src/core/worker/create-process-worker.test.ts +255 -0
- package/src/core/worker/create-process-worker.ts +211 -0
- package/src/core/worker/process-worker.ts +80 -0
- package/src/index.ts +29 -0
- package/src/metric-layer-switch/index.ts +6 -0
- package/src/metric-layer-switch/metric-layer-switch.stories.tsx +131 -0
- package/src/metric-layer-switch/metric-layer-switch.test.tsx +102 -0
- package/src/metric-layer-switch/metric-layer-switch.tsx +276 -0
- package/src/process-explorer.stories.tsx +392 -0
- package/src/process-kpi-strip/index.ts +6 -0
- package/src/process-kpi-strip/process-kpi-strip.stories.tsx +128 -0
- package/src/process-kpi-strip/process-kpi-strip.test.tsx +106 -0
- package/src/process-kpi-strip/process-kpi-strip.tsx +237 -0
- package/src/process-map/index.ts +13 -0
- package/src/process-map/map-model.test.ts +326 -0
- package/src/process-map/map-model.ts +873 -0
- package/src/process-map/process-activity-node.tsx +200 -0
- package/src/process-map/process-map-context.ts +71 -0
- package/src/process-map/process-map.stories.tsx +673 -0
- package/src/process-map/process-map.test.tsx +523 -0
- package/src/process-map/process-map.tsx +979 -0
- package/src/process-map/process-transition-edge.test.tsx +160 -0
- package/src/process-map/process-transition-edge.tsx +151 -0
- package/src/process-map/use-process-layout.test.tsx +265 -0
- package/src/process-map/use-process-layout.ts +315 -0
- package/src/test/contract.test.ts +99 -0
- package/src/test/contract.ts +118 -0
- package/src/test/doubles.test.tsx +51 -0
- package/src/test/doubles.tsx +82 -0
- package/src/test/index.ts +34 -0
- package/src/test/primitives.tsx +35 -0
- package/src/use-process-explorer/index.ts +8 -0
- package/src/use-process-explorer/use-process-explorer.test.ts +564 -0
- package/src/use-process-explorer/use-process-explorer.ts +540 -0
|
@@ -0,0 +1,1553 @@
|
|
|
1
|
+
// src/core/event-log.ts
|
|
2
|
+
function isNormalizedLog(log) {
|
|
3
|
+
return Array.isArray(log.cases);
|
|
4
|
+
}
|
|
5
|
+
function asNormalizedLog(log) {
|
|
6
|
+
return isNormalizedLog(log) ? log : normalizeLog(log);
|
|
7
|
+
}
|
|
8
|
+
function toEpochMs(value) {
|
|
9
|
+
if (value === void 0 || value === null) return Number.NaN;
|
|
10
|
+
if (typeof value === "number") return Number.isFinite(value) ? value : Number.NaN;
|
|
11
|
+
if (value instanceof Date) return value.getTime();
|
|
12
|
+
const parsed = Date.parse(value);
|
|
13
|
+
if (!Number.isNaN(parsed)) return parsed;
|
|
14
|
+
const asNumber = Number(value);
|
|
15
|
+
return Number.isFinite(asNumber) && value.trim() !== "" ? asNumber : Number.NaN;
|
|
16
|
+
}
|
|
17
|
+
function byTimestamp(a, b) {
|
|
18
|
+
if (Number.isNaN(a.at) || Number.isNaN(b.at)) return 0;
|
|
19
|
+
return a.at - b.at;
|
|
20
|
+
}
|
|
21
|
+
function normalizeLog(log) {
|
|
22
|
+
const staged = /* @__PURE__ */ new Map();
|
|
23
|
+
const order = [];
|
|
24
|
+
for (const row of log.events) {
|
|
25
|
+
if (!row || typeof row.caseId !== "string" || row.caseId === "") continue;
|
|
26
|
+
if (typeof row.activity !== "string" || row.activity === "") continue;
|
|
27
|
+
let bucket = staged.get(row.caseId);
|
|
28
|
+
if (bucket === void 0) {
|
|
29
|
+
bucket = [];
|
|
30
|
+
staged.set(row.caseId, bucket);
|
|
31
|
+
order.push(row.caseId);
|
|
32
|
+
}
|
|
33
|
+
bucket.push({ at: toEpochMs(row.timestamp), row });
|
|
34
|
+
}
|
|
35
|
+
const cases = [];
|
|
36
|
+
let events = 0;
|
|
37
|
+
for (const caseId of order) {
|
|
38
|
+
const bucket = staged.get(caseId);
|
|
39
|
+
bucket.sort(byTimestamp);
|
|
40
|
+
const instances = [];
|
|
41
|
+
const open = /* @__PURE__ */ new Map();
|
|
42
|
+
for (const { at, row } of bucket) {
|
|
43
|
+
if (row.lifecycle === "start") {
|
|
44
|
+
const index2 = instances.length;
|
|
45
|
+
instances.push(makeInstance(row, at, at, true));
|
|
46
|
+
const queue = open.get(row.activity);
|
|
47
|
+
if (queue === void 0) open.set(row.activity, [index2]);
|
|
48
|
+
else queue.push(index2);
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (row.lifecycle === "complete") {
|
|
52
|
+
const queue = open.get(row.activity);
|
|
53
|
+
const index2 = queue?.shift();
|
|
54
|
+
if (index2 !== void 0) {
|
|
55
|
+
const instance = instances[index2];
|
|
56
|
+
instance.end = at;
|
|
57
|
+
instance.duration = durationOf(instance.start, at);
|
|
58
|
+
instance.isOpen = false;
|
|
59
|
+
if (instance.resource === void 0 && row.resource !== void 0) {
|
|
60
|
+
instance.resource = row.resource;
|
|
61
|
+
}
|
|
62
|
+
if (instance.attributes === void 0 && row.attributes !== void 0) {
|
|
63
|
+
instance.attributes = row.attributes;
|
|
64
|
+
}
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const end = at;
|
|
69
|
+
const explicitStart = toEpochMs(row.startTimestamp);
|
|
70
|
+
const start = Number.isNaN(explicitStart) ? end : explicitStart;
|
|
71
|
+
instances.push(makeInstance(row, start, end, false));
|
|
72
|
+
}
|
|
73
|
+
if (instances.length === 0) continue;
|
|
74
|
+
instances.sort(byStart);
|
|
75
|
+
let caseStart = Number.POSITIVE_INFINITY;
|
|
76
|
+
let caseEnd = Number.NEGATIVE_INFINITY;
|
|
77
|
+
for (const instance of instances) {
|
|
78
|
+
if (Number.isFinite(instance.start) && instance.start < caseStart) caseStart = instance.start;
|
|
79
|
+
if (Number.isFinite(instance.end) && instance.end > caseEnd) caseEnd = instance.end;
|
|
80
|
+
}
|
|
81
|
+
const hasExtent = caseStart !== Number.POSITIVE_INFINITY && caseEnd !== Number.NEGATIVE_INFINITY;
|
|
82
|
+
const normalizedCase = {
|
|
83
|
+
caseId,
|
|
84
|
+
events: instances,
|
|
85
|
+
start: hasExtent ? caseStart : Number.NaN,
|
|
86
|
+
end: hasExtent ? caseEnd : Number.NaN,
|
|
87
|
+
duration: hasExtent ? durationOf(caseStart, caseEnd) : Number.NaN
|
|
88
|
+
};
|
|
89
|
+
const caseAttributes = log.caseAttributes?.[caseId];
|
|
90
|
+
if (caseAttributes !== void 0) normalizedCase.attributes = caseAttributes;
|
|
91
|
+
cases.push(normalizedCase);
|
|
92
|
+
events += instances.length;
|
|
93
|
+
}
|
|
94
|
+
return { cases, totals: { cases: cases.length, events } };
|
|
95
|
+
}
|
|
96
|
+
function byStart(a, b) {
|
|
97
|
+
if (Number.isNaN(a.start) || Number.isNaN(b.start)) return 0;
|
|
98
|
+
return a.start - b.start;
|
|
99
|
+
}
|
|
100
|
+
function durationOf(start, end) {
|
|
101
|
+
const delta = end - start;
|
|
102
|
+
if (!Number.isFinite(delta)) return Number.NaN;
|
|
103
|
+
return delta < 0 ? 0 : delta;
|
|
104
|
+
}
|
|
105
|
+
function makeInstance(row, start, end, isOpen) {
|
|
106
|
+
const instance = {
|
|
107
|
+
activity: row.activity,
|
|
108
|
+
start,
|
|
109
|
+
end,
|
|
110
|
+
duration: durationOf(start, end),
|
|
111
|
+
isOpen
|
|
112
|
+
};
|
|
113
|
+
if (row.resource !== void 0) instance.resource = row.resource;
|
|
114
|
+
if (row.attributes !== void 0) instance.attributes = row.attributes;
|
|
115
|
+
return instance;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// src/core/adapters/flat.ts
|
|
119
|
+
var DEFAULT_LIFECYCLE_VALUES = {
|
|
120
|
+
start: ["start", "started", "begin", "assign", "schedule"],
|
|
121
|
+
complete: ["complete", "completed", "end", "finish", "finished", "done"]
|
|
122
|
+
};
|
|
123
|
+
function readString(row, column) {
|
|
124
|
+
if (column === void 0) return void 0;
|
|
125
|
+
const value = row[column];
|
|
126
|
+
if (value === void 0 || value === null) return void 0;
|
|
127
|
+
const text = typeof value === "string" ? value.trim() : String(value);
|
|
128
|
+
return text === "" ? void 0 : text;
|
|
129
|
+
}
|
|
130
|
+
function readTimestamp(row, column) {
|
|
131
|
+
if (column === void 0) return void 0;
|
|
132
|
+
const value = row[column];
|
|
133
|
+
if (value === void 0 || value === null) return void 0;
|
|
134
|
+
if (value instanceof Date || typeof value === "number") return value;
|
|
135
|
+
const text = String(value).trim();
|
|
136
|
+
return text === "" ? void 0 : text;
|
|
137
|
+
}
|
|
138
|
+
function readAttribute(value) {
|
|
139
|
+
if (value === null || value === void 0) return null;
|
|
140
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
141
|
+
return value;
|
|
142
|
+
}
|
|
143
|
+
if (value instanceof Date) return value.toISOString();
|
|
144
|
+
return String(value);
|
|
145
|
+
}
|
|
146
|
+
function normalizeLifecycle(raw, values = DEFAULT_LIFECYCLE_VALUES) {
|
|
147
|
+
if (raw === void 0) return void 0;
|
|
148
|
+
const needle = raw.trim().toLowerCase();
|
|
149
|
+
if (needle === "") return void 0;
|
|
150
|
+
const starts = values.start ?? DEFAULT_LIFECYCLE_VALUES.start;
|
|
151
|
+
const completes = values.complete ?? DEFAULT_LIFECYCLE_VALUES.complete;
|
|
152
|
+
for (const candidate of starts) if (candidate.toLowerCase() === needle) return "start";
|
|
153
|
+
for (const candidate of completes) if (candidate.toLowerCase() === needle) return "complete";
|
|
154
|
+
return void 0;
|
|
155
|
+
}
|
|
156
|
+
function fromFlatRows(rows, mapping) {
|
|
157
|
+
const events = [];
|
|
158
|
+
let caseAttributes;
|
|
159
|
+
for (const row of rows) {
|
|
160
|
+
if (row === null || typeof row !== "object") continue;
|
|
161
|
+
const caseId = readString(row, mapping.caseId);
|
|
162
|
+
const activity = readString(row, mapping.activity);
|
|
163
|
+
const timestamp = readTimestamp(row, mapping.timestamp);
|
|
164
|
+
if (caseId === void 0 || activity === void 0 || timestamp === void 0) continue;
|
|
165
|
+
const event = { caseId, activity, timestamp };
|
|
166
|
+
const startTimestamp = readTimestamp(row, mapping.startTimestamp);
|
|
167
|
+
if (startTimestamp !== void 0) event.startTimestamp = startTimestamp;
|
|
168
|
+
const resource = readString(row, mapping.resource);
|
|
169
|
+
if (resource !== void 0) event.resource = resource;
|
|
170
|
+
const lifecycle = normalizeLifecycle(
|
|
171
|
+
readString(row, mapping.lifecycle),
|
|
172
|
+
mapping.lifecycleValues
|
|
173
|
+
);
|
|
174
|
+
if (lifecycle !== void 0) event.lifecycle = lifecycle;
|
|
175
|
+
if (mapping.attributes !== void 0 && mapping.attributes.length > 0) {
|
|
176
|
+
const attributes = {};
|
|
177
|
+
let any = false;
|
|
178
|
+
for (const column of mapping.attributes) {
|
|
179
|
+
if (!(column in row)) continue;
|
|
180
|
+
attributes[column] = readAttribute(row[column]);
|
|
181
|
+
any = true;
|
|
182
|
+
}
|
|
183
|
+
if (any) event.attributes = attributes;
|
|
184
|
+
}
|
|
185
|
+
if (mapping.caseAttributes !== void 0 && mapping.caseAttributes.length > 0) {
|
|
186
|
+
caseAttributes ??= {};
|
|
187
|
+
const bucket = caseAttributes[caseId] ??= {};
|
|
188
|
+
for (const column of mapping.caseAttributes) {
|
|
189
|
+
if (bucket[column] !== void 0) continue;
|
|
190
|
+
const value = readString(row, column);
|
|
191
|
+
if (value !== void 0) bucket[column] = value;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
events.push(event);
|
|
195
|
+
}
|
|
196
|
+
const log = { events };
|
|
197
|
+
if (caseAttributes !== void 0) log.caseAttributes = caseAttributes;
|
|
198
|
+
return log;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// src/core/adapters/csv.ts
|
|
202
|
+
var QUOTE = '"';
|
|
203
|
+
var CR = "\r";
|
|
204
|
+
var LF = "\n";
|
|
205
|
+
function parseDelimited(text, delimiter = ",") {
|
|
206
|
+
const source = text.charCodeAt(0) === 65279 ? text.slice(1) : text;
|
|
207
|
+
const records = [];
|
|
208
|
+
let record = [];
|
|
209
|
+
let field = "";
|
|
210
|
+
let quoted = false;
|
|
211
|
+
let dirty = false;
|
|
212
|
+
const delimiterChar = delimiter.length > 0 ? delimiter[0] : ",";
|
|
213
|
+
const endField = () => {
|
|
214
|
+
record.push(field);
|
|
215
|
+
field = "";
|
|
216
|
+
dirty = true;
|
|
217
|
+
};
|
|
218
|
+
const endRecord = () => {
|
|
219
|
+
endField();
|
|
220
|
+
records.push(record);
|
|
221
|
+
record = [];
|
|
222
|
+
dirty = false;
|
|
223
|
+
};
|
|
224
|
+
for (let i = 0; i < source.length; i += 1) {
|
|
225
|
+
const char = source[i];
|
|
226
|
+
if (quoted) {
|
|
227
|
+
if (char === QUOTE) {
|
|
228
|
+
if (source[i + 1] === QUOTE) {
|
|
229
|
+
field += QUOTE;
|
|
230
|
+
i += 1;
|
|
231
|
+
} else {
|
|
232
|
+
quoted = false;
|
|
233
|
+
}
|
|
234
|
+
} else {
|
|
235
|
+
field += char;
|
|
236
|
+
}
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
if (char === QUOTE && field === "") {
|
|
240
|
+
quoted = true;
|
|
241
|
+
dirty = true;
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
if (char === delimiterChar) {
|
|
245
|
+
endField();
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
if (char === CR) {
|
|
249
|
+
if (source[i + 1] === LF) i += 1;
|
|
250
|
+
endRecord();
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
if (char === LF) {
|
|
254
|
+
endRecord();
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
field += char;
|
|
258
|
+
dirty = true;
|
|
259
|
+
}
|
|
260
|
+
if (field !== "" || dirty) endRecord();
|
|
261
|
+
return records;
|
|
262
|
+
}
|
|
263
|
+
function toObjects(records, header) {
|
|
264
|
+
const rows = [];
|
|
265
|
+
for (const record of records) {
|
|
266
|
+
const row = {};
|
|
267
|
+
for (let i = 0; i < header.length; i += 1) {
|
|
268
|
+
row[header[i]] = record[i] ?? "";
|
|
269
|
+
}
|
|
270
|
+
rows.push(row);
|
|
271
|
+
}
|
|
272
|
+
return rows;
|
|
273
|
+
}
|
|
274
|
+
function fromCsv(text, mapping) {
|
|
275
|
+
const records = parseDelimited(text, mapping.delimiter ?? ",");
|
|
276
|
+
let header = mapping.header;
|
|
277
|
+
let body = records;
|
|
278
|
+
if (header === void 0) {
|
|
279
|
+
const first = records[0];
|
|
280
|
+
if (first === void 0) return { events: [] };
|
|
281
|
+
header = first.map((name) => name.trim());
|
|
282
|
+
body = records.slice(1);
|
|
283
|
+
}
|
|
284
|
+
return fromFlatRows(toObjects(body, header), mapping);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// src/core/scale.ts
|
|
288
|
+
function ascending(a, b) {
|
|
289
|
+
return a - b;
|
|
290
|
+
}
|
|
291
|
+
function finiteOnly(values) {
|
|
292
|
+
const out = [];
|
|
293
|
+
for (const v of values) if (Number.isFinite(v)) out.push(v);
|
|
294
|
+
return out;
|
|
295
|
+
}
|
|
296
|
+
function minMax(values) {
|
|
297
|
+
let min = Number.POSITIVE_INFINITY;
|
|
298
|
+
let max = Number.NEGATIVE_INFINITY;
|
|
299
|
+
let seen = false;
|
|
300
|
+
for (const v of values) {
|
|
301
|
+
if (!Number.isFinite(v)) continue;
|
|
302
|
+
seen = true;
|
|
303
|
+
if (v < min) min = v;
|
|
304
|
+
if (v > max) max = v;
|
|
305
|
+
}
|
|
306
|
+
return seen ? [min, max] : [0, 0];
|
|
307
|
+
}
|
|
308
|
+
function quantileSorted(sorted, q) {
|
|
309
|
+
const n = sorted.length;
|
|
310
|
+
if (n === 0) return 0;
|
|
311
|
+
if (n === 1) return sorted[0];
|
|
312
|
+
const clamped = q < 0 ? 0 : q > 1 ? 1 : q;
|
|
313
|
+
const pos = clamped * (n - 1);
|
|
314
|
+
const lo = Math.floor(pos);
|
|
315
|
+
const hi = Math.ceil(pos);
|
|
316
|
+
const loValue = sorted[lo];
|
|
317
|
+
if (lo === hi) return loValue;
|
|
318
|
+
const hiValue = sorted[hi];
|
|
319
|
+
return loValue + (hiValue - loValue) * (pos - lo);
|
|
320
|
+
}
|
|
321
|
+
function quantile(values, q) {
|
|
322
|
+
return quantileSorted(finiteOnly(values).sort(ascending), q);
|
|
323
|
+
}
|
|
324
|
+
function clampWidth(value, domain, range) {
|
|
325
|
+
const [d0, d1] = domain;
|
|
326
|
+
const [r0, r1] = range;
|
|
327
|
+
if (!Number.isFinite(value)) return r0;
|
|
328
|
+
const span = d1 - d0;
|
|
329
|
+
if (span === 0) return r0;
|
|
330
|
+
const t = (value - d0) / span;
|
|
331
|
+
const clamped = t < 0 ? 0 : t > 1 ? 1 : t;
|
|
332
|
+
return r0 + (r1 - r0) * clamped;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// src/core/duration-stats.ts
|
|
336
|
+
var TRIM_FRACTION = 0.1;
|
|
337
|
+
var EMPTY_DURATION_STATS = Object.freeze({
|
|
338
|
+
min: 0,
|
|
339
|
+
max: 0,
|
|
340
|
+
mean: 0,
|
|
341
|
+
median: 0,
|
|
342
|
+
p90: 0,
|
|
343
|
+
sum: 0,
|
|
344
|
+
trimmedMean: 0
|
|
345
|
+
});
|
|
346
|
+
function emptyDurationStats() {
|
|
347
|
+
return { ...EMPTY_DURATION_STATS };
|
|
348
|
+
}
|
|
349
|
+
function durationStats(samples) {
|
|
350
|
+
const sorted = [];
|
|
351
|
+
for (const s of samples) if (Number.isFinite(s)) sorted.push(s);
|
|
352
|
+
const n = sorted.length;
|
|
353
|
+
if (n === 0) return emptyDurationStats();
|
|
354
|
+
sorted.sort(ascending);
|
|
355
|
+
let sum = 0;
|
|
356
|
+
for (const s of sorted) sum += s;
|
|
357
|
+
const trim = Math.floor(n * TRIM_FRACTION);
|
|
358
|
+
const lo = trim;
|
|
359
|
+
const hi = n - trim;
|
|
360
|
+
let trimmedMean;
|
|
361
|
+
if (hi - lo <= 0) {
|
|
362
|
+
trimmedMean = sum / n;
|
|
363
|
+
} else {
|
|
364
|
+
let trimmedSum = 0;
|
|
365
|
+
for (let i = lo; i < hi; i += 1) trimmedSum += sorted[i];
|
|
366
|
+
trimmedMean = trimmedSum / (hi - lo);
|
|
367
|
+
}
|
|
368
|
+
return {
|
|
369
|
+
min: sorted[0],
|
|
370
|
+
max: sorted[n - 1],
|
|
371
|
+
mean: sum / n,
|
|
372
|
+
median: quantileSorted(sorted, 0.5),
|
|
373
|
+
p90: quantileSorted(sorted, 0.9),
|
|
374
|
+
sum,
|
|
375
|
+
trimmedMean
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
var DURATION_SAMPLE_CAP = 4096;
|
|
379
|
+
function mulberry32(seed) {
|
|
380
|
+
let a = seed >>> 0;
|
|
381
|
+
return () => {
|
|
382
|
+
a = a + 1831565813 >>> 0;
|
|
383
|
+
let t = a;
|
|
384
|
+
t = Math.imul(t ^ t >>> 15, t | 1);
|
|
385
|
+
t ^= t + Math.imul(t ^ t >>> 7, t | 61);
|
|
386
|
+
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
var DurationSampler = class {
|
|
390
|
+
capacity;
|
|
391
|
+
random;
|
|
392
|
+
reservoir = [];
|
|
393
|
+
/** Number of finite samples offered, including those the reservoir discarded. */
|
|
394
|
+
seen = 0;
|
|
395
|
+
total = 0;
|
|
396
|
+
lowest = Number.POSITIVE_INFINITY;
|
|
397
|
+
highest = Number.NEGATIVE_INFINITY;
|
|
398
|
+
constructor(seed = 2654435769, capacity = DURATION_SAMPLE_CAP) {
|
|
399
|
+
this.capacity = capacity > 0 ? capacity : 1;
|
|
400
|
+
this.random = mulberry32(seed);
|
|
401
|
+
}
|
|
402
|
+
/** Offer one sample. Non-finite values are ignored. */
|
|
403
|
+
add(sample) {
|
|
404
|
+
if (!Number.isFinite(sample)) return;
|
|
405
|
+
this.total += sample;
|
|
406
|
+
if (sample < this.lowest) this.lowest = sample;
|
|
407
|
+
if (sample > this.highest) this.highest = sample;
|
|
408
|
+
if (this.reservoir.length < this.capacity) {
|
|
409
|
+
this.reservoir.push(sample);
|
|
410
|
+
} else {
|
|
411
|
+
const j = Math.floor(this.random() * (this.seen + 1));
|
|
412
|
+
if (j < this.capacity) this.reservoir[j] = sample;
|
|
413
|
+
}
|
|
414
|
+
this.seen += 1;
|
|
415
|
+
}
|
|
416
|
+
/** True number of finite samples offered. */
|
|
417
|
+
get size() {
|
|
418
|
+
return this.seen;
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* Summarize what was collected. `sum`, `mean`, `min` and `max` are EXACT even past the
|
|
422
|
+
* cap (they are accumulated, not read off the reservoir); the remaining three are
|
|
423
|
+
* computed from the retained sample.
|
|
424
|
+
*/
|
|
425
|
+
stats() {
|
|
426
|
+
if (this.seen === 0) return emptyDurationStats();
|
|
427
|
+
const fromReservoir = durationStats(this.reservoir);
|
|
428
|
+
return {
|
|
429
|
+
...fromReservoir,
|
|
430
|
+
min: this.lowest,
|
|
431
|
+
max: this.highest,
|
|
432
|
+
sum: this.total,
|
|
433
|
+
mean: this.total / this.seen
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
// src/core/extract-variants.ts
|
|
439
|
+
var VARIANT_KEY_SEPARATOR = "";
|
|
440
|
+
function variantKey(sequence) {
|
|
441
|
+
return sequence.join(VARIANT_KEY_SEPARATOR);
|
|
442
|
+
}
|
|
443
|
+
function fnv1a32(text, basis) {
|
|
444
|
+
let hash = basis >>> 0;
|
|
445
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
446
|
+
hash ^= text.charCodeAt(i);
|
|
447
|
+
hash = Math.imul(hash, 16777619) >>> 0;
|
|
448
|
+
}
|
|
449
|
+
return hash >>> 0;
|
|
450
|
+
}
|
|
451
|
+
function hex8(value) {
|
|
452
|
+
return (value >>> 0).toString(16).padStart(8, "0");
|
|
453
|
+
}
|
|
454
|
+
function variantId(sequence) {
|
|
455
|
+
const key = variantKey(sequence);
|
|
456
|
+
return `v${hex8(fnv1a32(key, 2166136261))}${hex8(fnv1a32(key, 2216829733))}`;
|
|
457
|
+
}
|
|
458
|
+
function extractVariants(log) {
|
|
459
|
+
const normalized = asNormalizedLog(log);
|
|
460
|
+
const totalCases = normalized.cases.length;
|
|
461
|
+
if (totalCases === 0) return [];
|
|
462
|
+
const groups = /* @__PURE__ */ new Map();
|
|
463
|
+
for (const kase of normalized.cases) {
|
|
464
|
+
const sequence = sequenceOf(kase);
|
|
465
|
+
const key = variantKey(sequence);
|
|
466
|
+
let group = groups.get(key);
|
|
467
|
+
if (group === void 0) {
|
|
468
|
+
group = { key, sequence, caseIds: [], durations: [] };
|
|
469
|
+
groups.set(key, group);
|
|
470
|
+
}
|
|
471
|
+
group.caseIds.push(kase.caseId);
|
|
472
|
+
group.durations.push(kase.duration);
|
|
473
|
+
}
|
|
474
|
+
const ranked = [...groups.values()].sort(
|
|
475
|
+
(a, b) => b.caseIds.length - a.caseIds.length || (a.key < b.key ? -1 : a.key > b.key ? 1 : 0)
|
|
476
|
+
);
|
|
477
|
+
const taken = /* @__PURE__ */ new Set();
|
|
478
|
+
const variants = [];
|
|
479
|
+
let cumulativeCount = 0;
|
|
480
|
+
for (const group of ranked) {
|
|
481
|
+
const count = group.caseIds.length;
|
|
482
|
+
cumulativeCount += count;
|
|
483
|
+
let id = variantId(group.sequence);
|
|
484
|
+
if (taken.has(id)) {
|
|
485
|
+
let suffix = 1;
|
|
486
|
+
while (taken.has(`${id}-${suffix}`)) suffix += 1;
|
|
487
|
+
id = `${id}-${suffix}`;
|
|
488
|
+
}
|
|
489
|
+
taken.add(id);
|
|
490
|
+
variants.push({
|
|
491
|
+
id,
|
|
492
|
+
sequence: group.sequence,
|
|
493
|
+
count,
|
|
494
|
+
share: count / totalCases,
|
|
495
|
+
cumulativeShare: cumulativeCount / totalCases,
|
|
496
|
+
caseIds: group.caseIds,
|
|
497
|
+
duration: durationStats(group.durations)
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
return variants;
|
|
501
|
+
}
|
|
502
|
+
function sequenceOf(kase) {
|
|
503
|
+
const sequence = new Array(kase.events.length);
|
|
504
|
+
for (let i = 0; i < kase.events.length; i += 1) {
|
|
505
|
+
sequence[i] = kase.events[i].activity;
|
|
506
|
+
}
|
|
507
|
+
return sequence;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// src/core/discover-graph.ts
|
|
511
|
+
var EDGE_KEY_SEPARATOR = VARIANT_KEY_SEPARATOR;
|
|
512
|
+
function discoverGraph(log, options = {}) {
|
|
513
|
+
const flowTime = options.flowTime ?? "idle_time";
|
|
514
|
+
const capacity = options.maxDurationSamples ?? DURATION_SAMPLE_CAP;
|
|
515
|
+
const normalized = asNormalizedLog(log);
|
|
516
|
+
const activities = /* @__PURE__ */ new Map();
|
|
517
|
+
const transitions = /* @__PURE__ */ new Map();
|
|
518
|
+
const startActivities = /* @__PURE__ */ new Map();
|
|
519
|
+
const endActivities = /* @__PURE__ */ new Map();
|
|
520
|
+
const variantKeys = /* @__PURE__ */ new Set();
|
|
521
|
+
let samplerIndex = 0;
|
|
522
|
+
const nextSeed = () => {
|
|
523
|
+
samplerIndex += 1;
|
|
524
|
+
return (2654435769 ^ Math.imul(samplerIndex, 2654435761)) >>> 0;
|
|
525
|
+
};
|
|
526
|
+
let events = 0;
|
|
527
|
+
const seenActivities = /* @__PURE__ */ new Set();
|
|
528
|
+
const seenEdges = /* @__PURE__ */ new Set();
|
|
529
|
+
for (const kase of normalized.cases) {
|
|
530
|
+
const trace = kase.events;
|
|
531
|
+
if (trace.length === 0) continue;
|
|
532
|
+
events += trace.length;
|
|
533
|
+
seenActivities.clear();
|
|
534
|
+
seenEdges.clear();
|
|
535
|
+
const sequence = new Array(trace.length);
|
|
536
|
+
for (let i = 0; i < trace.length; i += 1) {
|
|
537
|
+
const event = trace[i];
|
|
538
|
+
const name = event.activity;
|
|
539
|
+
sequence[i] = name;
|
|
540
|
+
let activity = activities.get(name);
|
|
541
|
+
if (activity === void 0) {
|
|
542
|
+
activity = { instances: 0, cases: 0, duration: new DurationSampler(nextSeed(), capacity) };
|
|
543
|
+
activities.set(name, activity);
|
|
544
|
+
}
|
|
545
|
+
activity.instances += 1;
|
|
546
|
+
activity.duration.add(event.duration);
|
|
547
|
+
if (!seenActivities.has(name)) {
|
|
548
|
+
seenActivities.add(name);
|
|
549
|
+
activity.cases += 1;
|
|
550
|
+
}
|
|
551
|
+
if (i === 0) continue;
|
|
552
|
+
const previous = trace[i - 1];
|
|
553
|
+
const source = previous.activity;
|
|
554
|
+
const key = `${source}${EDGE_KEY_SEPARATOR}${name}`;
|
|
555
|
+
let edge = transitions.get(key);
|
|
556
|
+
if (edge === void 0) {
|
|
557
|
+
edge = {
|
|
558
|
+
source,
|
|
559
|
+
target: name,
|
|
560
|
+
count: 0,
|
|
561
|
+
caseCount: 0,
|
|
562
|
+
duration: new DurationSampler(nextSeed(), capacity)
|
|
563
|
+
};
|
|
564
|
+
transitions.set(key, edge);
|
|
565
|
+
}
|
|
566
|
+
edge.count += 1;
|
|
567
|
+
edge.duration.add(
|
|
568
|
+
flowTime === "inter_start_time" ? event.start - previous.start : event.start - previous.end
|
|
569
|
+
);
|
|
570
|
+
if (!seenEdges.has(key)) {
|
|
571
|
+
seenEdges.add(key);
|
|
572
|
+
edge.caseCount += 1;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
const first = trace[0];
|
|
576
|
+
const last = trace[trace.length - 1];
|
|
577
|
+
startActivities.set(first.activity, (startActivities.get(first.activity) ?? 0) + 1);
|
|
578
|
+
endActivities.set(last.activity, (endActivities.get(last.activity) ?? 0) + 1);
|
|
579
|
+
variantKeys.add(sequence.join(VARIANT_KEY_SEPARATOR));
|
|
580
|
+
}
|
|
581
|
+
const activityList = [];
|
|
582
|
+
for (const [id, accumulator] of activities) {
|
|
583
|
+
activityList.push({
|
|
584
|
+
id,
|
|
585
|
+
label: id,
|
|
586
|
+
instances: accumulator.instances,
|
|
587
|
+
cases: accumulator.cases,
|
|
588
|
+
isStart: startActivities.has(id),
|
|
589
|
+
isEnd: endActivities.has(id),
|
|
590
|
+
duration: accumulator.duration.stats()
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
activityList.sort((a, b) => b.instances - a.instances || compareStrings(a.id, b.id));
|
|
594
|
+
const transitionList = [];
|
|
595
|
+
for (const accumulator of transitions.values()) {
|
|
596
|
+
transitionList.push({
|
|
597
|
+
source: accumulator.source,
|
|
598
|
+
target: accumulator.target,
|
|
599
|
+
count: accumulator.count,
|
|
600
|
+
caseCount: accumulator.caseCount,
|
|
601
|
+
duration: accumulator.duration.stats(),
|
|
602
|
+
isSelfLoop: accumulator.source === accumulator.target,
|
|
603
|
+
isBackEdge: false
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
transitionList.sort(
|
|
607
|
+
(a, b) => b.count - a.count || compareStrings(a.source, b.source) || compareStrings(a.target, b.target)
|
|
608
|
+
);
|
|
609
|
+
return {
|
|
610
|
+
activities: activityList,
|
|
611
|
+
transitions: transitionList,
|
|
612
|
+
startActivities: toSortedRecord(startActivities),
|
|
613
|
+
endActivities: toSortedRecord(endActivities),
|
|
614
|
+
totals: { cases: normalized.cases.length, events, variants: variantKeys.size }
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
function compareStrings(a, b) {
|
|
618
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
619
|
+
}
|
|
620
|
+
function toSortedRecord(counts) {
|
|
621
|
+
const out = {};
|
|
622
|
+
for (const key of [...counts.keys()].sort()) out[key] = counts.get(key);
|
|
623
|
+
return out;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// src/core/fixtures/synthetic-log.ts
|
|
627
|
+
var SYNTHETIC_LOG_EPOCH = 17676e8;
|
|
628
|
+
var MINUTE = 6e4;
|
|
629
|
+
function mulberry322(seed) {
|
|
630
|
+
let a = seed >>> 0;
|
|
631
|
+
return () => {
|
|
632
|
+
a = a + 1831565813 >>> 0;
|
|
633
|
+
let t = a;
|
|
634
|
+
t = Math.imul(t ^ t >>> 15, t | 1);
|
|
635
|
+
t ^= t + Math.imul(t ^ t >>> 7, t | 61);
|
|
636
|
+
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
var RESOURCES = [
|
|
640
|
+
"A. Novak",
|
|
641
|
+
"B. Ferreira",
|
|
642
|
+
"C. Okafor",
|
|
643
|
+
"D. Lindqvist",
|
|
644
|
+
"E. Haddad",
|
|
645
|
+
"Credit Service",
|
|
646
|
+
"Warehouse Robot"
|
|
647
|
+
];
|
|
648
|
+
var REGIONS = ["North", "South", "East", "West"];
|
|
649
|
+
var SEGMENTS = ["Enterprise", "Mid-market", "SMB"];
|
|
650
|
+
var SYNTHETIC_ACTIVITIES = [
|
|
651
|
+
"Create Order",
|
|
652
|
+
"Check Credit",
|
|
653
|
+
"Amend Order",
|
|
654
|
+
"Approve Order",
|
|
655
|
+
"Reject Order",
|
|
656
|
+
"Reserve Stock",
|
|
657
|
+
"Cancel Order",
|
|
658
|
+
"Pick Items",
|
|
659
|
+
"Ship Order",
|
|
660
|
+
"Send Invoice",
|
|
661
|
+
"Receive Payment"
|
|
662
|
+
];
|
|
663
|
+
function buildTrace(random) {
|
|
664
|
+
const trace = ["Create Order", "Check Credit"];
|
|
665
|
+
let reworks = 0;
|
|
666
|
+
while (reworks < 2 && random() < 0.14) {
|
|
667
|
+
trace.push("Amend Order", "Check Credit");
|
|
668
|
+
reworks += 1;
|
|
669
|
+
}
|
|
670
|
+
if (random() < 0.09) {
|
|
671
|
+
trace.push("Reject Order");
|
|
672
|
+
return trace;
|
|
673
|
+
}
|
|
674
|
+
trace.push("Approve Order", "Reserve Stock");
|
|
675
|
+
if (random() < 0.05) {
|
|
676
|
+
trace.push("Cancel Order");
|
|
677
|
+
return trace;
|
|
678
|
+
}
|
|
679
|
+
trace.push("Pick Items");
|
|
680
|
+
if (random() < 0.07) trace.push("Pick Items");
|
|
681
|
+
if (random() < 0.32) trace.push("Send Invoice", "Ship Order");
|
|
682
|
+
else trace.push("Ship Order", "Send Invoice");
|
|
683
|
+
trace.push("Receive Payment");
|
|
684
|
+
return trace;
|
|
685
|
+
}
|
|
686
|
+
function generateSyntheticLog(options) {
|
|
687
|
+
const total = Math.max(0, Math.floor(options.cases));
|
|
688
|
+
const random = mulberry322(options.seed ?? 1);
|
|
689
|
+
const epoch = options.startTime ?? SYNTHETIC_LOG_EPOCH;
|
|
690
|
+
const events = [];
|
|
691
|
+
const caseAttributes = {};
|
|
692
|
+
const pad = String(total).length;
|
|
693
|
+
for (let index2 = 0; index2 < total; index2 += 1) {
|
|
694
|
+
const caseId = `case-${String(index2 + 1).padStart(Math.max(5, pad), "0")}`;
|
|
695
|
+
const trace = buildTrace(random);
|
|
696
|
+
caseAttributes[caseId] = {
|
|
697
|
+
region: REGIONS[Math.floor(random() * REGIONS.length)],
|
|
698
|
+
segment: SEGMENTS[Math.floor(random() * SEGMENTS.length)],
|
|
699
|
+
// Whole euros, so a table can render the value without a rounding decision.
|
|
700
|
+
orderValue: 250 + Math.floor(random() * 9750)
|
|
701
|
+
};
|
|
702
|
+
let cursor = epoch + index2 * 61 * MINUTE + Math.floor(random() * 40) * MINUTE;
|
|
703
|
+
for (const activity of trace) {
|
|
704
|
+
const idle = Math.floor(random() * 180) * MINUTE;
|
|
705
|
+
const work = (2 + Math.floor(random() * 44)) * MINUTE;
|
|
706
|
+
const start = cursor + idle;
|
|
707
|
+
const end = start + work;
|
|
708
|
+
events.push({
|
|
709
|
+
caseId,
|
|
710
|
+
activity,
|
|
711
|
+
timestamp: end,
|
|
712
|
+
startTimestamp: start,
|
|
713
|
+
resource: RESOURCES[Math.floor(random() * RESOURCES.length)]
|
|
714
|
+
});
|
|
715
|
+
cursor = end;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
return { events, caseAttributes };
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// src/core/fixtures/generate-bpi-2012-subset.ts
|
|
722
|
+
var BPI_2012_SUBSET_EPOCH = 13254876e5;
|
|
723
|
+
var MINUTE2 = 6e4;
|
|
724
|
+
function mulberry323(seed) {
|
|
725
|
+
let a = seed >>> 0;
|
|
726
|
+
return () => {
|
|
727
|
+
a = a + 1831565813 >>> 0;
|
|
728
|
+
let t = a;
|
|
729
|
+
t = Math.imul(t ^ t >>> 15, t | 1);
|
|
730
|
+
t ^= t + Math.imul(t ^ t >>> 7, t | 61);
|
|
731
|
+
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
var RESOURCES2 = [
|
|
735
|
+
"Loan Officer 1",
|
|
736
|
+
"Loan Officer 2",
|
|
737
|
+
"Loan Officer 3",
|
|
738
|
+
"Credit Assessor",
|
|
739
|
+
"Automated System"
|
|
740
|
+
];
|
|
741
|
+
var BPI_2012_ACTIVITIES = [
|
|
742
|
+
"A_SUBMITTED",
|
|
743
|
+
"A_PARTLYSUBMITTED",
|
|
744
|
+
"A_PREACCEPTED",
|
|
745
|
+
"W_Completeren aanvraag",
|
|
746
|
+
"A_ACCEPTED",
|
|
747
|
+
"O_SELECTED",
|
|
748
|
+
"A_FINALIZED",
|
|
749
|
+
"O_CREATED",
|
|
750
|
+
"O_SENT",
|
|
751
|
+
"W_Nabellen offertes",
|
|
752
|
+
"O_SENT_BACK",
|
|
753
|
+
"A_REGISTERED",
|
|
754
|
+
"A_APPROVED",
|
|
755
|
+
"A_ACTIVATED",
|
|
756
|
+
"O_ACCEPTED",
|
|
757
|
+
"A_DECLINED",
|
|
758
|
+
"O_DECLINED",
|
|
759
|
+
"O_CANCELLED",
|
|
760
|
+
"A_CANCELLED"
|
|
761
|
+
];
|
|
762
|
+
function buildTrace2(random) {
|
|
763
|
+
const trace = ["A_SUBMITTED", "A_PARTLYSUBMITTED"];
|
|
764
|
+
if (random() < 0.06) {
|
|
765
|
+
trace.push("A_DECLINED");
|
|
766
|
+
return trace;
|
|
767
|
+
}
|
|
768
|
+
trace.push("A_PREACCEPTED", "W_Completeren aanvraag");
|
|
769
|
+
let reworks = 0;
|
|
770
|
+
while (reworks < 2 && random() < 0.18) {
|
|
771
|
+
trace.push("W_Completeren aanvraag");
|
|
772
|
+
reworks += 1;
|
|
773
|
+
}
|
|
774
|
+
if (random() < 0.08) {
|
|
775
|
+
trace.push("A_CANCELLED");
|
|
776
|
+
return trace;
|
|
777
|
+
}
|
|
778
|
+
trace.push(
|
|
779
|
+
"A_ACCEPTED",
|
|
780
|
+
"O_SELECTED",
|
|
781
|
+
"A_FINALIZED",
|
|
782
|
+
"O_CREATED",
|
|
783
|
+
"O_SENT",
|
|
784
|
+
"W_Nabellen offertes"
|
|
785
|
+
);
|
|
786
|
+
if (random() < 0.25) trace.push("O_SENT_BACK", "O_SENT", "W_Nabellen offertes");
|
|
787
|
+
if (random() < 0.1) {
|
|
788
|
+
if (random() < 0.3) {
|
|
789
|
+
trace.push("O_CANCELLED", "A_CANCELLED");
|
|
790
|
+
} else {
|
|
791
|
+
trace.push("O_DECLINED", random() < 0.5 ? "A_CANCELLED" : "A_DECLINED");
|
|
792
|
+
}
|
|
793
|
+
return trace;
|
|
794
|
+
}
|
|
795
|
+
trace.push("O_ACCEPTED", "A_REGISTERED", "A_APPROVED", "A_ACTIVATED");
|
|
796
|
+
return trace;
|
|
797
|
+
}
|
|
798
|
+
function generateBpi2012Subset(options) {
|
|
799
|
+
const total = Math.max(0, Math.floor(options.cases));
|
|
800
|
+
const random = mulberry323(options.seed ?? 1);
|
|
801
|
+
const epoch = options.startTime ?? BPI_2012_SUBSET_EPOCH;
|
|
802
|
+
const events = [];
|
|
803
|
+
const pad = String(total).length;
|
|
804
|
+
for (let index2 = 0; index2 < total; index2 += 1) {
|
|
805
|
+
const caseId = `bpi-${String(index2 + 1).padStart(Math.max(6, pad), "0")}`;
|
|
806
|
+
const trace = buildTrace2(random);
|
|
807
|
+
let cursor = epoch + index2 * 47 * MINUTE2 + Math.floor(random() * 30) * MINUTE2;
|
|
808
|
+
for (const activity of trace) {
|
|
809
|
+
const idle = Math.floor(random() * 240) * MINUTE2;
|
|
810
|
+
const work = (1 + Math.floor(random() * 20)) * MINUTE2;
|
|
811
|
+
const start = cursor + idle;
|
|
812
|
+
const end = start + work;
|
|
813
|
+
events.push({
|
|
814
|
+
caseId,
|
|
815
|
+
activity,
|
|
816
|
+
timestamp: end,
|
|
817
|
+
startTimestamp: start,
|
|
818
|
+
resource: RESOURCES2[Math.floor(random() * RESOURCES2.length)]
|
|
819
|
+
});
|
|
820
|
+
cursor = end;
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
return { events };
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// src/core/abstract-graph.ts
|
|
827
|
+
function edgeKey(source, target) {
|
|
828
|
+
return `${source}${EDGE_KEY_SEPARATOR}${target}`;
|
|
829
|
+
}
|
|
830
|
+
function compareStrings2(a, b) {
|
|
831
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
832
|
+
}
|
|
833
|
+
function clampFraction(value) {
|
|
834
|
+
if (!Number.isFinite(value)) return 1;
|
|
835
|
+
return value < 0 ? 0 : value > 1 ? 1 : value;
|
|
836
|
+
}
|
|
837
|
+
function countToKeep(total, fraction, atLeast) {
|
|
838
|
+
if (total === 0) return 0;
|
|
839
|
+
const kept = Math.round(total * fraction);
|
|
840
|
+
return kept < atLeast ? Math.min(atLeast, total) : kept;
|
|
841
|
+
}
|
|
842
|
+
var MinHeap = class {
|
|
843
|
+
nodes = [];
|
|
844
|
+
costs = [];
|
|
845
|
+
get size() {
|
|
846
|
+
return this.nodes.length;
|
|
847
|
+
}
|
|
848
|
+
push(node, cost) {
|
|
849
|
+
this.nodes.push(node);
|
|
850
|
+
this.costs.push(cost);
|
|
851
|
+
let i = this.nodes.length - 1;
|
|
852
|
+
while (i > 0) {
|
|
853
|
+
const parent = i - 1 >> 1;
|
|
854
|
+
if (this.costs[parent] <= this.costs[i]) break;
|
|
855
|
+
this.swap(i, parent);
|
|
856
|
+
i = parent;
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
pop() {
|
|
860
|
+
if (this.nodes.length === 0) return void 0;
|
|
861
|
+
const node = this.nodes[0];
|
|
862
|
+
const cost = this.costs[0];
|
|
863
|
+
const lastNode = this.nodes.pop();
|
|
864
|
+
const lastCost = this.costs.pop();
|
|
865
|
+
if (this.nodes.length > 0) {
|
|
866
|
+
this.nodes[0] = lastNode;
|
|
867
|
+
this.costs[0] = lastCost;
|
|
868
|
+
let i = 0;
|
|
869
|
+
for (; ; ) {
|
|
870
|
+
const left = i * 2 + 1;
|
|
871
|
+
const right = left + 1;
|
|
872
|
+
let smallest = i;
|
|
873
|
+
const size = this.nodes.length;
|
|
874
|
+
if (left < size && this.costs[left] < this.costs[smallest]) {
|
|
875
|
+
smallest = left;
|
|
876
|
+
}
|
|
877
|
+
if (right < size && this.costs[right] < this.costs[smallest]) {
|
|
878
|
+
smallest = right;
|
|
879
|
+
}
|
|
880
|
+
if (smallest === i) break;
|
|
881
|
+
this.swap(i, smallest);
|
|
882
|
+
i = smallest;
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
return { node, cost };
|
|
886
|
+
}
|
|
887
|
+
swap(a, b) {
|
|
888
|
+
const node = this.nodes[a];
|
|
889
|
+
this.nodes[a] = this.nodes[b];
|
|
890
|
+
this.nodes[b] = node;
|
|
891
|
+
const cost = this.costs[a];
|
|
892
|
+
this.costs[a] = this.costs[b];
|
|
893
|
+
this.costs[b] = cost;
|
|
894
|
+
}
|
|
895
|
+
};
|
|
896
|
+
function abstractGraph(graph, opts) {
|
|
897
|
+
const keepConnected = opts.keepConnected ?? true;
|
|
898
|
+
const invert = opts.invert ?? false;
|
|
899
|
+
const activityFraction = clampFraction(opts.activities);
|
|
900
|
+
const pathFraction = clampFraction(opts.paths);
|
|
901
|
+
const rankedActivities = [...graph.activities].sort(
|
|
902
|
+
(a, b) => b.cases - a.cases || b.instances - a.instances || compareStrings2(a.id, b.id)
|
|
903
|
+
);
|
|
904
|
+
const activityKeepCount = countToKeep(rankedActivities.length, activityFraction, 1);
|
|
905
|
+
const keptActivities = new Set(
|
|
906
|
+
(invert ? rankedActivities.slice(rankedActivities.length - activityKeepCount) : rankedActivities.slice(0, activityKeepCount)).map((activity) => activity.id)
|
|
907
|
+
);
|
|
908
|
+
const candidateEdges = graph.transitions.filter(
|
|
909
|
+
(edge) => keptActivities.has(edge.source) && keptActivities.has(edge.target)
|
|
910
|
+
);
|
|
911
|
+
const rankedEdges = [...candidateEdges].sort(
|
|
912
|
+
(a, b) => b.count - a.count || b.caseCount - a.caseCount || compareStrings2(a.source, b.source) || compareStrings2(a.target, b.target)
|
|
913
|
+
);
|
|
914
|
+
const edgeKeepCount = countToKeep(rankedEdges.length, pathFraction, 0);
|
|
915
|
+
const keptEdges = new Set(
|
|
916
|
+
(invert ? rankedEdges.slice(rankedEdges.length - edgeKeepCount) : rankedEdges.slice(0, edgeKeepCount)).map((edge) => edgeKey(edge.source, edge.target))
|
|
917
|
+
);
|
|
918
|
+
if (keepConnected && keptActivities.size > 0) {
|
|
919
|
+
reconnect(graph, keptActivities, keptEdges);
|
|
920
|
+
}
|
|
921
|
+
const activities = graph.activities.filter(
|
|
922
|
+
(activity) => keptActivities.has(activity.id)
|
|
923
|
+
);
|
|
924
|
+
const transitions = graph.transitions.filter(
|
|
925
|
+
(edge) => keptEdges.has(edgeKey(edge.source, edge.target))
|
|
926
|
+
);
|
|
927
|
+
return {
|
|
928
|
+
activities,
|
|
929
|
+
transitions,
|
|
930
|
+
startActivities: pick(graph.startActivities, keptActivities),
|
|
931
|
+
endActivities: pick(graph.endActivities, keptActivities),
|
|
932
|
+
// Passed through by reference: totals describe the LOG, and abstraction is a view.
|
|
933
|
+
totals: graph.totals,
|
|
934
|
+
hidden: {
|
|
935
|
+
activities: graph.activities.length - activities.length,
|
|
936
|
+
paths: graph.transitions.length - transitions.length
|
|
937
|
+
}
|
|
938
|
+
};
|
|
939
|
+
}
|
|
940
|
+
function pick(record, keep) {
|
|
941
|
+
const out = {};
|
|
942
|
+
for (const name of Object.keys(record)) {
|
|
943
|
+
if (keep.has(name)) out[name] = record[name];
|
|
944
|
+
}
|
|
945
|
+
return out;
|
|
946
|
+
}
|
|
947
|
+
function reconnect(graph, keptActivities, keptEdges) {
|
|
948
|
+
const forward = /* @__PURE__ */ new Map();
|
|
949
|
+
const backward = /* @__PURE__ */ new Map();
|
|
950
|
+
let maxCount = 1;
|
|
951
|
+
for (const edge of graph.transitions) {
|
|
952
|
+
if (edge.count > maxCount) maxCount = edge.count;
|
|
953
|
+
index(forward, edge.source, edge);
|
|
954
|
+
index(backward, edge.target, edge);
|
|
955
|
+
}
|
|
956
|
+
const logMax = Math.log(maxCount);
|
|
957
|
+
const cost = (count) => count > 0 ? logMax - Math.log(count) : logMax;
|
|
958
|
+
const bound = graph.activities.length + graph.transitions.length + 2;
|
|
959
|
+
for (let round = 0; round < bound; round += 1) {
|
|
960
|
+
const before = keptActivities.size + keptEdges.size;
|
|
961
|
+
repairDirection(graph.startActivities, forward, true);
|
|
962
|
+
repairDirection(graph.endActivities, backward, false);
|
|
963
|
+
if (keptActivities.size + keptEdges.size === before) break;
|
|
964
|
+
}
|
|
965
|
+
function repairDirection(seedCounts, adjacency, downstream) {
|
|
966
|
+
const allSeeds = Object.keys(seedCounts);
|
|
967
|
+
if (allSeeds.length === 0) return;
|
|
968
|
+
if (!allSeeds.some((id) => keptActivities.has(id))) anchor(seedCounts, keptActivities);
|
|
969
|
+
const keptSeeds = allSeeds.filter((id) => keptActivities.has(id));
|
|
970
|
+
if (keptSeeds.length === 0) return;
|
|
971
|
+
const reached = spread(keptSeeds, adjacency, downstream);
|
|
972
|
+
for (const activity of graph.activities) {
|
|
973
|
+
if (!keptActivities.has(activity.id) || reached.has(activity.id)) continue;
|
|
974
|
+
const route = shortestRoute(keptSeeds, activity.id, adjacency, downstream, true) ?? shortestRoute(keptSeeds, activity.id, adjacency, downstream, false) ?? shortestRoute(allSeeds, activity.id, adjacency, downstream, false);
|
|
975
|
+
if (route === void 0) continue;
|
|
976
|
+
for (const id of route.activities) keptActivities.add(id);
|
|
977
|
+
for (const key of route.edges) keptEdges.add(key);
|
|
978
|
+
for (const id of spread([...route.activities], adjacency, downstream)) reached.add(id);
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
function spread(seeds, adjacency, downstream) {
|
|
982
|
+
const seen = new Set(seeds.filter((id) => keptActivities.has(id)));
|
|
983
|
+
const queue = [...seen];
|
|
984
|
+
for (let head = 0; head < queue.length; head += 1) {
|
|
985
|
+
const node = queue[head];
|
|
986
|
+
for (const edge of adjacency.get(node) ?? []) {
|
|
987
|
+
const next = downstream ? edge.target : edge.source;
|
|
988
|
+
if (!keptActivities.has(next)) continue;
|
|
989
|
+
if (!keptEdges.has(edgeKey(edge.source, edge.target))) continue;
|
|
990
|
+
if (seen.has(next)) continue;
|
|
991
|
+
seen.add(next);
|
|
992
|
+
queue.push(next);
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
return seen;
|
|
996
|
+
}
|
|
997
|
+
function shortestRoute(seeds, target, adjacency, downstream, keptOnly) {
|
|
998
|
+
const distance = /* @__PURE__ */ new Map();
|
|
999
|
+
const previous = /* @__PURE__ */ new Map();
|
|
1000
|
+
const settled = /* @__PURE__ */ new Set();
|
|
1001
|
+
const heap = new MinHeap();
|
|
1002
|
+
for (const seed of seeds) {
|
|
1003
|
+
distance.set(seed, 0);
|
|
1004
|
+
heap.push(seed, 0);
|
|
1005
|
+
}
|
|
1006
|
+
while (heap.size > 0) {
|
|
1007
|
+
const top = heap.pop();
|
|
1008
|
+
if (settled.has(top.node)) continue;
|
|
1009
|
+
settled.add(top.node);
|
|
1010
|
+
if (top.node === target) break;
|
|
1011
|
+
for (const edge of adjacency.get(top.node) ?? []) {
|
|
1012
|
+
const next = downstream ? edge.target : edge.source;
|
|
1013
|
+
if (next === top.node) continue;
|
|
1014
|
+
if (keptOnly && !keptActivities.has(next)) continue;
|
|
1015
|
+
const candidate = top.cost + cost(edge.count);
|
|
1016
|
+
const known = distance.get(next);
|
|
1017
|
+
if (known !== void 0 && known <= candidate) continue;
|
|
1018
|
+
distance.set(next, candidate);
|
|
1019
|
+
previous.set(next, edge);
|
|
1020
|
+
heap.push(next, candidate);
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
if (!settled.has(target)) return void 0;
|
|
1024
|
+
const route = { activities: /* @__PURE__ */ new Set([target]), edges: /* @__PURE__ */ new Set() };
|
|
1025
|
+
let cursor = target;
|
|
1026
|
+
for (; ; ) {
|
|
1027
|
+
const edge = previous.get(cursor);
|
|
1028
|
+
if (edge === void 0) break;
|
|
1029
|
+
route.edges.add(edgeKey(edge.source, edge.target));
|
|
1030
|
+
route.activities.add(edge.source);
|
|
1031
|
+
route.activities.add(edge.target);
|
|
1032
|
+
cursor = downstream ? edge.source : edge.target;
|
|
1033
|
+
}
|
|
1034
|
+
return route;
|
|
1035
|
+
}
|
|
1036
|
+
function anchor(counts, kept) {
|
|
1037
|
+
const names = Object.keys(counts);
|
|
1038
|
+
if (names.length === 0) return;
|
|
1039
|
+
if (names.some((name) => kept.has(name))) return;
|
|
1040
|
+
let best = names[0];
|
|
1041
|
+
for (const name of names) {
|
|
1042
|
+
const value = counts[name];
|
|
1043
|
+
const bestValue = counts[best];
|
|
1044
|
+
if (value > bestValue || value === bestValue && compareStrings2(name, best) < 0) best = name;
|
|
1045
|
+
}
|
|
1046
|
+
kept.add(best);
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
function index(map, key, edge) {
|
|
1050
|
+
const bucket = map.get(key);
|
|
1051
|
+
if (bucket === void 0) map.set(key, [edge]);
|
|
1052
|
+
else bucket.push(edge);
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
// src/core/reconcile-graph.ts
|
|
1056
|
+
function transitionKey(transition) {
|
|
1057
|
+
return `${transition.source}${EDGE_KEY_SEPARATOR}${transition.target}`;
|
|
1058
|
+
}
|
|
1059
|
+
function reconcileGraph(presented, filtered) {
|
|
1060
|
+
const filteredActivities = new Map(
|
|
1061
|
+
filtered.activities.map((activity) => [activity.id, activity])
|
|
1062
|
+
);
|
|
1063
|
+
const filteredTransitions = new Map(
|
|
1064
|
+
filtered.transitions.map((transition) => [transitionKey(transition), transition])
|
|
1065
|
+
);
|
|
1066
|
+
const excludedActivities = [];
|
|
1067
|
+
const activities = presented.activities.map((activity) => {
|
|
1068
|
+
const survivor = filteredActivities.get(activity.id);
|
|
1069
|
+
if (survivor) return survivor;
|
|
1070
|
+
excludedActivities.push(activity.id);
|
|
1071
|
+
return {
|
|
1072
|
+
...activity,
|
|
1073
|
+
instances: 0,
|
|
1074
|
+
cases: 0,
|
|
1075
|
+
isStart: false,
|
|
1076
|
+
isEnd: false,
|
|
1077
|
+
duration: emptyDurationStats()
|
|
1078
|
+
};
|
|
1079
|
+
});
|
|
1080
|
+
const excludedTransitions = [];
|
|
1081
|
+
const transitions = presented.transitions.map((transition) => {
|
|
1082
|
+
const key = transitionKey(transition);
|
|
1083
|
+
const survivor = filteredTransitions.get(key);
|
|
1084
|
+
if (survivor) return survivor;
|
|
1085
|
+
excludedTransitions.push(key);
|
|
1086
|
+
return {
|
|
1087
|
+
...transition,
|
|
1088
|
+
count: 0,
|
|
1089
|
+
caseCount: 0,
|
|
1090
|
+
duration: emptyDurationStats()
|
|
1091
|
+
};
|
|
1092
|
+
});
|
|
1093
|
+
const graph = {
|
|
1094
|
+
...presented,
|
|
1095
|
+
activities,
|
|
1096
|
+
transitions,
|
|
1097
|
+
startActivities: filtered.startActivities,
|
|
1098
|
+
endActivities: filtered.endActivities,
|
|
1099
|
+
totals: filtered.totals
|
|
1100
|
+
};
|
|
1101
|
+
return { graph, excludedActivities, excludedTransitions };
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
// src/core/aggregate-performance.ts
|
|
1105
|
+
var DURATION_UNIT_MS = Object.freeze({
|
|
1106
|
+
ms: 1,
|
|
1107
|
+
s: 1e3,
|
|
1108
|
+
min: 6e4,
|
|
1109
|
+
h: 36e5,
|
|
1110
|
+
d: 864e5
|
|
1111
|
+
});
|
|
1112
|
+
function performanceValue(stats, agg) {
|
|
1113
|
+
switch (agg) {
|
|
1114
|
+
case "mean":
|
|
1115
|
+
return stats.mean;
|
|
1116
|
+
case "min":
|
|
1117
|
+
return stats.min;
|
|
1118
|
+
case "max":
|
|
1119
|
+
return stats.max;
|
|
1120
|
+
case "sum":
|
|
1121
|
+
return stats.sum;
|
|
1122
|
+
case "p90":
|
|
1123
|
+
return stats.p90;
|
|
1124
|
+
case "trimmed_mean":
|
|
1125
|
+
return stats.trimmedMean;
|
|
1126
|
+
case "median":
|
|
1127
|
+
default:
|
|
1128
|
+
return stats.median;
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
function scaleStats(stats, factor) {
|
|
1132
|
+
if (factor === 1) return { ...stats };
|
|
1133
|
+
return {
|
|
1134
|
+
min: stats.min * factor,
|
|
1135
|
+
max: stats.max * factor,
|
|
1136
|
+
mean: stats.mean * factor,
|
|
1137
|
+
median: stats.median * factor,
|
|
1138
|
+
p90: stats.p90 * factor,
|
|
1139
|
+
sum: stats.sum * factor,
|
|
1140
|
+
trimmedMean: stats.trimmedMean * factor
|
|
1141
|
+
};
|
|
1142
|
+
}
|
|
1143
|
+
function edgeKey2(source, target) {
|
|
1144
|
+
return `${source}${EDGE_KEY_SEPARATOR}${target}`;
|
|
1145
|
+
}
|
|
1146
|
+
function aggregatePerformance(graph, opts) {
|
|
1147
|
+
const factor = 1 / (DURATION_UNIT_MS[opts.unit] ?? 1);
|
|
1148
|
+
let activitySource;
|
|
1149
|
+
let transitionSource;
|
|
1150
|
+
if (opts.log !== void 0) {
|
|
1151
|
+
const derived = discoverGraph(opts.log, {
|
|
1152
|
+
flowTime: opts.flowTime,
|
|
1153
|
+
...opts.maxDurationSamples === void 0 ? {} : { maxDurationSamples: opts.maxDurationSamples }
|
|
1154
|
+
});
|
|
1155
|
+
activitySource = new Map(derived.activities.map((a) => [a.id, a.duration]));
|
|
1156
|
+
transitionSource = new Map(
|
|
1157
|
+
derived.transitions.map((t) => [edgeKey2(t.source, t.target), t.duration])
|
|
1158
|
+
);
|
|
1159
|
+
}
|
|
1160
|
+
const activityValues = {};
|
|
1161
|
+
const activities = graph.activities.map((activity) => {
|
|
1162
|
+
const source = activitySource?.get(activity.id) ?? activity.duration;
|
|
1163
|
+
const duration = scaleStats(source, factor);
|
|
1164
|
+
activityValues[activity.id] = performanceValue(duration, opts.agg);
|
|
1165
|
+
return { ...activity, duration };
|
|
1166
|
+
});
|
|
1167
|
+
const transitionValues = {};
|
|
1168
|
+
const transitions = graph.transitions.map((edge) => {
|
|
1169
|
+
const key = edgeKey2(edge.source, edge.target);
|
|
1170
|
+
const source = transitionSource?.get(key) ?? edge.duration;
|
|
1171
|
+
const duration = scaleStats(source, factor);
|
|
1172
|
+
transitionValues[key] = performanceValue(duration, opts.agg);
|
|
1173
|
+
return { ...edge, duration };
|
|
1174
|
+
});
|
|
1175
|
+
return {
|
|
1176
|
+
activities,
|
|
1177
|
+
transitions,
|
|
1178
|
+
startActivities: { ...graph.startActivities },
|
|
1179
|
+
endActivities: { ...graph.endActivities },
|
|
1180
|
+
totals: { ...graph.totals },
|
|
1181
|
+
performance: {
|
|
1182
|
+
agg: opts.agg,
|
|
1183
|
+
flowTime: opts.flowTime,
|
|
1184
|
+
unit: opts.unit,
|
|
1185
|
+
activities: activityValues,
|
|
1186
|
+
transitions: transitionValues,
|
|
1187
|
+
activityDomain: minMax(Object.values(activityValues)),
|
|
1188
|
+
transitionDomain: minMax(Object.values(transitionValues))
|
|
1189
|
+
}
|
|
1190
|
+
};
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
// src/core/detect-rework.ts
|
|
1194
|
+
function detectRework(log) {
|
|
1195
|
+
const normalized = asNormalizedLog(log);
|
|
1196
|
+
const tallies = /* @__PURE__ */ new Map();
|
|
1197
|
+
let selfLoops = 0;
|
|
1198
|
+
let loops = 0;
|
|
1199
|
+
let casesWithRework = 0;
|
|
1200
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1201
|
+
for (const kase of normalized.cases) {
|
|
1202
|
+
seen.clear();
|
|
1203
|
+
let caseHasRework = false;
|
|
1204
|
+
let previous;
|
|
1205
|
+
for (const event of kase.events) {
|
|
1206
|
+
const name = event.activity;
|
|
1207
|
+
let tally = tallies.get(name);
|
|
1208
|
+
if (tally === void 0) {
|
|
1209
|
+
tally = { selfLoops: 0, loops: 0 };
|
|
1210
|
+
tallies.set(name, tally);
|
|
1211
|
+
}
|
|
1212
|
+
if (seen.has(name)) {
|
|
1213
|
+
caseHasRework = true;
|
|
1214
|
+
if (name === previous) {
|
|
1215
|
+
tally.selfLoops += 1;
|
|
1216
|
+
selfLoops += 1;
|
|
1217
|
+
} else {
|
|
1218
|
+
tally.loops += 1;
|
|
1219
|
+
loops += 1;
|
|
1220
|
+
}
|
|
1221
|
+
} else {
|
|
1222
|
+
seen.add(name);
|
|
1223
|
+
}
|
|
1224
|
+
previous = name;
|
|
1225
|
+
}
|
|
1226
|
+
if (caseHasRework) casesWithRework += 1;
|
|
1227
|
+
}
|
|
1228
|
+
const perActivity = {};
|
|
1229
|
+
for (const name of [...tallies.keys()].sort()) {
|
|
1230
|
+
perActivity[name] = tallies.get(name);
|
|
1231
|
+
}
|
|
1232
|
+
return {
|
|
1233
|
+
selfLoops,
|
|
1234
|
+
loops,
|
|
1235
|
+
caseReworkRate: normalized.cases.length === 0 ? 0 : casesWithRework / normalized.cases.length,
|
|
1236
|
+
perActivity
|
|
1237
|
+
};
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
// src/core/filter-log.ts
|
|
1241
|
+
function sequenceOf2(kase) {
|
|
1242
|
+
const sequence = new Array(kase.events.length);
|
|
1243
|
+
for (let i = 0; i < kase.events.length; i += 1) {
|
|
1244
|
+
sequence[i] = kase.events[i].activity;
|
|
1245
|
+
}
|
|
1246
|
+
return sequence;
|
|
1247
|
+
}
|
|
1248
|
+
function valuesFor(kase, key) {
|
|
1249
|
+
const attributes = kase.attributes;
|
|
1250
|
+
if (attributes !== void 0 && Object.hasOwn(attributes, key)) return [attributes[key]];
|
|
1251
|
+
const values = [];
|
|
1252
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1253
|
+
for (const event of kase.events) {
|
|
1254
|
+
const value = key === "resource" ? event.resource : event.attributes?.[key];
|
|
1255
|
+
if (value === void 0) continue;
|
|
1256
|
+
if (seen.has(value)) continue;
|
|
1257
|
+
seen.add(value);
|
|
1258
|
+
values.push(value);
|
|
1259
|
+
}
|
|
1260
|
+
return values;
|
|
1261
|
+
}
|
|
1262
|
+
function ordered(left, right, wantGreater) {
|
|
1263
|
+
if (typeof left === "number" && typeof right === "number") {
|
|
1264
|
+
return wantGreater ? left > right : left < right;
|
|
1265
|
+
}
|
|
1266
|
+
if (typeof left === "string" && typeof right === "string") {
|
|
1267
|
+
return wantGreater ? left > right : left < right;
|
|
1268
|
+
}
|
|
1269
|
+
return false;
|
|
1270
|
+
}
|
|
1271
|
+
function matchesAttribute(kase, spec) {
|
|
1272
|
+
const values = valuesFor(kase, spec.key);
|
|
1273
|
+
switch (spec.op) {
|
|
1274
|
+
case "eq":
|
|
1275
|
+
return values.some((value) => Object.is(value, spec.value));
|
|
1276
|
+
// `ne` is the negation of `eq`, not "some value differs" — otherwise a case offering
|
|
1277
|
+
// two resources would satisfy both `eq` and `ne` against the same value.
|
|
1278
|
+
case "ne":
|
|
1279
|
+
return !values.some((value) => Object.is(value, spec.value));
|
|
1280
|
+
case "gt":
|
|
1281
|
+
return values.some((value) => ordered(value, spec.value, true));
|
|
1282
|
+
case "lt":
|
|
1283
|
+
return values.some((value) => ordered(value, spec.value, false));
|
|
1284
|
+
case "in": {
|
|
1285
|
+
if (!Array.isArray(spec.value)) return false;
|
|
1286
|
+
const allowed = spec.value;
|
|
1287
|
+
return values.some((value) => allowed.some((candidate) => Object.is(value, candidate)));
|
|
1288
|
+
}
|
|
1289
|
+
default:
|
|
1290
|
+
return false;
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
function matchesOne(kase, sequence, spec) {
|
|
1294
|
+
switch (spec.kind) {
|
|
1295
|
+
case "with":
|
|
1296
|
+
return sequence.includes(spec.activity);
|
|
1297
|
+
case "without":
|
|
1298
|
+
return !sequence.includes(spec.activity);
|
|
1299
|
+
case "startsWith":
|
|
1300
|
+
return sequence[0] === spec.activity;
|
|
1301
|
+
case "endsWith":
|
|
1302
|
+
return sequence[sequence.length - 1] === spec.activity;
|
|
1303
|
+
case "follower": {
|
|
1304
|
+
if (spec.direct === true) {
|
|
1305
|
+
for (let i = 0; i + 1 < sequence.length; i += 1) {
|
|
1306
|
+
if (sequence[i] === spec.a && sequence[i + 1] === spec.b) return true;
|
|
1307
|
+
}
|
|
1308
|
+
return false;
|
|
1309
|
+
}
|
|
1310
|
+
const first = sequence.indexOf(spec.a);
|
|
1311
|
+
if (first < 0) return false;
|
|
1312
|
+
return sequence.indexOf(spec.b, first + 1) >= 0;
|
|
1313
|
+
}
|
|
1314
|
+
case "attribute":
|
|
1315
|
+
return matchesAttribute(kase, spec);
|
|
1316
|
+
case "duration": {
|
|
1317
|
+
if (spec.min === void 0 && spec.max === void 0) return true;
|
|
1318
|
+
if (!Number.isFinite(kase.duration)) return false;
|
|
1319
|
+
if (spec.min !== void 0 && kase.duration < spec.min) return false;
|
|
1320
|
+
if (spec.max !== void 0 && kase.duration > spec.max) return false;
|
|
1321
|
+
return true;
|
|
1322
|
+
}
|
|
1323
|
+
case "variant":
|
|
1324
|
+
return spec.ids.includes(variantId(sequence));
|
|
1325
|
+
case "cases":
|
|
1326
|
+
return spec.ids.includes(kase.caseId);
|
|
1327
|
+
default:
|
|
1328
|
+
return true;
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
function caseMatchesFilters(kase, specs) {
|
|
1332
|
+
if (specs.length === 0) return true;
|
|
1333
|
+
const sequence = sequenceOf2(kase);
|
|
1334
|
+
for (const spec of specs) {
|
|
1335
|
+
if (!matchesOne(kase, sequence, spec)) return false;
|
|
1336
|
+
}
|
|
1337
|
+
return true;
|
|
1338
|
+
}
|
|
1339
|
+
function filterNormalizedLog(log, specs) {
|
|
1340
|
+
const normalized = asNormalizedLog(log);
|
|
1341
|
+
if (specs.length === 0) return normalized;
|
|
1342
|
+
const cases = [];
|
|
1343
|
+
let events = 0;
|
|
1344
|
+
for (const kase of normalized.cases) {
|
|
1345
|
+
if (!caseMatchesFilters(kase, specs)) continue;
|
|
1346
|
+
cases.push(kase);
|
|
1347
|
+
events += kase.events.length;
|
|
1348
|
+
}
|
|
1349
|
+
return { cases, totals: { cases: cases.length, events } };
|
|
1350
|
+
}
|
|
1351
|
+
function filterLog(log, specs) {
|
|
1352
|
+
if (specs.length === 0) return log;
|
|
1353
|
+
const kept = /* @__PURE__ */ new Set();
|
|
1354
|
+
for (const kase of filterNormalizedLog(log, specs).cases) kept.add(kase.caseId);
|
|
1355
|
+
const filtered = { events: log.events.filter((row) => kept.has(row?.caseId)) };
|
|
1356
|
+
if (log.caseAttributes !== void 0) {
|
|
1357
|
+
const caseAttributes = {};
|
|
1358
|
+
for (const caseId of Object.keys(log.caseAttributes)) {
|
|
1359
|
+
if (kept.has(caseId)) {
|
|
1360
|
+
caseAttributes[caseId] = log.caseAttributes[caseId];
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
filtered.caseAttributes = caseAttributes;
|
|
1364
|
+
}
|
|
1365
|
+
return filtered;
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
// src/core/worker/process-worker.ts
|
|
1369
|
+
function handleProcessRequest(request) {
|
|
1370
|
+
try {
|
|
1371
|
+
if (request.kind === "discover") {
|
|
1372
|
+
return {
|
|
1373
|
+
id: request.id,
|
|
1374
|
+
ok: true,
|
|
1375
|
+
kind: "discover",
|
|
1376
|
+
graph: discoverGraph(request.log, request.options ?? {})
|
|
1377
|
+
};
|
|
1378
|
+
}
|
|
1379
|
+
return { id: request.id, ok: true, kind: "variants", variants: extractVariants(request.log) };
|
|
1380
|
+
} catch (error) {
|
|
1381
|
+
return { id: request.id, ok: false, error: describe(error) };
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
function describe(error) {
|
|
1385
|
+
if (error instanceof Error) return error.message;
|
|
1386
|
+
return String(error);
|
|
1387
|
+
}
|
|
1388
|
+
function inWorkerScope() {
|
|
1389
|
+
const scope = globalThis;
|
|
1390
|
+
const constructor = scope.WorkerGlobalScope;
|
|
1391
|
+
if (typeof constructor !== "function") return false;
|
|
1392
|
+
return scope.self instanceof constructor;
|
|
1393
|
+
}
|
|
1394
|
+
if (inWorkerScope()) {
|
|
1395
|
+
const scope = self;
|
|
1396
|
+
scope.addEventListener("message", (event) => {
|
|
1397
|
+
scope.postMessage(handleProcessRequest(event.data));
|
|
1398
|
+
});
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
// src/core/worker/create-process-worker.ts
|
|
1402
|
+
function workerConstructible() {
|
|
1403
|
+
return typeof Worker !== "undefined" && typeof URL !== "undefined";
|
|
1404
|
+
}
|
|
1405
|
+
function createProcessWorker(options = {}) {
|
|
1406
|
+
const construct = options.createWorker;
|
|
1407
|
+
let inline = options.forceInline === true || construct === void 0 && !workerConstructible();
|
|
1408
|
+
let terminated = false;
|
|
1409
|
+
let worker;
|
|
1410
|
+
let nextId = 0;
|
|
1411
|
+
const pending = /* @__PURE__ */ new Map();
|
|
1412
|
+
function degrade() {
|
|
1413
|
+
inline = true;
|
|
1414
|
+
const stale = [...pending.values()];
|
|
1415
|
+
pending.clear();
|
|
1416
|
+
if (worker !== void 0) {
|
|
1417
|
+
const dying = worker;
|
|
1418
|
+
worker = void 0;
|
|
1419
|
+
try {
|
|
1420
|
+
dying.terminate();
|
|
1421
|
+
} catch {
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
for (const entry of stale) entry.settle(handleProcessRequest(entry.request));
|
|
1425
|
+
}
|
|
1426
|
+
function onMessage(event) {
|
|
1427
|
+
const response = event.data;
|
|
1428
|
+
if (response === void 0 || typeof response.id !== "number") return;
|
|
1429
|
+
const entry = pending.get(response.id);
|
|
1430
|
+
if (entry === void 0) return;
|
|
1431
|
+
pending.delete(response.id);
|
|
1432
|
+
entry.settle(response);
|
|
1433
|
+
}
|
|
1434
|
+
function ensureWorker() {
|
|
1435
|
+
if (inline || terminated) return void 0;
|
|
1436
|
+
if (worker !== void 0) return worker;
|
|
1437
|
+
try {
|
|
1438
|
+
worker = construct === void 0 ? new Worker(new URL("./process-worker.js", import.meta.url), {
|
|
1439
|
+
type: "module"
|
|
1440
|
+
}) : construct();
|
|
1441
|
+
} catch {
|
|
1442
|
+
degrade();
|
|
1443
|
+
return void 0;
|
|
1444
|
+
}
|
|
1445
|
+
worker.addEventListener("message", onMessage);
|
|
1446
|
+
worker.addEventListener("error", degrade);
|
|
1447
|
+
worker.addEventListener("messageerror", degrade);
|
|
1448
|
+
return worker;
|
|
1449
|
+
}
|
|
1450
|
+
function send(build) {
|
|
1451
|
+
if (terminated) return Promise.reject(new Error("process worker terminated"));
|
|
1452
|
+
nextId += 1;
|
|
1453
|
+
const request = build(nextId);
|
|
1454
|
+
const active = ensureWorker();
|
|
1455
|
+
if (active === void 0) {
|
|
1456
|
+
return Promise.resolve().then(() => handleProcessRequest(request));
|
|
1457
|
+
}
|
|
1458
|
+
return new Promise((resolve, reject) => {
|
|
1459
|
+
pending.set(request.id, { request, settle: resolve });
|
|
1460
|
+
try {
|
|
1461
|
+
active.postMessage(request);
|
|
1462
|
+
} catch {
|
|
1463
|
+
pending.delete(request.id);
|
|
1464
|
+
degrade();
|
|
1465
|
+
try {
|
|
1466
|
+
resolve(handleProcessRequest(request));
|
|
1467
|
+
} catch (error) {
|
|
1468
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
});
|
|
1472
|
+
}
|
|
1473
|
+
function unwrap(response, read) {
|
|
1474
|
+
if (!response.ok) throw new Error(response.error);
|
|
1475
|
+
return read(response);
|
|
1476
|
+
}
|
|
1477
|
+
return {
|
|
1478
|
+
async discover(log, discoverOptions) {
|
|
1479
|
+
const response = await send(
|
|
1480
|
+
(id) => discoverOptions === void 0 ? { id, kind: "discover", log } : { id, kind: "discover", log, options: discoverOptions }
|
|
1481
|
+
);
|
|
1482
|
+
return unwrap(response, (ok) => ok.graph);
|
|
1483
|
+
},
|
|
1484
|
+
async variants(log) {
|
|
1485
|
+
const response = await send((id) => ({ id, kind: "variants", log }));
|
|
1486
|
+
return unwrap(response, (ok) => ok.variants);
|
|
1487
|
+
},
|
|
1488
|
+
terminate() {
|
|
1489
|
+
terminated = true;
|
|
1490
|
+
const stale = [...pending.values()];
|
|
1491
|
+
pending.clear();
|
|
1492
|
+
for (const entry of stale) {
|
|
1493
|
+
entry.settle({ id: entry.request.id, ok: false, error: "process worker terminated" });
|
|
1494
|
+
}
|
|
1495
|
+
if (worker !== void 0) {
|
|
1496
|
+
const dying = worker;
|
|
1497
|
+
worker = void 0;
|
|
1498
|
+
try {
|
|
1499
|
+
dying.terminate();
|
|
1500
|
+
} catch {
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
},
|
|
1504
|
+
get inline() {
|
|
1505
|
+
return inline;
|
|
1506
|
+
}
|
|
1507
|
+
};
|
|
1508
|
+
}
|
|
1509
|
+
export {
|
|
1510
|
+
BPI_2012_ACTIVITIES,
|
|
1511
|
+
BPI_2012_SUBSET_EPOCH,
|
|
1512
|
+
DEFAULT_LIFECYCLE_VALUES,
|
|
1513
|
+
DURATION_SAMPLE_CAP,
|
|
1514
|
+
DURATION_UNIT_MS,
|
|
1515
|
+
DurationSampler,
|
|
1516
|
+
EDGE_KEY_SEPARATOR,
|
|
1517
|
+
EMPTY_DURATION_STATS,
|
|
1518
|
+
SYNTHETIC_ACTIVITIES,
|
|
1519
|
+
SYNTHETIC_LOG_EPOCH,
|
|
1520
|
+
TRIM_FRACTION,
|
|
1521
|
+
VARIANT_KEY_SEPARATOR,
|
|
1522
|
+
abstractGraph,
|
|
1523
|
+
aggregatePerformance,
|
|
1524
|
+
asNormalizedLog,
|
|
1525
|
+
caseMatchesFilters,
|
|
1526
|
+
clampWidth,
|
|
1527
|
+
createProcessWorker,
|
|
1528
|
+
detectRework,
|
|
1529
|
+
discoverGraph,
|
|
1530
|
+
durationStats,
|
|
1531
|
+
emptyDurationStats,
|
|
1532
|
+
extractVariants,
|
|
1533
|
+
filterLog,
|
|
1534
|
+
filterNormalizedLog,
|
|
1535
|
+
fromCsv,
|
|
1536
|
+
fromFlatRows,
|
|
1537
|
+
generateBpi2012Subset,
|
|
1538
|
+
generateSyntheticLog,
|
|
1539
|
+
handleProcessRequest,
|
|
1540
|
+
isNormalizedLog,
|
|
1541
|
+
minMax,
|
|
1542
|
+
normalizeLifecycle,
|
|
1543
|
+
normalizeLog,
|
|
1544
|
+
parseDelimited,
|
|
1545
|
+
performanceValue,
|
|
1546
|
+
quantile,
|
|
1547
|
+
quantileSorted,
|
|
1548
|
+
reconcileGraph,
|
|
1549
|
+
toEpochMs,
|
|
1550
|
+
variantId,
|
|
1551
|
+
variantKey
|
|
1552
|
+
};
|
|
1553
|
+
//# sourceMappingURL=index.js.map
|