@cohesivesystems/processes-presentation 0.1.0-alpha.63 → 0.1.0-alpha.66

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.
@@ -0,0 +1,896 @@
1
+ import {
2
+ type ExecutionDefinitionReference,
3
+ type ExecutionStatus,
4
+ type ExecutionStatusDisclosure,
5
+ type ExecutionTokenStatus,
6
+ type ExecutionWaitStatus,
7
+ type NormalizedExecutionTrace,
8
+ type NormalizedExecutionTraceEvent,
9
+ type ProcessExecutionTraceArtifact,
10
+ } from '@cohesivesystems/processes'
11
+ import {
12
+ type ProcessPresentationGraph,
13
+ type ProcessPresentationNode,
14
+ } from './process-presentation-graph'
15
+ import {
16
+ cloneWireValue,
17
+ compareOrdinal,
18
+ deepFreeze,
19
+ isRecord,
20
+ type DeepReadonly,
21
+ } from './process-presentation-values'
22
+
23
+ export const canonicalProcessRuntimePresentationCompatibility = Object.freeze({
24
+ processPresentationProjectionVersions: Object.freeze([
25
+ 'cohesive-process-presentation/v1',
26
+ ] as const),
27
+ statusSchemaVersions: Object.freeze([
28
+ 'cohesive-execution-status/v1',
29
+ ] as const),
30
+ traceArtifactSchemaVersions: Object.freeze([
31
+ 'cohesive-process-execution-traces/v2',
32
+ ] as const),
33
+ traceSchemaVersions: Object.freeze([
34
+ 'cohesive-execution-trace/v2',
35
+ ] as const),
36
+ projectionVersion: 'cohesive-process-runtime-presentation/v1',
37
+ })
38
+
39
+ export type ProcessRuntimePresentationDiagnosticSeverity = 'error' | 'warning' | 'info'
40
+
41
+ export type ProcessRuntimePresentationDiagnosticCode =
42
+ | 'PROCESS_RUNTIME_DEFINITION_MISMATCH'
43
+ | 'PROCESS_RUNTIME_GRAPH_PROJECTION_UNSUPPORTED'
44
+ | 'PROCESS_RUNTIME_HEALTH_UNKNOWN'
45
+ | 'PROCESS_RUNTIME_INSTANCE_MISMATCH'
46
+ | 'PROCESS_RUNTIME_OCCURRENCE_DISCLOSURE_GAP'
47
+ | 'PROCESS_RUNTIME_STATUS_FACET_REDACTED'
48
+ | 'PROCESS_RUNTIME_STATUS_FACET_UNKNOWN'
49
+ | 'PROCESS_RUNTIME_STATUS_SCHEMA_UNSUPPORTED'
50
+ | 'PROCESS_RUNTIME_TRACE_ARTIFACT_SCHEMA_UNSUPPORTED'
51
+ | 'PROCESS_RUNTIME_TRACE_ATTEMPT_UNMATCHED'
52
+ | 'PROCESS_RUNTIME_TRACE_BRANCH_UNMATCHED'
53
+ | 'PROCESS_RUNTIME_TRACE_DEFINITION_REFERENCE_UNMATCHED'
54
+ | 'PROCESS_RUNTIME_TRACE_KIND_UNSUPPORTED'
55
+ | 'PROCESS_RUNTIME_TRACE_NODE_UNMATCHED'
56
+ | 'PROCESS_RUNTIME_TRACE_OUTCOME_UNMATCHED'
57
+ | 'PROCESS_RUNTIME_TRACE_PREFIX_MISSING'
58
+ | 'PROCESS_RUNTIME_TRACE_SCHEMA_UNSUPPORTED'
59
+ | 'PROCESS_RUNTIME_TRACE_UNAVAILABLE'
60
+ | 'PROCESS_RUNTIME_WAIT_NODE_UNMATCHED'
61
+ | 'PROCESS_RUNTIME_TOKEN_NODE_UNMATCHED'
62
+
63
+ export interface ProcessRuntimePresentationDiagnostic {
64
+ readonly code: ProcessRuntimePresentationDiagnosticCode
65
+ readonly evidenceId: string | null
66
+ readonly id: string
67
+ readonly message: string
68
+ readonly path: readonly string[]
69
+ readonly severity: ProcessRuntimePresentationDiagnosticSeverity
70
+ readonly subject: string | null
71
+ }
72
+
73
+ export interface ProcessRuntimeTokenOverlay {
74
+ readonly id: string
75
+ readonly token: DeepReadonly<ExecutionTokenStatus>
76
+ }
77
+
78
+ export interface ProcessRuntimeWaitOverlay {
79
+ readonly id: string
80
+ readonly wait: DeepReadonly<ExecutionWaitStatus>
81
+ }
82
+
83
+ export interface ProcessRuntimeTraceEventOverlay {
84
+ readonly activationId: string
85
+ readonly attemptId: string
86
+ readonly branchOrClauseElementId: string | null
87
+ readonly elementIds: readonly string[]
88
+ readonly event: DeepReadonly<NormalizedExecutionTraceEvent>
89
+ readonly id: string
90
+ readonly occurrenceDefinitionElementId: string | null
91
+ readonly primaryElementId: string | null
92
+ readonly relatedDefinitionElementId: string | null
93
+ readonly relatedNodeElementId: string | null
94
+ readonly requestOutcomeElementId: string | null
95
+ readonly traceDisposition: string
96
+ }
97
+
98
+ export interface ProcessRuntimeElementOverlay {
99
+ readonly elementId: string
100
+ readonly id: string
101
+ readonly traceEvents: readonly ProcessRuntimeTraceEventOverlay[]
102
+ readonly tokens: readonly ProcessRuntimeTokenOverlay[]
103
+ readonly waits: readonly ProcessRuntimeWaitOverlay[]
104
+ }
105
+
106
+ export type ProcessRuntimeUnmatchedEvidenceKind =
107
+ | 'definition-reference'
108
+ | 'status-token-node'
109
+ | 'status-wait-node'
110
+ | 'trace-branch-or-clause'
111
+ | 'trace-node'
112
+ | 'trace-related-node'
113
+ | 'trace-request-outcome'
114
+
115
+ export type ProcessRuntimeUnmatchedEvidenceSource =
116
+ | ExecutionTokenStatus
117
+ | ExecutionWaitStatus
118
+ | NormalizedExecutionTraceEvent
119
+
120
+ export interface ProcessRuntimeUnmatchedEvidence {
121
+ readonly evidenceId: string
122
+ readonly id: string
123
+ readonly kind: ProcessRuntimeUnmatchedEvidenceKind
124
+ readonly reference: string
125
+ readonly source: DeepReadonly<ProcessRuntimeUnmatchedEvidenceSource>
126
+ }
127
+
128
+ export interface ProcessRuntimePresentationOverlay {
129
+ readonly definition: DeepReadonly<ExecutionDefinitionReference>
130
+ readonly diagnostics: readonly ProcessRuntimePresentationDiagnostic[]
131
+ readonly elementOverlays: readonly ProcessRuntimeElementOverlay[]
132
+ readonly id: string
133
+ readonly missingTracePrefixCount: number
134
+ readonly processInstanceId: string
135
+ readonly projectionVersion: string
136
+ readonly status: DeepReadonly<ExecutionStatus>
137
+ readonly traceArtifact: DeepReadonly<ProcessExecutionTraceArtifact> | null
138
+ readonly unmatchedEvidence: readonly ProcessRuntimeUnmatchedEvidence[]
139
+ }
140
+
141
+ export interface ProcessRuntimePresentationProjectionResult {
142
+ readonly diagnostics: readonly ProcessRuntimePresentationDiagnostic[]
143
+ readonly overlay: ProcessRuntimePresentationOverlay | null
144
+ }
145
+
146
+ interface MutableProcessRuntimeElementOverlay {
147
+ readonly elementId: string
148
+ readonly traceEvents: ProcessRuntimeTraceEventOverlay[]
149
+ readonly tokens: ProcessRuntimeTokenOverlay[]
150
+ readonly waits: ProcessRuntimeWaitOverlay[]
151
+ }
152
+
153
+ interface GraphIndex {
154
+ readonly definitionReferences: readonly ProcessPresentationNode[]
155
+ readonly elements: Map<string, MutableProcessRuntimeElementOverlay>
156
+ readonly nodes: readonly ProcessPresentationNode[]
157
+ readonly processNodesByCanonicalId: Map<string, readonly ProcessPresentationNode[]>
158
+ }
159
+
160
+ interface RuntimeProjectionContext {
161
+ readonly diagnostics: Map<string, ProcessRuntimePresentationDiagnostic>
162
+ readonly graph: ProcessPresentationGraph
163
+ readonly index: GraphIndex
164
+ readonly processInstanceId: string
165
+ readonly unmatchedEvidence: Map<string, ProcessRuntimeUnmatchedEvidence>
166
+ }
167
+
168
+ export function processRuntimePresentationOverlayId(
169
+ processInstanceId: string,
170
+ definition: ExecutionDefinitionReference,
171
+ ): string {
172
+ return runtimeIdentity(
173
+ 'overlay',
174
+ processInstanceId,
175
+ definition.definitionId,
176
+ definition.revisionId,
177
+ definition.fingerprint.algorithm,
178
+ definition.fingerprint.canonicalization,
179
+ definition.fingerprint.value,
180
+ )
181
+ }
182
+
183
+ export function projectCanonicalProcessRuntime(
184
+ graph: ProcessPresentationGraph,
185
+ status: ExecutionStatus,
186
+ traceArtifact: ProcessExecutionTraceArtifact | null = null,
187
+ ): ProcessRuntimePresentationProjectionResult {
188
+ const diagnostics = new Map<string, ProcessRuntimePresentationDiagnostic>()
189
+ validateCompatibility(graph, status, traceArtifact, diagnostics)
190
+ if ([...diagnostics.values()].some((diagnostic) => diagnostic.severity === 'error')) {
191
+ return freezeProjectionResult(null, diagnostics)
192
+ }
193
+
194
+ const index = createGraphIndex(graph)
195
+ const context: RuntimeProjectionContext = {
196
+ diagnostics,
197
+ graph,
198
+ index,
199
+ processInstanceId: status.processInstanceId,
200
+ unmatchedEvidence: new Map(),
201
+ }
202
+
203
+ projectStatus(status, context)
204
+ if (traceArtifact) {
205
+ projectTraceArtifact(traceArtifact, context)
206
+ } else {
207
+ addDiagnostic(
208
+ context.diagnostics,
209
+ 'PROCESS_RUNTIME_TRACE_UNAVAILABLE',
210
+ 'warning',
211
+ ['traceArtifact'],
212
+ 'No retained trace artifact was supplied; event absence cannot imply execution or element completion.',
213
+ null,
214
+ )
215
+ }
216
+
217
+ const frozenDiagnostics = freezeDiagnostics(diagnostics)
218
+ const overlay: ProcessRuntimePresentationOverlay = deepFreeze({
219
+ definition: cloneWireValue(status.definition),
220
+ diagnostics: frozenDiagnostics,
221
+ elementOverlays: [...index.elements.values()]
222
+ .sort((left, right) => compareOrdinal(left.elementId, right.elementId))
223
+ .map((element) => ({
224
+ elementId: element.elementId,
225
+ id: runtimeIdentity('element', status.processInstanceId, element.elementId),
226
+ traceEvents: element.traceEvents.sort(compareEvidenceById),
227
+ tokens: element.tokens.sort(compareEvidenceById),
228
+ waits: element.waits.sort(compareEvidenceById),
229
+ })),
230
+ id: processRuntimePresentationOverlayId(status.processInstanceId, status.definition),
231
+ missingTracePrefixCount: traceArtifact?.missingTracePrefixCount ?? 0,
232
+ processInstanceId: status.processInstanceId,
233
+ projectionVersion: canonicalProcessRuntimePresentationCompatibility.projectionVersion,
234
+ status: cloneWireValue(status),
235
+ traceArtifact: traceArtifact ? cloneWireValue(traceArtifact) : null,
236
+ unmatchedEvidence: [...context.unmatchedEvidence.values()]
237
+ .sort(compareEvidenceById),
238
+ })
239
+
240
+ return deepFreeze({ diagnostics: frozenDiagnostics, overlay })
241
+ }
242
+
243
+ function validateCompatibility(
244
+ graph: ProcessPresentationGraph,
245
+ status: ExecutionStatus,
246
+ traceArtifact: ProcessExecutionTraceArtifact | null,
247
+ diagnostics: Map<string, ProcessRuntimePresentationDiagnostic>,
248
+ ) {
249
+ if (!canonicalProcessRuntimePresentationCompatibility.processPresentationProjectionVersions.includes(
250
+ graph.projectionVersion as (typeof canonicalProcessRuntimePresentationCompatibility.processPresentationProjectionVersions)[number],
251
+ )) {
252
+ addDiagnostic(
253
+ diagnostics,
254
+ 'PROCESS_RUNTIME_GRAPH_PROJECTION_UNSUPPORTED',
255
+ 'error',
256
+ ['graph', 'projectionVersion'],
257
+ `Runtime presentation does not support Process graph projection '${graph.projectionVersion}'.`,
258
+ graph.projectionVersion,
259
+ )
260
+ }
261
+
262
+ if (!canonicalProcessRuntimePresentationCompatibility.statusSchemaVersions.includes(
263
+ status.schemaVersion as (typeof canonicalProcessRuntimePresentationCompatibility.statusSchemaVersions)[number],
264
+ )) {
265
+ addDiagnostic(
266
+ diagnostics,
267
+ 'PROCESS_RUNTIME_STATUS_SCHEMA_UNSUPPORTED',
268
+ 'error',
269
+ ['status', 'schemaVersion'],
270
+ `Runtime presentation does not support execution status schema '${status.schemaVersion}'.`,
271
+ status.schemaVersion,
272
+ )
273
+ }
274
+
275
+ if (!sameDefinitionReference(graph.document.definition, status.definition)) {
276
+ addDiagnostic(
277
+ diagnostics,
278
+ 'PROCESS_RUNTIME_DEFINITION_MISMATCH',
279
+ 'error',
280
+ ['status', 'definition'],
281
+ 'Execution status does not identify the exact Process definition projected by the graph.',
282
+ definitionReferenceSubject(status.definition),
283
+ )
284
+ }
285
+
286
+ if (!traceArtifact) {
287
+ return
288
+ }
289
+
290
+ if (!canonicalProcessRuntimePresentationCompatibility.traceArtifactSchemaVersions.includes(
291
+ traceArtifact.schemaVersion as (typeof canonicalProcessRuntimePresentationCompatibility.traceArtifactSchemaVersions)[number],
292
+ )) {
293
+ addDiagnostic(
294
+ diagnostics,
295
+ 'PROCESS_RUNTIME_TRACE_ARTIFACT_SCHEMA_UNSUPPORTED',
296
+ 'error',
297
+ ['traceArtifact', 'schemaVersion'],
298
+ `Runtime presentation does not support trace artifact schema '${traceArtifact.schemaVersion}'.`,
299
+ traceArtifact.schemaVersion,
300
+ )
301
+ }
302
+
303
+ if (!sameDefinitionReference(status.definition, traceArtifact.definition)) {
304
+ addDiagnostic(
305
+ diagnostics,
306
+ 'PROCESS_RUNTIME_DEFINITION_MISMATCH',
307
+ 'error',
308
+ ['traceArtifact', 'definition'],
309
+ 'Trace artifact and execution status do not identify the same exact Process definition.',
310
+ definitionReferenceSubject(traceArtifact.definition),
311
+ )
312
+ }
313
+
314
+ if (status.processInstanceId !== traceArtifact.processInstanceId) {
315
+ addDiagnostic(
316
+ diagnostics,
317
+ 'PROCESS_RUNTIME_INSTANCE_MISMATCH',
318
+ 'error',
319
+ ['traceArtifact', 'processInstanceId'],
320
+ 'Trace artifact and execution status do not identify the same Process instance.',
321
+ traceArtifact.processInstanceId,
322
+ )
323
+ }
324
+
325
+ const attempts = new Set(status.attempts.map((attempt) => attempt.attemptId))
326
+ traceArtifact.traces.forEach((trace, traceIndex) => {
327
+ if (!canonicalProcessRuntimePresentationCompatibility.traceSchemaVersions.includes(
328
+ trace.schemaVersion as (typeof canonicalProcessRuntimePresentationCompatibility.traceSchemaVersions)[number],
329
+ )) {
330
+ addDiagnostic(
331
+ diagnostics,
332
+ 'PROCESS_RUNTIME_TRACE_SCHEMA_UNSUPPORTED',
333
+ 'error',
334
+ ['traceArtifact', 'traces', String(traceIndex), 'schemaVersion'],
335
+ `Runtime presentation does not support normalized trace schema '${trace.schemaVersion}'.`,
336
+ trace.schemaVersion,
337
+ )
338
+ }
339
+ if (trace.kind !== graph.document.kind) {
340
+ addDiagnostic(
341
+ diagnostics,
342
+ 'PROCESS_RUNTIME_TRACE_KIND_UNSUPPORTED',
343
+ 'error',
344
+ ['traceArtifact', 'traces', String(traceIndex), 'kind'],
345
+ `Normalized trace kind '${trace.kind}' does not match graph kind '${graph.document.kind}'.`,
346
+ trace.kind,
347
+ )
348
+ }
349
+ if (!sameDefinitionReference(status.definition, trace.definition)) {
350
+ addDiagnostic(
351
+ diagnostics,
352
+ 'PROCESS_RUNTIME_DEFINITION_MISMATCH',
353
+ 'error',
354
+ ['traceArtifact', 'traces', String(traceIndex), 'definition'],
355
+ 'Normalized trace does not identify the same exact Process definition as execution status.',
356
+ definitionReferenceSubject(trace.definition),
357
+ )
358
+ }
359
+ if (!trace.continuation
360
+ || trace.continuation.processInstanceId !== status.processInstanceId) {
361
+ addDiagnostic(
362
+ diagnostics,
363
+ 'PROCESS_RUNTIME_INSTANCE_MISMATCH',
364
+ 'error',
365
+ ['traceArtifact', 'traces', String(traceIndex), 'continuation'],
366
+ 'Normalized Process trace does not identify the status Process instance.',
367
+ trace.continuation?.processInstanceId ?? null,
368
+ )
369
+ } else if (!attempts.has(trace.continuation.processAttemptId)) {
370
+ addDiagnostic(
371
+ diagnostics,
372
+ 'PROCESS_RUNTIME_TRACE_ATTEMPT_UNMATCHED',
373
+ 'error',
374
+ ['traceArtifact', 'traces', String(traceIndex), 'continuation', 'processAttemptId'],
375
+ 'Normalized trace attempt is absent from the authoritative execution status lineage.',
376
+ trace.continuation.processAttemptId,
377
+ )
378
+ }
379
+ })
380
+ }
381
+
382
+ function projectStatus(status: ExecutionStatus, context: RuntimeProjectionContext) {
383
+ discloseStatusFacet('tokens', status.runtime.tokensDisclosure, context)
384
+ discloseStatusFacet('waits', status.runtime.waitsDisclosure, context)
385
+ discloseStatusFacet('progress', status.runtime.progressDisclosure, context)
386
+ discloseStatusFacet('demand', status.runtime.demandDisclosure, context)
387
+ discloseStatusFacet('capacity', status.runtime.capacityDisclosure, context)
388
+ if (status.terminalOutcome.detail) {
389
+ discloseStatusValue(
390
+ 'terminalOutcome.detail',
391
+ status.terminalOutcome.detail.disclosure,
392
+ ['status', 'terminalOutcome', 'detail', 'disclosure'],
393
+ context,
394
+ )
395
+ }
396
+ status.runtime.extensions.forEach((extension, extensionIndex) => {
397
+ discloseStatusValue(
398
+ `extension:${extension.id}`,
399
+ extension.value.disclosure,
400
+ ['status', 'runtime', 'extensions', String(extensionIndex), 'value', 'disclosure'],
401
+ context,
402
+ )
403
+ })
404
+
405
+ if (status.runtime.health === 'Unknown') {
406
+ addDiagnostic(
407
+ context.diagnostics,
408
+ 'PROCESS_RUNTIME_HEALTH_UNKNOWN',
409
+ 'warning',
410
+ ['status', 'runtime', 'health'],
411
+ 'The runtime did not disclose an authoritative health observation.',
412
+ 'health',
413
+ )
414
+ }
415
+
416
+ status.runtime.tokens.forEach((token, tokenIndex) => {
417
+ const id = runtimeIdentity('token', status.processInstanceId, token.tokenId)
418
+ const node = findSingleProcessNode(token.node, context.index)
419
+ if (!node) {
420
+ addUnmatchedEvidence(
421
+ context,
422
+ 'status-token-node',
423
+ id,
424
+ token.node,
425
+ token,
426
+ 'PROCESS_RUNTIME_TOKEN_NODE_UNMATCHED',
427
+ ['status', 'runtime', 'tokens', String(tokenIndex), 'node'],
428
+ `Token '${token.tokenId}' references Process node '${token.node}', which is absent or ambiguous in the graph.`,
429
+ )
430
+ return
431
+ }
432
+
433
+ context.index.elements.get(node.id)!.tokens.push(deepFreeze({
434
+ id,
435
+ token: cloneWireValue(token),
436
+ }))
437
+ })
438
+
439
+ status.runtime.waits.forEach((wait, waitIndex) => {
440
+ const id = runtimeIdentity('wait', status.processInstanceId, wait.tokenId, wait.node)
441
+ const node = findSingleProcessNode(wait.node, context.index)
442
+ if (!node) {
443
+ addUnmatchedEvidence(
444
+ context,
445
+ 'status-wait-node',
446
+ id,
447
+ wait.node,
448
+ wait,
449
+ 'PROCESS_RUNTIME_WAIT_NODE_UNMATCHED',
450
+ ['status', 'runtime', 'waits', String(waitIndex), 'node'],
451
+ `Wait for token '${wait.tokenId}' references Process node '${wait.node}', which is absent or ambiguous in the graph.`,
452
+ )
453
+ return
454
+ }
455
+
456
+ context.index.elements.get(node.id)!.waits.push(deepFreeze({
457
+ id,
458
+ wait: cloneWireValue(wait),
459
+ }))
460
+ })
461
+ }
462
+
463
+ function discloseStatusFacet(
464
+ facet: string,
465
+ disclosure: ExecutionStatusDisclosure,
466
+ context: RuntimeProjectionContext,
467
+ ) {
468
+ if (disclosure === 'Disclosed') {
469
+ return
470
+ }
471
+
472
+ const redacted = disclosure === 'Redacted'
473
+ addDiagnostic(
474
+ context.diagnostics,
475
+ redacted
476
+ ? 'PROCESS_RUNTIME_STATUS_FACET_REDACTED'
477
+ : 'PROCESS_RUNTIME_STATUS_FACET_UNKNOWN',
478
+ redacted ? 'info' : 'warning',
479
+ ['status', 'runtime', `${facet}Disclosure`],
480
+ redacted
481
+ ? `Runtime ${facet} evidence exists but was redacted.`
482
+ : `The runtime has no authoritative ${facet} observation.`,
483
+ facet,
484
+ )
485
+ }
486
+
487
+ function discloseStatusValue(
488
+ subject: string,
489
+ disclosure: ExecutionStatusDisclosure,
490
+ path: readonly string[],
491
+ context: RuntimeProjectionContext,
492
+ ) {
493
+ if (disclosure === 'Disclosed') {
494
+ return
495
+ }
496
+
497
+ const redacted = disclosure === 'Redacted'
498
+ addDiagnostic(
499
+ context.diagnostics,
500
+ redacted
501
+ ? 'PROCESS_RUNTIME_STATUS_FACET_REDACTED'
502
+ : 'PROCESS_RUNTIME_STATUS_FACET_UNKNOWN',
503
+ redacted ? 'info' : 'warning',
504
+ path,
505
+ redacted
506
+ ? `Runtime status value '${subject}' exists but was redacted.`
507
+ : `The runtime status value '${subject}' is explicitly unknown.`,
508
+ subject,
509
+ )
510
+ }
511
+
512
+ function projectTraceArtifact(
513
+ artifact: ProcessExecutionTraceArtifact,
514
+ context: RuntimeProjectionContext,
515
+ ) {
516
+ if (artifact.missingTracePrefixCount > 0) {
517
+ addDiagnostic(
518
+ context.diagnostics,
519
+ 'PROCESS_RUNTIME_TRACE_PREFIX_MISSING',
520
+ 'warning',
521
+ ['traceArtifact', 'missingTracePrefixCount'],
522
+ `The retained trace omits ${artifact.missingTracePrefixCount} earlier activation(s); absence cannot imply completion.`,
523
+ String(artifact.missingTracePrefixCount),
524
+ )
525
+ }
526
+
527
+ artifact.traces.forEach((trace, traceIndex) => projectTrace(trace, traceIndex, context))
528
+ }
529
+
530
+ function projectTrace(
531
+ trace: NormalizedExecutionTrace,
532
+ traceIndex: number,
533
+ context: RuntimeProjectionContext,
534
+ ) {
535
+ const attemptId = trace.continuation!.processAttemptId
536
+ trace.events.forEach((event, eventIndex) => {
537
+ const id = runtimeIdentity(
538
+ 'trace-event',
539
+ context.processInstanceId,
540
+ attemptId,
541
+ trace.activation,
542
+ String(event.sequence),
543
+ )
544
+ const elementIds = new Set<string>()
545
+ const primary = findSingleProcessNode(event.node, context.index)
546
+ if (primary) {
547
+ elementIds.add(primary.id)
548
+ } else {
549
+ addUnmatchedEvidence(
550
+ context,
551
+ 'trace-node',
552
+ id,
553
+ event.node,
554
+ event,
555
+ 'PROCESS_RUNTIME_TRACE_NODE_UNMATCHED',
556
+ ['traceArtifact', 'traces', String(traceIndex), 'events', String(eventIndex), 'node'],
557
+ `Trace event references Process node '${event.node}', which is absent or ambiguous in the graph.`,
558
+ )
559
+ }
560
+
561
+ const branchOrClause = event.branchOrClause
562
+ ? findOwnedElement(event.node, event.branchOrClause, context.index)
563
+ : null
564
+ if (event.branchOrClause) {
565
+ if (branchOrClause) {
566
+ elementIds.add(branchOrClause.id)
567
+ } else {
568
+ addUnmatchedEvidence(
569
+ context,
570
+ 'trace-branch-or-clause',
571
+ id,
572
+ event.branchOrClause,
573
+ event,
574
+ 'PROCESS_RUNTIME_TRACE_BRANCH_UNMATCHED',
575
+ ['traceArtifact', 'traces', String(traceIndex), 'events', String(eventIndex), 'branchOrClause'],
576
+ `Trace event branch or clause '${event.branchOrClause}' is absent or ambiguous under Process node '${event.node}'.`,
577
+ )
578
+ }
579
+ }
580
+
581
+ const requestOutcome = event.requestOutcome
582
+ ? findRequestOutcome(event.node, event.requestOutcome, context.index)
583
+ : null
584
+ if (event.requestOutcome) {
585
+ if (requestOutcome) {
586
+ elementIds.add(requestOutcome.id)
587
+ } else if (!primary || hasOwnedRequestOutcomes(event.node, context.index)) {
588
+ addUnmatchedEvidence(
589
+ context,
590
+ 'trace-request-outcome',
591
+ id,
592
+ event.requestOutcome,
593
+ event,
594
+ 'PROCESS_RUNTIME_TRACE_OUTCOME_UNMATCHED',
595
+ ['traceArtifact', 'traces', String(traceIndex), 'events', String(eventIndex), 'requestOutcome'],
596
+ `Trace Request outcome '${event.requestOutcome}' is absent or ambiguous under Process node '${event.node}'.`,
597
+ )
598
+ }
599
+ }
600
+
601
+ const relatedDefinition = matchDefinitionReference(
602
+ event.relatedDefinition ?? null,
603
+ id,
604
+ event,
605
+ ['traceArtifact', 'traces', String(traceIndex), 'events', String(eventIndex), 'relatedDefinition'],
606
+ context,
607
+ )
608
+ if (relatedDefinition) {
609
+ elementIds.add(relatedDefinition.id)
610
+ }
611
+
612
+ let relatedNode: ProcessPresentationNode | null = null
613
+ if (event.relatedNode) {
614
+ if (event.relatedDefinition
615
+ && sameDefinitionReference(event.relatedDefinition, context.graph.document.definition)) {
616
+ relatedNode = findSingleProcessNode(event.relatedNode, context.index)
617
+ if (relatedNode) {
618
+ elementIds.add(relatedNode.id)
619
+ } else {
620
+ addUnmatchedEvidence(
621
+ context,
622
+ 'trace-related-node',
623
+ id,
624
+ event.relatedNode,
625
+ event,
626
+ 'PROCESS_RUNTIME_TRACE_NODE_UNMATCHED',
627
+ ['traceArtifact', 'traces', String(traceIndex), 'events', String(eventIndex), 'relatedNode'],
628
+ `Trace event related node '${event.relatedNode}' claims the current Process definition but is absent or ambiguous.`,
629
+ )
630
+ }
631
+ } else if (!event.relatedDefinition) {
632
+ addUnmatchedEvidence(
633
+ context,
634
+ 'trace-related-node',
635
+ id,
636
+ event.relatedNode,
637
+ event,
638
+ 'PROCESS_RUNTIME_TRACE_NODE_UNMATCHED',
639
+ ['traceArtifact', 'traces', String(traceIndex), 'events', String(eventIndex), 'relatedNode'],
640
+ `Trace event related node '${event.relatedNode}' has no exact related definition for a safe graph join.`,
641
+ )
642
+ }
643
+ }
644
+
645
+ const occurrenceDefinition = matchDefinitionReference(
646
+ event.processOccurrence?.definition ?? null,
647
+ id,
648
+ event,
649
+ ['traceArtifact', 'traces', String(traceIndex), 'events', String(eventIndex), 'processOccurrence', 'definition'],
650
+ context,
651
+ )
652
+ if (occurrenceDefinition) {
653
+ elementIds.add(occurrenceDefinition.id)
654
+ }
655
+
656
+ if (event.processOccurrence && event.processOccurrence.disclosure !== 'Disclosed') {
657
+ const disclosure = event.processOccurrence.disclosure
658
+ addDiagnostic(
659
+ context.diagnostics,
660
+ 'PROCESS_RUNTIME_OCCURRENCE_DISCLOSURE_GAP',
661
+ disclosure === 'Redacted' ? 'info' : 'warning',
662
+ ['traceArtifact', 'traces', String(traceIndex), 'events', String(eventIndex), 'processOccurrence', 'disclosure'],
663
+ `Process occurrence evidence is explicitly ${disclosure.toLowerCase()}; no occurrence completion or lineage may be inferred.`,
664
+ event.processOccurrence.kind,
665
+ id,
666
+ )
667
+ }
668
+
669
+ const traceEvent: ProcessRuntimeTraceEventOverlay = deepFreeze({
670
+ activationId: trace.activation,
671
+ attemptId,
672
+ branchOrClauseElementId: branchOrClause?.id ?? null,
673
+ elementIds: [...elementIds].sort(),
674
+ event: cloneWireValue(event),
675
+ id,
676
+ occurrenceDefinitionElementId: occurrenceDefinition?.id ?? null,
677
+ primaryElementId: primary?.id ?? null,
678
+ relatedDefinitionElementId: relatedDefinition?.id ?? null,
679
+ relatedNodeElementId: relatedNode?.id ?? null,
680
+ requestOutcomeElementId: requestOutcome?.id ?? null,
681
+ traceDisposition: trace.disposition,
682
+ })
683
+
684
+ for (const elementId of traceEvent.elementIds) {
685
+ context.index.elements.get(elementId)!.traceEvents.push(traceEvent)
686
+ }
687
+ })
688
+ }
689
+
690
+ function matchDefinitionReference(
691
+ reference: ExecutionDefinitionReference | null,
692
+ evidenceId: string,
693
+ source: NormalizedExecutionTraceEvent,
694
+ path: readonly string[],
695
+ context: RuntimeProjectionContext,
696
+ ): ProcessPresentationNode | null {
697
+ if (!reference || sameDefinitionReference(reference, context.graph.document.definition)) {
698
+ return null
699
+ }
700
+
701
+ const matches = context.index.definitionReferences.filter((node) =>
702
+ isDefinitionReference(node.details.source)
703
+ && sameDefinitionReference(node.details.source, reference))
704
+ if (matches.length === 1) {
705
+ return matches[0]!
706
+ }
707
+
708
+ const subject = definitionReferenceSubject(reference)
709
+ addUnmatchedEvidence(
710
+ context,
711
+ 'definition-reference',
712
+ evidenceId,
713
+ subject,
714
+ source,
715
+ 'PROCESS_RUNTIME_TRACE_DEFINITION_REFERENCE_UNMATCHED',
716
+ path,
717
+ `Trace lineage definition '${subject}' is absent or ambiguous in the Process presentation graph.`,
718
+ )
719
+ return null
720
+ }
721
+
722
+ function createGraphIndex(graph: ProcessPresentationGraph): GraphIndex {
723
+ const elements = new Map<string, MutableProcessRuntimeElementOverlay>()
724
+ const processNodesByCanonicalId = new Map<string, ProcessPresentationNode[]>()
725
+ const definitionReferences: ProcessPresentationNode[] = []
726
+
727
+ for (const node of graph.nodes) {
728
+ elements.set(node.id, {
729
+ elementId: node.id,
730
+ traceEvents: [],
731
+ tokens: [],
732
+ waits: [],
733
+ })
734
+ if (node.category === 'process-node') {
735
+ const matches = processNodesByCanonicalId.get(node.canonicalId) ?? []
736
+ matches.push(node)
737
+ processNodesByCanonicalId.set(node.canonicalId, matches)
738
+ }
739
+ if (node.category === 'definition-reference') {
740
+ definitionReferences.push(node)
741
+ }
742
+ }
743
+
744
+ return {
745
+ definitionReferences,
746
+ elements,
747
+ nodes: graph.nodes,
748
+ processNodesByCanonicalId,
749
+ }
750
+ }
751
+
752
+ function findSingleProcessNode(
753
+ canonicalId: string,
754
+ index: GraphIndex,
755
+ ): ProcessPresentationNode | null {
756
+ const matches = index.processNodesByCanonicalId.get(canonicalId) ?? []
757
+ return matches.length === 1 ? matches[0]! : null
758
+ }
759
+
760
+ function findOwnedElement(
761
+ ownerCanonicalId: string,
762
+ canonicalId: string,
763
+ index: GraphIndex,
764
+ ): ProcessPresentationNode | null {
765
+ const matches = index.nodes.filter((node) =>
766
+ node.ownerId === ownerCanonicalId
767
+ && node.canonicalId === canonicalId)
768
+ return matches.length === 1 ? matches[0]! : null
769
+ }
770
+
771
+ function findRequestOutcome(
772
+ ownerCanonicalId: string,
773
+ outcome: string,
774
+ index: GraphIndex,
775
+ ): ProcessPresentationNode | null {
776
+ const matches = index.nodes.filter((node) =>
777
+ node.category === 'request-outcome'
778
+ && node.ownerId === ownerCanonicalId
779
+ && node.role === outcome)
780
+ return matches.length === 1 ? matches[0]! : null
781
+ }
782
+
783
+ function hasOwnedRequestOutcomes(
784
+ ownerCanonicalId: string,
785
+ index: GraphIndex,
786
+ ): boolean {
787
+ return index.nodes.some((node) =>
788
+ node.category === 'request-outcome'
789
+ && node.ownerId === ownerCanonicalId)
790
+ }
791
+
792
+ function addUnmatchedEvidence(
793
+ context: RuntimeProjectionContext,
794
+ kind: ProcessRuntimeUnmatchedEvidenceKind,
795
+ evidenceId: string,
796
+ reference: string,
797
+ source: ProcessRuntimeUnmatchedEvidenceSource,
798
+ diagnosticCode: ProcessRuntimePresentationDiagnosticCode,
799
+ path: readonly string[],
800
+ message: string,
801
+ ) {
802
+ const id = runtimeIdentity('unmatched', evidenceId, kind, reference)
803
+ if (!context.unmatchedEvidence.has(id)) {
804
+ context.unmatchedEvidence.set(id, deepFreeze({
805
+ evidenceId,
806
+ id,
807
+ kind,
808
+ reference,
809
+ source: cloneWireValue(source),
810
+ }))
811
+ }
812
+ addDiagnostic(
813
+ context.diagnostics,
814
+ diagnosticCode,
815
+ 'warning',
816
+ path,
817
+ message,
818
+ reference,
819
+ evidenceId,
820
+ )
821
+ }
822
+
823
+ function addDiagnostic(
824
+ diagnostics: Map<string, ProcessRuntimePresentationDiagnostic>,
825
+ code: ProcessRuntimePresentationDiagnosticCode,
826
+ severity: ProcessRuntimePresentationDiagnosticSeverity,
827
+ path: readonly string[],
828
+ message: string,
829
+ subject: string | null,
830
+ evidenceId: string | null = null,
831
+ ) {
832
+ const id = runtimeIdentity('diagnostic', code, evidenceId ?? '', subject ?? '', ...path)
833
+ if (diagnostics.has(id)) {
834
+ return
835
+ }
836
+ diagnostics.set(id, deepFreeze({
837
+ code,
838
+ evidenceId,
839
+ id,
840
+ message,
841
+ path: [...path],
842
+ severity,
843
+ subject,
844
+ }))
845
+ }
846
+
847
+ function freezeProjectionResult(
848
+ overlay: ProcessRuntimePresentationOverlay | null,
849
+ diagnostics: Map<string, ProcessRuntimePresentationDiagnostic>,
850
+ ): ProcessRuntimePresentationProjectionResult {
851
+ return deepFreeze({
852
+ diagnostics: freezeDiagnostics(diagnostics),
853
+ overlay,
854
+ })
855
+ }
856
+
857
+ function freezeDiagnostics(
858
+ diagnostics: Map<string, ProcessRuntimePresentationDiagnostic>,
859
+ ): readonly ProcessRuntimePresentationDiagnostic[] {
860
+ return deepFreeze([...diagnostics.values()].sort(compareEvidenceById))
861
+ }
862
+
863
+ function sameDefinitionReference(
864
+ left: DeepReadonly<ExecutionDefinitionReference>,
865
+ right: DeepReadonly<ExecutionDefinitionReference>,
866
+ ): boolean {
867
+ return left.definitionId === right.definitionId
868
+ && left.revisionId === right.revisionId
869
+ && left.fingerprint.algorithm === right.fingerprint.algorithm
870
+ && left.fingerprint.canonicalization === right.fingerprint.canonicalization
871
+ && left.fingerprint.value === right.fingerprint.value
872
+ }
873
+
874
+ function definitionReferenceSubject(reference: DeepReadonly<ExecutionDefinitionReference>): string {
875
+ return `${reference.definitionId}@${reference.revisionId}`
876
+ + `#${reference.fingerprint.algorithm}:${reference.fingerprint.canonicalization}:${reference.fingerprint.value}`
877
+ }
878
+
879
+ function isDefinitionReference(value: unknown): value is ExecutionDefinitionReference {
880
+ if (!isRecord(value) || !isRecord(value.fingerprint)) {
881
+ return false
882
+ }
883
+ return typeof value.definitionId === 'string'
884
+ && typeof value.revisionId === 'string'
885
+ && typeof value.fingerprint.algorithm === 'string'
886
+ && typeof value.fingerprint.canonicalization === 'string'
887
+ && typeof value.fingerprint.value === 'string'
888
+ }
889
+
890
+ function runtimeIdentity(kind: string, ...parts: readonly string[]): string {
891
+ return `process-runtime/${kind}/${parts.map((part) => encodeURIComponent(part)).join('/')}`
892
+ }
893
+
894
+ function compareEvidenceById<T extends { readonly id: string }>(left: T, right: T): number {
895
+ return compareOrdinal(left.id, right.id)
896
+ }