@elabs-ai/components-process 4.1.0 → 5.0.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 (145) hide show
  1. package/README.md +8 -1
  2. package/dist/core/index.d.ts +801 -3
  3. package/dist/core/index.js +1334 -0
  4. package/dist/core/index.js.map +1 -1
  5. package/dist/index.d.ts +1889 -34
  6. package/dist/index.js +5512 -196
  7. package/dist/index.js.map +1 -1
  8. package/dist/test/index.d.ts +223 -5
  9. package/dist/test/index.js +346 -191
  10. package/dist/test/index.js.map +1 -1
  11. package/package.json +14 -13
  12. package/src/__contract__/case-table.contract.test.tsx +49 -0
  13. package/src/__contract__/compare-kpi-strip.contract.test.tsx +49 -0
  14. package/src/__contract__/conformance-overlay.contract.test.tsx +49 -0
  15. package/src/__contract__/happy-path-editor.contract.test.tsx +49 -0
  16. package/src/__contract__/process-kpi-strip.contract.test.tsx +49 -0
  17. package/src/__contract__/violation-list.contract.test.tsx +49 -0
  18. package/src/abstraction-controls/abstraction-controls-per-type.test.tsx +80 -0
  19. package/src/abstraction-controls/abstraction-controls.stories.tsx +43 -1
  20. package/src/abstraction-controls/abstraction-controls.tsx +198 -5
  21. package/src/case-table/case-table.stories.tsx +89 -0
  22. package/src/case-table/case-table.test.tsx +148 -0
  23. package/src/case-table/case-table.tsx +144 -0
  24. package/src/case-table/columns.ts +116 -0
  25. package/src/case-table/index.ts +11 -0
  26. package/src/case-timeline/case-timeline-model.test.ts +72 -0
  27. package/src/case-timeline/case-timeline-model.ts +112 -0
  28. package/src/case-timeline/case-timeline.stories.tsx +94 -0
  29. package/src/case-timeline/case-timeline.test.tsx +51 -0
  30. package/src/case-timeline/case-timeline.tsx +109 -0
  31. package/src/case-timeline/index.ts +9 -0
  32. package/src/conformance-overlay/conformance-fixture.ts +59 -0
  33. package/src/conformance-overlay/conformance-legend.tsx +109 -0
  34. package/src/conformance-overlay/conformance-overlay.stories.tsx +116 -0
  35. package/src/conformance-overlay/conformance-overlay.test.tsx +88 -0
  36. package/src/conformance-overlay/conformance-overlay.tsx +107 -0
  37. package/src/conformance-overlay/conformance-state.test.ts +79 -0
  38. package/src/conformance-overlay/conformance-state.ts +220 -0
  39. package/src/conformance-overlay/index.ts +4 -0
  40. package/src/core/activity-color-scale.test.ts +107 -0
  41. package/src/core/activity-color-scale.ts +133 -0
  42. package/src/core/adapters/ocel.test.ts +112 -0
  43. package/src/core/adapters/ocel.ts +359 -0
  44. package/src/core/adapters/xes.test.ts +293 -0
  45. package/src/core/adapters/xes.ts +384 -0
  46. package/src/core/cases-from-log.test.ts +72 -0
  47. package/src/core/cases-from-log.ts +85 -0
  48. package/src/core/conformance.test.ts +80 -0
  49. package/src/core/conformance.ts +91 -0
  50. package/src/core/diff-graphs.test.ts +151 -0
  51. package/src/core/diff-graphs.ts +118 -0
  52. package/src/core/discover-object-centric-graph.test.ts +94 -0
  53. package/src/core/discover-object-centric-graph.ts +296 -0
  54. package/src/core/fixtures/ocel-sample.ts +82 -0
  55. package/src/core/fixtures/sample.xes +68 -0
  56. package/src/core/index.ts +113 -0
  57. package/src/core/reference-model.test.ts +43 -0
  58. package/src/core/reference-model.ts +116 -0
  59. package/src/core/replay-timeline.test.ts +161 -0
  60. package/src/core/replay-timeline.ts +260 -0
  61. package/src/core/segments.test.ts +185 -0
  62. package/src/core/segments.ts +153 -0
  63. package/src/core/token-replay.test.ts +218 -0
  64. package/src/core/token-replay.ts +456 -0
  65. package/src/core/types.ts +2 -2
  66. package/src/dotted-chart/compute-dots.test.ts +176 -0
  67. package/src/dotted-chart/compute-dots.ts +241 -0
  68. package/src/dotted-chart/dotted-chart-labels.ts +93 -0
  69. package/src/dotted-chart/dotted-chart.stories.tsx +182 -0
  70. package/src/dotted-chart/dotted-chart.test.tsx +135 -0
  71. package/src/dotted-chart/dotted-chart.tsx +841 -0
  72. package/src/dotted-chart/index.ts +23 -0
  73. package/src/dotted-chart/use-element-size.ts +33 -0
  74. package/src/happy-path-editor/happy-path-editor-context.ts +81 -0
  75. package/src/happy-path-editor/happy-path-editor.stories.tsx +116 -0
  76. package/src/happy-path-editor/happy-path-editor.test.tsx +142 -0
  77. package/src/happy-path-editor/happy-path-editor.tsx +239 -0
  78. package/src/happy-path-editor/happy-path-step-node.tsx +175 -0
  79. package/src/happy-path-editor/index.ts +4 -0
  80. package/src/index.ts +51 -1
  81. package/src/performance-spectrum/aggregate-segments.test.ts +107 -0
  82. package/src/performance-spectrum/aggregate-segments.ts +174 -0
  83. package/src/performance-spectrum/index.ts +25 -0
  84. package/src/performance-spectrum/performance-spectrum-context.tsx +116 -0
  85. package/src/performance-spectrum/performance-spectrum.stories.tsx +128 -0
  86. package/src/performance-spectrum/performance-spectrum.test.tsx +190 -0
  87. package/src/performance-spectrum/performance-spectrum.tsx +870 -0
  88. package/src/process-compare/compare-kpi-strip.stories.tsx +48 -0
  89. package/src/process-compare/compare-kpi-strip.tsx +94 -0
  90. package/src/process-compare/compare-model.ts +83 -0
  91. package/src/process-compare/compare-side.tsx +42 -0
  92. package/src/process-compare/diff-to-graph.ts +104 -0
  93. package/src/process-compare/index.ts +23 -0
  94. package/src/process-compare/process-compare.stories.tsx +184 -0
  95. package/src/process-compare/process-compare.test.tsx +224 -0
  96. package/src/process-compare/process-compare.tsx +251 -0
  97. package/src/process-explorer.stories.tsx +1 -1
  98. package/src/process-filter-bar/index.ts +2 -0
  99. package/src/process-filter-bar/process-filter-bar.stories.tsx +156 -0
  100. package/src/process-filter-bar/process-filter-bar.test.tsx +201 -0
  101. package/src/process-filter-bar/process-filter-bar.tsx +167 -0
  102. package/src/process-kpi-strip/process-kpi-strip.stories.tsx +47 -0
  103. package/src/process-kpi-strip/process-kpi-strip.test.tsx +67 -0
  104. package/src/process-kpi-strip/process-kpi-strip.tsx +148 -8
  105. package/src/process-map/activity-accent.ts +25 -0
  106. package/src/process-map/index.ts +1 -0
  107. package/src/process-map/map-model.test.ts +16 -0
  108. package/src/process-map/map-model.ts +323 -1
  109. package/src/process-map/object-centric-map.test.tsx +132 -0
  110. package/src/process-map/process-activity-node.tsx +152 -14
  111. package/src/process-map/process-map-object-centric.stories.tsx +219 -0
  112. package/src/process-map/process-map.stories.tsx +64 -0
  113. package/src/process-map/process-map.tsx +240 -16
  114. package/src/process-map/process-transition-edge.test.tsx +47 -0
  115. package/src/process-map/process-transition-edge.tsx +134 -7
  116. package/src/process-map/use-process-layout.ts +30 -9
  117. package/src/process-replay/congestion-heat.tsx +107 -0
  118. package/src/process-replay/index.ts +14 -0
  119. package/src/process-replay/process-replay.stories.tsx +168 -0
  120. package/src/process-replay/process-replay.test.tsx +170 -0
  121. package/src/process-replay/process-replay.tsx +285 -0
  122. package/src/process-replay/replay-controls.tsx +147 -0
  123. package/src/process-replay/replay-format.ts +83 -0
  124. package/src/process-replay/replay-tokens-context.ts +30 -0
  125. package/src/process-replay/use-controllable-value.ts +30 -0
  126. package/src/templates-process-explorer.stories.tsx +1304 -0
  127. package/src/test/contract.test.ts +66 -0
  128. package/src/test/contract.ts +107 -6
  129. package/src/test/doubles.test.tsx +87 -1
  130. package/src/test/doubles.tsx +174 -3
  131. package/src/test/index.ts +25 -1
  132. package/src/use-process-explorer/use-process-explorer.test.ts +44 -0
  133. package/src/use-process-explorer/use-process-explorer.ts +34 -2
  134. package/src/variant-explorer/coverage-bar.tsx +36 -0
  135. package/src/variant-explorer/index.ts +16 -0
  136. package/src/variant-explorer/sequence-chips.tsx +103 -0
  137. package/src/variant-explorer/variant-explorer-model.ts +42 -0
  138. package/src/variant-explorer/variant-explorer.stories.tsx +226 -0
  139. package/src/variant-explorer/variant-explorer.test.tsx +302 -0
  140. package/src/variant-explorer/variant-explorer.tsx +567 -0
  141. package/src/variant-explorer/variant-row.tsx +137 -0
  142. package/src/violation-list/index.ts +2 -0
  143. package/src/violation-list/violation-list.stories.tsx +73 -0
  144. package/src/violation-list/violation-list.test.tsx +84 -0
  145. package/src/violation-list/violation-list.tsx +259 -0
@@ -0,0 +1,94 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { fromOcel } from "./adapters/ocel";
4
+ import {
5
+ abstractObjectCentricGraph,
6
+ discoverObjectCentricGraph,
7
+ objectCentricProcessGraph,
8
+ objectTypeColorScale,
9
+ } from "./discover-object-centric-graph";
10
+ import { OCEL_SAMPLE } from "./fixtures/ocel-sample";
11
+
12
+ function sampleGraph() {
13
+ const result = fromOcel(OCEL_SAMPLE);
14
+ if (!result.ok) throw new Error("fixture must parse");
15
+ return discoverObjectCentricGraph(result.logs, { objectTypes: result.objectTypes });
16
+ }
17
+
18
+ describe("discoverObjectCentricGraph", () => {
19
+ it("merges an activity shared by all three types into one node with three perType entries", () => {
20
+ const graph = sampleGraph();
21
+ const pack = graph.activities.find((activity) => activity.id === "Pack")!;
22
+ expect(pack.perType).toEqual({
23
+ order: { instances: 1, cases: 1 },
24
+ item: { instances: 2, cases: 2 },
25
+ package: { instances: 1, cases: 1 },
26
+ });
27
+ // One OCEL event, however many objects it touched.
28
+ expect(pack.events).toBe(1);
29
+ });
30
+
31
+ it("keeps an activity present in only one type single-type", () => {
32
+ const graph = sampleGraph();
33
+ const pick = graph.activities.find((activity) => activity.id === "Pick Item")!;
34
+ expect(Object.keys(pick.perType)).toEqual(["item"]);
35
+ expect(pick.perType.item).toEqual({ instances: 1, cases: 1 });
36
+ });
37
+
38
+ it("keeps edges per type — never merged across types", () => {
39
+ const graph = sampleGraph();
40
+ expect(graph.objectTypes).toEqual(["order", "item", "package"]);
41
+ const pairs = (type: string) =>
42
+ graph.transitionsByType[type]!.map((t) => `${t.source}>${t.target}:${t.count}`).sort();
43
+ expect(pairs("order")).toEqual(["Pack>Ship:1", "Place Order>Pack:1"]);
44
+ expect(pairs("item")).toEqual([
45
+ "Pick Item>Pack:1",
46
+ "Place Order>Pack:1",
47
+ "Place Order>Pick Item:1",
48
+ ]);
49
+ expect(pairs("package")).toEqual(["Pack>Ship:1"]);
50
+ });
51
+
52
+ it("flattens to a ProcessGraph that sums shared transitions for layout", () => {
53
+ const flat = objectCentricProcessGraph(sampleGraph());
54
+ expect(flat.activities.map((a) => a.id).sort()).toEqual([
55
+ "Pack",
56
+ "Pick Item",
57
+ "Place Order",
58
+ "Ship",
59
+ ]);
60
+ const placeToPack = flat.transitions.find(
61
+ (t) => t.source === "Place Order" && t.target === "Pack",
62
+ )!;
63
+ expect(placeToPack.count).toBe(2);
64
+ expect(flat.totals).toEqual({ cases: 4, events: 4, variants: 4 });
65
+ expect(flat.activities.find((a) => a.id === "Pack")).toMatchObject({ instances: 1, cases: 4 });
66
+ });
67
+
68
+ it("abstracts one type without touching another", () => {
69
+ const graph = sampleGraph();
70
+ const abstracted = abstractObjectCentricGraph(graph, {
71
+ item: { activities: 0.34, paths: 1, keepConnected: false },
72
+ });
73
+ expect(abstracted.transitionsByType.order).toBe(graph.transitionsByType.order);
74
+ expect(abstracted.transitionsByType.item!.length).toBeLessThan(
75
+ graph.transitionsByType.item!.length,
76
+ );
77
+ expect(abstracted.hiddenByType.order).toEqual({ activities: 0, paths: 0 });
78
+ expect(abstracted.hiddenByType.item!.activities).toBeGreaterThan(0);
79
+ // Event counts are carried over, never restated.
80
+ expect(abstracted.activities.find((a) => a.id === "Pack")!.events).toBe(1);
81
+ });
82
+
83
+ it("colours object types with the shared RM-054 scale and a text code per type", () => {
84
+ const scale = objectTypeColorScale(sampleGraph());
85
+ // Ranked by object count: item (2) first, then order and package (1 each) by name.
86
+ expect(scale.legend.map((entry) => [entry.activityId, entry.token])).toEqual([
87
+ ["item", "--chart-1"],
88
+ ["order", "--chart-2"],
89
+ ["package", "--chart-3"],
90
+ ]);
91
+ const codes = scale.legend.map((entry) => entry.code);
92
+ expect(new Set(codes).size).toBe(3);
93
+ });
94
+ });
@@ -0,0 +1,296 @@
1
+ /**
2
+ * Object-centric directly-follows graph (OC-DFG) discovery — RM-066.
3
+ *
4
+ * An OC-DFG is one directly-follows graph PER OBJECT TYPE, drawn on one canvas: activities
5
+ * shared between types merge into a single node that keeps a per-type breakdown, while
6
+ * edges stay per type — an `order` following and an `item` following are different
7
+ * relations, so they are never summed into one arrow. The merge semantics (per-type
8
+ * projection, shared-activity join, per-type edges) follow pm4js's documented OC-DFG
9
+ * construction; see `ATTRIBUTION.md`. No pm4js code is copied, and nothing here derives
10
+ * from the AGPL Python reference implementation.
11
+ *
12
+ * Built entirely on `discoverGraph` and `abstractGraph`: every per-type number is exactly
13
+ * what the single-case pipeline would print for that type's projection.
14
+ */
15
+ import { abstractGraph, type AbstractionOptions } from "./abstract-graph";
16
+ import { activityColorScale, type ActivityColorScale } from "./activity-color-scale";
17
+ import { OCEL_EVENT_ID_ATTRIBUTE } from "./adapters/ocel";
18
+ import { discoverGraph, EDGE_KEY_SEPARATOR, type DiscoverGraphOptions } from "./discover-graph";
19
+ import type { ActivityStats, EventLog, ProcessGraph, TransitionStats } from "./types";
20
+
21
+ /** One object type's share of a merged activity. */
22
+ export interface ObjectTypeActivityCounts {
23
+ /** Occurrences in that type's projection (one per event × referenced object). */
24
+ instances: number;
25
+ /** Distinct objects of that type the activity touches. */
26
+ cases: number;
27
+ }
28
+
29
+ /** A merged activity: shared across object types, with a per-type breakdown. */
30
+ export interface ObjectCentricActivityStats extends Omit<ActivityStats, "instances" | "cases"> {
31
+ /**
32
+ * Distinct OCEL events of this activity across every type — an event that references an
33
+ * order and two items counts once. Falls back to the largest per-type `instances` when
34
+ * the logs carry no `__ocelEventId` (hand-built logs).
35
+ */
36
+ events: number;
37
+ /** Object type → counts, only for the types this activity occurs in. */
38
+ perType: Record<string, ObjectTypeActivityCounts>;
39
+ }
40
+
41
+ /** An object-centric directly-follows graph. */
42
+ export interface ObjectCentricGraph {
43
+ /** Merged activities, busiest (by `events`) first, ties by id. */
44
+ activities: ObjectCentricActivityStats[];
45
+ /** Object type → that type's own directly-follows edges. Never merged across types. */
46
+ transitionsByType: Record<string, TransitionStats[]>;
47
+ /** The object types, in the caller's order. */
48
+ objectTypes: string[];
49
+ /** Object type → the full per-type graph the merge was built from. */
50
+ graphsByType: Record<string, ProcessGraph>;
51
+ }
52
+
53
+ /** {@link abstractObjectCentricGraph}'s result — what each type's abstraction hid. */
54
+ export interface AbstractedObjectCentricGraph extends ObjectCentricGraph {
55
+ hiddenByType: Record<string, { activities: number; paths: number }>;
56
+ }
57
+
58
+ function compareStrings(a: string, b: string): number {
59
+ return a < b ? -1 : a > b ? 1 : 0;
60
+ }
61
+
62
+ /** Distinct `__ocelEventId`s per activity across every log, or `undefined` if none carry one. */
63
+ function countDistinctEvents(logs: readonly EventLog[]): Map<string, number> | undefined {
64
+ const ids = new Map<string, Set<string>>();
65
+ let any = false;
66
+ for (const log of logs) {
67
+ for (const row of log.events) {
68
+ const id = row.attributes?.[OCEL_EVENT_ID_ATTRIBUTE];
69
+ if (typeof id !== "string") continue;
70
+ any = true;
71
+ let set = ids.get(row.activity);
72
+ if (set === undefined) {
73
+ set = new Set();
74
+ ids.set(row.activity, set);
75
+ }
76
+ set.add(id);
77
+ }
78
+ }
79
+ if (!any) return undefined;
80
+ const out = new Map<string, number>();
81
+ for (const [activity, set] of ids) out.set(activity, set.size);
82
+ return out;
83
+ }
84
+
85
+ /**
86
+ * Merge per-type graphs into one {@link ObjectCentricGraph}.
87
+ *
88
+ * `eventCounts` supplies each activity's distinct-event count; an activity it does not
89
+ * name falls back to its largest per-type `instances`. A merged activity's `duration` is
90
+ * the duration of the type with the most instances of it (ties: earlier type), because
91
+ * durations of the same events seen through two projections are not additive.
92
+ */
93
+ export function mergeObjectCentricGraphs(
94
+ graphsByType: Record<string, ProcessGraph>,
95
+ objectTypes: readonly string[] = Object.keys(graphsByType),
96
+ eventCounts?: ReadonlyMap<string, number>,
97
+ ): ObjectCentricGraph {
98
+ const merged = new Map<string, ObjectCentricActivityStats>();
99
+ const bestInstances = new Map<string, number>();
100
+ const transitionsByType: Record<string, TransitionStats[]> = {};
101
+ const types = objectTypes.filter((type) => graphsByType[type] !== undefined);
102
+
103
+ for (const type of types) {
104
+ const graph = graphsByType[type] as ProcessGraph;
105
+ transitionsByType[type] = graph.transitions;
106
+ for (const activity of graph.activities) {
107
+ let entry = merged.get(activity.id);
108
+ if (entry === undefined) {
109
+ entry = {
110
+ id: activity.id,
111
+ label: activity.label,
112
+ isStart: false,
113
+ isEnd: false,
114
+ duration: activity.duration,
115
+ events: 0,
116
+ perType: {},
117
+ };
118
+ merged.set(activity.id, entry);
119
+ }
120
+ entry.perType[type] = { instances: activity.instances, cases: activity.cases };
121
+ entry.isStart ||= activity.isStart;
122
+ entry.isEnd ||= activity.isEnd;
123
+ if (activity.instances > (bestInstances.get(activity.id) ?? -1)) {
124
+ bestInstances.set(activity.id, activity.instances);
125
+ entry.duration = activity.duration;
126
+ }
127
+ }
128
+ }
129
+
130
+ const activities = [...merged.values()];
131
+ for (const activity of activities) {
132
+ activity.events = eventCounts?.get(activity.id) ?? bestInstances.get(activity.id) ?? 0;
133
+ }
134
+ activities.sort((a, b) => b.events - a.events || compareStrings(a.id, b.id));
135
+
136
+ const graphs: Record<string, ProcessGraph> = {};
137
+ for (const type of types) graphs[type] = graphsByType[type] as ProcessGraph;
138
+
139
+ return { activities, transitionsByType, objectTypes: types, graphsByType: graphs };
140
+ }
141
+
142
+ /**
143
+ * Discover an {@link ObjectCentricGraph} from per-object-type logs (typically
144
+ * `fromOcel(…).logs`). Runs `discoverGraph` once per type, then merges.
145
+ */
146
+ export function discoverObjectCentricGraph(
147
+ logsByType: Record<string, EventLog>,
148
+ options: DiscoverGraphOptions & { objectTypes?: string[] } = {},
149
+ ): ObjectCentricGraph {
150
+ const { objectTypes = Object.keys(logsByType), ...discoverOptions } = options;
151
+ const types = objectTypes.filter((type) => logsByType[type] !== undefined);
152
+ const graphsByType: Record<string, ProcessGraph> = {};
153
+ for (const type of types) {
154
+ graphsByType[type] = discoverGraph(logsByType[type] as EventLog, discoverOptions);
155
+ }
156
+ const eventCounts = countDistinctEvents(types.map((type) => logsByType[type] as EventLog));
157
+ return mergeObjectCentricGraphs(graphsByType, types, eventCounts);
158
+ }
159
+
160
+ /**
161
+ * Abstract each object type independently, then re-merge.
162
+ *
163
+ * `perType[type]` wins for a type it names; `fallback` applies to every other type; a type
164
+ * with neither is kept whole. Activity `events` counts are carried over from the input,
165
+ * so abstraction hides nodes and edges without restating a statistic.
166
+ */
167
+ export function abstractObjectCentricGraph(
168
+ graph: ObjectCentricGraph,
169
+ perType: Readonly<Record<string, AbstractionOptions>> = {},
170
+ fallback?: AbstractionOptions,
171
+ ): AbstractedObjectCentricGraph {
172
+ const graphsByType: Record<string, ProcessGraph> = {};
173
+ const hiddenByType: Record<string, { activities: number; paths: number }> = {};
174
+ for (const type of graph.objectTypes) {
175
+ const source = graph.graphsByType[type] as ProcessGraph;
176
+ const options = perType[type] ?? fallback;
177
+ if (options === undefined) {
178
+ graphsByType[type] = source;
179
+ hiddenByType[type] = { activities: 0, paths: 0 };
180
+ continue;
181
+ }
182
+ const { hidden, ...abstracted } = abstractGraph(source, options);
183
+ graphsByType[type] = abstracted;
184
+ hiddenByType[type] = hidden;
185
+ }
186
+ const eventCounts = new Map(graph.activities.map((activity) => [activity.id, activity.events]));
187
+ return {
188
+ ...mergeObjectCentricGraphs(graphsByType, graph.objectTypes, eventCounts),
189
+ hiddenByType,
190
+ };
191
+ }
192
+
193
+ /**
194
+ * Flatten an {@link ObjectCentricGraph} into one {@link ProcessGraph} — the shape the
195
+ * process map lays out and reads its node metrics from.
196
+ *
197
+ * - an activity's `instances` is its distinct-event count and its `cases` is the number of
198
+ * objects (of any type) it touches;
199
+ * - a pair of activities joined in several types becomes ONE transition whose `count` and
200
+ * `caseCount` sum the types' — used for layout and selection only; the map still draws
201
+ * the per-type edges from `transitionsByType`;
202
+ * - `totals.cases` counts objects, `totals.events` distinct events.
203
+ */
204
+ export function objectCentricProcessGraph(graph: ObjectCentricGraph): ProcessGraph {
205
+ const activities: ActivityStats[] = graph.activities.map((activity) => {
206
+ let cases = 0;
207
+ for (const counts of Object.values(activity.perType)) cases += counts.cases;
208
+ return {
209
+ id: activity.id,
210
+ label: activity.label,
211
+ instances: activity.events,
212
+ cases,
213
+ isStart: activity.isStart,
214
+ isEnd: activity.isEnd,
215
+ duration: activity.duration,
216
+ };
217
+ });
218
+
219
+ const transitions = new Map<string, TransitionStats>();
220
+ const startActivities: Record<string, number> = {};
221
+ const endActivities: Record<string, number> = {};
222
+ const totals = { cases: 0, events: 0, variants: 0 };
223
+ for (const activity of graph.activities) totals.events += activity.events;
224
+
225
+ for (const type of graph.objectTypes) {
226
+ const typeGraph = graph.graphsByType[type] as ProcessGraph;
227
+ totals.cases += typeGraph.totals.cases;
228
+ totals.variants += typeGraph.totals.variants;
229
+ for (const [id, count] of Object.entries(typeGraph.startActivities)) {
230
+ startActivities[id] = (startActivities[id] ?? 0) + count;
231
+ }
232
+ for (const [id, count] of Object.entries(typeGraph.endActivities)) {
233
+ endActivities[id] = (endActivities[id] ?? 0) + count;
234
+ }
235
+ for (const transition of graph.transitionsByType[type] ?? []) {
236
+ const key = `${transition.source}${EDGE_KEY_SEPARATOR}${transition.target}`;
237
+ const existing = transitions.get(key);
238
+ if (existing === undefined) {
239
+ transitions.set(key, { ...transition });
240
+ continue;
241
+ }
242
+ if (transition.count > existing.count) existing.duration = transition.duration;
243
+ existing.count += transition.count;
244
+ existing.caseCount += transition.caseCount;
245
+ }
246
+ }
247
+
248
+ const transitionList = [...transitions.values()].sort(
249
+ (a, b) =>
250
+ b.count - a.count || compareStrings(a.source, b.source) || compareStrings(a.target, b.target),
251
+ );
252
+
253
+ const sorted = (record: Record<string, number>): Record<string, number> => {
254
+ const out: Record<string, number> = {};
255
+ for (const key of Object.keys(record).sort()) out[key] = record[key] as number;
256
+ return out;
257
+ };
258
+
259
+ return {
260
+ activities,
261
+ transitions: transitionList,
262
+ startActivities: sorted(startActivities),
263
+ endActivities: sorted(endActivities),
264
+ totals,
265
+ };
266
+ }
267
+
268
+ /**
269
+ * The object-type colour scale — RM-054's `activityColorScale`, reused rather than
270
+ * reimplemented: object types are ranked by how many objects each has (ties by name) and
271
+ * take `--chart-1` … `--chart-11` in that order, beyond which they share the hatched
272
+ * "other" slot. `codeFor(type)` is the two-character text code painted beside every
273
+ * swatch, so a type is never identified by colour alone.
274
+ */
275
+ export function objectTypeColorScale(graph: ObjectCentricGraph): ActivityColorScale {
276
+ const zero = { min: 0, max: 0, mean: 0, median: 0, p90: 0, sum: 0, trimmedMean: 0 };
277
+ const activities: ActivityStats[] = graph.objectTypes.map((type) => {
278
+ const typeGraph = graph.graphsByType[type] as ProcessGraph;
279
+ return {
280
+ id: type,
281
+ label: type,
282
+ instances: typeGraph.totals.events,
283
+ cases: typeGraph.totals.cases,
284
+ isStart: false,
285
+ isEnd: false,
286
+ duration: zero,
287
+ };
288
+ });
289
+ return activityColorScale({
290
+ activities,
291
+ transitions: [],
292
+ startActivities: {},
293
+ endActivities: {},
294
+ totals: { cases: 0, events: 0, variants: 0 },
295
+ });
296
+ }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * A 3-object-type, 4-event OCEL 2.0 JSON log (RM-066) — small enough to verify by hand.
3
+ *
4
+ * | event | activity | time | order | item | package |
5
+ * | ----- | ----------- | ----- | ----- | ------ | ------- |
6
+ * | e1 | Place Order | 09:00 | o1 | i1, i2 | |
7
+ * | e2 | Pick Item | 10:00 | | i1 | |
8
+ * | e3 | Pack | 11:00 | o1 | i1, i2 | p1 |
9
+ * | e4 | Ship | 12:00 | o1 | | p1 |
10
+ *
11
+ * Projections: `order` has 3 rows (o1: Place Order → Pack → Ship), `item` has 5 (i1: Place
12
+ * Order → Pick Item → Pack; i2: Place Order → Pack), `package` has 2 (p1: Pack → Ship).
13
+ * `Pack` occurs in all three types; `Pick Item` only in `item`.
14
+ */
15
+ import type { OcelJson } from "../adapters/ocel";
16
+
17
+ export const OCEL_SAMPLE: OcelJson = {
18
+ objectTypes: [
19
+ { name: "order", attributes: [{ name: "value", type: "float" }] },
20
+ { name: "item", attributes: [] },
21
+ { name: "package", attributes: [] },
22
+ ],
23
+ eventTypes: [
24
+ { name: "Place Order", attributes: [{ name: "channel", type: "string" }] },
25
+ { name: "Pick Item", attributes: [] },
26
+ { name: "Pack", attributes: [] },
27
+ { name: "Ship", attributes: [] },
28
+ ],
29
+ objects: [
30
+ {
31
+ id: "o1",
32
+ type: "order",
33
+ attributes: [
34
+ { name: "value", time: "2026-01-05T08:00:00.000Z", value: 120 },
35
+ { name: "value", time: "2026-01-05T11:30:00.000Z", value: 95 },
36
+ ],
37
+ },
38
+ { id: "i1", type: "item" },
39
+ { id: "i2", type: "item" },
40
+ { id: "p1", type: "package" },
41
+ ],
42
+ events: [
43
+ {
44
+ id: "e1",
45
+ type: "Place Order",
46
+ time: "2026-01-05T09:00:00.000Z",
47
+ attributes: [{ name: "channel", value: "web" }],
48
+ relationships: [
49
+ { objectId: "o1", qualifier: "placed" },
50
+ { objectId: "i1", qualifier: "contains" },
51
+ { objectId: "i2", qualifier: "contains" },
52
+ ],
53
+ },
54
+ {
55
+ id: "e2",
56
+ type: "Pick Item",
57
+ time: "2026-01-05T10:00:00.000Z",
58
+ relationships: [{ objectId: "i1", qualifier: "picked" }],
59
+ },
60
+ {
61
+ id: "e3",
62
+ type: "Pack",
63
+ time: "2026-01-05T11:00:00.000Z",
64
+ relationships: [
65
+ { objectId: "o1", qualifier: "for" },
66
+ { objectId: "i1", qualifier: "packed" },
67
+ { objectId: "i2", qualifier: "packed" },
68
+ { objectId: "i2", qualifier: "checked" },
69
+ { objectId: "p1", qualifier: "into" },
70
+ ],
71
+ },
72
+ {
73
+ id: "e4",
74
+ type: "Ship",
75
+ time: "2026-01-05T12:00:00.000Z",
76
+ relationships: [
77
+ { objectId: "o1", qualifier: "for" },
78
+ { objectId: "p1", qualifier: "shipped" },
79
+ ],
80
+ },
81
+ ],
82
+ };
@@ -0,0 +1,68 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!--
3
+ RM-063 fixture — a 3-trace IEEE 1849 XES log exercising: a complete-only (atomic) trace,
4
+ a trace using lifecycle:transition start/complete pairs, and a trace relying on a
5
+ <global scope="event"> default for org:resource. Hand-computed and asserted in xes.test.ts.
6
+ -->
7
+ <log xes.version="1.0" xes.features="nested-attributes">
8
+ <global scope="event">
9
+ <string key="org:resource" value="Unassigned"/>
10
+ </global>
11
+ <classifier name="Activity classifier" keys="concept:name"/>
12
+ <trace>
13
+ <string key="concept:name" value="case-1"/>
14
+ <string key="channel" value="web"/>
15
+ <event>
16
+ <string key="concept:name" value="Create Order"/>
17
+ <date key="time:timestamp" value="2026-01-05T09:00:00.000Z"/>
18
+ <string key="org:resource" value="A. Novak"/>
19
+ </event>
20
+ <event>
21
+ <string key="concept:name" value="Check Credit"/>
22
+ <date key="time:timestamp" value="2026-01-05T09:15:00.000Z"/>
23
+ <string key="org:resource" value="Credit Service"/>
24
+ <float key="cost:total" value="120.5"/>
25
+ </event>
26
+ </trace>
27
+ <trace>
28
+ <string key="concept:name" value="case-2"/>
29
+ <string key="channel" value="mobile"/>
30
+ <event>
31
+ <string key="concept:name" value="Register"/>
32
+ <date key="time:timestamp" value="2026-01-06T09:00:00.000Z"/>
33
+ <string key="org:resource" value="System"/>
34
+ <string key="lifecycle:transition" value="start"/>
35
+ </event>
36
+ <event>
37
+ <string key="concept:name" value="Register"/>
38
+ <date key="time:timestamp" value="2026-01-06T09:10:00.000Z"/>
39
+ <string key="org:resource" value="System"/>
40
+ <string key="lifecycle:transition" value="complete"/>
41
+ </event>
42
+ <event>
43
+ <string key="concept:name" value="Approve"/>
44
+ <date key="time:timestamp" value="2026-01-06T09:10:00.000Z"/>
45
+ <string key="org:resource" value="B. Kowalski"/>
46
+ <string key="lifecycle:transition" value="start"/>
47
+ </event>
48
+ <event>
49
+ <string key="concept:name" value="Approve"/>
50
+ <date key="time:timestamp" value="2026-01-06T09:30:00.000Z"/>
51
+ <string key="org:resource" value="B. Kowalski"/>
52
+ <string key="lifecycle:transition" value="complete"/>
53
+ </event>
54
+ </trace>
55
+ <trace>
56
+ <string key="concept:name" value="case-3"/>
57
+ <string key="channel" value="phone"/>
58
+ <event>
59
+ <string key="concept:name" value="Create Order"/>
60
+ <date key="time:timestamp" value="2026-01-07T09:00:00.000Z"/>
61
+ </event>
62
+ <event>
63
+ <string key="concept:name" value="Ship Order"/>
64
+ <date key="time:timestamp" value="2026-01-07T09:20:00.000Z"/>
65
+ <string key="org:resource" value="Courier"/>
66
+ </event>
67
+ </trace>
68
+ </log>
package/src/core/index.ts CHANGED
@@ -116,3 +116,116 @@ export type { ProcessWorkerRequest, ProcessWorkerResponse } from "./worker/proce
116
116
  // EDGE_KEY_SEPARATOR — RM-050 (the edge-key convention abstraction and the
117
117
  // performance layer both index on; discovery has always used it internally)
118
118
  export { EDGE_KEY_SEPARATOR } from "./discover-graph";
119
+
120
+ // activityColorScale — RM-054
121
+ export {
122
+ ACTIVITY_COLOR_SLOTS,
123
+ ACTIVITY_OTHER_TOKEN,
124
+ activityColorScale,
125
+ } from "./activity-color-scale";
126
+ export type {
127
+ ActivityColor,
128
+ ActivityColorLegendEntry,
129
+ ActivityColorScale,
130
+ } from "./activity-color-scale";
131
+
132
+ // casesFromLog — RM-055
133
+ export { casesFromLog } from "./cases-from-log";
134
+ export type { CaseRow } from "./cases-from-log";
135
+
136
+ // tokenReplay / conformance — RM-061
137
+ export { liftHappyPath } from "./reference-model";
138
+ export type {
139
+ HappyPath,
140
+ HappyPathStep,
141
+ ReplayModel,
142
+ ReplayTransition,
143
+ ReplayTransitionKind,
144
+ } from "./reference-model";
145
+ export {
146
+ DEVIATION_TYPES,
147
+ emptyDeviationCounts,
148
+ replayActivities,
149
+ replayTrace,
150
+ tokenReplay,
151
+ } from "./token-replay";
152
+ export type { Deviation, DeviationType, TraceReplayResult } from "./token-replay";
153
+ export { conformanceRateSeries } from "./conformance";
154
+ export type { ConformanceBucket, ConformanceRatePoint, ConformanceResult } from "./conformance";
155
+
156
+ // fromXes — RM-063
157
+ export { fromXes } from "./adapters/xes";
158
+ export type { XesParseError, XesParseOptions, XesParseResult } from "./adapters/xes";
159
+
160
+ // segments — RM-060
161
+ export {
162
+ durationQuartile,
163
+ durationQuartileThresholds,
164
+ quartileOf,
165
+ segmentKey,
166
+ segmentOrderByFrequency,
167
+ segmentOrderForVariant,
168
+ segmentsFor,
169
+ } from "./segments";
170
+ export type {
171
+ DurationQuartile,
172
+ QuartileThresholds,
173
+ SegmentDefinition,
174
+ SegmentOccurrence,
175
+ } from "./segments";
176
+
177
+ // diffGraphs — RM-064
178
+ export { diffGraphs } from "./diff-graphs";
179
+ export type { DiffEntry, DiffState, ProcessGraphDiff } from "./diff-graphs";
180
+
181
+ // replayTimeline — RM-065
182
+ export {
183
+ defaultReplayBucketMs,
184
+ rankReplayCongestion,
185
+ REPLAY_MAX_FRAMES,
186
+ REPLAY_TARGET_FRAMES,
187
+ REPLAY_TOKEN_RADIUS_RANGE,
188
+ replayFrameAt,
189
+ replayTimeline,
190
+ replayTokenRadius,
191
+ } from "./replay-timeline";
192
+ export type {
193
+ ReplayCongestionEntry,
194
+ ReplayFrame,
195
+ ReplayFrameToken,
196
+ ReplaySegment,
197
+ ReplayTimeline,
198
+ ReplayTimelineOptions,
199
+ } from "./replay-timeline";
200
+
201
+ // Object-centric — RM-066
202
+ export {
203
+ fromOcel,
204
+ OCEL_EVENT_ID_ATTRIBUTE,
205
+ OCEL_OBJECT_REFS_ATTRIBUTE,
206
+ readObjectRefs,
207
+ } from "./adapters/ocel";
208
+ export type {
209
+ OcelAttribute,
210
+ OcelEvent,
211
+ OcelJson,
212
+ OcelObject,
213
+ OcelParseError,
214
+ OcelParseOptions,
215
+ OcelParseResult,
216
+ OcelRelationship,
217
+ OcelTypeDeclaration,
218
+ } from "./adapters/ocel";
219
+ export {
220
+ abstractObjectCentricGraph,
221
+ discoverObjectCentricGraph,
222
+ mergeObjectCentricGraphs,
223
+ objectCentricProcessGraph,
224
+ objectTypeColorScale,
225
+ } from "./discover-object-centric-graph";
226
+ export type {
227
+ AbstractedObjectCentricGraph,
228
+ ObjectCentricActivityStats,
229
+ ObjectCentricGraph,
230
+ ObjectTypeActivityCounts,
231
+ } from "./discover-object-centric-graph";