@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,112 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { OCEL_SAMPLE } from "../fixtures/ocel-sample";
4
+ import { normalizeLog } from "../event-log";
5
+ import { fromOcel, readObjectRefs, type OcelJson } from "./ocel";
6
+
7
+ /** `[caseId, activity]` per row — the hand-verified table in `ocel-sample.ts`. */
8
+ function rowsOf(result: ReturnType<typeof fromOcel>, type: string) {
9
+ if (!result.ok) throw new Error("expected ok: true");
10
+ return result.logs[type]!.events.map((row) => [row.caseId, row.activity]);
11
+ }
12
+
13
+ describe("fromOcel", () => {
14
+ it("projects the 3-type, 4-event fixture into one log per object type", () => {
15
+ const result = fromOcel(OCEL_SAMPLE);
16
+ expect(result.ok).toBe(true);
17
+ if (!result.ok) return;
18
+
19
+ expect(result.objectTypes).toEqual(["order", "item", "package"]);
20
+ expect(result.activities).toEqual(["Place Order", "Pick Item", "Pack", "Ship"]);
21
+
22
+ // type | rows | caseId assignment
23
+ // order | 3 | o1 ×3
24
+ // item | 5 | i1 ×3, i2 ×2
25
+ // package | 2 | p1 ×2
26
+ expect(rowsOf(result, "order")).toEqual([
27
+ ["o1", "Place Order"],
28
+ ["o1", "Pack"],
29
+ ["o1", "Ship"],
30
+ ]);
31
+ expect(rowsOf(result, "item")).toEqual([
32
+ ["i1", "Place Order"],
33
+ ["i2", "Place Order"],
34
+ ["i1", "Pick Item"],
35
+ ["i1", "Pack"],
36
+ ["i2", "Pack"],
37
+ ]);
38
+ expect(rowsOf(result, "package")).toEqual([
39
+ ["p1", "Pack"],
40
+ ["p1", "Ship"],
41
+ ]);
42
+ });
43
+
44
+ it("keeps the event id, the object map and scalar event attributes on every row", () => {
45
+ const result = fromOcel(OCEL_SAMPLE);
46
+ if (!result.ok) throw new Error("expected ok: true");
47
+ const [first] = result.logs.item!.events;
48
+ expect(first!.timestamp).toBe("2026-01-05T09:00:00.000Z");
49
+ expect(first!.attributes).toMatchObject({ channel: "web", __ocelEventId: "e1" });
50
+ expect(readObjectRefs(first!)).toEqual({ order: ["o1"], item: ["i1", "i2"] });
51
+ // The same object under two qualifiers is one reference, not two rows.
52
+ const pack = result.logs.item!.events.filter((row) => row.activity === "Pack");
53
+ expect(pack.map((row) => row.caseId)).toEqual(["i1", "i2"]);
54
+ expect(readObjectRefs({ caseId: "x", activity: "y", timestamp: 0 })).toBeUndefined();
55
+ });
56
+
57
+ it("uses an object's latest attribute values as its case attributes", () => {
58
+ const result = fromOcel(OCEL_SAMPLE);
59
+ if (!result.ok) throw new Error("expected ok: true");
60
+ expect(result.logs.order!.caseAttributes).toEqual({ o1: { value: 95 } });
61
+ expect(result.logs.item!.caseAttributes).toBeUndefined();
62
+ });
63
+
64
+ it("projects only the requested object types, in the requested order", () => {
65
+ const result = fromOcel(OCEL_SAMPLE, { objectTypes: ["package", "order"] });
66
+ if (!result.ok) throw new Error("expected ok: true");
67
+ expect(Object.keys(result.logs)).toEqual(["package", "order"]);
68
+ });
69
+
70
+ it("accepts a JSON string and feeds normalizeLog unchanged", () => {
71
+ const result = fromOcel(JSON.stringify(OCEL_SAMPLE));
72
+ if (!result.ok) throw new Error("expected ok: true");
73
+ expect(normalizeLog(result.logs.order!).cases).toHaveLength(1);
74
+ });
75
+
76
+ it("rejects malformed JSON and non-OCEL documents", () => {
77
+ const malformed = fromOcel("{ nope");
78
+ expect(malformed.ok).toBe(false);
79
+ if (!malformed.ok) expect(malformed.errors[0]!.type).toBe("malformed_json");
80
+
81
+ const invalid = fromOcel(JSON.stringify({ events: [] }));
82
+ expect(invalid.ok).toBe(false);
83
+ if (!invalid.ok) expect(invalid.errors[0]!.type).toBe("invalid_document");
84
+ });
85
+
86
+ it("collects every per-event problem instead of stopping at the first", () => {
87
+ const broken: OcelJson = {
88
+ objectTypes: [{ name: "order" }],
89
+ objects: [
90
+ { id: "o1", type: "order" },
91
+ { id: "x1", type: "ghost" },
92
+ ],
93
+ events: [
94
+ { id: "", type: "A", time: "2026-01-01T00:00:00Z" },
95
+ { id: "e2", type: "", time: "2026-01-01T00:00:00Z" },
96
+ { id: "e3", type: "C", time: "not a date" },
97
+ { id: "e4", type: "D", time: "2026-01-01T00:00:00Z", relationships: [{ objectId: "zz" }] },
98
+ ],
99
+ };
100
+ const result = fromOcel(broken, { objectTypes: ["order", "nope"] });
101
+ expect(result.ok).toBe(false);
102
+ if (result.ok) return;
103
+ expect(result.errors.map((error) => error.type)).toEqual([
104
+ "unknown_object_type",
105
+ "unknown_object_type",
106
+ "missing_event_id",
107
+ "missing_event_type",
108
+ "missing_timestamp",
109
+ "unknown_object",
110
+ ]);
111
+ });
112
+ });
@@ -0,0 +1,359 @@
1
+ /**
2
+ * OCEL 2.0 adapter — RM-066.
3
+ *
4
+ * The Object-Centric Event Log standard (OCEL 2.0, ocel-standard.org) drops the single
5
+ * case notion: an event references any number of OBJECTS of any number of object TYPES
6
+ * (`order`, `item`, `package`, …). This adapter reads the standard's JSON serialization
7
+ * and FLATTENS it once per object type — the projection every object-centric
8
+ * directly-follows graph starts from: for object type `T`, every object of type `T` is a
9
+ * case, and every event that references that object is one row of that case.
10
+ *
11
+ * ## JSON only, on purpose
12
+ *
13
+ * OCEL 2.0 also ships as XML and SQLite. Only JSON is read here: it needs nothing beyond
14
+ * `JSON.parse`, which runs identically in Node, a worker and a browser main thread, and
15
+ * `/core` takes no parser dependency (the same format-choice precedent `fromXes` set in
16
+ * RM-063). SQLite would need a WASM engine; XML would need a second hand-rolled tokenizer
17
+ * for a format whose producers all also emit JSON. Convert an XML/SQLite log to JSON with
18
+ * the producing tool first.
19
+ *
20
+ * ## What survives the flattening
21
+ *
22
+ * Each emitted {@link EventRow} carries two reserved attributes so the object-centric
23
+ * discovery step can re-merge the projections:
24
+ *
25
+ * - `__ocelEventId` — the OCEL event's own id, so a shared activity is counted once per
26
+ * EVENT rather than once per referenced object;
27
+ * - `__objectRefs` — the event's full object map (`{ order: ["o1"], item: ["i1", "i2"] }`),
28
+ * JSON-encoded because `EventRow.attributes` values are scalars by contract (RM-049's
29
+ * frozen types). Read it back with {@link readObjectRefs}.
30
+ */
31
+ import type { EventLog, EventRow } from "../types";
32
+
33
+ /** One `{ name, … }` type declaration (`objectTypes` / `eventTypes`). */
34
+ export interface OcelTypeDeclaration {
35
+ name: string;
36
+ attributes?: { name: string; type?: string }[];
37
+ }
38
+
39
+ /** An event or object attribute value. Object attributes carry a `time` (they can change). */
40
+ export interface OcelAttribute {
41
+ name: string;
42
+ value: string | number | boolean | null;
43
+ time?: string;
44
+ }
45
+
46
+ /** A qualified reference from an event (or object) to an object. */
47
+ export interface OcelRelationship {
48
+ objectId: string;
49
+ qualifier?: string;
50
+ }
51
+
52
+ /** One OCEL 2.0 event. `type` is the activity; `time` is an ISO-8601 timestamp. */
53
+ export interface OcelEvent {
54
+ id: string;
55
+ type: string;
56
+ time: string;
57
+ attributes?: OcelAttribute[];
58
+ relationships?: OcelRelationship[];
59
+ }
60
+
61
+ /** One OCEL 2.0 object. */
62
+ export interface OcelObject {
63
+ id: string;
64
+ type: string;
65
+ attributes?: OcelAttribute[];
66
+ relationships?: OcelRelationship[];
67
+ }
68
+
69
+ /** An OCEL 2.0 JSON document (OCEL 2.0 specification, JSON serialization). */
70
+ export interface OcelJson {
71
+ objectTypes: OcelTypeDeclaration[];
72
+ eventTypes?: OcelTypeDeclaration[];
73
+ objects: OcelObject[];
74
+ events: OcelEvent[];
75
+ }
76
+
77
+ /** Options for {@link fromOcel}. */
78
+ export interface OcelParseOptions {
79
+ /**
80
+ * Which object types to project a log for, in output order. Defaults to every declared
81
+ * object type, in declaration order. Naming an undeclared type is an error.
82
+ */
83
+ objectTypes?: string[];
84
+ }
85
+
86
+ /** One problem found while reading an OCEL 2.0 document. */
87
+ export interface OcelParseError {
88
+ type:
89
+ | "malformed_json"
90
+ | "invalid_document"
91
+ | "missing_event_id"
92
+ | "missing_event_type"
93
+ | "missing_timestamp"
94
+ | "unknown_object"
95
+ | "unknown_object_type";
96
+ message: string;
97
+ eventIndex?: number;
98
+ objectIndex?: number;
99
+ }
100
+
101
+ /**
102
+ * `fromOcel`'s result — the same `{ ok: true, … } | { ok: false, errors }` shape as
103
+ * `fromXes`. `logs` is keyed by object type; `activities` lists every event type that
104
+ * occurs, in first-appearance order; `objectTypes` is the projected types, in order.
105
+ */
106
+ export type OcelParseResult =
107
+ | { ok: true; logs: Record<string, EventLog>; activities: string[]; objectTypes: string[] }
108
+ | { ok: false; errors: OcelParseError[] };
109
+
110
+ /** The reserved attribute holding an emitted row's OCEL event id. */
111
+ export const OCEL_EVENT_ID_ATTRIBUTE = "__ocelEventId";
112
+ /** The reserved attribute holding an emitted row's JSON-encoded object map. */
113
+ export const OCEL_OBJECT_REFS_ATTRIBUTE = "__objectRefs";
114
+
115
+ function isRecord(value: unknown): value is Record<string, unknown> {
116
+ return typeof value === "object" && value !== null && !Array.isArray(value);
117
+ }
118
+
119
+ function isScalar(value: unknown): value is string | number | boolean | null {
120
+ return (
121
+ value === null ||
122
+ typeof value === "string" ||
123
+ typeof value === "number" ||
124
+ typeof value === "boolean"
125
+ );
126
+ }
127
+
128
+ function nonEmptyString(value: unknown): value is string {
129
+ return typeof value === "string" && value !== "";
130
+ }
131
+
132
+ /**
133
+ * An object's attributes as a flat record: for a time-varying attribute the value with the
134
+ * LATEST `time` wins (document order breaks ties), which is the object's final state.
135
+ */
136
+ function objectAttributes(object: Record<string, unknown>): Record<string, unknown> | undefined {
137
+ if (!Array.isArray(object.attributes)) return undefined;
138
+ const latest = new Map<string, { time: number; value: unknown }>();
139
+ for (const raw of object.attributes) {
140
+ if (!isRecord(raw) || !nonEmptyString(raw.name) || !isScalar(raw.value)) continue;
141
+ const parsed = typeof raw.time === "string" ? Date.parse(raw.time) : Number.NEGATIVE_INFINITY;
142
+ const time = Number.isNaN(parsed) ? Number.NEGATIVE_INFINITY : parsed;
143
+ const previous = latest.get(raw.name);
144
+ if (previous === undefined || time >= previous.time) {
145
+ latest.set(raw.name, { time, value: raw.value });
146
+ }
147
+ }
148
+ if (latest.size === 0) return undefined;
149
+ const out: Record<string, unknown> = {};
150
+ for (const [name, entry] of latest) out[name] = entry.value;
151
+ return out;
152
+ }
153
+
154
+ /**
155
+ * Read an OCEL 2.0 JSON document (a string, or an already-parsed object) into one
156
+ * {@link EventLog} per object type.
157
+ *
158
+ * Collects every per-event problem (a missing id, type or parsable `time`; a relationship
159
+ * to an object the document never declares) rather than stopping at the first; only a
160
+ * document that is not JSON, or not shaped like OCEL at all, short-circuits. Any error
161
+ * fails the whole read — a silently-dropped event would change every count downstream.
162
+ */
163
+ export function fromOcel(input: string | OcelJson, options?: OcelParseOptions): OcelParseResult {
164
+ let document: unknown = input;
165
+ if (typeof input === "string") {
166
+ try {
167
+ document = JSON.parse(input);
168
+ } catch (error) {
169
+ const message = error instanceof Error ? error.message : String(error);
170
+ return { ok: false, errors: [{ type: "malformed_json", message }] };
171
+ }
172
+ }
173
+
174
+ if (
175
+ !isRecord(document) ||
176
+ !Array.isArray(document.objectTypes) ||
177
+ !Array.isArray(document.objects) ||
178
+ !Array.isArray(document.events)
179
+ ) {
180
+ return {
181
+ ok: false,
182
+ errors: [
183
+ {
184
+ type: "invalid_document",
185
+ message: "expected an object with objectTypes, objects and events arrays",
186
+ },
187
+ ],
188
+ };
189
+ }
190
+
191
+ const errors: OcelParseError[] = [];
192
+
193
+ const declaredTypes: string[] = [];
194
+ for (const declaration of document.objectTypes) {
195
+ if (isRecord(declaration) && nonEmptyString(declaration.name)) {
196
+ if (!declaredTypes.includes(declaration.name)) declaredTypes.push(declaration.name);
197
+ }
198
+ }
199
+
200
+ const projected = options?.objectTypes ?? declaredTypes;
201
+ for (const type of projected) {
202
+ if (!declaredTypes.includes(type)) {
203
+ errors.push({
204
+ type: "unknown_object_type",
205
+ message: `object type "${type}" is not declared in objectTypes`,
206
+ });
207
+ }
208
+ }
209
+ const projectedSet = new Set(projected);
210
+
211
+ const typeOfObject = new Map<string, string>();
212
+ const attributesOfObject = new Map<string, Record<string, unknown>>();
213
+ document.objects.forEach((object, objectIndex) => {
214
+ if (!isRecord(object) || !nonEmptyString(object.id) || !nonEmptyString(object.type)) {
215
+ errors.push({
216
+ type: "invalid_document",
217
+ message: "object is missing its id or type",
218
+ objectIndex,
219
+ });
220
+ return;
221
+ }
222
+ if (!declaredTypes.includes(object.type)) {
223
+ errors.push({
224
+ type: "unknown_object_type",
225
+ message: `object "${object.id}" has undeclared type "${object.type}"`,
226
+ objectIndex,
227
+ });
228
+ return;
229
+ }
230
+ typeOfObject.set(object.id, object.type);
231
+ const attributes = objectAttributes(object);
232
+ if (attributes !== undefined) attributesOfObject.set(object.id, attributes);
233
+ });
234
+
235
+ const rowsByType = new Map<string, EventRow[]>(projected.map((type) => [type, []]));
236
+ const activities: string[] = [];
237
+ const seenActivities = new Set<string>();
238
+
239
+ document.events.forEach((event, eventIndex) => {
240
+ if (!isRecord(event)) {
241
+ errors.push({ type: "invalid_document", message: "event is not an object", eventIndex });
242
+ return;
243
+ }
244
+ if (!nonEmptyString(event.id)) {
245
+ errors.push({ type: "missing_event_id", message: "event is missing its id", eventIndex });
246
+ return;
247
+ }
248
+ if (!nonEmptyString(event.type)) {
249
+ errors.push({
250
+ type: "missing_event_type",
251
+ message: `event "${event.id}" is missing its type`,
252
+ eventIndex,
253
+ });
254
+ return;
255
+ }
256
+ if (!nonEmptyString(event.time) || Number.isNaN(Date.parse(event.time))) {
257
+ errors.push({
258
+ type: "missing_timestamp",
259
+ message: `event "${event.id}" has no parsable time`,
260
+ eventIndex,
261
+ });
262
+ return;
263
+ }
264
+
265
+ // Group this event's references by object type, deduplicated (the same object can be
266
+ // referenced twice under two qualifiers) and in document order.
267
+ const refs: Record<string, string[]> = {};
268
+ let broken = false;
269
+ const relationships = Array.isArray(event.relationships) ? event.relationships : [];
270
+ for (const relationship of relationships) {
271
+ if (!isRecord(relationship) || !nonEmptyString(relationship.objectId)) continue;
272
+ const type = typeOfObject.get(relationship.objectId);
273
+ if (type === undefined) {
274
+ errors.push({
275
+ type: "unknown_object",
276
+ message: `event "${event.id}" references undeclared object "${relationship.objectId}"`,
277
+ eventIndex,
278
+ });
279
+ broken = true;
280
+ continue;
281
+ }
282
+ const ids = (refs[type] ??= []);
283
+ if (!ids.includes(relationship.objectId)) ids.push(relationship.objectId);
284
+ }
285
+ if (broken) return;
286
+
287
+ if (!seenActivities.has(event.type)) {
288
+ seenActivities.add(event.type);
289
+ activities.push(event.type);
290
+ }
291
+
292
+ let attributes: Record<string, string | number | boolean | null> | undefined;
293
+ if (Array.isArray(event.attributes)) {
294
+ for (const attribute of event.attributes) {
295
+ if (!isRecord(attribute) || !nonEmptyString(attribute.name)) continue;
296
+ if (!isScalar(attribute.value)) continue;
297
+ attributes ??= {};
298
+ attributes[attribute.name] = attribute.value;
299
+ }
300
+ }
301
+ const encodedRefs = JSON.stringify(refs);
302
+
303
+ for (const [type, objectIds] of Object.entries(refs)) {
304
+ if (!projectedSet.has(type)) continue;
305
+ const rows = rowsByType.get(type) as EventRow[];
306
+ for (const objectId of objectIds) {
307
+ rows.push({
308
+ caseId: objectId,
309
+ activity: event.type,
310
+ timestamp: event.time,
311
+ attributes: {
312
+ ...attributes,
313
+ [OCEL_EVENT_ID_ATTRIBUTE]: event.id,
314
+ [OCEL_OBJECT_REFS_ATTRIBUTE]: encodedRefs,
315
+ },
316
+ });
317
+ }
318
+ }
319
+ });
320
+
321
+ if (errors.length > 0) return { ok: false, errors };
322
+
323
+ const logs: Record<string, EventLog> = {};
324
+ for (const type of projected) {
325
+ const events = rowsByType.get(type) as EventRow[];
326
+ const log: EventLog = { events };
327
+ let caseAttributes: Record<string, Record<string, unknown>> | undefined;
328
+ for (const row of events) {
329
+ const attributes = attributesOfObject.get(row.caseId);
330
+ if (attributes === undefined || caseAttributes?.[row.caseId] !== undefined) continue;
331
+ caseAttributes ??= {};
332
+ caseAttributes[row.caseId] = attributes;
333
+ }
334
+ if (caseAttributes !== undefined) log.caseAttributes = caseAttributes;
335
+ logs[type] = log;
336
+ }
337
+
338
+ return { ok: true, logs, activities, objectTypes: [...projected] };
339
+ }
340
+
341
+ /**
342
+ * The object map an emitted row was flattened from (`{ order: ["o1"], item: ["i1"] }`), or
343
+ * `undefined` for a row that did not come from {@link fromOcel}.
344
+ */
345
+ export function readObjectRefs(row: EventRow): Record<string, string[]> | undefined {
346
+ const raw = row.attributes?.[OCEL_OBJECT_REFS_ATTRIBUTE];
347
+ if (typeof raw !== "string") return undefined;
348
+ try {
349
+ const parsed: unknown = JSON.parse(raw);
350
+ if (!isRecord(parsed)) return undefined;
351
+ const out: Record<string, string[]> = {};
352
+ for (const [type, ids] of Object.entries(parsed)) {
353
+ if (Array.isArray(ids)) out[type] = ids.filter((id): id is string => typeof id === "string");
354
+ }
355
+ return out;
356
+ } catch {
357
+ return undefined;
358
+ }
359
+ }