@elabs-ai/components-process 4.2.0 → 5.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -1
- package/dist/core/index.d.ts +801 -3
- package/dist/core/index.js +1334 -0
- package/dist/core/index.js.map +1 -1
- package/dist/index.d.ts +1889 -34
- package/dist/index.js +5512 -196
- package/dist/index.js.map +1 -1
- package/dist/test/index.d.ts +223 -5
- package/dist/test/index.js +346 -191
- package/dist/test/index.js.map +1 -1
- package/package.json +14 -13
- package/src/__contract__/case-table.contract.test.tsx +49 -0
- package/src/__contract__/compare-kpi-strip.contract.test.tsx +49 -0
- package/src/__contract__/conformance-overlay.contract.test.tsx +49 -0
- package/src/__contract__/happy-path-editor.contract.test.tsx +49 -0
- package/src/__contract__/violation-list.contract.test.tsx +49 -0
- package/src/abstraction-controls/abstraction-controls-per-type.test.tsx +80 -0
- package/src/abstraction-controls/abstraction-controls.stories.tsx +43 -1
- package/src/abstraction-controls/abstraction-controls.tsx +198 -5
- package/src/case-table/case-table.stories.tsx +89 -0
- package/src/case-table/case-table.test.tsx +148 -0
- package/src/case-table/case-table.tsx +144 -0
- package/src/case-table/columns.ts +116 -0
- package/src/case-table/index.ts +11 -0
- package/src/case-timeline/case-timeline-model.test.ts +72 -0
- package/src/case-timeline/case-timeline-model.ts +112 -0
- package/src/case-timeline/case-timeline.stories.tsx +94 -0
- package/src/case-timeline/case-timeline.test.tsx +51 -0
- package/src/case-timeline/case-timeline.tsx +109 -0
- package/src/case-timeline/index.ts +9 -0
- package/src/conformance-overlay/conformance-fixture.ts +59 -0
- package/src/conformance-overlay/conformance-legend.tsx +109 -0
- package/src/conformance-overlay/conformance-overlay.stories.tsx +116 -0
- package/src/conformance-overlay/conformance-overlay.test.tsx +88 -0
- package/src/conformance-overlay/conformance-overlay.tsx +107 -0
- package/src/conformance-overlay/conformance-state.test.ts +79 -0
- package/src/conformance-overlay/conformance-state.ts +220 -0
- package/src/conformance-overlay/index.ts +4 -0
- package/src/core/activity-color-scale.test.ts +107 -0
- package/src/core/activity-color-scale.ts +133 -0
- package/src/core/adapters/ocel.test.ts +112 -0
- package/src/core/adapters/ocel.ts +359 -0
- package/src/core/adapters/xes.test.ts +293 -0
- package/src/core/adapters/xes.ts +384 -0
- package/src/core/cases-from-log.test.ts +72 -0
- package/src/core/cases-from-log.ts +85 -0
- package/src/core/conformance.test.ts +80 -0
- package/src/core/conformance.ts +91 -0
- package/src/core/diff-graphs.test.ts +151 -0
- package/src/core/diff-graphs.ts +118 -0
- package/src/core/discover-object-centric-graph.test.ts +94 -0
- package/src/core/discover-object-centric-graph.ts +296 -0
- package/src/core/fixtures/ocel-sample.ts +82 -0
- package/src/core/fixtures/sample.xes +68 -0
- package/src/core/index.ts +113 -0
- package/src/core/reference-model.test.ts +43 -0
- package/src/core/reference-model.ts +116 -0
- package/src/core/replay-timeline.test.ts +161 -0
- package/src/core/replay-timeline.ts +260 -0
- package/src/core/segments.test.ts +185 -0
- package/src/core/segments.ts +153 -0
- package/src/core/token-replay.test.ts +218 -0
- package/src/core/token-replay.ts +456 -0
- package/src/core/types.ts +2 -2
- package/src/dotted-chart/compute-dots.test.ts +176 -0
- package/src/dotted-chart/compute-dots.ts +241 -0
- package/src/dotted-chart/dotted-chart-labels.ts +93 -0
- package/src/dotted-chart/dotted-chart.stories.tsx +182 -0
- package/src/dotted-chart/dotted-chart.test.tsx +135 -0
- package/src/dotted-chart/dotted-chart.tsx +841 -0
- package/src/dotted-chart/index.ts +23 -0
- package/src/dotted-chart/use-element-size.ts +33 -0
- package/src/happy-path-editor/happy-path-editor-context.ts +81 -0
- package/src/happy-path-editor/happy-path-editor.stories.tsx +116 -0
- package/src/happy-path-editor/happy-path-editor.test.tsx +142 -0
- package/src/happy-path-editor/happy-path-editor.tsx +239 -0
- package/src/happy-path-editor/happy-path-step-node.tsx +175 -0
- package/src/happy-path-editor/index.ts +4 -0
- package/src/index.ts +51 -1
- package/src/performance-spectrum/aggregate-segments.test.ts +107 -0
- package/src/performance-spectrum/aggregate-segments.ts +174 -0
- package/src/performance-spectrum/index.ts +25 -0
- package/src/performance-spectrum/performance-spectrum-context.tsx +116 -0
- package/src/performance-spectrum/performance-spectrum.stories.tsx +128 -0
- package/src/performance-spectrum/performance-spectrum.test.tsx +190 -0
- package/src/performance-spectrum/performance-spectrum.tsx +870 -0
- package/src/process-compare/compare-kpi-strip.stories.tsx +48 -0
- package/src/process-compare/compare-kpi-strip.tsx +94 -0
- package/src/process-compare/compare-model.ts +83 -0
- package/src/process-compare/compare-side.tsx +42 -0
- package/src/process-compare/diff-to-graph.ts +104 -0
- package/src/process-compare/index.ts +23 -0
- package/src/process-compare/process-compare.stories.tsx +184 -0
- package/src/process-compare/process-compare.test.tsx +224 -0
- package/src/process-compare/process-compare.tsx +251 -0
- package/src/process-explorer.stories.tsx +1 -1
- package/src/process-filter-bar/index.ts +2 -0
- package/src/process-filter-bar/process-filter-bar.stories.tsx +156 -0
- package/src/process-filter-bar/process-filter-bar.test.tsx +201 -0
- package/src/process-filter-bar/process-filter-bar.tsx +167 -0
- package/src/process-kpi-strip/process-kpi-strip.stories.tsx +47 -0
- package/src/process-kpi-strip/process-kpi-strip.test.tsx +67 -0
- package/src/process-kpi-strip/process-kpi-strip.tsx +148 -8
- package/src/process-map/activity-accent.ts +25 -0
- package/src/process-map/index.ts +1 -0
- package/src/process-map/map-model.test.ts +16 -0
- package/src/process-map/map-model.ts +323 -1
- package/src/process-map/object-centric-map.test.tsx +132 -0
- package/src/process-map/process-activity-node.tsx +152 -14
- package/src/process-map/process-map-object-centric.stories.tsx +219 -0
- package/src/process-map/process-map.stories.tsx +64 -0
- package/src/process-map/process-map.tsx +240 -16
- package/src/process-map/process-transition-edge.test.tsx +47 -0
- package/src/process-map/process-transition-edge.tsx +134 -7
- package/src/process-map/use-process-layout.ts +30 -9
- package/src/process-replay/congestion-heat.tsx +107 -0
- package/src/process-replay/index.ts +14 -0
- package/src/process-replay/process-replay.stories.tsx +168 -0
- package/src/process-replay/process-replay.test.tsx +170 -0
- package/src/process-replay/process-replay.tsx +285 -0
- package/src/process-replay/replay-controls.tsx +147 -0
- package/src/process-replay/replay-format.ts +83 -0
- package/src/process-replay/replay-tokens-context.ts +30 -0
- package/src/process-replay/use-controllable-value.ts +30 -0
- package/src/templates-process-explorer.stories.tsx +1304 -0
- package/src/test/contract.test.ts +66 -0
- package/src/test/contract.ts +107 -6
- package/src/test/doubles.test.tsx +87 -1
- package/src/test/doubles.tsx +174 -3
- package/src/test/index.ts +25 -1
- package/src/use-process-explorer/use-process-explorer.test.ts +44 -0
- package/src/use-process-explorer/use-process-explorer.ts +34 -2
- package/src/variant-explorer/coverage-bar.tsx +36 -0
- package/src/variant-explorer/index.ts +16 -0
- package/src/variant-explorer/sequence-chips.tsx +103 -0
- package/src/variant-explorer/variant-explorer-model.ts +42 -0
- package/src/variant-explorer/variant-explorer.stories.tsx +226 -0
- package/src/variant-explorer/variant-explorer.test.tsx +302 -0
- package/src/variant-explorer/variant-explorer.tsx +567 -0
- package/src/variant-explorer/variant-row.tsx +137 -0
- package/src/violation-list/index.ts +2 -0
- package/src/violation-list/violation-list.stories.tsx +73 -0
- package/src/violation-list/violation-list.test.tsx +84 -0
- package/src/violation-list/violation-list.tsx +259 -0
|
@@ -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";
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { liftHappyPath } from "./reference-model";
|
|
4
|
+
|
|
5
|
+
describe("liftHappyPath", () => {
|
|
6
|
+
it("lifts a plain sequence to one place between consecutive steps", () => {
|
|
7
|
+
const model = liftHappyPath({
|
|
8
|
+
id: "p",
|
|
9
|
+
label: "Plain",
|
|
10
|
+
steps: [{ activity: "A" }, { activity: "B" }],
|
|
11
|
+
});
|
|
12
|
+
expect(model.places).toEqual(["p0", "p1", "p2"]);
|
|
13
|
+
expect(model.initialMarking).toEqual(["p0"]);
|
|
14
|
+
expect(model.finalMarking).toEqual(["p2"]);
|
|
15
|
+
expect(model.transitions).toEqual([
|
|
16
|
+
{ id: "t0", activity: "A", kind: "step", consumes: ["p0"], produces: ["p1"] },
|
|
17
|
+
{ id: "t1", activity: "B", kind: "step", consumes: ["p1"], produces: ["p2"] },
|
|
18
|
+
]);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("adds a silent skip arc for an optional step and a self-loop for a repeatable one", () => {
|
|
22
|
+
const model = liftHappyPath({
|
|
23
|
+
id: "p",
|
|
24
|
+
label: "Flags",
|
|
25
|
+
steps: [{ activity: "A", optional: true, repeatable: true }],
|
|
26
|
+
});
|
|
27
|
+
expect(model.transitions).toEqual([
|
|
28
|
+
{ id: "t0", activity: "A", kind: "step", consumes: ["p0"], produces: ["p1"] },
|
|
29
|
+
{ id: "skip0", activity: "A", kind: "skip", consumes: ["p0"], produces: ["p1"] },
|
|
30
|
+
{ id: "repeat0", activity: "A", kind: "repeat", consumes: ["p1"], produces: ["p1"] },
|
|
31
|
+
]);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("lifts an empty path to a single place that is both initial and final", () => {
|
|
35
|
+
const model = liftHappyPath({ id: "e", label: "Empty", steps: [] });
|
|
36
|
+
expect(model).toEqual({
|
|
37
|
+
places: ["p0"],
|
|
38
|
+
initialMarking: ["p0"],
|
|
39
|
+
finalMarking: ["p0"],
|
|
40
|
+
transitions: [],
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
});
|