@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,1153 @@
1
+ import { Node, Edge, NodeProps, EdgeProps, XYPosition } from '@xyflow/react';
2
+ import * as react from 'react';
3
+ import { HTMLAttributes, KeyboardEvent } from 'react';
4
+ import { FlowLayoutDirection } from '@elabs-ai/components-flow';
5
+
6
+ /**
7
+ * The framework-free event-log and process-graph model — RM-049.
8
+ *
9
+ * Every downstream item in the process-mining track (RM-050 abstraction, RM-051 the
10
+ * process map, RM-052 the variant explorer, RM-053 the case table, RM-054 the coverage
11
+ * strip) reads these shapes, so they are the wave's frozen contract. Keep additions
12
+ * additive: a required field added here is a breaking change for five items at once.
13
+ *
14
+ * NOTHING in this module — or anywhere under `src/core/` — may import React, React Flow,
15
+ * visx, d3 or an `@elabs-ai/components-*` package. See `.claude/rules/process-components.md`
16
+ * and `pnpm process:reuse:check`.
17
+ */
18
+ /**
19
+ * One raw row of an event log, before normalization.
20
+ *
21
+ * `timestamp` accepts the three shapes tabular sources actually produce — an ISO string,
22
+ * an epoch number, or a `Date` — because the adapters hand rows through unchanged and a
23
+ * consumer should not have to pre-convert. {@link normalizeLog} resolves all three to
24
+ * epoch milliseconds.
25
+ */
26
+ interface EventRow {
27
+ /** Case (process instance) this event belongs to. */
28
+ caseId: string;
29
+ /** Activity name. This is the node identity in the discovered graph. */
30
+ activity: string;
31
+ /** When the event completed (or, for a `lifecycle: "start"` row, when it started). */
32
+ timestamp: string | number | Date;
33
+ /**
34
+ * Optional explicit start of an interval event. Ignored when the row is one half of a
35
+ * `lifecycle` pair — the paired `"start"` row wins, because it is the observed value.
36
+ */
37
+ startTimestamp?: string | number | Date;
38
+ /** Who or what executed the event (a user, a queue, a system). */
39
+ resource?: string;
40
+ /** Lifecycle transition. Absent means the row is an atomic (already-complete) event. */
41
+ lifecycle?: "start" | "complete";
42
+ /** Free-form event-level attributes carried through normalization untouched. */
43
+ attributes?: Record<string, string | number | boolean | null>;
44
+ }
45
+ /** A raw event log: the rows, plus optional per-case attributes keyed by `caseId`. */
46
+ interface EventLog {
47
+ events: EventRow[];
48
+ caseAttributes?: Record<string, Record<string, unknown>>;
49
+ }
50
+ /**
51
+ * Summary statistics over a set of duration samples, in milliseconds.
52
+ *
53
+ * All seven members are always present; an empty sample set yields zeros rather than
54
+ * `null`, so a renderer never has to branch on absence.
55
+ */
56
+ interface DurationStats {
57
+ min: number;
58
+ max: number;
59
+ mean: number;
60
+ median: number;
61
+ p90: number;
62
+ sum: number;
63
+ /** Mean after discarding the lowest and highest 10% of samples. */
64
+ trimmedMean: number;
65
+ }
66
+ /** Per-activity aggregates in a discovered graph. */
67
+ interface ActivityStats {
68
+ /** Stable node identity — the activity name. */
69
+ id: string;
70
+ /** Human-readable label. Equal to `id` unless a caller relabels the graph. */
71
+ label: string;
72
+ /** Total occurrences across all cases. */
73
+ instances: number;
74
+ /** Number of distinct cases the activity occurs in at least once. */
75
+ cases: number;
76
+ /** True when the activity starts at least one case. */
77
+ isStart: boolean;
78
+ /** True when the activity ends at least one case. */
79
+ isEnd: boolean;
80
+ /** Distribution of the activity's own execution durations. */
81
+ duration: DurationStats;
82
+ }
83
+ /** Per-edge aggregates in a discovered graph (a directly-follows relation). */
84
+ interface TransitionStats {
85
+ source: string;
86
+ target: string;
87
+ /** Total occurrences of the directly-follows pair across all cases. */
88
+ count: number;
89
+ /** Number of distinct cases the pair occurs in at least once. */
90
+ caseCount: number;
91
+ /** Distribution of the flow time between the two activities. */
92
+ duration: DurationStats;
93
+ /** `source === target`. */
94
+ isSelfLoop: boolean;
95
+ /**
96
+ * Whether the edge points backwards in the laid-out graph. Discovery does not lay out,
97
+ * so this is always `false` here; a layout pass sets it (RM-044).
98
+ */
99
+ isBackEdge: boolean;
100
+ }
101
+ /** A directly-follows graph plus its totals. */
102
+ interface ProcessGraph {
103
+ activities: ActivityStats[];
104
+ transitions: TransitionStats[];
105
+ /** Activity name → number of cases that start with it. */
106
+ startActivities: Record<string, number>;
107
+ /** Activity name → number of cases that end with it. */
108
+ endActivities: Record<string, number>;
109
+ totals: {
110
+ cases: number;
111
+ events: number;
112
+ variants: number;
113
+ };
114
+ }
115
+ /** One distinct activity sequence, with the cases that follow it. */
116
+ interface Variant {
117
+ /** Stable, reproducible id derived from the sequence — see `variantId`. */
118
+ id: string;
119
+ sequence: string[];
120
+ /** Number of cases following this sequence. */
121
+ count: number;
122
+ /** `count / totalCases`. */
123
+ share: number;
124
+ /** Running share across the descending-frequency order — monotonically non-decreasing. */
125
+ cumulativeShare: number;
126
+ /** Case ids following this sequence, in first-appearance order. */
127
+ caseIds: string[];
128
+ /** Distribution of the end-to-end case durations of `caseIds`. */
129
+ duration: DurationStats;
130
+ }
131
+ /** How an edge or node frequency is expressed to the reader. */
132
+ type FrequencyMode = "absolute" | "absolute_case" | "relative" | "relative_case" | "relative_antecedent" | "relative_consequent" | "max_repetitions";
133
+ /** Which member of a {@link DurationStats} a performance view reads. */
134
+ type PerformanceAgg = "median" | "mean" | "min" | "max" | "sum" | "p90" | "trimmed_mean";
135
+
136
+ /**
137
+ * Rework detection — RM-050.
138
+ *
139
+ * "Rework" is the part of a process that happens more than once inside a single case: a
140
+ * step retried immediately (a SELF-LOOP), or a step the case comes back to after going
141
+ * somewhere else (a LOOP). Both are repeats, and separating them matters — a self-loop is
142
+ * usually a retry or a batch, a loop is usually a rejection sending work backwards, and
143
+ * the two lead to different conversations about the process.
144
+ *
145
+ * Counted per OCCURRENCE, over the normalized trace: the first execution of an activity in
146
+ * a case is never rework, and every later one is exactly one of the two kinds. So
147
+ * `selfLoops + loops` is precisely the number of repeated executions in the log.
148
+ *
149
+ * Deterministic and framework-free, like everything else in `/core`.
150
+ */
151
+
152
+ /** Per-activity rework tallies. */
153
+ interface ActivityRework {
154
+ /** Repeats of this activity immediately after itself. */
155
+ selfLoops: number;
156
+ /** Repeats of this activity later in the same case, with something else in between. */
157
+ loops: number;
158
+ }
159
+ /** What {@link detectRework} answers. */
160
+ interface ReworkStats {
161
+ /** Adjacent repeats across the whole log. */
162
+ selfLoops: number;
163
+ /** Non-adjacent repeats across the whole log. */
164
+ loops: number;
165
+ /** Fraction of cases carrying at least one self-loop or loop. `0` for an empty log. */
166
+ caseReworkRate: number;
167
+ /**
168
+ * Every activity in the log, in ascending name order, with its own two tallies. An
169
+ * activity that never repeats is present with zeros — a renderer joining this against
170
+ * a graph's activities never has to distinguish "no rework" from "unknown activity".
171
+ */
172
+ perActivity: Record<string, ActivityRework>;
173
+ }
174
+
175
+ /**
176
+ * Case-level log filtering — RM-050.
177
+ *
178
+ * Every filter in a process-mining tool is a CASE predicate, not an event predicate:
179
+ * "cases that contain Reject Order", "cases that took longer than a day". Removing single
180
+ * events would rewrite traces and invent directly-follows relations that never happened,
181
+ * so a filter here only ever keeps or drops a case whole. That is what makes the result
182
+ * safe to hand straight back to `discoverGraph` / `extractVariants`.
183
+ *
184
+ * Specs AND together — an empty list keeps everything.
185
+ */
186
+
187
+ /** One case-level predicate. {@link filterLog} ANDs a list of them. */
188
+ type FilterSpec =
189
+ /** The case contains `activity` at least once. */
190
+ {
191
+ kind: "with";
192
+ activity: string;
193
+ }
194
+ /** The case never contains `activity`. */
195
+ | {
196
+ kind: "without";
197
+ activity: string;
198
+ }
199
+ /** The case's FIRST activity is `activity`. */
200
+ | {
201
+ kind: "startsWith";
202
+ activity: string;
203
+ }
204
+ /** The case's LAST activity is `activity`. */
205
+ | {
206
+ kind: "endsWith";
207
+ activity: string;
208
+ }
209
+ /**
210
+ * `b` follows `a` in the case — directly (adjacent) when `direct`, otherwise eventually
211
+ * (anywhere later in the trace).
212
+ */
213
+ | {
214
+ kind: "follower";
215
+ a: string;
216
+ b: string;
217
+ direct?: boolean;
218
+ }
219
+ /** An attribute comparison — see {@link caseMatchesFilters} for how `key` is resolved. */
220
+ | {
221
+ kind: "attribute";
222
+ key: string;
223
+ op: "eq" | "ne" | "gt" | "lt" | "in";
224
+ value: unknown;
225
+ }
226
+ /** The case's throughput time, in milliseconds. Both bounds are INCLUSIVE. */
227
+ | {
228
+ kind: "duration";
229
+ min?: number;
230
+ max?: number;
231
+ }
232
+ /** The case follows one of these variants (see `variantId`). */
233
+ | {
234
+ kind: "variant";
235
+ ids: string[];
236
+ }
237
+ /** The case is one of these case ids. */
238
+ | {
239
+ kind: "cases";
240
+ ids: string[];
241
+ };
242
+
243
+ /**
244
+ * map-model — the pure `ProcessGraph → React Flow nodes/edges` mapping (RM-051).
245
+ *
246
+ * Everything the process map DECIDES about a graph lives here, and nothing here renders:
247
+ * no React, no DOM, no engine calls. That is deliberate and load-bearing in three ways.
248
+ *
249
+ * 1. **The canvas and the accessible `TableView` twin read ONE model.** Both surfaces are
250
+ * built from the same {@link ProcessMapModel}, so a number can never drift between the
251
+ * picture and the table a screen-reader user is actually given.
252
+ * 2. **Encoding is testable without a browser.** Stroke widths, saturations, printed
253
+ * labels and selection states are plain data here, so a unit test can assert the
254
+ * encoding rules instead of screenshotting them.
255
+ * 3. **Layout can be cached independently of metric.** The model changes whenever the
256
+ * metric changes; the STRUCTURE key (see {@link processGraphStructureKey}) does not, so
257
+ * `useProcessLayout` reuses positions across a metric switch.
258
+ *
259
+ * ## The encoding contract (WCAG 1.4.1 — colour is never the only channel)
260
+ *
261
+ * Two flow-layer defects (#321 edge colour collapsing on a low-chroma palette, #297 edge
262
+ * colour not refreshing on a theme switch) mean edge COLOUR cannot be trusted to separate
263
+ * two values. This module is designed so it never has to be:
264
+ *
265
+ * - an **edge** metric reaches the reader through `weight` (stroke width, the
266
+ * `[1.5, 8]` px linear min-max clamp `computeEdgeWeightScale` applies) AND through a
267
+ * printed `label` pill — colour (`value`/`valueDomain`) is the third channel, never the
268
+ * first;
269
+ * - a **node** metric reaches the reader through a printed value AND a bar whose LENGTH is
270
+ * the same fraction the fill saturation encodes — saturation alone would be colour-only;
271
+ * - **self-loop**, **back-edge** and each **selection** state carry a shape, dash or
272
+ * opacity change, not a hue change.
273
+ *
274
+ * Render any story in greyscale and two different metric values are still distinguishable.
275
+ */
276
+
277
+ /**
278
+ * What a node or an edge is painted with: a frequency reading of the discovered counts,
279
+ * or an aggregate of the measured durations.
280
+ */
281
+ type ProcessMetric = FrequencyMode | PerformanceAgg;
282
+ /** The three metric slots a process map paints from. */
283
+ interface ProcessMetricSpec {
284
+ /** Drives node saturation, the node's meter bar and the node's printed value. */
285
+ node: ProcessMetric;
286
+ /** Drives edge stroke width and the edge's printed label pill. */
287
+ edge: ProcessMetric;
288
+ /** Optional second reading, printed beside the first on both nodes and edges. */
289
+ secondary?: ProcessMetric;
290
+ }
291
+ /** Which half of the graph a selection points at. */
292
+ type ProcessSelectionKind = "activity" | "transition";
293
+ /**
294
+ * A coordinated selection. `id` is the ACTIVITY NAME for `"activity"` and the
295
+ * `source`+separator+`target` edge key (see {@link processEdgeId}) for `"transition"` —
296
+ * the same keys `/core` uses, so a selection round-trips through `filterLog` and a URL
297
+ * unchanged.
298
+ */
299
+ interface ProcessSelection {
300
+ kind: ProcessSelectionKind;
301
+ id: string;
302
+ }
303
+ /**
304
+ * What the map's filter menu emits. A strict subset of `/core`'s `FilterSpec`, so a
305
+ * consumer can hand it straight to `filterLog` with no translation layer — which is the
306
+ * point: an intent the map invents and nothing can consume is not an intent.
307
+ */
308
+ type ProcessFilterIntent = Extract<FilterSpec, {
309
+ kind: "with";
310
+ } | {
311
+ kind: "without";
312
+ } | {
313
+ kind: "startsWith";
314
+ } | {
315
+ kind: "endsWith";
316
+ }>;
317
+ /** The four intents the map's own menu offers, in menu order. */
318
+ declare const PROCESS_FILTER_INTENT_KINDS: readonly ["with", "without", "startsWith", "endsWith"];
319
+ /** Human wording for each intent, used by the menu's VISIBLE item text. */
320
+ declare const PROCESS_FILTER_INTENT_LABELS: Readonly<Record<ProcessFilterIntent["kind"], string>>;
321
+ /**
322
+ * The locale message key for each intent's ACCESSIBLE name (#346), one activity's block at
323
+ * a time — the visible text alone ({@link PROCESS_FILTER_INTENT_LABELS}) is not unique for
324
+ * a transition's menu, which offers the same four intents once per endpoint: eight items,
325
+ * four distinct visible strings, each doubled. Each message takes an `{activity}` var, so
326
+ * the accessible name states which activity the item filters by even though the visible
327
+ * text (kept unsuffixed, so the menu stays visually compact) does not.
328
+ */
329
+ declare const PROCESS_FILTER_INTENT_MESSAGE_KEYS: Readonly<Record<ProcessFilterIntent["kind"], string>>;
330
+ /**
331
+ * How an element relates to the current selection AND the active filter.
332
+ *
333
+ * - `"selected"` — the element the reader picked.
334
+ * - `"associated"` — ordinary; either nothing is selected, or this element touches the
335
+ * selection, and no active filter excludes it.
336
+ * - `"excluded"` — outside the selection's neighbourhood, or dropped by the active filter;
337
+ * dimmed, but still fully operable. Clicking an excluded element is how a reader filters
338
+ * it back in, so it is never `aria-disabled` — see {@link resolveSelectionState}. The
339
+ * dimming is a REDUNDANT cue: `activityAriaLabel`/`transitionAriaLabel` already append the
340
+ * word "excluded" to the element's accessible name, so the state reaches assistive
341
+ * technology through real text, not only through opacity.
342
+ */
343
+ type ProcessSelectionState = "selected" | "associated" | "excluded";
344
+ /**
345
+ * The ONE ghosting rung a filter-excluded element dims against, shared by every call site
346
+ * that owns a non-text mark: the activity node's meter fill, the transition edge's stroke
347
+ * (`ProcessTransitionEdge`'s wrapping `<g>`), and — via `labelProps.className` — the
348
+ * transition's own label pill's frame (`FlowWeightedEdge`/`FlowSelfLoopEdge`). Before #351/
349
+ * #352 this was TWO separate literals (`opacity-35` on the activity node's whole subtree,
350
+ * `EXCLUDED_OPACITY` on the edge) and the pill had no ghost treatment at all.
351
+ *
352
+ * **This value dims marks, never text ink.** `ProcessActivityNode` and the pill's own label
353
+ * text are deliberately excluded from it — measured (see #352's follow-up), a `0.35` alpha
354
+ * over EITHER's own already-modest undimmed contrast (5.97:1–6.08:1 for the eyebrow, higher
355
+ * for the title/label) composites well under the WCAG 4.5:1 floor in both reference themes.
356
+ * Ghosted TEXT stays at full ink; the excluded state instead reaches the reader through the
357
+ * activity node's `border-strong` boundary swap and the pill's dashed frame — non-opacity,
358
+ * non-colour-alone channels (WCAG 1.4.1) — plus the accessible name's own appended
359
+ * "excluded" word, same as before.
360
+ */
361
+ declare const GHOST_OPACITY = 0.35;
362
+ /**
363
+ * Per-element selection/filter states, sparse — an id with no entry defaults through
364
+ * {@link resolveSelectionState}'s own rules. Keyed exactly like {@link ProcessSelection}:
365
+ * activity name for `activities`, {@link processEdgeId} for `transitions`.
366
+ *
367
+ * `variants` is carried here for API symmetry with the other two namespaces even though
368
+ * `ProcessMap` does not read it (the variant LIST narrows rather than ghosts — decision
369
+ * `RM-052-tristate-decision.md` §3); a future variant-explorer view may.
370
+ */
371
+ interface ProcessSelectionStates {
372
+ activities?: Readonly<Record<string, ProcessSelectionState>>;
373
+ transitions?: Readonly<Record<string, ProcessSelectionState>>;
374
+ variants?: Readonly<Record<string, ProcessSelectionState>>;
375
+ }
376
+ /** `data` carried by every {@link ProcessMapNode}. */
377
+ interface ProcessActivityNodeData extends Record<string, unknown> {
378
+ /** Activity name — the node identity and its visible title. */
379
+ title: string;
380
+ /** What the primary number MEANS ("Cases", "Median duration"), shown as the eyebrow. */
381
+ metricLabel: string;
382
+ /** The primary metric, already formatted for display. */
383
+ primaryLabel: string;
384
+ /** The primary metric as a number, for the meter and for tests. */
385
+ primaryValue: number;
386
+ /** The secondary metric, already formatted. Absent when `metric.secondary` is unset. */
387
+ secondaryLabel?: string;
388
+ /** `primaryValue` as a `0..1` fraction of the graph's node-metric domain. */
389
+ saturation: number;
390
+ isStart: boolean;
391
+ isEnd: boolean;
392
+ /** Repeat executions of this activity across the log; omitted when no rework data. */
393
+ reworkCount?: number;
394
+ selectionState: ProcessSelectionState;
395
+ }
396
+ /** `data` carried by every {@link ProcessMapEdge}. */
397
+ interface ProcessTransitionEdgeData extends Record<string, unknown> {
398
+ source: string;
399
+ target: string;
400
+ /** Drives stroke width through `computeEdgeWeightScale`'s `[1.5, 8]` px min-max clamp. */
401
+ weight: number;
402
+ /** Drives stroke colour — the THIRD channel, never the first. */
403
+ value: number;
404
+ /** `[min, max]` the colour ramp interpolates `value` across. */
405
+ valueDomain: [number, number];
406
+ /** The printed pill — the second, colour-free channel for the same measure as `weight`. */
407
+ label: string;
408
+ secondaryLabel?: string;
409
+ isSelfLoop: boolean;
410
+ isBackEdge: boolean;
411
+ selectionState: ProcessSelectionState;
412
+ }
413
+ /** A process-map activity node. Register as `nodeTypes={{ "process-activity": … }}`. */
414
+ type ProcessMapNode = Node<ProcessActivityNodeData, "process-activity">;
415
+ /** A process-map transition edge. Register as `edgeTypes={{ "process-transition": … }}`. */
416
+ type ProcessMapEdge = Edge<ProcessTransitionEdgeData, "process-transition">;
417
+ /** One row of the accessible `TableView` twin — activities half. */
418
+ interface ProcessActivityRow {
419
+ id: string;
420
+ title: string;
421
+ primaryLabel: string;
422
+ secondaryLabel?: string;
423
+ reworkCount?: number;
424
+ role: string;
425
+ selectionState: ProcessSelectionState;
426
+ }
427
+ /** One row of the accessible `TableView` twin — transitions half. */
428
+ interface ProcessTransitionRow {
429
+ id: string;
430
+ source: string;
431
+ target: string;
432
+ primaryLabel: string;
433
+ secondaryLabel?: string;
434
+ shape: string;
435
+ selectionState: ProcessSelectionState;
436
+ }
437
+ /** Everything the canvas and the table are both rendered from. */
438
+ interface ProcessMapModel {
439
+ nodes: ProcessMapNode[];
440
+ edges: ProcessMapEdge[];
441
+ /** `[min, max]` of the node metric across the graph. */
442
+ nodeDomain: [number, number];
443
+ /** `[min, max]` of the edge metric across the graph — the `Legend` scale's domain. */
444
+ edgeDomain: [number, number];
445
+ /** What the node metric means, e.g. "Cases". */
446
+ nodeMetricLabel: string;
447
+ /** What the edge metric means, e.g. "Transitions". */
448
+ edgeMetricLabel: string;
449
+ activityRows: ProcessActivityRow[];
450
+ transitionRows: ProcessTransitionRow[];
451
+ /** Format one edge-metric value the way the map prints it — used by the `Legend`. */
452
+ formatEdgeValue: (value: number) => string;
453
+ /**
454
+ * How many activities/transitions the active selection or filter has excluded (#375) —
455
+ * derived once here so the canvas branch, the table branch and any future consumer read
456
+ * one number instead of recomputing it three times, and so the summary is unit-testable
457
+ * without a render.
458
+ */
459
+ excludedCounts: ProcessMapExcludedCounts;
460
+ }
461
+ /** See {@link ProcessMapModel.excludedCounts}. */
462
+ interface ProcessMapExcludedCounts {
463
+ activities: number;
464
+ totalActivities: number;
465
+ transitions: number;
466
+ totalTransitions: number;
467
+ }
468
+ /**
469
+ * The frequency readings an ACTIVITY can actually answer from `ActivityStats`.
470
+ *
471
+ * `relative_antecedent` / `relative_consequent` are edge-only readings (they are shares of
472
+ * a source's or a target's traffic — a node has no antecedent), and `max_repetitions`
473
+ * needs the per-case maximum, which discovery does not retain. Rather than invent a
474
+ * number for those three, {@link resolveActivityFrequencyMode} maps them onto the nearest
475
+ * reading the data supports and the RESOLVED mode is what labels the value — so the map
476
+ * never prints one measure under another measure's name.
477
+ */
478
+ type ActivityFrequencyMode = Extract<FrequencyMode, "absolute" | "absolute_case" | "relative" | "relative_case">;
479
+ /** See {@link ActivityFrequencyMode} — a total, documented, tested resolution. */
480
+ declare function resolveActivityFrequencyMode(mode: FrequencyMode): ActivityFrequencyMode;
481
+ /**
482
+ * The frequency readings a TRANSITION can answer from `TransitionStats`.
483
+ *
484
+ * Only `max_repetitions` is unsupported — the per-case maximum is not retained by
485
+ * discovery — and it resolves to `absolute`, the count it is a maximum of.
486
+ */
487
+ type TransitionFrequencyMode = Exclude<FrequencyMode, "max_repetitions">;
488
+ /** See {@link TransitionFrequencyMode}. */
489
+ declare function resolveTransitionFrequencyMode(mode: FrequencyMode): TransitionFrequencyMode;
490
+ /** Whether a metric names a duration aggregate rather than a frequency reading. */
491
+ declare function isPerformanceMetric(metric: ProcessMetric): metric is PerformanceAgg;
492
+ /** What a node's metric is CALLED once resolved — the eyebrow and the a11y name use it. */
493
+ declare function nodeMetricLabel(metric: ProcessMetric): string;
494
+ /** What an edge's metric is CALLED once resolved. */
495
+ declare function edgeMetricLabel(metric: ProcessMetric): string;
496
+ /** The node metric of one activity, as a plain number. Pure and total. */
497
+ declare function activityMetricValue(activity: ActivityStats, metric: ProcessMetric, totals: ProcessGraph["totals"]): number;
498
+ /** Denominators the two per-endpoint edge shares need, computed once per graph. */
499
+ interface ProcessEdgeDenominators {
500
+ /** Total of every transition count in the graph. */
501
+ total: number;
502
+ /** Activity → total count of the transitions LEAVING it. */
503
+ outgoing: Map<string, number>;
504
+ /** Activity → total count of the transitions ENTERING it. */
505
+ incoming: Map<string, number>;
506
+ }
507
+ /** Build the per-graph denominators the antecedent/consequent shares divide by. */
508
+ declare function processEdgeDenominators(transitions: readonly TransitionStats[]): ProcessEdgeDenominators;
509
+ /** The edge metric of one transition, as a plain number. Pure and total. */
510
+ declare function transitionMetricValue(transition: TransitionStats, metric: ProcessMetric, totals: ProcessGraph["totals"], denominators: ProcessEdgeDenominators): number;
511
+ /**
512
+ * A duration in milliseconds, spoken in the largest unit that leaves a number at or above
513
+ * one.
514
+ *
515
+ * One decimal below 10 and none above, so "3.4 d" and "18 h" both read as measurements
516
+ * rather than as false precision. A non-finite or negative input answers an em dash — a
517
+ * process map has legitimate holes (an activity that never followed anything), and a
518
+ * printed `NaN` is worse than a printed dash.
519
+ */
520
+ declare function formatDurationMs(ms: number): string;
521
+ /** A count, grouped by the runtime locale; a share, as a percentage with one decimal. */
522
+ declare function formatMetricValue(value: number, metric: ProcessMetric): string;
523
+ /**
524
+ * The stable id of a transition: source, {@link EDGE_KEY_SEPARATOR}, target — keyed
525
+ * exactly the way `/core` keys it. A selection, a performance-layer lookup and a React
526
+ * Flow edge therefore all name the same edge with the same string, with no translation.
527
+ */
528
+ declare function processEdgeId(source: string, target: string): string;
529
+ /**
530
+ * A cheap, order-independent key over the graph's STRUCTURE — node ids and edge ids only,
531
+ * never a metric. `useProcessLayout` caches on it, which is what lets a metric switch
532
+ * reuse the existing positions instead of re-running dagre.
533
+ */
534
+ declare function processGraphStructureKey(graph: ProcessGraph): string;
535
+ /** The activities and transitions a selection keeps in play. */
536
+ interface ProcessSelectionNeighbourhood {
537
+ activities: Set<string>;
538
+ transitions: Set<string>;
539
+ }
540
+ /**
541
+ * Which activities and transitions a selection keeps in play.
542
+ *
543
+ * Selecting an ACTIVITY keeps that activity, every activity directly connected to it, and
544
+ * every edge incident to it. Selecting a TRANSITION keeps its two endpoints and itself.
545
+ * Everything else becomes `"excluded"` — dimmed, but never `aria-disabled` (see
546
+ * {@link resolveSelectionState}).
547
+ */
548
+ declare function selectionNeighbourhood(graph: ProcessGraph, selection: ProcessSelection | null | undefined): ProcessSelectionNeighbourhood | null;
549
+ /**
550
+ * Resolve one element's tri-state {@link ProcessSelectionState} from BOTH the coordinated
551
+ * click selection and the active filter's per-element states — the two channels
552
+ * `ProcessMap` renders together. Five rules, in precedence order (RM-052 round 2, #227):
553
+ *
554
+ * 1. **The element IS the click target** → `"selected"`. This wins over everything else,
555
+ * including an active filter's own `"excluded"` — clicking an excluded element is how a
556
+ * reader re-focuses it, so the click must always win.
557
+ * 2. **`states` already says `"excluded"`** → `"excluded"`. This must be checked BEFORE
558
+ * rule 3: a filter-excluded element stays excluded even when it happens to sit inside
559
+ * the neighbourhood of whatever is currently click-selected — the filter's exclusion is
560
+ * not something a click's neighbourhood should be able to override.
561
+ * 3. **A click target exists and this element sits outside its neighbourhood** →
562
+ * `"excluded"`.
563
+ * 4. **`states` carries an entry for this element** → that entry (`"associated"` or
564
+ * `"selected"` from a filter's own point of view — reserved for a future filter shape
565
+ * that can name a "primary" match; today filters only ever produce `"excluded"`
566
+ * entries, which rule 2 already handles).
567
+ * 5. **Otherwise** → `"associated"`, the default.
568
+ */
569
+ declare function resolveSelectionState(id: string, kind: ProcessSelectionKind, selection: ProcessSelection | null | undefined, neighbourhood: ProcessSelectionNeighbourhood | null, states?: ProcessSelectionStates): ProcessSelectionState;
570
+ /** Inputs to {@link buildProcessMapModel}. */
571
+ interface BuildProcessMapModelOptions {
572
+ graph: ProcessGraph;
573
+ metric: ProcessMetricSpec;
574
+ /** Optional rework tallies (from `/core`'s `detectRework`) for the node badge. */
575
+ rework?: ReworkStats;
576
+ selection?: ProcessSelection | null;
577
+ /**
578
+ * Per-element states an active filter contributes (RM-052 round 2, #227) — merged with
579
+ * the click `selection` by {@link resolveSelectionState}. Omitting it reproduces the
580
+ * pre-filter behaviour exactly: every element resolves through rules 1/3/5 alone.
581
+ */
582
+ selectionStates?: ProcessSelectionStates;
583
+ /**
584
+ * Edge ids `layoutFlow` reported as running against the layout direction. Supplied on
585
+ * the SECOND pass, after a layout exists — the first pass has no ranks to read, so
586
+ * every edge starts `isBackEdge: false` exactly as `discoverGraph` leaves it.
587
+ */
588
+ backEdgeIds?: ReadonlySet<string>;
589
+ }
590
+ /**
591
+ * Map a discovered (or abstracted) graph plus a metric choice into everything both the
592
+ * canvas and the table render. Pure: same inputs, same model, no clock and no DOM.
593
+ */
594
+ declare function buildProcessMapModel({ graph, metric, rework, selection, selectionStates, backEdgeIds, }: BuildProcessMapModelOptions): ProcessMapModel;
595
+ /**
596
+ * The word for an activity's position in the process — printed in the table's own column
597
+ * and folded into the node's accessible name, so "this is where cases start" survives
598
+ * without the start/end glyph.
599
+ */
600
+ declare function activityRole(data: ProcessActivityNodeData): string;
601
+ /** The word for an edge's shape — the non-colour channel, said in text. */
602
+ declare function transitionShape(data: ProcessTransitionEdgeData): string;
603
+ /**
604
+ * The CANONICAL (English, un-localized) word for a row's selection/filter state (#373) —
605
+ * this module is pure data shaping with no locale seam of its own, so it cannot resolve a
606
+ * translation. It stays exported for a non-React caller (tests, a console, a non-locale
607
+ * context). The State column's own rendered CELLS resolve the localized text instead, via
608
+ * {@link PROCESS_SELECTION_STATE_MESSAGE_KEYS} + `t()` at the render site
609
+ * (`process-map.tsx`) — see that key map's own comment (#413 review). `"associated"` is the
610
+ * ordinary case and prints nothing, so the column reads as a marker for the two states that
611
+ * matter, not as noise repeated on every row.
612
+ */
613
+ declare function selectionStateLabel(state: ProcessSelectionState): string;
614
+ /**
615
+ * The locale message key for each NON-ORDINARY selection/filter state (#413 review,
616
+ * PRRT_kwDOT6D7ts6gJX2C) — the State column's cells are the table twin's own
617
+ * screen-reader channel for row state (#373), so leaving them printing
618
+ * {@link selectionStateLabel}'s bare English word left a non-English `LocaleProvider`
619
+ * with untranslated visible AND accessible content. `"associated"` has no entry: it is the
620
+ * ordinary case and the column prints nothing for it, same as {@link selectionStateLabel}.
621
+ */
622
+ declare const PROCESS_SELECTION_STATE_MESSAGE_KEYS: Readonly<Partial<Record<ProcessSelectionState, string>>>;
623
+ /** The accessible name of one activity node. */
624
+ declare function activityAriaLabel(data: ProcessActivityNodeData): string;
625
+ /** The accessible name of one transition edge. */
626
+ declare function transitionAriaLabel(data: ProcessTransitionEdgeData, metricLabel: string): string;
627
+
628
+ /** Options for {@link abstractGraph}. Both fractions are `0..1` and both are required. */
629
+ interface AbstractionOptions {
630
+ /** Fraction of activities to KEEP, `0..1`. At least one activity is always kept. */
631
+ activities: number;
632
+ /** Fraction of paths (transitions) to KEEP, `0..1`, over the kept-activity subgraph. */
633
+ paths: number;
634
+ /**
635
+ * Re-add whatever it takes to keep every kept activity reachable from a start activity
636
+ * and able to reach an end activity. Defaults to `true` — an island reads as a broken
637
+ * process, not a simplified one.
638
+ */
639
+ keepConnected?: boolean;
640
+ /** Hide the MOST frequent instead of the least — the "what is rare here" view. */
641
+ invert?: boolean;
642
+ }
643
+ /** What {@link abstractGraph} returns: a `ProcessGraph` plus what it left out. */
644
+ interface AbstractedGraph extends ProcessGraph {
645
+ hidden: {
646
+ /** Activities present in the input graph and absent from this one. */
647
+ activities: number;
648
+ /** Transitions present in the input graph and absent from this one. */
649
+ paths: number;
650
+ };
651
+ }
652
+
653
+ /**
654
+ * The zoom the map refuses to open below, however big the process is.
655
+ *
656
+ * A discovered log is routinely wider than any pane, and a fit that shows ALL of it shows
657
+ * none of it: the 24-activity fixture frames at 0.28, where the activity name (14 px on
658
+ * the card) prints at 3.9 px and the card is a grey smudge. The floor is set from the
659
+ * card's own type rather than from a graph size: at 0.75 the name renders at 10.5 px and
660
+ * the secondary line (12 px) at 9 px, which is the smallest either stays a word.
661
+ *
662
+ * It is the OPENING zoom only — {@link MIN_ZOOM} still lets the reader pull all the way
663
+ * back to an overview, and the minimap says where in the process the pane is sitting. A
664
+ * fit clamped by this floor is anchored on the START of the process, not its middle;
665
+ * `CanvasShell` does that (see `anchorToStartWhenClamped`).
666
+ */
667
+ declare const PROCESS_MAP_LEGIBLE_ZOOM = 0.75;
668
+ /** Props for {@link ProcessMap}. `onSelect` shadows the DOM handler, so it is omitted. */
669
+ interface ProcessMapProps extends Omit<HTMLAttributes<HTMLDivElement>, "onSelect"> {
670
+ /** A discovered graph. Takes precedence over {@link log} when both are given. */
671
+ graph?: ProcessGraph;
672
+ /**
673
+ * An event log to discover from, for a surface with no explorer around it. Runs
674
+ * `/core`'s `discoverGraph` and `detectRework` internally, memoized on the log.
675
+ */
676
+ log?: EventLog;
677
+ /** Fractions of activities / paths to KEEP, as `/core`'s `abstractGraph` reads them. */
678
+ abstraction?: Pick<AbstractionOptions, "activities" | "paths">;
679
+ /** Which readings the nodes and edges paint. */
680
+ metric: ProcessMetricSpec;
681
+ /** Rework tallies for the node badge. Derived from {@link log} when that is given. */
682
+ rework?: ReworkStats;
683
+ /** Controlled selection. Omit for an uncontrolled map that owns its own. */
684
+ selection?: ProcessSelection | null;
685
+ /**
686
+ * Per-element states an active filter contributes (RM-052 round 2, #227) — e.g. from
687
+ * `useProcessExplorer`'s own `selectionStates`. Applying or clearing a filter never adds
688
+ * or removes a node or an edge (Invariant F): it only re-inks the elements a filter
689
+ * dropped as `"excluded"` (dimmed, never `aria-disabled`). Omit for a map with no
690
+ * filtering, which reproduces today's selection-only behaviour exactly.
691
+ */
692
+ selectionStates?: ProcessSelectionStates;
693
+ /** Fires with the new selection, or `null` when the reader deselects. */
694
+ onSelect?: (target: ProcessSelection | null) => void;
695
+ /** Fires with an intent from the filter menu — hand it straight to `filterLog`. */
696
+ onFilterIntent?: (intent: ProcessFilterIntent) => void;
697
+ /** @default "TB" */
698
+ direction?: FlowLayoutDirection;
699
+ /** @default true */
700
+ showMiniMap?: boolean;
701
+ /** @default true */
702
+ showLegend?: boolean;
703
+ /** Render the accessible table twin instead of the canvas. @default false */
704
+ tableView?: boolean;
705
+ /** No graph yet. Renders the loading panel rather than an empty canvas. */
706
+ loading?: boolean;
707
+ /**
708
+ * Accessible name for the canvas region. Defaults to the localized
709
+ * `process.map.label` message.
710
+ */
711
+ label?: string;
712
+ }
713
+ /**
714
+ * The process map.
715
+ *
716
+ * @example
717
+ * ```tsx
718
+ * <ProcessMap log={log} metric={{ node: "absolute_case", edge: "absolute" }} />
719
+ * ```
720
+ */
721
+ declare function ProcessMap({ graph, log, abstraction, metric, rework, selection, selectionStates, onSelect, onFilterIntent, direction, showMiniMap, showLegend, tableView, loading, label, className, ...props }: ProcessMapProps): react.JSX.Element;
722
+
723
+ /** What the node and edge components read while something is hovered. */
724
+ interface ProcessMapHoverState {
725
+ /** The hovered activity id, or `null` when the pointer is not on a node. */
726
+ activityId: string | null;
727
+ /** Ids of the edges incident to {@link activityId}. Empty when nothing is hovered. */
728
+ incidentEdgeIds: ReadonlySet<string>;
729
+ }
730
+ /** The resting value — nothing hovered. Frozen and shared, so it is referentially stable. */
731
+ declare const EMPTY_PROCESS_MAP_HOVER: ProcessMapHoverState;
732
+ /**
733
+ * Hover state for one process map. `ProcessMap` provides it; `ProcessActivityNode` and
734
+ * `ProcessTransitionEdge` consume it. Default is {@link EMPTY_PROCESS_MAP_HOVER}, so both
735
+ * components render correctly outside a `ProcessMap` (in a story or a unit test).
736
+ */
737
+ declare const ProcessMapHoverContext: react.Context<ProcessMapHoverState>;
738
+ /** Read the current hover state. `use()` per the repo's new-context-read convention. */
739
+ declare function useProcessMapHover(): ProcessMapHoverState;
740
+ /**
741
+ * How an edge hands a key press back to the map (RM-051).
742
+ *
743
+ * An edge's label pill is rendered through React Flow's `EdgeLabelRenderer`, which is a
744
+ * PORTAL: the button lands in `.react-flow__edgelabel-renderer`, outside the edge's own
745
+ * `<g>`, so it has no `[data-id]` ancestor for the map's root key handler to read. React
746
+ * portal events still bubble up the REACT tree, though — so `ProcessTransitionEdge`, which
747
+ * knows its own `id`, catches the key on its wrapper `<g>` and calls this. It is the ONLY
748
+ * way `Enter` and `f` stay usable on a transition now that the edge `<g>` itself is not a
749
+ * tab stop.
750
+ *
751
+ * The default is a no-op, so `ProcessTransitionEdge` renders standalone (a story, a unit
752
+ * test) without a provider.
753
+ */
754
+ type ProcessMapEdgeKeyHandler = (edgeId: string, event: KeyboardEvent) => void;
755
+ /** Provided by `ProcessMap`; consumed by `ProcessTransitionEdge`. */
756
+ declare const ProcessMapEdgeKeyContext: react.Context<ProcessMapEdgeKeyHandler>;
757
+ /** Read the map's edge key handler. `use()` per the repo's new-context-read convention. */
758
+ declare function useProcessMapEdgeKeys(): ProcessMapEdgeKeyHandler;
759
+
760
+ /**
761
+ * Branded process-map activity node. Register it in
762
+ * `nodeTypes={{ "process-activity": ProcessActivityNode }}` and build nodes with
763
+ * `buildProcessMapModel` rather than by hand — the model is what keeps the canvas and the
764
+ * `TableView` twin printing the same numbers.
765
+ */
766
+ declare function ProcessActivityNode(props: NodeProps<ProcessMapNode>): react.JSX.Element;
767
+
768
+ /**
769
+ * The `scaleGroup` every process-map edge shares, so `computeEdgeWeightScale` min-maxes
770
+ * the whole map against ONE domain — the `Legend variant="scale"` beside it reads that
771
+ * same domain, which is what stops the key and the picture from disagreeing.
772
+ */
773
+ declare const PROCESS_MAP_EDGE_SCALE_GROUP = "process-map";
774
+ /**
775
+ * Branded process-map transition edge. Register it in
776
+ * `edgeTypes={{ "process-transition": ProcessTransitionEdge }}`; build edges with
777
+ * `buildProcessMapModel`.
778
+ */
779
+ declare function ProcessTransitionEdge(props: EdgeProps<ProcessMapEdge>): react.JSX.Element;
780
+
781
+ /** How long a RE-layout waits for the structure to settle. */
782
+ declare const DEFAULT_LAYOUT_DEBOUNCE_MS = 80;
783
+ /**
784
+ * The class that animates node position deltas.
785
+ *
786
+ * React Flow writes `transform: translate(x, y)` onto its own node element, so the
787
+ * transition has to live there — not on anything this package renders. The element is
788
+ * addressed as `div[data-id]` rather than by React Flow's own `.react-flow__node` class
789
+ * on purpose: that class contains underscores, which have to be backslash-escaped inside
790
+ * a Tailwind arbitrary variant and would then be eaten by JavaScript string escaping the
791
+ * moment the class string passes through `cn()`. Nodes are `div`s and edges are `g`s, so
792
+ * the attribute form is both simpler and correctly scoped. Same reasoning as the ancestor
793
+ * selector in `FlowNode`'s focus indicator.
794
+ *
795
+ * ## The `:not([data-handlepos])` is load-bearing
796
+ *
797
+ * React Flow renders a `<Handle>` as a `<div>` carrying BOTH `data-id` and
798
+ * `data-handlepos`, so a bare `div[data-id]` also matched every connector dot — and a dot
799
+ * changes sides when the layout direction flips (bottom/top becomes right/left). The dot
800
+ * then ANIMATED to its new side, React Flow measured `handleBounds` while it was in
801
+ * flight, and never measured again: the stored bounds said `x: 164` where the DOM had
802
+ * settled at `172`, and every edge on the map terminated in mid-air. Measured at 69 px of
803
+ * drift left-to-right, 24 px top-to-bottom.
804
+ *
805
+ * A handle is not a node position; it has no delta to animate, and it must be measurable
806
+ * the instant it is placed. Excluding it is the fix, not a workaround.
807
+ */
808
+ declare const PROCESS_MAP_NODE_MOTION_CLASS: string;
809
+ /** One cached dagre result: where every node sits, plus the structure dagre reported. */
810
+ interface ProcessLayoutSnapshot {
811
+ positions: Record<string, XYPosition>;
812
+ /** Node id → the handle side edges LEAVE from, as `layoutFlow` set it per direction. */
813
+ sourcePosition: Record<string, ProcessMapNode["sourcePosition"]>;
814
+ /** Node id → the handle side edges ENTER on. */
815
+ targetPosition: Record<string, ProcessMapNode["targetPosition"]>;
816
+ /** Edge ids running against the layout direction (`layoutFlow`'s `backEdges`). */
817
+ backEdges: string[];
818
+ /** Edge ids whose source equals their target (`layoutFlow`'s `selfLoops`). */
819
+ selfLoops: string[];
820
+ /** Wall-clock cost of the `layoutFlow` call that produced this, in milliseconds. */
821
+ durationMs: number;
822
+ }
823
+ /** Inputs to {@link useProcessLayout}. */
824
+ interface UseProcessLayoutOptions {
825
+ nodes: ProcessMapNode[];
826
+ edges: ProcessMapEdge[];
827
+ /** Structure-only cache key — see `processGraphStructureKey`. NEVER include a metric. */
828
+ structureKey: string;
829
+ direction: FlowLayoutDirection;
830
+ /** @default {@link DEFAULT_LAYOUT_DEBOUNCE_MS} */
831
+ debounceMs?: number;
832
+ }
833
+ /** What {@link useProcessLayout} answers. */
834
+ interface UseProcessLayoutResult {
835
+ /** `nodes`, positioned. Identity changes whenever `nodes` does, so metrics stay live. */
836
+ nodes: ProcessMapNode[];
837
+ /** Edge ids `layoutFlow` reported as running backwards, for the back-edge shape. */
838
+ backEdgeIds: ReadonlySet<string>;
839
+ /** Edge ids `layoutFlow` withheld as self-loops. */
840
+ selfLoopIds: ReadonlySet<string>;
841
+ /** How many times `layoutFlow` has actually run for this hook instance. */
842
+ layoutRuns: number;
843
+ /** Cost of the most recent `layoutFlow` call, in milliseconds. `0` before the first. */
844
+ lastLayoutMs: number;
845
+ /** True between a structural change and the debounced layout that answers it. */
846
+ pending: boolean;
847
+ }
848
+ /** Apply a cached snapshot to a fresh set of nodes, matching on id. */
849
+ declare function applyLayoutSnapshot(nodes: ProcessMapNode[], snapshot: ProcessLayoutSnapshot): ProcessMapNode[];
850
+ /**
851
+ * Lay a process graph out, reusing a cached layout whenever the structure is unchanged.
852
+ *
853
+ * @see {@link UseProcessLayoutResult.layoutRuns} — the observable proof that a metric-only
854
+ * change does not re-run dagre.
855
+ */
856
+ declare function useProcessLayout({ nodes, edges, structureKey, direction, debounceMs, }: UseProcessLayoutOptions): UseProcessLayoutResult;
857
+
858
+ interface AbstractionControlsProps extends Omit<HTMLAttributes<HTMLDivElement>, "onChange"> {
859
+ /** The current abstraction — both fractions, `invert`, and (read-only here) `keepConnected`. */
860
+ abstraction: AbstractionOptions;
861
+ /** Called with a PARTIAL patch — mirrors `useProcessExplorer`'s own `setAbstraction`. */
862
+ onAbstractionChange(next: Partial<AbstractionOptions>): void;
863
+ /** The CURRENT (already-abstracted) graph — only `activities.length` is read. */
864
+ graph: Pick<ProcessGraph, "activities">;
865
+ /** What the current abstraction hides, straight from `AbstractedGraph.hidden`. */
866
+ hiddenCounts: {
867
+ activities: number;
868
+ paths: number;
869
+ };
870
+ /** Node budget the "Auto" button searches for. Default `25`. */
871
+ autoMaxActivities?: number;
872
+ /** Accessible name for the control group. Default from locale. */
873
+ label?: string;
874
+ }
875
+ declare const AbstractionControls: react.ForwardRefExoticComponent<AbstractionControlsProps & react.RefAttributes<HTMLDivElement>>;
876
+
877
+ /**
878
+ * Auto abstraction heuristic — RM-052 (issue #227).
879
+ *
880
+ * The "Auto" button on {@link AbstractionControls} picks an activities fraction that fits
881
+ * a node budget, so a reader opens a busy graph and gets a readable one in one click
882
+ * instead of hand-dragging a slider. This module is pure and framework-free on purpose —
883
+ * it is a small, independently testable piece of judgment, not a UI concern.
884
+ *
885
+ * ## The heuristic
886
+ *
887
+ * "Smallest activities % whose graph fits the budget" is the phrase this feature is
888
+ * usually described by, but taken literally it is degenerate: a fraction near 0 always
889
+ * fits any budget, so the "smallest fraction" reading is trivially satisfied by hiding
890
+ * almost everything. What a reader actually wants from "Auto" is the OPPOSITE end of
891
+ * that: the LARGEST fraction — the most detail — that still fits the budget. This module
892
+ * implements that reading.
893
+ *
894
+ * ## Bounded and terminating, by construction
895
+ *
896
+ * The search is a fixed-iteration binary search over the fraction range `[minFraction,
897
+ * 1]` — `maxSteps` (default 8) iterations, no early-exit convergence check, no
898
+ * data-dependent loop bound. That makes termination a property of the `for` loop itself,
899
+ * not of the input: `computeAutoAbstraction` always does at most `maxSteps` probes of
900
+ * `keptAt(fraction)`, regardless of how many activities the graph has. See
901
+ * `auto-abstraction.test.ts` for the explicit bound assertion.
902
+ *
903
+ * ## Known limitation: this heuristic is topology-blind (RM-052 round 2, #227, F2)
904
+ *
905
+ * `keptActivityCount` predicts how many activities a fraction keeps from the COUNT alone —
906
+ * `round(total * fraction)`. It has no visibility into which activities `abstractGraph`
907
+ * would actually drop, or how they connect. `abstractGraph`'s default `keepConnected: true`
908
+ * adds activities back in to restore reachability to a start/end activity that truncation
909
+ * would otherwise strand — and on a graph with no bypass edges (a strict, unbranched chain,
910
+ * where every activity's only path to the end activity runs through every activity after
911
+ * it), dropping ANY suffix forces the ENTIRE remainder back in. The naive prediction and the
912
+ * real, reconnected result can then diverge by the graph's full size: a 30-activity chain
913
+ * with `maxActivities: 10` predicts a fraction that keeps 10 activities, but
914
+ * `abstractGraph(graph, { activities: fraction, keepConnected: true })` actually keeps all
915
+ * 30 — see `auto-abstraction.test.ts`'s "known limitation" describe block, which measures
916
+ * this exact case and isolates `keepConnected` as the sole cause (turning it off makes the
917
+ * real result match the naive prediction exactly).
918
+ *
919
+ * This is accepted as a best-effort gap, not fixed here: fixing it precisely would mean this
920
+ * module accepting a real `ProcessGraph` and searching against `abstractGraph`'s actual
921
+ * output rather than a plain count — a signature change that would also require updating
922
+ * `AbstractionControls`'s call site (`handleAuto`), which sits outside this round's write-set
923
+ * (RM-052-fix-brief.md / RM-052-tristate-decision.md §10). A future round that wants an exact
924
+ * search should route that signature change through `brand-ui-design-system-architect` (it
925
+ * changes this module's public contract) rather than patching around it here.
926
+ */
927
+ interface AutoAbstractionOptions {
928
+ /** Largest number of activities the resulting view should keep. Default `25`. */
929
+ maxActivities?: number;
930
+ /** Maximum fraction candidates probed before returning the best found so far. Default `8`. */
931
+ maxSteps?: number;
932
+ /** The smallest fraction ever offered — "Auto" never hides everything. Default `0.05`. */
933
+ minFraction?: number;
934
+ }
935
+ interface AutoAbstractionResult {
936
+ /** The activities fraction to hand to `setAbstraction`. */
937
+ activities: number;
938
+ /** How many fraction candidates were actually probed — always `<= maxSteps`. */
939
+ steps: number;
940
+ }
941
+ /**
942
+ * Picks the activities fraction "Auto" applies, from a plain activity COUNT — not a graph.
943
+ * Paths are left to `abstractGraph`'s own connectivity repair, and duration/statistic
944
+ * fields never enter the search, so this heuristic's result cannot be perturbed by the
945
+ * discovery layer's own reservoir-sampling nondeterminism. Taking a count rather than a
946
+ * `ProcessGraph` also lets a caller pass the graph's PRE-abstraction total — e.g.
947
+ * `graph.activities.length + hiddenCounts.activities` — without building a throwaway array.
948
+ */
949
+ declare function computeAutoAbstraction(totalActivities: number, opts?: AutoAbstractionOptions): AutoAbstractionResult;
950
+
951
+ type MetricLayer = "frequency" | "performance" | "rework";
952
+ interface MetricLayerSwitchMetric {
953
+ node: ProcessMetric;
954
+ edge: ProcessMetric;
955
+ }
956
+ interface MetricLayerSwitchProps extends Omit<HTMLAttributes<HTMLDivElement>, "onChange"> {
957
+ layer: MetricLayer;
958
+ onLayerChange(layer: MetricLayer): void;
959
+ metric: MetricLayerSwitchMetric;
960
+ onMetricChange(next: Partial<MetricLayerSwitchMetric>): void;
961
+ /** Controlled lock state. Omit to let the component manage it (`defaultLocked`). */
962
+ locked?: boolean;
963
+ /** Initial lock state when uncontrolled. Default `true`. */
964
+ defaultLocked?: boolean;
965
+ onLockedChange?(locked: boolean): void;
966
+ label?: string;
967
+ }
968
+ declare const MetricLayerSwitch: react.ForwardRefExoticComponent<MetricLayerSwitchProps & react.RefAttributes<HTMLDivElement>>;
969
+
970
+ interface ProcessKpiStripKpis {
971
+ cases: number;
972
+ events: number;
973
+ variants: number;
974
+ medianThroughput: number;
975
+ reworkRate: number;
976
+ }
977
+ type ProcessKpiStripTrendKey = keyof ProcessKpiStripKpis | "conformance";
978
+ interface ProcessKpiStripProps extends HTMLAttributes<HTMLDivElement> {
979
+ kpis: ProcessKpiStripKpis;
980
+ /** `0..1`, or `null`/`undefined` when no conformance model has been fitted. */
981
+ conformance?: number | null;
982
+ /** Optional per-tile trend series (oldest -> newest), keyed by KPI. */
983
+ trends?: Partial<Record<ProcessKpiStripTrendKey, number[]>>;
984
+ loading?: boolean;
985
+ }
986
+ declare function ProcessKpiStrip({ kpis, conformance, trends, loading, className, ...props }: ProcessKpiStripProps): react.JSX.Element;
987
+
988
+ /**
989
+ * Off-thread discovery — RM-050.
990
+ *
991
+ * Discovering a graph from a raw log is O(events) with a couple of hash lookups per event;
992
+ * on a 200 000-row import that is long enough to drop frames, and a dropped frame during a
993
+ * file import is exactly the moment an app reads as broken. So the derivation moves to a
994
+ * worker — but ONLY as an optimization: the caller never branches on the environment.
995
+ *
996
+ * **Degrade, never fail.** When there is no `Worker` (a server render, a Node test, an
997
+ * embedded webview), when constructing one throws, or when a live one errors, the same
998
+ * functions run inline on the calling thread and the promise still resolves with the same
999
+ * answer. `handleProcessRequest` is literally shared with the worker entry, so "the inline
1000
+ * path agrees with the worker path" is a property of the code, not of a test.
1001
+ *
1002
+ * **Normalize once.** Every method takes an `AnyLog`; hand it an already-normalized log
1003
+ * (`asNormalizedLog`) and neither side re-parses. A `NormalizedLog` is plain data and
1004
+ * clones across `postMessage` unchanged.
1005
+ */
1006
+
1007
+ /**
1008
+ * The slice of the `Worker` interface this module uses.
1009
+ *
1010
+ * Narrow on purpose: it is the seam a test (or a bundler with its own worker construction)
1011
+ * substitutes, and demanding the full DOM `Worker` surface for that would be pointless
1012
+ * ceremony. A real `Worker` satisfies it.
1013
+ */
1014
+ interface ProcessWorkerLike {
1015
+ postMessage(message: unknown): void;
1016
+ terminate(): void;
1017
+ addEventListener(type: "message" | "error" | "messageerror", listener: (event: unknown) => void): void;
1018
+ }
1019
+ /** Options for {@link createProcessWorker}. */
1020
+ interface CreateProcessWorkerOptions {
1021
+ /** Skip the worker entirely and run inline. Useful for benchmarks and for tests. */
1022
+ forceInline?: boolean;
1023
+ /**
1024
+ * Build the worker yourself.
1025
+ *
1026
+ * The default construction is
1027
+ * `new Worker(new URL("./process-worker.ts", import.meta.url), { type: "module" })`.
1028
+ * A bundler compiling THIS SOURCE (Vite, webpack) rewrites that literal itself; a
1029
+ * consumer of the BUILT package gets `dist/core/process-worker.js`, which
1030
+ * `tsup.config.ts` emits as its own pass and points the built bundle at — a bundler
1031
+ * rewrites the source it compiles, never a `dist` it merely consumes, so that file has
1032
+ * to be real. A host that needs its own URL supplies this instead. Throwing from it is
1033
+ * safe: the handle falls back to the inline path.
1034
+ */
1035
+ createWorker?: () => ProcessWorkerLike;
1036
+ }
1037
+
1038
+ /** What a node or an edge is painted with — the same domain `ProcessMap`'s metric reads. */
1039
+ type ProcessExplorerMetric = FrequencyMode | PerformanceAgg;
1040
+ /** The two metric slots a process explorer coordinates. */
1041
+ interface ProcessExplorerMetricSpec {
1042
+ node: ProcessExplorerMetric;
1043
+ edge: ProcessExplorerMetric;
1044
+ }
1045
+ /**
1046
+ * Every intent this hook's `applyIntent` accepts — wider than `ProcessMap`'s own menu.
1047
+ *
1048
+ * `ProcessFilterIntent` (RM-051) is the four kinds `ProcessMap`'s filter-intent menu
1049
+ * offers (`with`/`without`/`startsWith`/`endsWith`) and stays scoped to exactly that menu.
1050
+ * This hook's `FilterIntent` widens it with `{ kind: "variant" }` (RM-052 round 2, #227) so
1051
+ * a variant-explorer view can drive the SAME `applyIntent`/`intents` pair to filter by a
1052
+ * variant selection — a case a click on the process map can never produce, but a click on a
1053
+ * variant row can. The widening is local to this hook's own type alias; it does not touch
1054
+ * `ProcessFilterIntent` or `ProcessMap`'s menu, which still only ever emits the original four.
1055
+ */
1056
+ type FilterIntent = ProcessFilterIntent | Extract<FilterSpec, {
1057
+ kind: "variant";
1058
+ }>;
1059
+ /** Options for {@link useProcessExplorer}. */
1060
+ interface ProcessExplorerOptions {
1061
+ /** Initial abstraction sliders. Default: `{ activities: 1, paths: 1 }` — the identity. */
1062
+ abstraction?: Partial<AbstractionOptions>;
1063
+ /** Initial metric choice. Default: `{ node: "absolute", edge: "absolute" }`. */
1064
+ metric?: Partial<ProcessExplorerMetricSpec>;
1065
+ /** Initial metric layer — feed straight into `MetricLayerSwitch`'s `layer` prop. Default `"frequency"`. */
1066
+ layer?: MetricLayer;
1067
+ /**
1068
+ * Event count above which discovery and variant extraction move to a worker (RM-050's
1069
+ * own stated figure). Default `50_000`. Lower it in a test that wants to exercise the
1070
+ * worker path without a 50k-row fixture.
1071
+ */
1072
+ workerThreshold?: number;
1073
+ /**
1074
+ * Forwarded verbatim to `createProcessWorker` — override `forceInline`/`createWorker`
1075
+ * for a test or a host with its own worker construction. The handle is created lazily,
1076
+ * on the first request that crosses `workerThreshold`, so passing this costs nothing in
1077
+ * a session that never does.
1078
+ */
1079
+ worker?: CreateProcessWorkerOptions;
1080
+ }
1081
+ /** What {@link useProcessExplorer} returns. See the module docblock for the recompute rules. */
1082
+ interface UseProcessExplorerResult {
1083
+ /** The graph to render — already abstracted. Superset of `ProcessGraph`; see `hidden`. */
1084
+ graph: AbstractedGraph;
1085
+ /** Variants of the FILTERED (not abstracted — variants have no node/edge concept) log. */
1086
+ variants: Variant[];
1087
+ abstraction: AbstractionOptions;
1088
+ setAbstraction(next: Partial<AbstractionOptions>): void;
1089
+ metric: ProcessExplorerMetricSpec;
1090
+ setMetric(next: Partial<ProcessExplorerMetricSpec>): void;
1091
+ /** The single explicit selection — `ProcessMap`'s own `selection` prop shape. */
1092
+ selection: ProcessSelection | null;
1093
+ /** Pass straight through as `ProcessMap`'s `onSelect`. */
1094
+ onSelect(next: ProcessSelection | null): void;
1095
+ applyIntent(intent: FilterIntent): void;
1096
+ clearIntent(index: number): void;
1097
+ intents: FilterIntent[];
1098
+ filteredLog: EventLog;
1099
+ /**
1100
+ * Per-element states the active filter contributes — pass straight into `ProcessMap`'s
1101
+ * `selectionStates` prop (RM-052 round 2, #227, Invariant F). Every activity/transition an
1102
+ * intent excluded is marked `"excluded"` here; nothing is ever removed from `graph` itself.
1103
+ * `variants` (RM-052 round 3, #227, G2) marks every id named by an active
1104
+ * `{ kind: "variant" }` intent `"selected"` — read by `VariantExplorer` (RM-054), not by
1105
+ * `ProcessMap`, which has no variant nodes.
1106
+ */
1107
+ selectionStates: ProcessSelectionStates;
1108
+ /** Activities/paths abstraction is currently hiding — sourced from `abstractGraph`'s own `hidden` field. */
1109
+ hiddenCounts: {
1110
+ activities: number;
1111
+ paths: number;
1112
+ };
1113
+ /**
1114
+ * Activities/transitions the active FILTER excluded — rendered, dimmed, never removed.
1115
+ * Disjoint from `hiddenCounts` by construction: `hiddenCounts` is what abstraction removed
1116
+ * from the render entirely, `excludedCounts` is what the filter re-inked but kept drawn.
1117
+ */
1118
+ excludedCounts: {
1119
+ activities: number;
1120
+ paths: number;
1121
+ };
1122
+ /** The active metric layer — feed straight into `MetricLayerSwitch`'s `layer` prop. */
1123
+ layer: MetricLayer;
1124
+ /** Pass straight through as `MetricLayerSwitch`'s `onLayerChange` — a plain setter; the
1125
+ * frequency/performance metric coercion lives in `MetricLayerSwitch` itself, not here. */
1126
+ setLayer(next: MetricLayer): void;
1127
+ kpis: {
1128
+ cases: number;
1129
+ events: number;
1130
+ variants: number;
1131
+ /** Median case throughput time, in milliseconds. */
1132
+ medianThroughput: number;
1133
+ /** Fraction of cases carrying at least one repeated activity. */
1134
+ reworkRate: number;
1135
+ };
1136
+ /** Full rework tallies — feed straight into `ProcessMap`'s `rework` prop. */
1137
+ rework: ReworkStats;
1138
+ /**
1139
+ * `true` while a discovery/variant request for the CURRENT `filteredLog` is running off
1140
+ * a worker. Always `false` when `filteredLog` stays at or under `workerThreshold` — the
1141
+ * synchronous path has no gap to express. See loading-states.md: this is `loading`, not
1142
+ * `isStreaming` — a settled recomputation, not token-by-token output.
1143
+ */
1144
+ loading: boolean;
1145
+ }
1146
+ /**
1147
+ * Coordinate abstraction, metric choice, selection and filter intents over one event log
1148
+ * into everything `ProcessMap` / `AbstractionControls` / `MetricLayerSwitch` /
1149
+ * `ProcessKpiStrip` need. See the module docblock.
1150
+ */
1151
+ declare function useProcessExplorer(log: EventLog, opts?: ProcessExplorerOptions): UseProcessExplorerResult;
1152
+
1153
+ export { AbstractionControls, type AbstractionControlsProps, type ActivityFrequencyMode, type AutoAbstractionOptions, type AutoAbstractionResult, type BuildProcessMapModelOptions, DEFAULT_LAYOUT_DEBOUNCE_MS, EMPTY_PROCESS_MAP_HOVER, type FilterIntent, GHOST_OPACITY, type MetricLayer, MetricLayerSwitch, type MetricLayerSwitchMetric, type MetricLayerSwitchProps, PROCESS_FILTER_INTENT_KINDS, PROCESS_FILTER_INTENT_LABELS, PROCESS_FILTER_INTENT_MESSAGE_KEYS, PROCESS_MAP_EDGE_SCALE_GROUP, PROCESS_MAP_LEGIBLE_ZOOM, PROCESS_MAP_NODE_MOTION_CLASS, PROCESS_SELECTION_STATE_MESSAGE_KEYS, ProcessActivityNode, type ProcessActivityNodeData, type ProcessActivityRow, type ProcessEdgeDenominators, type ProcessExplorerMetric, type ProcessExplorerMetricSpec, type ProcessExplorerOptions, type ProcessFilterIntent, ProcessKpiStrip, type ProcessKpiStripKpis, type ProcessKpiStripProps, type ProcessKpiStripTrendKey, type ProcessLayoutSnapshot, ProcessMap, type ProcessMapEdge, ProcessMapEdgeKeyContext, type ProcessMapEdgeKeyHandler, type ProcessMapExcludedCounts, ProcessMapHoverContext, type ProcessMapHoverState, type ProcessMapModel, type ProcessMapNode, type ProcessMapProps, type ProcessMetric, type ProcessMetricSpec, type ProcessSelection, type ProcessSelectionKind, type ProcessSelectionNeighbourhood, type ProcessSelectionState, type ProcessSelectionStates, ProcessTransitionEdge, type ProcessTransitionEdgeData, type ProcessTransitionRow, type TransitionFrequencyMode, type UseProcessExplorerResult, type UseProcessLayoutOptions, type UseProcessLayoutResult, activityAriaLabel, activityMetricValue, activityRole, applyLayoutSnapshot, buildProcessMapModel, computeAutoAbstraction, edgeMetricLabel, formatDurationMs, formatMetricValue, isPerformanceMetric, nodeMetricLabel, processEdgeDenominators, processEdgeId, processGraphStructureKey, resolveActivityFrequencyMode, resolveSelectionState, resolveTransitionFrequencyMode, selectionNeighbourhood, selectionStateLabel, transitionAriaLabel, transitionMetricValue, transitionShape, useProcessExplorer, useProcessLayout, useProcessMapEdgeKeys, useProcessMapHover };