@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
@@ -0,0 +1,1029 @@
1
+ /**
2
+ * The framework-free event-log and process-graph model — RM-049.
3
+ *
4
+ * Every downstream item in the process-mining track (RM-050 abstraction, RM-051 the
5
+ * process map, RM-052 the variant explorer, RM-053 the case table, RM-054 the coverage
6
+ * strip) reads these shapes, so they are the wave's frozen contract. Keep additions
7
+ * additive: a required field added here is a breaking change for five items at once.
8
+ *
9
+ * NOTHING in this module — or anywhere under `src/core/` — may import React, React Flow,
10
+ * visx, d3 or an `@elabs-ai/components-*` package. See `.claude/rules/process-components.md`
11
+ * and `pnpm process:reuse:check`.
12
+ */
13
+ /**
14
+ * One raw row of an event log, before normalization.
15
+ *
16
+ * `timestamp` accepts the three shapes tabular sources actually produce — an ISO string,
17
+ * an epoch number, or a `Date` — because the adapters hand rows through unchanged and a
18
+ * consumer should not have to pre-convert. {@link normalizeLog} resolves all three to
19
+ * epoch milliseconds.
20
+ */
21
+ interface EventRow {
22
+ /** Case (process instance) this event belongs to. */
23
+ caseId: string;
24
+ /** Activity name. This is the node identity in the discovered graph. */
25
+ activity: string;
26
+ /** When the event completed (or, for a `lifecycle: "start"` row, when it started). */
27
+ timestamp: string | number | Date;
28
+ /**
29
+ * Optional explicit start of an interval event. Ignored when the row is one half of a
30
+ * `lifecycle` pair — the paired `"start"` row wins, because it is the observed value.
31
+ */
32
+ startTimestamp?: string | number | Date;
33
+ /** Who or what executed the event (a user, a queue, a system). */
34
+ resource?: string;
35
+ /** Lifecycle transition. Absent means the row is an atomic (already-complete) event. */
36
+ lifecycle?: "start" | "complete";
37
+ /** Free-form event-level attributes carried through normalization untouched. */
38
+ attributes?: Record<string, string | number | boolean | null>;
39
+ }
40
+ /** A raw event log: the rows, plus optional per-case attributes keyed by `caseId`. */
41
+ interface EventLog {
42
+ events: EventRow[];
43
+ caseAttributes?: Record<string, Record<string, unknown>>;
44
+ }
45
+ /**
46
+ * Summary statistics over a set of duration samples, in milliseconds.
47
+ *
48
+ * All seven members are always present; an empty sample set yields zeros rather than
49
+ * `null`, so a renderer never has to branch on absence.
50
+ */
51
+ interface DurationStats {
52
+ min: number;
53
+ max: number;
54
+ mean: number;
55
+ median: number;
56
+ p90: number;
57
+ sum: number;
58
+ /** Mean after discarding the lowest and highest 10% of samples. */
59
+ trimmedMean: number;
60
+ }
61
+ /** Per-activity aggregates in a discovered graph. */
62
+ interface ActivityStats {
63
+ /** Stable node identity — the activity name. */
64
+ id: string;
65
+ /** Human-readable label. Equal to `id` unless a caller relabels the graph. */
66
+ label: string;
67
+ /** Total occurrences across all cases. */
68
+ instances: number;
69
+ /** Number of distinct cases the activity occurs in at least once. */
70
+ cases: number;
71
+ /** True when the activity starts at least one case. */
72
+ isStart: boolean;
73
+ /** True when the activity ends at least one case. */
74
+ isEnd: boolean;
75
+ /** Distribution of the activity's own execution durations. */
76
+ duration: DurationStats;
77
+ }
78
+ /** Per-edge aggregates in a discovered graph (a directly-follows relation). */
79
+ interface TransitionStats {
80
+ source: string;
81
+ target: string;
82
+ /** Total occurrences of the directly-follows pair across all cases. */
83
+ count: number;
84
+ /** Number of distinct cases the pair occurs in at least once. */
85
+ caseCount: number;
86
+ /** Distribution of the flow time between the two activities. */
87
+ duration: DurationStats;
88
+ /** `source === target`. */
89
+ isSelfLoop: boolean;
90
+ /**
91
+ * Whether the edge points backwards in the laid-out graph. Discovery does not lay out,
92
+ * so this is always `false` here; a layout pass sets it (RM-044).
93
+ */
94
+ isBackEdge: boolean;
95
+ }
96
+ /** A directly-follows graph plus its totals. */
97
+ interface ProcessGraph {
98
+ activities: ActivityStats[];
99
+ transitions: TransitionStats[];
100
+ /** Activity name → number of cases that start with it. */
101
+ startActivities: Record<string, number>;
102
+ /** Activity name → number of cases that end with it. */
103
+ endActivities: Record<string, number>;
104
+ totals: {
105
+ cases: number;
106
+ events: number;
107
+ variants: number;
108
+ };
109
+ }
110
+ /** One distinct activity sequence, with the cases that follow it. */
111
+ interface Variant {
112
+ /** Stable, reproducible id derived from the sequence — see `variantId`. */
113
+ id: string;
114
+ sequence: string[];
115
+ /** Number of cases following this sequence. */
116
+ count: number;
117
+ /** `count / totalCases`. */
118
+ share: number;
119
+ /** Running share across the descending-frequency order — monotonically non-decreasing. */
120
+ cumulativeShare: number;
121
+ /** Case ids following this sequence, in first-appearance order. */
122
+ caseIds: string[];
123
+ /** Distribution of the end-to-end case durations of `caseIds`. */
124
+ duration: DurationStats;
125
+ }
126
+ /** How an edge or node frequency is expressed to the reader. */
127
+ type FrequencyMode = "absolute" | "absolute_case" | "relative" | "relative_case" | "relative_antecedent" | "relative_consequent" | "max_repetitions";
128
+ /** Which member of a {@link DurationStats} a performance view reads. */
129
+ type PerformanceAgg = "median" | "mean" | "min" | "max" | "sum" | "p90" | "trimmed_mean";
130
+ /**
131
+ * Which elapsed time an edge measures.
132
+ *
133
+ * - `idle_time` — from the source's completion to the target's start (waiting time).
134
+ * - `inter_start_time` — from the source's start to the target's start (cycle time).
135
+ *
136
+ * For atomic events the two coincide, because start and completion are the same instant.
137
+ */
138
+ type FlowTime = "idle_time" | "inter_start_time";
139
+
140
+ /**
141
+ * Event-log normalization — RM-049.
142
+ *
143
+ * Turns a flat `EventRow[]` into per-case traces of INSTANCES: one entry per real
144
+ * activity execution, carrying a resolved `start` and `end` in epoch milliseconds. Every
145
+ * other module in `/core` consumes the normalized form, so the messy parts of a raw log
146
+ * (three timestamp encodings, lifecycle rows split across two lines, rows arriving out of
147
+ * order) are handled exactly once, here.
148
+ *
149
+ * Deterministic: same rows in, same traces out. No `Date.now()`, no randomness.
150
+ */
151
+
152
+ /** One activity execution, with both ends resolved to epoch milliseconds. */
153
+ interface NormalizedEvent {
154
+ activity: string;
155
+ /**
156
+ * Start of the execution. Equals `end` for an atomic event — an event with no
157
+ * `lifecycle` pair and no `startTimestamp` has no observed duration, and inventing one
158
+ * would put fabricated numbers into every downstream statistic.
159
+ */
160
+ start: number;
161
+ /** Completion of the execution. */
162
+ end: number;
163
+ /** `end - start`, never negative (a clock-skewed pair is floored at 0). */
164
+ duration: number;
165
+ resource?: string;
166
+ attributes?: Record<string, string | number | boolean | null>;
167
+ /**
168
+ * True when this instance came from a `lifecycle: "start"` row that never got a
169
+ * matching `"complete"`. Its `end` is provisional (it equals `start`), so a performance
170
+ * view can choose to exclude it.
171
+ */
172
+ isOpen: boolean;
173
+ }
174
+ /** One case (process instance): its trace, plus the case's own extent. */
175
+ interface NormalizedCase {
176
+ caseId: string;
177
+ /** Instances in ascending `start` order. Never empty. */
178
+ events: NormalizedEvent[];
179
+ /** Earliest `start` in the trace. */
180
+ start: number;
181
+ /** Latest `end` in the trace. */
182
+ end: number;
183
+ /** `end - start` — the case's throughput time. */
184
+ duration: number;
185
+ /** Per-case attributes carried over from `EventLog.caseAttributes`, when present. */
186
+ attributes?: Record<string, unknown>;
187
+ }
188
+ /** A normalized log: cases in first-appearance order, plus totals. */
189
+ interface NormalizedLog {
190
+ cases: NormalizedCase[];
191
+ totals: {
192
+ cases: number;
193
+ events: number;
194
+ };
195
+ }
196
+ /** A log in either shape. Every derivation entry point accepts both. */
197
+ type AnyLog = EventLog | NormalizedLog;
198
+ /** Narrowing predicate — `true` when `log` has already been through {@link normalizeLog}. */
199
+ declare function isNormalizedLog(log: AnyLog): log is NormalizedLog;
200
+ /** Normalize `log` unless it already is normalized. The idempotent entry point. */
201
+ declare function asNormalizedLog(log: AnyLog): NormalizedLog;
202
+ /**
203
+ * Resolve the three accepted timestamp encodings to epoch milliseconds.
204
+ *
205
+ * Returns `NaN` for an unparseable value rather than throwing: one bad cell in a
206
+ * ten-thousand-row CSV must not abort the whole import. Downstream, a `NaN` sorts as
207
+ * equal (so the row keeps its input position) and is dropped from duration samples.
208
+ */
209
+ declare function toEpochMs(value: string | number | Date | undefined | null): number;
210
+ /**
211
+ * Group rows by case, order each case in time, and pair `lifecycle` rows into instances.
212
+ *
213
+ * The pairing rule, stated precisely because five downstream items depend on it:
214
+ *
215
+ * - Rows are grouped by `caseId` in ONE pass and each case is sorted independently. The
216
+ * whole log is never sorted — sorting 200 000 rows to then split them is the shape that
217
+ * makes a browser import feel broken.
218
+ * - A `lifecycle: "start"` row OPENS an instance at its position in the case's timeline.
219
+ * - A `lifecycle: "complete"` row CLOSES the oldest still-open instance of the SAME
220
+ * activity, wherever that start sits in the trace. The two rows therefore do not have
221
+ * to be adjacent: interleaved `A-start, B-start, A-complete, B-complete` pairs
222
+ * correctly, and so does a log whose rows arrive in the wrong order entirely, because
223
+ * the per-case sort runs first.
224
+ * - Repeated executions of one activity in a case pair oldest-start-to-earliest-complete
225
+ * (FIFO), which is the only pairing that keeps intervals non-overlapping for a
226
+ * sequential resource.
227
+ * - A `"complete"` with no open start becomes an instance in its own right, using an
228
+ * explicit `startTimestamp` when the row carries one.
229
+ * - A `"start"` that is never completed stays OPEN: `end === start` and `isOpen` is true.
230
+ * - A row with no `lifecycle` is atomic: `start` comes from `startTimestamp` when given,
231
+ * otherwise from `timestamp` itself.
232
+ *
233
+ * Rows with an empty `caseId` or `activity` are dropped — they cannot take part in a
234
+ * directly-follows relation, and keeping them would put an unnamed node in every graph.
235
+ */
236
+ declare function normalizeLog(log: EventLog): NormalizedLog;
237
+
238
+ /**
239
+ * Flat tabular adapter — RM-049.
240
+ *
241
+ * Pre-shaped tabular data is the lingua franca of event logs: whatever the source (a
242
+ * warehouse query, a spreadsheet export, an API page), it arrives as rows of columns. This
243
+ * adapter maps those columns onto `EventRow`, so every other importer — the CSV reader
244
+ * next door, and any future one — can be a thin front end over this single mapping step
245
+ * rather than a second, subtly different interpretation of the same log.
246
+ *
247
+ * Pure and deterministic: no clock, no I/O, no mutation of the caller's rows.
248
+ */
249
+
250
+ /** One row of a flat source. Values are read defensively — a source may hand back anything. */
251
+ type FlatRow = Record<string, unknown>;
252
+ /** Which raw cell to read a lifecycle transition's `"start"` / `"complete"` from. */
253
+ interface LifecycleValues {
254
+ start?: readonly string[];
255
+ complete?: readonly string[];
256
+ }
257
+ /** Column names that `fromFlatRows` reads. Only `caseId`, `activity` and `timestamp` are required. */
258
+ interface FlatRowMapping {
259
+ /** Column holding the case (process instance) id. */
260
+ caseId: string;
261
+ /** Column holding the activity name. */
262
+ activity: string;
263
+ /** Column holding the event timestamp. */
264
+ timestamp: string;
265
+ /** Column holding an explicit interval start, when the source models one. */
266
+ startTimestamp?: string;
267
+ /** Column holding the executing resource. */
268
+ resource?: string;
269
+ /** Column holding a lifecycle transition. */
270
+ lifecycle?: string;
271
+ /** Extra columns copied verbatim into `EventRow.attributes`. */
272
+ attributes?: readonly string[];
273
+ /**
274
+ * Columns copied into `EventLog.caseAttributes`, taking the FIRST non-empty value seen
275
+ * per case. Case attributes are case-level by definition, so a later row disagreeing
276
+ * with an earlier one is a data problem, not something an adapter should silently
277
+ * resolve by overwriting.
278
+ */
279
+ caseAttributes?: readonly string[];
280
+ /**
281
+ * Raw cell values that mean `"start"` / `"complete"`, compared case-insensitively after
282
+ * trimming. Defaults to {@link DEFAULT_LIFECYCLE_VALUES}; supply your own for a source
283
+ * that spells them differently (`"S"` / `"C"`, `"begin"` / `"done"`).
284
+ */
285
+ lifecycleValues?: LifecycleValues;
286
+ }
287
+ /** The lifecycle spellings recognized when a mapping does not override them. */
288
+ declare const DEFAULT_LIFECYCLE_VALUES: Required<LifecycleValues>;
289
+ /** Map a raw lifecycle cell onto the canonical pair, or `undefined` when it matches neither. */
290
+ declare function normalizeLifecycle(raw: string | undefined, values?: LifecycleValues): "start" | "complete" | undefined;
291
+ /**
292
+ * Map flat rows onto an {@link EventLog}.
293
+ *
294
+ * Rows missing a case id, an activity or a timestamp are SKIPPED rather than throwing: a
295
+ * single blank trailing line or one incomplete record must not fail an entire import, and
296
+ * an event with no activity cannot take part in a directly-follows relation anyway. Count
297
+ * the difference between `rows.length` and `log.events.length` when you need to surface
298
+ * how many were dropped.
299
+ */
300
+ declare function fromFlatRows(rows: readonly FlatRow[], mapping: FlatRowMapping): EventLog;
301
+
302
+ /**
303
+ * CSV adapter — RM-049.
304
+ *
305
+ * A minimal RFC 4180 reader plus a thin front end onto {@link fromFlatRows}. It is written
306
+ * here rather than pulled from npm on purpose: `/core` is the framework-free,
307
+ * worker-safe leaf, and a parser this small (quoted fields, escaped quotes, embedded
308
+ * newlines, CRLF, a BOM) does not justify a runtime dependency in a package a consumer
309
+ * imports into a web worker.
310
+ *
311
+ * What it deliberately does NOT do: type inference, header de-duplication, streaming, or
312
+ * dialect sniffing. A source that needs any of those parses itself and calls
313
+ * {@link fromFlatRows} with the rows.
314
+ */
315
+
316
+ /** Options for {@link parseDelimited} and {@link fromCsv}. */
317
+ interface CsvOptions {
318
+ /** Field separator. Defaults to `","`; pass `"\t"` for TSV, `";"` for a European export. */
319
+ delimiter?: string;
320
+ /**
321
+ * Column names, when the text has NO header row. Omit for the usual case, where the
322
+ * first record is the header.
323
+ */
324
+ header?: readonly string[];
325
+ }
326
+ /** Everything `fromCsv` needs: the column mapping plus the dialect. */
327
+ type CsvMapping = FlatRowMapping & CsvOptions;
328
+ /**
329
+ * Parse RFC 4180 delimited text into records of raw string fields.
330
+ *
331
+ * Handles: quoted fields; a doubled `""` inside a quoted field as one literal quote;
332
+ * delimiters, `CR`, `LF` and `CRLF` inside a quoted field; `CRLF` or bare `LF` record
333
+ * separators; a leading UTF-8 BOM; and a trailing newline (which does NOT produce a
334
+ * phantom empty record).
335
+ *
336
+ * A field is returned exactly as written, without trimming — trimming is a MAPPING
337
+ * decision, and `fromFlatRows` makes it. Unterminated quotes are tolerated: the field runs
338
+ * to the end of the text rather than throwing, so a truncated download yields the rows it
339
+ * did contain.
340
+ */
341
+ declare function parseDelimited(text: string, delimiter?: string): string[][];
342
+ /**
343
+ * Read delimited text into an {@link EventLog}.
344
+ *
345
+ * The first record is the header unless `mapping.header` supplies one. Column names in
346
+ * the header are trimmed (a `", activity"` from a hand-edited export still matches
347
+ * `activity`); field VALUES are not, so `fromFlatRows` stays the only place that decides
348
+ * what an empty cell means.
349
+ *
350
+ * Empty text yields an empty log rather than throwing.
351
+ */
352
+ declare function fromCsv(text: string, mapping: CsvMapping): EventLog;
353
+
354
+ /**
355
+ * Separator used to key an edge. The same C0 control character the variant key uses, and
356
+ * for the same reason: no activity name can contain it, so `source + SEP + target` is an
357
+ * injective key.
358
+ */
359
+ declare const EDGE_KEY_SEPARATOR = "\u0001";
360
+ /** Options for {@link discoverGraph}. Every field has a default; `discoverGraph(log)` works. */
361
+ interface DiscoverGraphOptions {
362
+ /**
363
+ * Which elapsed time an edge measures. Defaults to `"idle_time"` — the wait between the
364
+ * source completing and the target starting, which is the number a bottleneck view
365
+ * wants. For a log of atomic events the two choices coincide.
366
+ */
367
+ flowTime?: FlowTime;
368
+ /**
369
+ * Reservoir capacity per activity and per edge. Lower it for a very wide log; raise it
370
+ * for sharper tail statistics. `sum`/`mean`/`min`/`max` stay exact regardless.
371
+ */
372
+ maxDurationSamples?: number;
373
+ }
374
+ /**
375
+ * Derive a {@link ProcessGraph} from a raw or already-normalized log.
376
+ *
377
+ * One pass per trace: each case's activity sequence is walked once, incrementing
378
+ * per-activity and per-edge counters and feeding two duration samplers. Per-case
379
+ * uniqueness (the `cases` and `caseCount` fields) is tracked with two `Set`s that are
380
+ * cleared per case rather than a per-key `Set` of case ids, so memory stays proportional
381
+ * to the widest trace, not to the log.
382
+ *
383
+ * `totals.variants` is counted from the same walk (the joined activity sequence goes into
384
+ * a `Set`), so a caller that only needs the headline number does not also have to run
385
+ * `extractVariants`.
386
+ */
387
+ declare function discoverGraph(log: AnyLog, options?: DiscoverGraphOptions): ProcessGraph;
388
+
389
+ /**
390
+ * Separator joining a sequence into its variant key.
391
+ *
392
+ * U+0001 (START OF HEADING) is a C0 control character. No real activity name contains
393
+ * one, which is what makes joining on it lossless where a printable separator
394
+ * (`" -> "`, `","`) would collide with a name that happens to contain the separator.
395
+ */
396
+ declare const VARIANT_KEY_SEPARATOR = "\u0001";
397
+ /** Join an activity sequence into its canonical variant key. */
398
+ declare function variantKey(sequence: readonly string[]): string;
399
+ /**
400
+ * The stable id for an activity sequence: `v` followed by 64 bits of FNV-1a over the
401
+ * variant key, as hex.
402
+ *
403
+ * Exported because a selection is a wave-wide concern — RM-052's variant explorer and
404
+ * RM-053's case table both need to name the same variant without holding the object, and
405
+ * a hash of the sequence is the only identifier that survives a re-run, a worker boundary
406
+ * and a URL.
407
+ */
408
+ declare function variantId(sequence: readonly string[]): string;
409
+ /**
410
+ * Group `log`'s cases into variants, ranked by frequency.
411
+ *
412
+ * Ordering is `count` descending, ties broken by the variant key ascending — a total
413
+ * order, so the same log always produces the same array in the same positions.
414
+ *
415
+ * `share` is `count / cases`. `cumulativeShare` is the running `cumulativeCount / cases`,
416
+ * which is monotonically non-decreasing by construction and exactly `1` on the last
417
+ * entry. `duration` summarizes the END-TO-END case durations of the variant's cases, so
418
+ * it answers "how long does this path take", not "how long does one step take".
419
+ *
420
+ * An empty log yields an empty array — never a single zero-count variant.
421
+ */
422
+ declare function extractVariants(log: AnyLog): Variant[];
423
+
424
+ /** The fraction discarded from EACH tail by `trimmedMean`. */
425
+ declare const TRIM_FRACTION = 0.1;
426
+ /** All-zero statistics — the answer for an empty sample set. */
427
+ declare const EMPTY_DURATION_STATS: Readonly<DurationStats>;
428
+ /** A fresh mutable copy of {@link EMPTY_DURATION_STATS}. */
429
+ declare function emptyDurationStats(): DurationStats;
430
+ /**
431
+ * Summarize `samples` (milliseconds) as {@link DurationStats}.
432
+ *
433
+ * - Non-finite samples are DROPPED, not propagated — one unparseable timestamp upstream
434
+ * must not turn an entire activity's statistics into `NaN`.
435
+ * - The input array is not mutated (a copy is sorted).
436
+ * - `p90` uses linear interpolation between order statistics (R-7 / `d3.quantile`).
437
+ * - `trimmedMean` discards the lowest and highest {@link TRIM_FRACTION} of samples. When
438
+ * trimming would leave nothing (fewer than 5 samples, where `floor(n * 0.1)` is 0 or
439
+ * the tails meet), it degrades to the plain mean rather than to `NaN`.
440
+ * - An empty (or all-non-finite) input answers all zeros.
441
+ */
442
+ declare function durationStats(samples: readonly number[]): DurationStats;
443
+ /** Default reservoir capacity for a single activity's or edge's duration samples. */
444
+ declare const DURATION_SAMPLE_CAP = 4096;
445
+ /**
446
+ * Bounded duration-sample collector.
447
+ *
448
+ * A log with a million events would otherwise hold a million numbers per hot edge just to
449
+ * compute seven statistics from them. The first {@link DURATION_SAMPLE_CAP} samples are
450
+ * kept outright; past the cap the collector switches to reservoir sampling (Algorithm R),
451
+ * so the retained set stays a uniform sample of everything seen while memory stays flat.
452
+ *
453
+ * `count` keeps the TRUE number of samples offered, and `sum` the true total, so a
454
+ * capped edge still reports an exact `sum` and an exact `mean`; only the order statistics
455
+ * (`median`, `p90`, `min`, `max`, `trimmedMean`) are estimated from the reservoir.
456
+ */
457
+ declare class DurationSampler {
458
+ private readonly capacity;
459
+ private readonly random;
460
+ private readonly reservoir;
461
+ /** Number of finite samples offered, including those the reservoir discarded. */
462
+ private seen;
463
+ private total;
464
+ private lowest;
465
+ private highest;
466
+ constructor(seed?: number, capacity?: number);
467
+ /** Offer one sample. Non-finite values are ignored. */
468
+ add(sample: number): void;
469
+ /** True number of finite samples offered. */
470
+ get size(): number;
471
+ /**
472
+ * Summarize what was collected. `sum`, `mean`, `min` and `max` are EXACT even past the
473
+ * cap (they are accumulated, not read off the reservoir); the remaining three are
474
+ * computed from the retained sample.
475
+ */
476
+ stats(): DurationStats;
477
+ }
478
+
479
+ /**
480
+ * The extent of `values`, ignoring non-finite entries.
481
+ *
482
+ * @returns `[min, max]`, or `[0, 0]` when there is nothing finite to measure.
483
+ */
484
+ declare function minMax(values: readonly number[]): [number, number];
485
+ /**
486
+ * The `q`-quantile of an ALREADY ASCENDING-SORTED, all-finite array, by linear
487
+ * interpolation between order statistics (the R-7 / `d3.quantile` definition).
488
+ *
489
+ * Exported for callers that already hold a sorted array — {@link durationStats} sorts
490
+ * once and reads several quantiles off it. Use {@link quantile} when the input is not
491
+ * known to be sorted.
492
+ */
493
+ declare function quantileSorted(sorted: readonly number[], q: number): number;
494
+ /**
495
+ * The `q`-quantile of `values` (0 ≤ `q` ≤ 1; out-of-range values are clamped).
496
+ *
497
+ * Sorts a COPY, so the caller's array is untouched. Non-finite entries are dropped;
498
+ * an empty (or all-non-finite) input answers `0`.
499
+ */
500
+ declare function quantile(values: readonly number[], q: number): number;
501
+ /**
502
+ * Map `value` from `domain` onto `range`, clamped to the range's endpoints.
503
+ *
504
+ * The workhorse behind "frequency → stroke width" and "share → bar length". A
505
+ * DEGENERATE domain (`d0 === d1`, e.g. a graph whose every edge has the same count)
506
+ * answers `range[0]`: with a zero-width domain every input sits at the domain floor, so
507
+ * the range floor is the consistent answer — and it is what the clamped interpolation
508
+ * gives for any `value <= d0`. A caller that wants a different neutral (the widest
509
+ * stroke, say) special-cases it explicitly rather than relying on a surprise here.
510
+ *
511
+ * A descending domain (`d0 > d1`) is honoured — it simply inverts the mapping.
512
+ */
513
+ declare function clampWidth(value: number, domain: readonly [number, number], range: readonly [number, number]): number;
514
+
515
+ /**
516
+ * Seeded synthetic event-log generator — RM-049.
517
+ *
518
+ * Produces an order-to-cash shaped log — a happy path, two exception branches, a rework
519
+ * loop back to an earlier activity, an occasional repeat of one step, and a step-order
520
+ * swap — at any size, from a seed. That is what lets tests, stories and benchmarks work
521
+ * with a realistically messy log without this repo ever shipping a large binary fixture,
522
+ * and without any of them depending on a wall clock.
523
+ *
524
+ * Determinism is the whole contract: `generateSyntheticLog({ cases: n, seed: s })` returns
525
+ * a byte-identical log on every run, in every engine, forever. Downstream items snapshot
526
+ * against it.
527
+ */
528
+
529
+ /** Epoch ms the first case starts at: 2026-01-05T08:00:00.000Z, a Monday morning. */
530
+ declare const SYNTHETIC_LOG_EPOCH = 1767600000000;
531
+ /** Options for {@link generateSyntheticLog}. */
532
+ interface SyntheticLogOptions {
533
+ /** How many cases to generate. Values below 1 yield an empty log. */
534
+ cases: number;
535
+ /** PRNG seed. The same seed always produces the same log. */
536
+ seed?: number;
537
+ /** Epoch ms the first case starts at. Defaults to {@link SYNTHETIC_LOG_EPOCH}. */
538
+ startTime?: number;
539
+ }
540
+ /** Activity vocabulary. Exported so a story can label or colour by name without guessing. */
541
+ declare const SYNTHETIC_ACTIVITIES: readonly ["Create Order", "Check Credit", "Amend Order", "Approve Order", "Reject Order", "Reserve Stock", "Cancel Order", "Pick Items", "Ship Order", "Send Invoice", "Receive Payment"];
542
+ /** One activity name from {@link SYNTHETIC_ACTIVITIES}. */
543
+ type SyntheticActivity = (typeof SYNTHETIC_ACTIVITIES)[number];
544
+ /**
545
+ * Generate a synthetic order-to-cash log.
546
+ *
547
+ * Every event is an INTERVAL event: it carries both a `startTimestamp` and a `timestamp`,
548
+ * so activity durations and edge idle times are both non-zero and a performance view has
549
+ * something real to show. Timestamps are epoch numbers rather than ISO strings — a
550
+ * 13 000-case log is a quarter of a million rows, and making a benchmark of the discovery
551
+ * pass mostly measure `Date.parse` would be measuring the wrong thing.
552
+ *
553
+ * Cases are emitted in order, one per `caseId` of the form `case-00001`, zero-padded so
554
+ * lexical and numeric order agree.
555
+ */
556
+ declare function generateSyntheticLog(options: SyntheticLogOptions): EventLog;
557
+
558
+ /**
559
+ * Seeded BPI-2012-shaped event-log generator — RM-053.
560
+ *
561
+ * A SEPARATE generator from `synthetic-log.ts` (RM-049), not a second copy of it: RM-049's
562
+ * `generateSyntheticLog` produces an order-to-cash shaped log (Create Order, Check Credit,
563
+ * Ship Order, …); this one produces a log shaped like the public BPI Challenge 2012 event
564
+ * log (van Dongen, 2012) — a Dutch financial institution's loan-application process, whose
565
+ * activity vocabulary follows the dataset's own three-lifecycle-prefix convention
566
+ * (`A_` application state, `O_` offer state, `W_` work item). The BPI Challenge datasets are
567
+ * a public academic research corpus published for the annual Business Process Intelligence
568
+ * Challenge; naming one here names a published research artifact, not a commercial vendor.
569
+ *
570
+ * This is what analysis §4 R21 asks for: a ~13,000-case fixture large enough to exercise
571
+ * `discoverGraph`'s performance budget (already covered by RM-049's own
572
+ * `generateSyntheticLog({ cases: 13_000 })` in `discover-graph.test.ts`) and, from RM-051
573
+ * onward, a Storybook "LargeGraph" story that wants a visually denser, more tangled graph
574
+ * than the order-to-cash shape produces (more branching, more rework, a wider activity
575
+ * vocabulary).
576
+ *
577
+ * Determinism is the whole contract, exactly as in `synthetic-log.ts`:
578
+ * `generateBpi2012Subset({ cases: n, seed: s })` returns a byte-identical log on every run.
579
+ *
580
+ * ## This module is BROWSER-SAFE — no Node built-ins
581
+ *
582
+ * Deliberately: `discoverGraph`/`extractVariants` demos (the RM-053 placeholder Storybook
583
+ * story, and any future browser-rendered fixture preview) import this module directly, and
584
+ * Storybook's story tests run in a real browser (`@storybook/addon-vitest`'s Playwright
585
+ * provider) where `node:fs`/`node:path`/`node:url` are externalized and throw on access. So
586
+ * this file exports the PURE generator only — no filesystem write, no `import.meta.url`
587
+ * resolution, no `process.argv` read. The file that writes the on-disk fixture is the
588
+ * sibling `generate-bpi-2012-subset.write.ts` (see below), which is the only place those
589
+ * Node-only imports appear.
590
+ *
591
+ * ## Producing the on-disk fixture
592
+ *
593
+ * The 13k-case output is a ~250k-row JSON document — too large to commit as a binary (see
594
+ * the repo's "no committed large binary fixture" convention, already followed by
595
+ * `synthetic-log.ts`). It is a GENERATED artifact: run
596
+ *
597
+ * ```
598
+ * pnpm --filter @elabs-ai/components-process generate:fixtures
599
+ * ```
600
+ *
601
+ * which writes `bpi-2012-subset.json` next to this file (git-ignored — see the repo root
602
+ * `.gitignore`), via `generate-bpi-2012-subset.write.ts` (run through `vitest`, not `tsx` —
603
+ * see that file's docblock for why). A consumer of the generated JSON should treat it as a
604
+ * build artifact, not a checked-in fixture; import {@link generateBpi2012Subset} directly
605
+ * wherever possible instead of reading the file.
606
+ */
607
+
608
+ /** Epoch ms the first case starts at: 2012-01-02T08:00:00.000Z, a Monday morning. */
609
+ declare const BPI_2012_SUBSET_EPOCH = 1325487600000;
610
+ /** Options for {@link generateBpi2012Subset}. */
611
+ interface Bpi2012SubsetOptions {
612
+ /** How many cases to generate. Values below 1 yield an empty log. */
613
+ cases: number;
614
+ /** PRNG seed. The same seed always produces the same log. */
615
+ seed?: number;
616
+ /** Epoch ms the first case starts at. Defaults to {@link BPI_2012_SUBSET_EPOCH}. */
617
+ startTime?: number;
618
+ }
619
+ /**
620
+ * Activity vocabulary, following the public BPI Challenge 2012 log's own naming convention:
621
+ * `A_` (application), `O_` (offer) and `W_` (work item) lifecycle prefixes.
622
+ */
623
+ declare const BPI_2012_ACTIVITIES: readonly ["A_SUBMITTED", "A_PARTLYSUBMITTED", "A_PREACCEPTED", "W_Completeren aanvraag", "A_ACCEPTED", "O_SELECTED", "A_FINALIZED", "O_CREATED", "O_SENT", "W_Nabellen offertes", "O_SENT_BACK", "A_REGISTERED", "A_APPROVED", "A_ACTIVATED", "O_ACCEPTED", "A_DECLINED", "O_DECLINED", "O_CANCELLED", "A_CANCELLED"];
624
+ /** One activity name from {@link BPI_2012_ACTIVITIES}. */
625
+ type Bpi2012Activity = (typeof BPI_2012_ACTIVITIES)[number];
626
+ /**
627
+ * Generate a synthetic BPI-2012-shaped loan-application log.
628
+ *
629
+ * Every event is an INTERVAL event (`startTimestamp` + `timestamp`), and cases are emitted
630
+ * as `caseId`s of the form `bpi-000001`, zero-padded so lexical and numeric order agree —
631
+ * the same shape `generateSyntheticLog` uses, so downstream consumers do not need a second
632
+ * case-id convention.
633
+ */
634
+ declare function generateBpi2012Subset(options: Bpi2012SubsetOptions): EventLog;
635
+
636
+ /** Options for {@link abstractGraph}. Both fractions are `0..1` and both are required. */
637
+ interface AbstractionOptions {
638
+ /** Fraction of activities to KEEP, `0..1`. At least one activity is always kept. */
639
+ activities: number;
640
+ /** Fraction of paths (transitions) to KEEP, `0..1`, over the kept-activity subgraph. */
641
+ paths: number;
642
+ /**
643
+ * Re-add whatever it takes to keep every kept activity reachable from a start activity
644
+ * and able to reach an end activity. Defaults to `true` — an island reads as a broken
645
+ * process, not a simplified one.
646
+ */
647
+ keepConnected?: boolean;
648
+ /** Hide the MOST frequent instead of the least — the "what is rare here" view. */
649
+ invert?: boolean;
650
+ }
651
+ /** What {@link abstractGraph} returns: a `ProcessGraph` plus what it left out. */
652
+ interface AbstractedGraph extends ProcessGraph {
653
+ hidden: {
654
+ /** Activities present in the input graph and absent from this one. */
655
+ activities: number;
656
+ /** Transitions present in the input graph and absent from this one. */
657
+ paths: number;
658
+ };
659
+ }
660
+ /**
661
+ * Reduce `graph` to the fraction of activities and paths a reader asked to see.
662
+ *
663
+ * Activities rank by `cases` (how many process instances touch them), ties broken by
664
+ * `instances` then by name; paths rank by `count`, ties broken by `caseCount` then by
665
+ * endpoint names. Ranking on `cases` rather than `instances` is deliberate: an activity
666
+ * executed forty times inside one case is a loop, not a backbone step, and a frequency
667
+ * slider that promoted it would hide the shape of the process.
668
+ *
669
+ * The returned arrays hold the INPUT's own statistic objects, in the input's order.
670
+ * Treat the result as read-only: mutating a returned `ActivityStats` mutates the source
671
+ * graph's, by design — that shared identity is what proves no recomputation happened.
672
+ */
673
+ declare function abstractGraph(graph: ProcessGraph, opts: AbstractionOptions): AbstractedGraph;
674
+
675
+ /** The result of {@link reconcileGraph}. */
676
+ interface ReconciledGraph<G extends ProcessGraph> {
677
+ /** `presented`'s exact element set, with `filtered`'s statistics where an element survived. */
678
+ graph: G;
679
+ /** Activity ids `presented` carries that `filtered` dropped — rendered as zeroed ghosts. */
680
+ excludedActivities: string[];
681
+ /** Transition keys (`source` + {@link EDGE_KEY_SEPARATOR} + `target`) `filtered` dropped. */
682
+ excludedTransitions: string[];
683
+ }
684
+ /**
685
+ * Reconcile a presented graph's element set against what a filter actually produced.
686
+ *
687
+ * `G` is generic over `ProcessGraph` so an already-abstracted graph (`AbstractedGraph`, which
688
+ * adds `hidden`) reconciles without losing its extra field — the return type is `G`, and the
689
+ * implementation spreads `presented` before overwriting only the fields this function owns.
690
+ */
691
+ declare function reconcileGraph<G extends ProcessGraph>(presented: G, filtered: ProcessGraph): ReconciledGraph<G>;
692
+
693
+ /** The units a duration can be spoken in. Everything upstream is milliseconds. */
694
+ type DurationUnit = "ms" | "s" | "min" | "h" | "d";
695
+ /** How many milliseconds each {@link DurationUnit} is worth. */
696
+ declare const DURATION_UNIT_MS: Readonly<Record<DurationUnit, number>>;
697
+ /** Options for {@link aggregatePerformance}. */
698
+ interface PerformanceOptions {
699
+ /** Which member of a {@link DurationStats} the layer reads. */
700
+ agg: PerformanceAgg;
701
+ /**
702
+ * Which elapsed time an edge measures. Honoured only when `log` is supplied — a graph
703
+ * alone cannot be converted from waiting time to cycle time, because the two are not
704
+ * derivable from each other's summary statistics. Without `log` this field DECLARES
705
+ * what the graph already carries and nothing is re-derived.
706
+ */
707
+ flowTime: FlowTime;
708
+ /** The unit every returned duration is expressed in. */
709
+ unit: DurationUnit;
710
+ /**
711
+ * The log the graph came from. Supply it to re-derive edge durations at `flowTime`;
712
+ * pass an already-normalized log (see `asNormalizedLog`) to skip a second parse.
713
+ */
714
+ log?: AnyLog;
715
+ /**
716
+ * Reservoir capacity per activity and per edge when re-deriving. Order statistics
717
+ * (`median`, `p90`, `trimmedMean`) are ESTIMATES past this many samples per key; raise
718
+ * it when a threshold has to be exact. Only meaningful together with `log`.
719
+ */
720
+ maxDurationSamples?: number;
721
+ }
722
+ /** The per-element scalars a performance view paints with, plus the domains to scale them on. */
723
+ interface PerformanceLayer {
724
+ agg: PerformanceAgg;
725
+ flowTime: FlowTime;
726
+ unit: DurationUnit;
727
+ /** Activity id → the selected aggregate of its execution time, in `unit`. */
728
+ activities: Record<string, number>;
729
+ /** `source␁target` (see `EDGE_KEY_SEPARATOR`) → the selected aggregate, in `unit`. */
730
+ transitions: Record<string, number>;
731
+ /** `[min, max]` over `activities` — `[0, 0]` when there are none. */
732
+ activityDomain: [number, number];
733
+ /** `[min, max]` over `transitions` — `[0, 0]` when there are none. */
734
+ transitionDomain: [number, number];
735
+ }
736
+ /** What {@link aggregatePerformance} returns: a `ProcessGraph` plus its metric layer. */
737
+ interface PerformanceGraph extends ProcessGraph {
738
+ performance: PerformanceLayer;
739
+ }
740
+ /**
741
+ * Read the member of `stats` a {@link PerformanceAgg} names.
742
+ *
743
+ * The one place the mapping lives, so a control, a legend and a renderer cannot disagree
744
+ * about what "p90" is. `trimmed_mean` is the snake_case spelling of `trimmedMean` —
745
+ * the type is a wire/URL value, the field is a property name.
746
+ */
747
+ declare function performanceValue(stats: DurationStats, agg: PerformanceAgg): number;
748
+ /**
749
+ * Express `graph`'s durations as a performance layer.
750
+ *
751
+ * Every `DurationStats` in the returned graph is converted to `opts.unit`, and
752
+ * `performance` carries the single scalar per activity and per transition that
753
+ * `opts.agg` selects, together with the two domains a scale needs.
754
+ *
755
+ * With `opts.log`, edge and activity durations are RE-DERIVED at `opts.flowTime` from the
756
+ * FULL log — which is what keeps a performance layer honest on an abstracted graph: the
757
+ * numbers describe everything that happened, not only the part currently drawn. Elements
758
+ * the re-derivation does not know about keep the statistics they arrived with.
759
+ *
760
+ * Order statistics past the reservoir cap are estimates (see `maxDurationSamples`);
761
+ * `sum`, `mean`, `min` and `max` are exact at any log size.
762
+ */
763
+ declare function aggregatePerformance(graph: ProcessGraph, opts: PerformanceOptions): PerformanceGraph;
764
+
765
+ /**
766
+ * Rework detection — RM-050.
767
+ *
768
+ * "Rework" is the part of a process that happens more than once inside a single case: a
769
+ * step retried immediately (a SELF-LOOP), or a step the case comes back to after going
770
+ * somewhere else (a LOOP). Both are repeats, and separating them matters — a self-loop is
771
+ * usually a retry or a batch, a loop is usually a rejection sending work backwards, and
772
+ * the two lead to different conversations about the process.
773
+ *
774
+ * Counted per OCCURRENCE, over the normalized trace: the first execution of an activity in
775
+ * a case is never rework, and every later one is exactly one of the two kinds. So
776
+ * `selfLoops + loops` is precisely the number of repeated executions in the log.
777
+ *
778
+ * Deterministic and framework-free, like everything else in `/core`.
779
+ */
780
+
781
+ /** Per-activity rework tallies. */
782
+ interface ActivityRework {
783
+ /** Repeats of this activity immediately after itself. */
784
+ selfLoops: number;
785
+ /** Repeats of this activity later in the same case, with something else in between. */
786
+ loops: number;
787
+ }
788
+ /** What {@link detectRework} answers. */
789
+ interface ReworkStats {
790
+ /** Adjacent repeats across the whole log. */
791
+ selfLoops: number;
792
+ /** Non-adjacent repeats across the whole log. */
793
+ loops: number;
794
+ /** Fraction of cases carrying at least one self-loop or loop. `0` for an empty log. */
795
+ caseReworkRate: number;
796
+ /**
797
+ * Every activity in the log, in ascending name order, with its own two tallies. An
798
+ * activity that never repeats is present with zeros — a renderer joining this against
799
+ * a graph's activities never has to distinguish "no rework" from "unknown activity".
800
+ */
801
+ perActivity: Record<string, ActivityRework>;
802
+ }
803
+ /**
804
+ * Count rework in `log`.
805
+ *
806
+ * Accepts a raw or an already-normalized log and normalizes at most once
807
+ * ({@link asNormalizedLog} is idempotent), so a caller that has already normalized for
808
+ * `discoverGraph` pays nothing here.
809
+ */
810
+ declare function detectRework(log: AnyLog): ReworkStats;
811
+
812
+ /**
813
+ * Case-level log filtering — RM-050.
814
+ *
815
+ * Every filter in a process-mining tool is a CASE predicate, not an event predicate:
816
+ * "cases that contain Reject Order", "cases that took longer than a day". Removing single
817
+ * events would rewrite traces and invent directly-follows relations that never happened,
818
+ * so a filter here only ever keeps or drops a case whole. That is what makes the result
819
+ * safe to hand straight back to `discoverGraph` / `extractVariants`.
820
+ *
821
+ * Specs AND together — an empty list keeps everything.
822
+ */
823
+
824
+ /** One case-level predicate. {@link filterLog} ANDs a list of them. */
825
+ type FilterSpec =
826
+ /** The case contains `activity` at least once. */
827
+ {
828
+ kind: "with";
829
+ activity: string;
830
+ }
831
+ /** The case never contains `activity`. */
832
+ | {
833
+ kind: "without";
834
+ activity: string;
835
+ }
836
+ /** The case's FIRST activity is `activity`. */
837
+ | {
838
+ kind: "startsWith";
839
+ activity: string;
840
+ }
841
+ /** The case's LAST activity is `activity`. */
842
+ | {
843
+ kind: "endsWith";
844
+ activity: string;
845
+ }
846
+ /**
847
+ * `b` follows `a` in the case — directly (adjacent) when `direct`, otherwise eventually
848
+ * (anywhere later in the trace).
849
+ */
850
+ | {
851
+ kind: "follower";
852
+ a: string;
853
+ b: string;
854
+ direct?: boolean;
855
+ }
856
+ /** An attribute comparison — see {@link caseMatchesFilters} for how `key` is resolved. */
857
+ | {
858
+ kind: "attribute";
859
+ key: string;
860
+ op: "eq" | "ne" | "gt" | "lt" | "in";
861
+ value: unknown;
862
+ }
863
+ /** The case's throughput time, in milliseconds. Both bounds are INCLUSIVE. */
864
+ | {
865
+ kind: "duration";
866
+ min?: number;
867
+ max?: number;
868
+ }
869
+ /** The case follows one of these variants (see `variantId`). */
870
+ | {
871
+ kind: "variant";
872
+ ids: string[];
873
+ }
874
+ /** The case is one of these case ids. */
875
+ | {
876
+ kind: "cases";
877
+ ids: string[];
878
+ };
879
+ /**
880
+ * Does `kase` satisfy every spec?
881
+ *
882
+ * Exported because a case table and a variant list want to HIGHLIGHT what a filter
883
+ * selects as often as they want to remove what it does not.
884
+ */
885
+ declare function caseMatchesFilters(kase: NormalizedCase, specs: readonly FilterSpec[]): boolean;
886
+ /**
887
+ * Filter a NORMALIZED log — the composable form.
888
+ *
889
+ * This is the one to reach for in a pipeline: it accepts either shape, normalizes at most
890
+ * once, and returns the normalized result, so
891
+ * `discoverGraph(filterNormalizedLog(normalized, specs))` re-parses nothing. `totals` are
892
+ * recomputed over the surviving cases; case order is preserved.
893
+ */
894
+ declare function filterNormalizedLog(log: AnyLog, specs: readonly FilterSpec[]): NormalizedLog;
895
+ /**
896
+ * Filter a raw {@link EventLog}, answering a raw {@link EventLog}.
897
+ *
898
+ * Every ROW of a surviving case is kept, in input order, including rows normalization
899
+ * itself discards (an unpaired lifecycle half, say) — so the answer round-trips: it can be
900
+ * re-exported, handed to a different adapter, or re-normalized with different options.
901
+ * `caseAttributes` is narrowed to the surviving cases.
902
+ *
903
+ * Use {@link filterNormalizedLog} instead when the next step is another `/core` derivation;
904
+ * this form necessarily makes the caller normalize again.
905
+ */
906
+ declare function filterLog(log: EventLog, specs: readonly FilterSpec[]): EventLog;
907
+
908
+ /**
909
+ * Off-thread discovery — RM-050.
910
+ *
911
+ * Discovering a graph from a raw log is O(events) with a couple of hash lookups per event;
912
+ * on a 200 000-row import that is long enough to drop frames, and a dropped frame during a
913
+ * file import is exactly the moment an app reads as broken. So the derivation moves to a
914
+ * worker — but ONLY as an optimization: the caller never branches on the environment.
915
+ *
916
+ * **Degrade, never fail.** When there is no `Worker` (a server render, a Node test, an
917
+ * embedded webview), when constructing one throws, or when a live one errors, the same
918
+ * functions run inline on the calling thread and the promise still resolves with the same
919
+ * answer. `handleProcessRequest` is literally shared with the worker entry, so "the inline
920
+ * path agrees with the worker path" is a property of the code, not of a test.
921
+ *
922
+ * **Normalize once.** Every method takes an `AnyLog`; hand it an already-normalized log
923
+ * (`asNormalizedLog`) and neither side re-parses. A `NormalizedLog` is plain data and
924
+ * clones across `postMessage` unchanged.
925
+ */
926
+
927
+ /**
928
+ * The slice of the `Worker` interface this module uses.
929
+ *
930
+ * Narrow on purpose: it is the seam a test (or a bundler with its own worker construction)
931
+ * substitutes, and demanding the full DOM `Worker` surface for that would be pointless
932
+ * ceremony. A real `Worker` satisfies it.
933
+ */
934
+ interface ProcessWorkerLike {
935
+ postMessage(message: unknown): void;
936
+ terminate(): void;
937
+ addEventListener(type: "message" | "error" | "messageerror", listener: (event: unknown) => void): void;
938
+ }
939
+ /** Options for {@link createProcessWorker}. */
940
+ interface CreateProcessWorkerOptions {
941
+ /** Skip the worker entirely and run inline. Useful for benchmarks and for tests. */
942
+ forceInline?: boolean;
943
+ /**
944
+ * Build the worker yourself.
945
+ *
946
+ * The default construction is
947
+ * `new Worker(new URL("./process-worker.ts", import.meta.url), { type: "module" })`.
948
+ * A bundler compiling THIS SOURCE (Vite, webpack) rewrites that literal itself; a
949
+ * consumer of the BUILT package gets `dist/core/process-worker.js`, which
950
+ * `tsup.config.ts` emits as its own pass and points the built bundle at — a bundler
951
+ * rewrites the source it compiles, never a `dist` it merely consumes, so that file has
952
+ * to be real. A host that needs its own URL supplies this instead. Throwing from it is
953
+ * safe: the handle falls back to the inline path.
954
+ */
955
+ createWorker?: () => ProcessWorkerLike;
956
+ }
957
+ /** What {@link createProcessWorker} hands back. */
958
+ interface ProcessWorkerHandle {
959
+ /** Discover a directly-follows graph. */
960
+ discover(log: AnyLog, options?: DiscoverGraphOptions): Promise<ProcessGraph>;
961
+ /** Group the log's cases into variants. */
962
+ variants(log: AnyLog): Promise<Variant[]>;
963
+ /** Stop the worker. Pending promises reject, and so does every later call. */
964
+ terminate(): void;
965
+ /**
966
+ * `true` when work is currently running on the CALLING thread — either because the
967
+ * environment has no worker, or because one failed and the handle degraded.
968
+ */
969
+ readonly inline: boolean;
970
+ }
971
+ /**
972
+ * Create a handle that computes off-thread when it can and inline when it cannot.
973
+ *
974
+ * The worker is created LAZILY, on the first request, so constructing a handle costs
975
+ * nothing in an environment that never uses it.
976
+ */
977
+ declare function createProcessWorker(options?: CreateProcessWorkerOptions): ProcessWorkerHandle;
978
+
979
+ /**
980
+ * The worker entry point — RM-050.
981
+ *
982
+ * Two things live here, deliberately in one module: the PURE request handler, and the
983
+ * `message` bootstrap that runs it inside a real worker. `createProcessWorker` imports the
984
+ * handler for its inline path, so the code that answers on the main thread and the code
985
+ * that answers off it are the SAME function — parity is structural, not something a test
986
+ * has to keep true.
987
+ *
988
+ * The bootstrap is guarded by a real worker-scope check, so importing this module from the
989
+ * main thread registers nothing.
990
+ */
991
+
992
+ /** What the main thread asks the worker to compute. Structured-cloneable, by construction. */
993
+ type ProcessWorkerRequest = {
994
+ id: number;
995
+ kind: "discover";
996
+ log: AnyLog;
997
+ options?: DiscoverGraphOptions;
998
+ } | {
999
+ id: number;
1000
+ kind: "variants";
1001
+ log: AnyLog;
1002
+ };
1003
+ /** What comes back. `ok: false` carries a message, never an `Error` (which clones poorly). */
1004
+ type ProcessWorkerResponse = {
1005
+ id: number;
1006
+ ok: true;
1007
+ kind: "discover";
1008
+ graph: ProcessGraph;
1009
+ } | {
1010
+ id: number;
1011
+ ok: true;
1012
+ kind: "variants";
1013
+ variants: Variant[];
1014
+ } | {
1015
+ id: number;
1016
+ ok: false;
1017
+ error: string;
1018
+ };
1019
+ /**
1020
+ * Answer one request.
1021
+ *
1022
+ * Pure and synchronous: the whole point of the worker is that this is the expensive part,
1023
+ * and moving it off the main thread is the only thing the worker adds. Throwing is
1024
+ * converted to an `ok: false` response so a bad log cannot silently kill the worker and
1025
+ * leave every later request hanging.
1026
+ */
1027
+ declare function handleProcessRequest(request: ProcessWorkerRequest): ProcessWorkerResponse;
1028
+
1029
+ export { type AbstractedGraph, type AbstractionOptions, type ActivityRework, type ActivityStats, type AnyLog, BPI_2012_ACTIVITIES, BPI_2012_SUBSET_EPOCH, type Bpi2012Activity, type Bpi2012SubsetOptions, type CreateProcessWorkerOptions, type CsvMapping, type CsvOptions, DEFAULT_LIFECYCLE_VALUES, DURATION_SAMPLE_CAP, DURATION_UNIT_MS, type DiscoverGraphOptions, DurationSampler, type DurationStats, type DurationUnit, EDGE_KEY_SEPARATOR, EMPTY_DURATION_STATS, type EventLog, type EventRow, type FilterSpec, type FlatRow, type FlatRowMapping, type FlowTime, type FrequencyMode, type LifecycleValues, type NormalizedCase, type NormalizedEvent, type NormalizedLog, type PerformanceAgg, type PerformanceGraph, type PerformanceLayer, type PerformanceOptions, type ProcessGraph, type ProcessWorkerHandle, type ProcessWorkerLike, type ProcessWorkerRequest, type ProcessWorkerResponse, type ReconciledGraph, type ReworkStats, SYNTHETIC_ACTIVITIES, SYNTHETIC_LOG_EPOCH, type SyntheticActivity, type SyntheticLogOptions, TRIM_FRACTION, type TransitionStats, VARIANT_KEY_SEPARATOR, type Variant, abstractGraph, aggregatePerformance, asNormalizedLog, caseMatchesFilters, clampWidth, createProcessWorker, detectRework, discoverGraph, durationStats, emptyDurationStats, extractVariants, filterLog, filterNormalizedLog, fromCsv, fromFlatRows, generateBpi2012Subset, generateSyntheticLog, handleProcessRequest, isNormalizedLog, minMax, normalizeLifecycle, normalizeLog, parseDelimited, performanceValue, quantile, quantileSorted, reconcileGraph, toEpochMs, variantId, variantKey };