@cohesivesystems/processes-presentation 0.1.0-alpha.129

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,1517 @@
1
+ import {
2
+ canonicalProcessAwaitClauseKinds,
3
+ canonicalProcessNodeKinds,
4
+ type ExecutionDefinitionDocument,
5
+ type ExecutionDefinitionFingerprint,
6
+ type ExecutionDefinitionMetadata,
7
+ type ExecutionDefinitionReference,
8
+ type ExecutionSourceProvenance,
9
+ type ProcessAwaitClause,
10
+ type ProcessChoiceCase,
11
+ type ProcessDefinition,
12
+ type ProcessEdge,
13
+ type ProcessFallback,
14
+ type ProcessForkBranch,
15
+ type ProcessJoinBranchResult,
16
+ type ProcessMatchCase,
17
+ type ProcessNode,
18
+ type ProcessRequestOutcomeBranch,
19
+ type ValueContract,
20
+ } from '@cohesivesystems/processes'
21
+ import {
22
+ cloneWireValue,
23
+ deepFreeze,
24
+ isRecord,
25
+ type DeepReadonly,
26
+ } from './process-presentation-values'
27
+
28
+ export type { DeepReadonly } from './process-presentation-values'
29
+
30
+ export const canonicalProcessPresentationCompatibility = Object.freeze({
31
+ definitionKind: 'process',
32
+ projectionVersion: 'cohesive-process-presentation/v1',
33
+ schemaVersions: Object.freeze(['cohesive-execution/v4'] as const),
34
+ })
35
+
36
+ export type CanonicalProcessNodeKind = (typeof canonicalProcessNodeKinds)[number]
37
+ export type CanonicalProcessAwaitClauseKind = (typeof canonicalProcessAwaitClauseKinds)[number]
38
+
39
+ export type ProcessPresentationElementCategory =
40
+ | 'await-clause'
41
+ | 'choice-case'
42
+ | 'definition-reference'
43
+ | 'fallback'
44
+ | 'fork-branch'
45
+ | 'join-result'
46
+ | 'match-case'
47
+ | 'process-node'
48
+ | 'request-outcome'
49
+ | 'unresolved-node'
50
+
51
+ export type ProcessPresentationEdgeKind =
52
+ | 'contains'
53
+ | 'control'
54
+ | 'definition-reference'
55
+ | 'join-result'
56
+ | 'reciprocal'
57
+
58
+ export type ProcessPresentationDiagnosticSeverity = 'error' | 'warning'
59
+
60
+ export type ProcessPresentationDiagnosticCode =
61
+ | 'PROCESS_CONSTRUCT_DISPOSITION_MISSING'
62
+ | 'PROCESS_CONSTRUCT_DISPOSITION_STALE'
63
+ | 'PROCESS_DEFINITION_INVALID'
64
+ | 'PROCESS_DEFINITION_KIND_UNSUPPORTED'
65
+ | 'PROCESS_DEFINITION_REFERENCE_INVALID'
66
+ | 'PROCESS_DEFINITION_SCHEMA_UNSUPPORTED'
67
+ | 'PROCESS_EDGE_INVALID'
68
+ | 'PROCESS_EDGE_TARGET_UNRESOLVED'
69
+ | 'PROCESS_ELEMENT_ID_DUPLICATED'
70
+ | 'PROCESS_ELEMENT_ID_REQUIRED'
71
+ | 'PROCESS_ENTRY_UNRESOLVED'
72
+ | 'PROCESS_NODE_KIND_REQUIRED'
73
+ | 'PROCESS_NODE_KIND_UNSUPPORTED'
74
+ | 'PROCESS_NODE_TABLE_INVALID'
75
+
76
+ export interface ProcessPresentationDiagnostic {
77
+ readonly code: ProcessPresentationDiagnosticCode
78
+ readonly id: string
79
+ readonly message: string
80
+ readonly path: readonly string[]
81
+ readonly severity: ProcessPresentationDiagnosticSeverity
82
+ readonly subject: string | null
83
+ }
84
+
85
+ export interface ProcessPresentationDefinitionEvidence {
86
+ readonly reference: DeepReadonly<ExecutionDefinitionReference>
87
+ readonly role: string
88
+ }
89
+
90
+ export interface ProcessPresentationValueContractEvidence {
91
+ readonly contract: DeepReadonly<Omit<ValueContract, 'type'> & { type: ValueContract['type'] | number }>
92
+ readonly role: string
93
+ }
94
+
95
+ export type ProcessPresentationSource =
96
+ | ProcessAwaitClause
97
+ | ProcessChoiceCase
98
+ | ProcessDefinition
99
+ | ProcessEdge
100
+ | ProcessFallback
101
+ | ProcessForkBranch
102
+ | ProcessJoinBranchResult
103
+ | ProcessMatchCase
104
+ | ProcessNode
105
+ | ProcessRequestOutcomeBranch
106
+ | ExecutionDefinitionReference
107
+ | unknown
108
+
109
+ export interface ProcessPresentationElementDetails {
110
+ readonly definitionReferences: readonly ProcessPresentationDefinitionEvidence[]
111
+ readonly source: DeepReadonly<ProcessPresentationSource>
112
+ readonly sourceMap: readonly DeepReadonly<ExecutionSourceProvenance>[]
113
+ readonly sourcePath: readonly string[]
114
+ readonly valueContracts: readonly ProcessPresentationValueContractEvidence[]
115
+ }
116
+
117
+ export interface ProcessPresentationNode {
118
+ readonly canonicalId: string
119
+ readonly category: ProcessPresentationElementCategory
120
+ readonly id: string
121
+ readonly label: string
122
+ readonly ownerId: string | null
123
+ readonly processNodeKind: string | null
124
+ readonly role: string | null
125
+ readonly terminal: boolean
126
+ readonly details: ProcessPresentationElementDetails
127
+ }
128
+
129
+ export interface ProcessPresentationEdge {
130
+ readonly canonicalEdgeId: string | null
131
+ readonly id: string
132
+ readonly kind: ProcessPresentationEdgeKind
133
+ readonly role: string
134
+ readonly source: string
135
+ readonly target: string
136
+ readonly details: ProcessPresentationElementDetails
137
+ }
138
+
139
+ export interface ProcessPresentationDocumentEvidence {
140
+ readonly definition: DeepReadonly<ExecutionDefinitionReference>
141
+ readonly kind: string
142
+ readonly metadata: DeepReadonly<ExecutionDefinitionMetadata>
143
+ readonly schemaVersion: string
144
+ /** Document-local type entries referenced by integer type uses in source evidence. */
145
+ readonly typeDefinitions: readonly unknown[]
146
+ }
147
+
148
+ export interface ProcessPresentationGraph {
149
+ readonly diagnostics: readonly ProcessPresentationDiagnostic[]
150
+ readonly document: ProcessPresentationDocumentEvidence
151
+ readonly edges: readonly ProcessPresentationEdge[]
152
+ readonly entryNodeId: string | null
153
+ readonly nodes: readonly ProcessPresentationNode[]
154
+ readonly projectionVersion: string
155
+ }
156
+
157
+ export interface ProcessPresentationProjectionResult {
158
+ readonly diagnostics: readonly ProcessPresentationDiagnostic[]
159
+ readonly graph: ProcessPresentationGraph | null
160
+ }
161
+
162
+ interface MutableElementDetails {
163
+ definitionReferences: ProcessPresentationDefinitionEvidence[]
164
+ source: unknown
165
+ sourceMap: DeepReadonly<ExecutionSourceProvenance>[]
166
+ sourcePath: string[]
167
+ valueContracts: ProcessPresentationValueContractEvidence[]
168
+ }
169
+
170
+ interface MutableNode {
171
+ canonicalId: string
172
+ category: ProcessPresentationElementCategory
173
+ id: string
174
+ label: string
175
+ ownerId: string | null
176
+ processNodeKind: string | null
177
+ role: string | null
178
+ terminal: boolean
179
+ details: MutableElementDetails
180
+ }
181
+
182
+ interface MutableEdge {
183
+ canonicalEdgeId: string | null
184
+ id: string
185
+ kind: ProcessPresentationEdgeKind
186
+ role: string
187
+ source: string
188
+ target: string
189
+ details: MutableElementDetails
190
+ }
191
+
192
+ interface ProjectionContext {
193
+ readonly constructPaths: Map<string, readonly string[]>
194
+ readonly diagnostics: ProcessPresentationDiagnostic[]
195
+ readonly document: ExecutionDefinitionDocument
196
+ readonly edges: MutableEdge[]
197
+ readonly edgePaths: Map<string, readonly string[]>
198
+ readonly nodeIds: Set<string>
199
+ readonly nodes: MutableNode[]
200
+ readonly pendingTargets: Array<{
201
+ readonly edge: MutableEdge
202
+ readonly path: readonly string[]
203
+ readonly targetCanonicalId: string
204
+ }>
205
+ }
206
+
207
+ interface ProcessNodeDisposition<TKind extends CanonicalProcessNodeKind> {
208
+ readonly label: string
209
+ readonly terminal?: boolean
210
+ readonly project: (
211
+ node: Extract<ProcessNode, { readonly $node: TKind }>,
212
+ owner: MutableNode,
213
+ path: readonly string[],
214
+ context: ProjectionContext,
215
+ ) => void
216
+ }
217
+
218
+ type ProcessNodeDispositionLedger = {
219
+ readonly [TKind in CanonicalProcessNodeKind]: ProcessNodeDisposition<TKind>
220
+ }
221
+
222
+ interface ProcessAwaitClauseDisposition<TKind extends CanonicalProcessAwaitClauseKind> {
223
+ readonly label: string
224
+ readonly project: (
225
+ clause: Extract<ProcessAwaitClause, { readonly $clause: TKind }>,
226
+ owner: MutableNode,
227
+ path: readonly string[],
228
+ context: ProjectionContext,
229
+ ) => void
230
+ }
231
+
232
+ type ProcessAwaitClauseDispositionLedger = {
233
+ readonly [TKind in CanonicalProcessAwaitClauseKind]: ProcessAwaitClauseDisposition<TKind>
234
+ }
235
+
236
+ export function processPresentationElementId(
237
+ category: ProcessPresentationElementCategory,
238
+ canonicalId: string,
239
+ ownerId?: string | null,
240
+ ): string {
241
+ const owner = ownerId ? `${encodeURIComponent(ownerId)}/` : ''
242
+ return `${category}/${owner}${encodeURIComponent(canonicalId)}`
243
+ }
244
+
245
+ export function processPresentationDefinitionReferenceId(
246
+ reference: ExecutionDefinitionReference,
247
+ ): string {
248
+ return processPresentationElementId(
249
+ 'definition-reference',
250
+ definitionReferenceKey(reference),
251
+ )
252
+ }
253
+
254
+ export function projectCanonicalProcessDocument(
255
+ document: ExecutionDefinitionDocument,
256
+ ): ProcessPresentationProjectionResult {
257
+ const diagnostics: ProcessPresentationDiagnostic[] = []
258
+ if (!isRecord(document)) {
259
+ addDiagnostic(
260
+ diagnostics,
261
+ 'PROCESS_DEFINITION_INVALID',
262
+ [],
263
+ 'A canonical Process projection requires an execution-definition document object.',
264
+ )
265
+ return freezeProjectionResult(null, diagnostics)
266
+ }
267
+
268
+ const kind = readString(document.kind)
269
+ if (kind !== canonicalProcessPresentationCompatibility.definitionKind) {
270
+ addDiagnostic(
271
+ diagnostics,
272
+ 'PROCESS_DEFINITION_KIND_UNSUPPORTED',
273
+ ['kind'],
274
+ `Process presentation supports definition kind '${canonicalProcessPresentationCompatibility.definitionKind}', ` +
275
+ `but observed '${kind ?? '<missing>'}'.`,
276
+ kind,
277
+ )
278
+ return freezeProjectionResult(null, diagnostics)
279
+ }
280
+
281
+ const metadata = isRecord(document.metadata) ? document.metadata : null
282
+ const schemaVersion = readString(metadata?.schemaVersion)
283
+ if (!schemaVersion || !canonicalProcessPresentationCompatibility.schemaVersions.includes(
284
+ schemaVersion as (typeof canonicalProcessPresentationCompatibility.schemaVersions)[number],
285
+ )) {
286
+ addDiagnostic(
287
+ diagnostics,
288
+ 'PROCESS_DEFINITION_SCHEMA_UNSUPPORTED',
289
+ ['metadata', 'schemaVersion'],
290
+ `Process presentation does not support execution schema '${schemaVersion ?? '<missing>'}'.`,
291
+ schemaVersion,
292
+ )
293
+ return freezeProjectionResult(null, diagnostics)
294
+ }
295
+
296
+ if (!metadata) {
297
+ addDiagnostic(
298
+ diagnostics,
299
+ 'PROCESS_DEFINITION_INVALID',
300
+ ['metadata'],
301
+ 'A canonical Process document requires definition metadata.',
302
+ )
303
+ return freezeProjectionResult(null, diagnostics)
304
+ }
305
+
306
+ const definition = isRecord(document.definition) ? document.definition : null
307
+ if (!definition) {
308
+ addDiagnostic(
309
+ diagnostics,
310
+ 'PROCESS_DEFINITION_INVALID',
311
+ ['definition'],
312
+ 'A canonical Process document requires an object definition payload.',
313
+ )
314
+ return freezeProjectionResult(null, diagnostics)
315
+ }
316
+
317
+ const context: ProjectionContext = {
318
+ constructPaths: new Map(),
319
+ diagnostics,
320
+ document,
321
+ edgePaths: new Map(),
322
+ edges: [],
323
+ nodeIds: new Set(),
324
+ nodes: [],
325
+ pendingTargets: [],
326
+ }
327
+ verifyDispositionCompleteness(context)
328
+ projectProcessDefinition(definition, context)
329
+ resolvePendingTargets(context)
330
+
331
+ const definitionReference = readDefinitionReference(metadata)
332
+ if (!definitionReference) {
333
+ addDiagnostic(
334
+ diagnostics,
335
+ 'PROCESS_DEFINITION_REFERENCE_INVALID',
336
+ ['metadata'],
337
+ 'Process metadata must retain definition id, revision id, and a complete fingerprint.',
338
+ )
339
+ }
340
+
341
+ const entry = readString(definition.entry)
342
+ const entryNodeId = entry ? processPresentationElementId('process-node', entry) : null
343
+ if (!entry || !context.nodeIds.has(entryNodeId!)) {
344
+ addDiagnostic(
345
+ diagnostics,
346
+ 'PROCESS_ENTRY_UNRESOLVED',
347
+ ['definition', 'entry'],
348
+ 'The Process entry must identify a projected canonical Process node.',
349
+ entry,
350
+ )
351
+ }
352
+
353
+ sortProjection(context)
354
+ const frozenDiagnostics = freezeDiagnostics(diagnostics)
355
+ const graph: ProcessPresentationGraph = deepFreeze({
356
+ diagnostics: frozenDiagnostics,
357
+ document: {
358
+ definition: definitionReference ?? missingDefinitionReference(metadata),
359
+ kind,
360
+ metadata: cloneWireValue(metadata),
361
+ schemaVersion,
362
+ typeDefinitions: Array.isArray(definition.$types) ? cloneWireValue(definition.$types) : [],
363
+ },
364
+ edges: context.edges.map(freezeEdge),
365
+ entryNodeId: entry && context.nodeIds.has(entryNodeId!) ? entryNodeId : null,
366
+ nodes: context.nodes.map(freezeNode),
367
+ projectionVersion: canonicalProcessPresentationCompatibility.projectionVersion,
368
+ })
369
+ return deepFreeze({ graph, diagnostics: frozenDiagnostics })
370
+ }
371
+
372
+ function projectProcessDefinition(
373
+ definition: Readonly<Record<string, unknown>>,
374
+ context: ProjectionContext,
375
+ ) {
376
+ const nodes = definition.nodes
377
+ if (!Array.isArray(nodes)) {
378
+ addDiagnostic(
379
+ context.diagnostics,
380
+ 'PROCESS_NODE_TABLE_INVALID',
381
+ ['definition', 'nodes'],
382
+ 'A canonical Process definition requires a node array.',
383
+ )
384
+ return
385
+ }
386
+
387
+ for (let index = 0; index < nodes.length; index += 1) {
388
+ const path = ['definition', 'nodes', String(index)]
389
+ const rawNode = nodes[index]
390
+ if (!isRecord(rawNode)) {
391
+ addDiagnostic(
392
+ context.diagnostics,
393
+ 'PROCESS_DEFINITION_INVALID',
394
+ path,
395
+ 'A Process node must be an object.',
396
+ )
397
+ continue
398
+ }
399
+
400
+ const id = readString(rawNode.id)
401
+ if (!id) {
402
+ addDiagnostic(
403
+ context.diagnostics,
404
+ 'PROCESS_ELEMENT_ID_REQUIRED',
405
+ [...path, 'id'],
406
+ 'A Process node requires a stable canonical identity.',
407
+ )
408
+ continue
409
+ }
410
+
411
+ const kind = readString(rawNode.$node)
412
+ if (!kind) {
413
+ const owner = addElementNode({
414
+ canonicalId: id,
415
+ category: 'process-node',
416
+ context,
417
+ label: 'Unknown Process node',
418
+ path,
419
+ processNodeKind: null,
420
+ source: rawNode,
421
+ })
422
+ addDiagnostic(
423
+ context.diagnostics,
424
+ 'PROCESS_NODE_KIND_REQUIRED',
425
+ [...path, '$node'],
426
+ 'A Process node requires its persisted wire discriminator.',
427
+ owner?.canonicalId ?? id,
428
+ )
429
+ continue
430
+ }
431
+
432
+ if (!isCanonicalProcessNodeKind(kind)) {
433
+ addElementNode({
434
+ canonicalId: id,
435
+ category: 'process-node',
436
+ context,
437
+ label: `Unsupported Process node (${kind})`,
438
+ path,
439
+ processNodeKind: kind,
440
+ source: rawNode,
441
+ })
442
+ addDiagnostic(
443
+ context.diagnostics,
444
+ 'PROCESS_NODE_KIND_UNSUPPORTED',
445
+ [...path, '$node'],
446
+ `Process node kind '${kind}' is outside the projector disposition inventory.`,
447
+ id,
448
+ )
449
+ continue
450
+ }
451
+
452
+ const disposition = processNodeDispositions[kind] as ProcessNodeDisposition<typeof kind>
453
+ const owner = addElementNode({
454
+ canonicalId: id,
455
+ category: 'process-node',
456
+ context,
457
+ label: disposition.label,
458
+ path,
459
+ processNodeKind: kind,
460
+ source: rawNode,
461
+ terminal: disposition.terminal,
462
+ })
463
+ if (!owner) {
464
+ continue
465
+ }
466
+
467
+ disposition.project(
468
+ rawNode as unknown as Extract<ProcessNode, { readonly $node: typeof kind }>,
469
+ owner,
470
+ path,
471
+ context,
472
+ )
473
+ }
474
+ }
475
+
476
+ const processNodeDispositions: ProcessNodeDispositionLedger = {
477
+ invokeTransition: {
478
+ label: 'Invoke transition',
479
+ project(node, owner, path, context) {
480
+ addDefinitionReference(owner, 'transition', node.transition, [...path, 'transition'], context)
481
+ addContinuation(owner, node.continuation, 'continuation', [...path, 'continuation'], context)
482
+ },
483
+ },
484
+ evaluateRelation: {
485
+ label: 'Evaluate relation',
486
+ project(node, owner, path, context) {
487
+ addDefinitionReference(owner, 'relation', node.relation, [...path, 'relation'], context)
488
+ addContinuation(owner, node.continuation, 'continuation', [...path, 'continuation'], context)
489
+ },
490
+ },
491
+ request: {
492
+ label: 'Request',
493
+ project(node, owner, path, context) {
494
+ addContractReference(owner, 'request-contract', node.contract, [...path, 'contract'], context)
495
+ projectRequestOutcomes(owner, node.outcomes, [...path, 'outcomes'], context)
496
+ },
497
+ },
498
+ emitEvent: {
499
+ label: 'Emit event',
500
+ project(node, owner, path, context) {
501
+ addContractReference(owner, 'domain-event-contract', node.contract, [...path, 'contract'], context)
502
+ addControlEdge(owner, node.next, 'next', [...path, 'next'], context)
503
+ },
504
+ },
505
+ sendSignal: {
506
+ label: 'Send signal',
507
+ project(node, owner, path, context) {
508
+ addContractReference(owner, 'signal-contract', node.contract, [...path, 'contract'], context)
509
+ addControlEdge(owner, node.next, 'next', [...path, 'next'], context)
510
+ },
511
+ },
512
+ choice: {
513
+ label: 'Choice',
514
+ project(node, owner, path, context) {
515
+ projectChoiceCases(owner, node.cases, [...path, 'cases'], context)
516
+ projectFallback(owner, node.fallback, [...path, 'fallback'], context)
517
+ },
518
+ },
519
+ match: {
520
+ label: 'Match',
521
+ project(node, owner, path, context) {
522
+ addValueContract(owner, 'match-value', node.contract)
523
+ projectMatchCases(owner, node.cases, [...path, 'cases'], context)
524
+ projectFallback(owner, node.fallback, [...path, 'fallback'], context)
525
+ },
526
+ },
527
+ fork: {
528
+ label: 'Fork',
529
+ project(node, owner, path, context) {
530
+ projectForkBranches(owner, node.branches, [...path, 'branches'], context)
531
+ const join = readString(node.join)
532
+ if (join) {
533
+ addDerivedEdge(owner, processPresentationElementId('process-node', join), 'reciprocal', 'join', path, context)
534
+ } else {
535
+ addDiagnostic(
536
+ context.diagnostics,
537
+ 'PROCESS_EDGE_TARGET_UNRESOLVED',
538
+ [...path, 'join'],
539
+ 'A Fork requires the stable identity of its reciprocal Join.',
540
+ owner.canonicalId,
541
+ )
542
+ }
543
+ },
544
+ },
545
+ join: {
546
+ label: 'Join',
547
+ project(node, owner, path, context) {
548
+ addControlEdge(owner, node.next, 'next', [...path, 'next'], context)
549
+ projectJoinResults(owner, node.fork, node.result, [...path, 'result'], context)
550
+ },
551
+ },
552
+ awaitMatch: {
553
+ label: 'Await match',
554
+ project(node, owner, path, context) {
555
+ projectAwaitClauses(owner, node.clauses, [...path, 'clauses'], context)
556
+ },
557
+ },
558
+ timer: {
559
+ label: 'Timer',
560
+ project(node, owner, path, context) {
561
+ addControlEdge(owner, node.next, 'next', [...path, 'next'], context)
562
+ },
563
+ },
564
+ reply: {
565
+ label: 'Reply',
566
+ project(node, owner, path, context) {
567
+ addContractReference(owner, 'reply-contract', node.contract, [...path, 'contract'], context)
568
+ addControlEdge(owner, node.next, 'next', [...path, 'next'], context)
569
+ },
570
+ },
571
+ durableCut: {
572
+ label: 'Durable cut',
573
+ project(node, owner, path, context) {
574
+ addControlEdge(owner, node.resume, 'resume', [...path, 'resume'], context)
575
+ },
576
+ },
577
+ invokeProcess: {
578
+ label: 'Invoke process',
579
+ project(node, owner, path, context) {
580
+ addDefinitionReference(owner, 'child-process', node.process, [...path, 'process'], context)
581
+ addContractReference(owner, 'request-contract', node.contract, [...path, 'contract'], context)
582
+ projectRequestOutcomes(owner, node.outcomes, [...path, 'outcomes'], context)
583
+ },
584
+ },
585
+ forEachPartition: {
586
+ label: 'For each partition',
587
+ project(node, owner, path, context) {
588
+ addDefinitionReference(owner, 'child-process', node.process, [...path, 'process'], context)
589
+ addContractReference(owner, 'request-contract', node.contract, [...path, 'contract'], context)
590
+ addValueContract(owner, 'partition-output', node.partition?.contract)
591
+ addControlEdge(owner, node.completed, 'completed', [...path, 'completed'], context)
592
+ addControlEdge(owner, node.failed, 'failed', [...path, 'failed'], context)
593
+ },
594
+ },
595
+ repeatAcrossActivation: {
596
+ label: 'Repeat across activation',
597
+ project(node, owner, path, context) {
598
+ addValueContract(owner, 'progress', node.progressContract)
599
+ addValueContract(owner, 'state', node.stateContract)
600
+ addControlEdge(owner, node.repeat, 'repeat', [...path, 'repeat'], context)
601
+ addControlEdge(owner, node.completed, 'completed', [...path, 'completed'], context)
602
+ addControlEdge(owner, node.exhausted, 'exhausted', [...path, 'exhausted'], context)
603
+ addControlEdge(owner, node.stalled, 'stalled', [...path, 'stalled'], context)
604
+ },
605
+ },
606
+ cancellationFinalizer: {
607
+ label: 'Cancellation finalizer',
608
+ project(node, owner, path, context) {
609
+ addDefinitionReference(owner, 'cancellation-finalizer-process', node.process, [...path, 'process'], context)
610
+ addContractReference(owner, 'request-contract', node.contract, [...path, 'contract'], context)
611
+ },
612
+ },
613
+ return: {
614
+ label: 'Return',
615
+ terminal: true,
616
+ project() {},
617
+ },
618
+ fail: {
619
+ label: 'Fail',
620
+ terminal: true,
621
+ project() {},
622
+ },
623
+ }
624
+
625
+ const processAwaitClauseDispositions: ProcessAwaitClauseDispositionLedger = {
626
+ interaction: {
627
+ label: 'Await interaction',
628
+ project(clause, owner, path, context) {
629
+ addContractReference(owner, 'interaction-contract', clause.contract, [...path, 'contract'], context)
630
+ addValueContract(owner, 'input', clause.input?.contract)
631
+ },
632
+ },
633
+ timer: {
634
+ label: 'Await timer',
635
+ project() {},
636
+ },
637
+ }
638
+
639
+ export const canonicalProcessPresentationDispositionKinds = Object.freeze(
640
+ Object.keys(processNodeDispositions).sort() as CanonicalProcessNodeKind[],
641
+ )
642
+
643
+ export const canonicalProcessAwaitClausePresentationDispositionKinds = Object.freeze(
644
+ Object.keys(processAwaitClauseDispositions).sort() as CanonicalProcessAwaitClauseKind[],
645
+ )
646
+
647
+ function projectRequestOutcomes(
648
+ owner: MutableNode,
649
+ rawOutcomes: unknown,
650
+ path: readonly string[],
651
+ context: ProjectionContext,
652
+ ) {
653
+ forEachElement(rawOutcomes, path, context, (rawOutcome, outcomePath) => {
654
+ const id = readString(rawOutcome.id)
655
+ if (!id) {
656
+ missingElementId('Request outcome', outcomePath, owner, context)
657
+ return
658
+ }
659
+
660
+ const role = readString(rawOutcome.outcome) ?? 'outcome'
661
+ const outcome = addElementNode({
662
+ canonicalId: id,
663
+ category: 'request-outcome',
664
+ context,
665
+ label: `Request outcome: ${role}`,
666
+ ownerId: owner.canonicalId,
667
+ path: outcomePath,
668
+ role,
669
+ source: rawOutcome,
670
+ })
671
+ if (!outcome) {
672
+ return
673
+ }
674
+
675
+ addContainmentEdge(owner, outcome, 'outcome', outcomePath, context)
676
+ addContinuation(outcome, rawOutcome.continuation, role, [...outcomePath, 'continuation'], context)
677
+ })
678
+ }
679
+
680
+ function projectChoiceCases(
681
+ owner: MutableNode,
682
+ rawCases: unknown,
683
+ path: readonly string[],
684
+ context: ProjectionContext,
685
+ ) {
686
+ forEachElement(rawCases, path, context, (rawCase, casePath) => {
687
+ const id = readString(rawCase.id)
688
+ if (!id) {
689
+ missingElementId('Choice case', casePath, owner, context)
690
+ return
691
+ }
692
+
693
+ const choiceCase = addElementNode({
694
+ canonicalId: id,
695
+ category: 'choice-case',
696
+ context,
697
+ label: 'Choice case',
698
+ ownerId: owner.canonicalId,
699
+ path: casePath,
700
+ role: 'case',
701
+ source: rawCase,
702
+ })
703
+ if (!choiceCase) {
704
+ return
705
+ }
706
+
707
+ addContainmentEdge(owner, choiceCase, 'case', casePath, context)
708
+ addControlEdge(choiceCase, rawCase.next, 'selected', [...casePath, 'next'], context)
709
+ })
710
+ }
711
+
712
+ function projectMatchCases(
713
+ owner: MutableNode,
714
+ rawCases: unknown,
715
+ path: readonly string[],
716
+ context: ProjectionContext,
717
+ ) {
718
+ forEachElement(rawCases, path, context, (rawCase, casePath) => {
719
+ const id = readString(rawCase.id)
720
+ if (!id) {
721
+ missingElementId('Match case', casePath, owner, context)
722
+ return
723
+ }
724
+
725
+ const matchCase = addElementNode({
726
+ canonicalId: id,
727
+ category: 'match-case',
728
+ context,
729
+ label: 'Match case',
730
+ ownerId: owner.canonicalId,
731
+ path: casePath,
732
+ role: 'case',
733
+ source: rawCase,
734
+ })
735
+ if (!matchCase) {
736
+ return
737
+ }
738
+
739
+ const pattern = isRecord(rawCase.pattern) ? rawCase.pattern : null
740
+ addValueContract(matchCase, 'pattern', pattern?.contract)
741
+ addContainmentEdge(owner, matchCase, 'case', casePath, context)
742
+ addControlEdge(matchCase, rawCase.next, 'selected', [...casePath, 'next'], context)
743
+ })
744
+ }
745
+
746
+ function projectFallback(
747
+ owner: MutableNode,
748
+ rawFallback: unknown,
749
+ path: readonly string[],
750
+ context: ProjectionContext,
751
+ ) {
752
+ if (rawFallback === null || rawFallback === undefined) {
753
+ return
754
+ }
755
+ if (!isRecord(rawFallback)) {
756
+ addDiagnostic(
757
+ context.diagnostics,
758
+ 'PROCESS_DEFINITION_INVALID',
759
+ path,
760
+ 'A Process fallback must be an object.',
761
+ owner.canonicalId,
762
+ )
763
+ return
764
+ }
765
+
766
+ const id = readString(rawFallback.id)
767
+ if (!id) {
768
+ missingElementId('Fallback', path, owner, context)
769
+ return
770
+ }
771
+
772
+ const fallback = addElementNode({
773
+ canonicalId: id,
774
+ category: 'fallback',
775
+ context,
776
+ label: 'Fallback',
777
+ ownerId: owner.canonicalId,
778
+ path,
779
+ role: 'fallback',
780
+ source: rawFallback,
781
+ })
782
+ if (!fallback) {
783
+ return
784
+ }
785
+
786
+ addContainmentEdge(owner, fallback, 'fallback', path, context)
787
+ addControlEdge(fallback, rawFallback.next, 'fallback', [...path, 'next'], context)
788
+ }
789
+
790
+ function projectForkBranches(
791
+ owner: MutableNode,
792
+ rawBranches: unknown,
793
+ path: readonly string[],
794
+ context: ProjectionContext,
795
+ ) {
796
+ forEachElement(rawBranches, path, context, (rawBranch, branchPath) => {
797
+ const id = readString(rawBranch.id)
798
+ if (!id) {
799
+ missingElementId('Fork branch', branchPath, owner, context)
800
+ return
801
+ }
802
+
803
+ const branch = addElementNode({
804
+ canonicalId: id,
805
+ category: 'fork-branch',
806
+ context,
807
+ label: 'Fork branch',
808
+ ownerId: owner.canonicalId,
809
+ path: branchPath,
810
+ role: readString(rawBranch.capacityDomain) ?? 'branch',
811
+ source: rawBranch,
812
+ })
813
+ if (!branch) {
814
+ return
815
+ }
816
+
817
+ addContainmentEdge(owner, branch, 'branch', branchPath, context)
818
+ addControlEdge(branch, rawBranch.start, 'start', [...branchPath, 'start'], context)
819
+ })
820
+ }
821
+
822
+ function projectAwaitClauses(
823
+ owner: MutableNode,
824
+ rawClauses: unknown,
825
+ path: readonly string[],
826
+ context: ProjectionContext,
827
+ ) {
828
+ forEachElement(rawClauses, path, context, (rawClause, clausePath) => {
829
+ const id = readString(rawClause.id)
830
+ if (!id) {
831
+ missingElementId('Await clause', clausePath, owner, context)
832
+ return
833
+ }
834
+
835
+ const clauseKind = readString(rawClause.$clause)
836
+ const disposition = clauseKind && isCanonicalProcessAwaitClauseKind(clauseKind)
837
+ ? processAwaitClauseDispositions[clauseKind] as ProcessAwaitClauseDisposition<CanonicalProcessAwaitClauseKind>
838
+ : null
839
+ const clause = addElementNode({
840
+ canonicalId: id,
841
+ category: 'await-clause',
842
+ context,
843
+ label: disposition?.label ?? `Unsupported Await clause (${clauseKind ?? 'missing'})`,
844
+ ownerId: owner.canonicalId,
845
+ path: clausePath,
846
+ role: clauseKind ?? 'unknown',
847
+ source: rawClause,
848
+ })
849
+ if (!clause) {
850
+ return
851
+ }
852
+
853
+ addContainmentEdge(owner, clause, 'clause', clausePath, context)
854
+ if (!disposition || !clauseKind) {
855
+ addDiagnostic(
856
+ context.diagnostics,
857
+ 'PROCESS_DEFINITION_INVALID',
858
+ [...clausePath, '$clause'],
859
+ `Await clause kind '${clauseKind ?? '<missing>'}' is unsupported.`,
860
+ id,
861
+ )
862
+ } else {
863
+ disposition.project(
864
+ rawClause as unknown as Extract<ProcessAwaitClause, { readonly $clause: typeof clauseKind }>,
865
+ clause,
866
+ clausePath,
867
+ context,
868
+ )
869
+ }
870
+ addContinuation(clause, rawClause.continuation, 'selected', [...clausePath, 'continuation'], context)
871
+ })
872
+ }
873
+
874
+ function projectJoinResults(
875
+ owner: MutableNode,
876
+ rawForkId: unknown,
877
+ rawResult: unknown,
878
+ path: readonly string[],
879
+ context: ProjectionContext,
880
+ ) {
881
+ if (rawResult === null || rawResult === undefined) {
882
+ return
883
+ }
884
+ if (!isRecord(rawResult)) {
885
+ addDiagnostic(
886
+ context.diagnostics,
887
+ 'PROCESS_DEFINITION_INVALID',
888
+ path,
889
+ 'A Join result projection must be an object.',
890
+ owner.canonicalId,
891
+ )
892
+ return
893
+ }
894
+
895
+ const forkId = readString(rawForkId)
896
+ addValueContract(owner, 'join-result', rawResult.resultContract)
897
+ forEachElement(rawResult.branches, [...path, 'branches'], context, (rawBranch, branchPath) => {
898
+ const branchId = readString(rawBranch.branch)
899
+ if (!branchId) {
900
+ missingElementId('Join result branch', branchPath, owner, context)
901
+ return
902
+ }
903
+
904
+ const result = addElementNode({
905
+ canonicalId: branchId,
906
+ category: 'join-result',
907
+ context,
908
+ label: 'Join branch result',
909
+ ownerId: owner.canonicalId,
910
+ path: branchPath,
911
+ registerConstruct: false,
912
+ role: 'result',
913
+ source: rawBranch,
914
+ })
915
+ if (!result) {
916
+ return
917
+ }
918
+
919
+ addValueContract(result, 'join-result', rawResult.resultContract)
920
+ addContainmentEdge(owner, result, 'result', branchPath, context)
921
+ addDerivedEdge(
922
+ result,
923
+ processPresentationElementId('fork-branch', branchId, forkId),
924
+ 'join-result',
925
+ 'branch',
926
+ branchPath,
927
+ context,
928
+ )
929
+ })
930
+ }
931
+
932
+ function addElementNode({
933
+ canonicalId,
934
+ category,
935
+ context,
936
+ label,
937
+ ownerId = null,
938
+ path,
939
+ processNodeKind = null,
940
+ registerConstruct = true,
941
+ role = null,
942
+ source,
943
+ terminal = false,
944
+ }: {
945
+ readonly canonicalId: string
946
+ readonly category: ProcessPresentationElementCategory
947
+ readonly context: ProjectionContext
948
+ readonly label: string
949
+ readonly ownerId?: string | null
950
+ readonly path: readonly string[]
951
+ readonly processNodeKind?: string | null
952
+ readonly registerConstruct?: boolean
953
+ readonly role?: string | null
954
+ readonly source: unknown
955
+ readonly terminal?: boolean
956
+ }): MutableNode | null {
957
+ if (registerConstruct) {
958
+ const priorPath = context.constructPaths.get(canonicalId)
959
+ if (priorPath) {
960
+ addDiagnostic(
961
+ context.diagnostics,
962
+ 'PROCESS_ELEMENT_ID_DUPLICATED',
963
+ path,
964
+ `Canonical Process element identity '${canonicalId}' is duplicated; first observed at '${formatPath(priorPath)}'.`,
965
+ canonicalId,
966
+ )
967
+ return null
968
+ }
969
+ context.constructPaths.set(canonicalId, [...path])
970
+ }
971
+
972
+ const id = processPresentationElementId(category, canonicalId, ownerId)
973
+ if (context.nodeIds.has(id)) {
974
+ return context.nodes.find((node) => node.id === id) ?? null
975
+ }
976
+
977
+ const node: MutableNode = {
978
+ canonicalId,
979
+ category,
980
+ details: createDetails(source, path, context.document),
981
+ id,
982
+ label,
983
+ ownerId,
984
+ processNodeKind,
985
+ role,
986
+ terminal,
987
+ }
988
+ context.nodeIds.add(id)
989
+ context.nodes.push(node)
990
+ return node
991
+ }
992
+
993
+ function addContainmentEdge(
994
+ owner: MutableNode,
995
+ child: MutableNode,
996
+ role: string,
997
+ path: readonly string[],
998
+ context: ProjectionContext,
999
+ ) {
1000
+ addDerivedEdge(owner, child.id, 'contains', role, path, context)
1001
+ }
1002
+
1003
+ function addDerivedEdge(
1004
+ owner: MutableNode,
1005
+ target: string,
1006
+ kind: Exclude<ProcessPresentationEdgeKind, 'control' | 'definition-reference'>,
1007
+ role: string,
1008
+ path: readonly string[],
1009
+ context: ProjectionContext,
1010
+ ) {
1011
+ const id = derivedEdgeId(kind, owner.id, target, role)
1012
+ if (context.edges.some((edge) => edge.id === id)) {
1013
+ return
1014
+ }
1015
+ context.edges.push({
1016
+ canonicalEdgeId: null,
1017
+ details: createDetails(null, path, context.document),
1018
+ id,
1019
+ kind,
1020
+ role,
1021
+ source: owner.id,
1022
+ target,
1023
+ })
1024
+ context.pendingTargets.push({ edge: context.edges.at(-1)!, path, targetCanonicalId: target })
1025
+ }
1026
+
1027
+ function addControlEdge(
1028
+ owner: MutableNode,
1029
+ rawEdge: unknown,
1030
+ role: string,
1031
+ path: readonly string[],
1032
+ context: ProjectionContext,
1033
+ ) {
1034
+ if (!isRecord(rawEdge)) {
1035
+ addDiagnostic(
1036
+ context.diagnostics,
1037
+ 'PROCESS_EDGE_INVALID',
1038
+ path,
1039
+ `Process control edge '${role}' must be an object.`,
1040
+ owner.canonicalId,
1041
+ )
1042
+ return
1043
+ }
1044
+
1045
+ const edgeId = readString(rawEdge.id)
1046
+ const targetCanonicalId = readString(rawEdge.target)
1047
+ if (!edgeId || !targetCanonicalId) {
1048
+ addDiagnostic(
1049
+ context.diagnostics,
1050
+ 'PROCESS_EDGE_INVALID',
1051
+ path,
1052
+ `Process control edge '${role}' requires stable edge and target identities.`,
1053
+ edgeId ?? owner.canonicalId,
1054
+ )
1055
+ return
1056
+ }
1057
+
1058
+ const priorPath = context.edgePaths.get(edgeId)
1059
+ if (priorPath) {
1060
+ addDiagnostic(
1061
+ context.diagnostics,
1062
+ 'PROCESS_ELEMENT_ID_DUPLICATED',
1063
+ path,
1064
+ `Canonical Process edge identity '${edgeId}' is duplicated; first observed at '${formatPath(priorPath)}'.`,
1065
+ edgeId,
1066
+ )
1067
+ return
1068
+ }
1069
+ context.edgePaths.set(edgeId, [...path])
1070
+
1071
+ const edge: MutableEdge = {
1072
+ canonicalEdgeId: edgeId,
1073
+ details: createDetails(rawEdge, path, context.document),
1074
+ id: `control/${encodeURIComponent(edgeId)}`,
1075
+ kind: 'control',
1076
+ role,
1077
+ source: owner.id,
1078
+ target: processPresentationElementId('process-node', targetCanonicalId),
1079
+ }
1080
+ context.edges.push(edge)
1081
+ context.pendingTargets.push({ edge, path, targetCanonicalId })
1082
+ }
1083
+
1084
+ function addContinuation(
1085
+ owner: MutableNode,
1086
+ rawContinuation: unknown,
1087
+ role: string,
1088
+ path: readonly string[],
1089
+ context: ProjectionContext,
1090
+ ) {
1091
+ if (!isRecord(rawContinuation)) {
1092
+ addDiagnostic(
1093
+ context.diagnostics,
1094
+ 'PROCESS_EDGE_INVALID',
1095
+ path,
1096
+ `Process continuation '${role}' must be an object.`,
1097
+ owner.canonicalId,
1098
+ )
1099
+ return
1100
+ }
1101
+
1102
+ const output = isRecord(rawContinuation.output) ? rawContinuation.output : null
1103
+ addValueContract(owner, `${role}-output`, output?.contract)
1104
+ addControlEdge(owner, rawContinuation.edge, role, [...path, 'edge'], context)
1105
+ }
1106
+
1107
+ function addContractReference(
1108
+ owner: MutableNode,
1109
+ role: string,
1110
+ rawContract: unknown,
1111
+ path: readonly string[],
1112
+ context: ProjectionContext,
1113
+ ) {
1114
+ if (!isRecord(rawContract)) {
1115
+ addDiagnostic(
1116
+ context.diagnostics,
1117
+ 'PROCESS_DEFINITION_REFERENCE_INVALID',
1118
+ path,
1119
+ `Interaction contract '${role}' must retain an exact definition reference.`,
1120
+ owner.canonicalId,
1121
+ )
1122
+ return
1123
+ }
1124
+ addDefinitionReference(owner, role, rawContract.definition, [...path, 'definition'], context)
1125
+ }
1126
+
1127
+ function addDefinitionReference(
1128
+ owner: MutableNode,
1129
+ role: string,
1130
+ rawReference: unknown,
1131
+ path: readonly string[],
1132
+ context: ProjectionContext,
1133
+ ) {
1134
+ const reference = readDefinitionReference(rawReference)
1135
+ if (!reference) {
1136
+ addDiagnostic(
1137
+ context.diagnostics,
1138
+ 'PROCESS_DEFINITION_REFERENCE_INVALID',
1139
+ path,
1140
+ `Definition link '${role}' must retain definition id, revision id, and a complete fingerprint.`,
1141
+ owner.canonicalId,
1142
+ )
1143
+ return
1144
+ }
1145
+
1146
+ const frozenReference = cloneWireValue(reference)
1147
+ owner.details.definitionReferences.push(deepFreeze({ reference: frozenReference, role }))
1148
+ const key = definitionReferenceKey(reference)
1149
+ const referenceNodeId = processPresentationDefinitionReferenceId(reference)
1150
+ let referenceNode = context.nodes.find((node) => node.id === referenceNodeId)
1151
+ if (!referenceNode) {
1152
+ referenceNode = addElementNode({
1153
+ canonicalId: key,
1154
+ category: 'definition-reference',
1155
+ context,
1156
+ label: `Definition: ${reference.definitionId}`,
1157
+ path,
1158
+ registerConstruct: false,
1159
+ role: 'definition',
1160
+ source: reference,
1161
+ }) ?? undefined
1162
+ }
1163
+ if (!referenceNode) {
1164
+ return
1165
+ }
1166
+
1167
+ const edgeId = derivedEdgeId('definition-reference', owner.id, referenceNode.id, role)
1168
+ if (context.edges.some((edge) => edge.id === edgeId)) {
1169
+ return
1170
+ }
1171
+ context.edges.push({
1172
+ canonicalEdgeId: null,
1173
+ details: createDetails(reference, path, context.document),
1174
+ id: edgeId,
1175
+ kind: 'definition-reference',
1176
+ role,
1177
+ source: owner.id,
1178
+ target: referenceNode.id,
1179
+ })
1180
+ }
1181
+
1182
+ function addValueContract(owner: MutableNode, role: string, rawContract: unknown) {
1183
+ if (!isRecord(rawContract)) {
1184
+ return
1185
+ }
1186
+ owner.details.valueContracts.push(deepFreeze({
1187
+ contract: cloneWireValue(rawContract) as ProcessPresentationValueContractEvidence['contract'],
1188
+ role,
1189
+ }))
1190
+ }
1191
+
1192
+ function resolvePendingTargets(context: ProjectionContext) {
1193
+ for (const pending of context.pendingTargets) {
1194
+ if (context.nodeIds.has(pending.edge.target)) {
1195
+ continue
1196
+ }
1197
+
1198
+ const targetCanonicalId = pending.edge.kind === 'control'
1199
+ ? pending.targetCanonicalId
1200
+ : pending.edge.target
1201
+ const unresolvedId = processPresentationElementId('unresolved-node', targetCanonicalId)
1202
+ addElementNode({
1203
+ canonicalId: targetCanonicalId,
1204
+ category: 'unresolved-node',
1205
+ context,
1206
+ label: `Unresolved target: ${targetCanonicalId}`,
1207
+ path: pending.path,
1208
+ registerConstruct: false,
1209
+ role: 'unresolved-target',
1210
+ source: null,
1211
+ })
1212
+ pending.edge.target = unresolvedId
1213
+
1214
+ addDiagnostic(
1215
+ context.diagnostics,
1216
+ 'PROCESS_EDGE_TARGET_UNRESOLVED',
1217
+ pending.path,
1218
+ `Projected edge '${pending.edge.id}' targets an unresolved semantic element.`,
1219
+ targetCanonicalId,
1220
+ )
1221
+ }
1222
+ }
1223
+
1224
+ function verifyDispositionCompleteness(context: ProjectionContext) {
1225
+ verifyInventoryDispositionCompleteness(
1226
+ canonicalProcessNodeKinds,
1227
+ Object.keys(processNodeDispositions),
1228
+ 'Process construct',
1229
+ context,
1230
+ )
1231
+ verifyInventoryDispositionCompleteness(
1232
+ canonicalProcessAwaitClauseKinds,
1233
+ Object.keys(processAwaitClauseDispositions),
1234
+ 'Process Await clause',
1235
+ context,
1236
+ )
1237
+ }
1238
+
1239
+ function verifyInventoryDispositionCompleteness(
1240
+ authoritativeValues: readonly string[],
1241
+ dispositionValues: readonly string[],
1242
+ label: string,
1243
+ context: ProjectionContext,
1244
+ ) {
1245
+ const authoritative = new Set<string>(authoritativeValues)
1246
+ const dispositions = new Set<string>(dispositionValues)
1247
+ for (const kind of [...authoritative].sort()) {
1248
+ if (!dispositions.has(kind)) {
1249
+ addDiagnostic(
1250
+ context.diagnostics,
1251
+ 'PROCESS_CONSTRUCT_DISPOSITION_MISSING',
1252
+ [],
1253
+ `Canonical ${label} '${kind}' has no presentation disposition.`,
1254
+ kind,
1255
+ )
1256
+ }
1257
+ }
1258
+ for (const kind of [...dispositions].sort()) {
1259
+ if (!authoritative.has(kind)) {
1260
+ addDiagnostic(
1261
+ context.diagnostics,
1262
+ 'PROCESS_CONSTRUCT_DISPOSITION_STALE',
1263
+ [],
1264
+ `${label} presentation disposition '${kind}' is absent from the canonical inventory.`,
1265
+ kind,
1266
+ )
1267
+ }
1268
+ }
1269
+ }
1270
+
1271
+ function forEachElement(
1272
+ rawElements: unknown,
1273
+ path: readonly string[],
1274
+ context: ProjectionContext,
1275
+ project: (element: Readonly<Record<string, unknown>>, path: readonly string[]) => void,
1276
+ ) {
1277
+ if (!Array.isArray(rawElements)) {
1278
+ addDiagnostic(
1279
+ context.diagnostics,
1280
+ 'PROCESS_DEFINITION_INVALID',
1281
+ path,
1282
+ 'A canonical Process element collection must be an array.',
1283
+ )
1284
+ return
1285
+ }
1286
+
1287
+ for (let index = 0; index < rawElements.length; index += 1) {
1288
+ const elementPath = [...path, String(index)]
1289
+ const element = rawElements[index]
1290
+ if (!isRecord(element)) {
1291
+ addDiagnostic(
1292
+ context.diagnostics,
1293
+ 'PROCESS_DEFINITION_INVALID',
1294
+ elementPath,
1295
+ 'A canonical Process element must be an object.',
1296
+ )
1297
+ continue
1298
+ }
1299
+ project(element, elementPath)
1300
+ }
1301
+ }
1302
+
1303
+ function missingElementId(
1304
+ label: string,
1305
+ path: readonly string[],
1306
+ owner: MutableNode,
1307
+ context: ProjectionContext,
1308
+ ) {
1309
+ addDiagnostic(
1310
+ context.diagnostics,
1311
+ 'PROCESS_ELEMENT_ID_REQUIRED',
1312
+ [...path, 'id'],
1313
+ `${label} requires a stable canonical identity.`,
1314
+ owner.canonicalId,
1315
+ )
1316
+ }
1317
+
1318
+ function createDetails(
1319
+ source: unknown,
1320
+ path: readonly string[],
1321
+ document: ExecutionDefinitionDocument,
1322
+ ): MutableElementDetails {
1323
+ return {
1324
+ definitionReferences: [],
1325
+ source: cloneWireValue(source),
1326
+ sourceMap: sourceMapForPath(document, path),
1327
+ sourcePath: [...path],
1328
+ valueContracts: [],
1329
+ }
1330
+ }
1331
+
1332
+ function sourceMapForPath(
1333
+ document: ExecutionDefinitionDocument,
1334
+ path: readonly string[],
1335
+ ): DeepReadonly<ExecutionSourceProvenance>[] {
1336
+ const entries = document.metadata?.sourceMap?.entries
1337
+ if (!Array.isArray(entries)) {
1338
+ return []
1339
+ }
1340
+
1341
+ const definitionPath = path[0] === 'definition' ? path.slice(1) : path
1342
+ let deepest = -1
1343
+ const matches: ExecutionSourceProvenance[] = []
1344
+ for (const entry of entries) {
1345
+ const mapped = entry?.semanticPath?.segments
1346
+ if (
1347
+ !Array.isArray(mapped) ||
1348
+ !isStringArray(mapped) ||
1349
+ mapped.length < deepest ||
1350
+ !isPrefix(mapped, definitionPath)
1351
+ ) {
1352
+ continue
1353
+ }
1354
+ if (mapped.length > deepest) {
1355
+ deepest = mapped.length
1356
+ matches.length = 0
1357
+ }
1358
+ matches.push(entry)
1359
+ }
1360
+ return matches
1361
+ .sort((left, right) => sourceMapKey(left).localeCompare(sourceMapKey(right)))
1362
+ .map((entry) => cloneWireValue(entry))
1363
+ }
1364
+
1365
+ function sortProjection(context: ProjectionContext) {
1366
+ context.nodes.sort((left, right) => left.id.localeCompare(right.id))
1367
+ context.edges.sort((left, right) => left.id.localeCompare(right.id))
1368
+ context.diagnostics.sort((left, right) => left.id.localeCompare(right.id))
1369
+ }
1370
+
1371
+ function freezeNode(node: MutableNode): ProcessPresentationNode {
1372
+ return deepFreeze({
1373
+ ...node,
1374
+ details: freezeDetails(node.details),
1375
+ })
1376
+ }
1377
+
1378
+ function freezeEdge(edge: MutableEdge): ProcessPresentationEdge {
1379
+ return deepFreeze({
1380
+ ...edge,
1381
+ details: freezeDetails(edge.details),
1382
+ })
1383
+ }
1384
+
1385
+ function freezeDetails(details: MutableElementDetails): ProcessPresentationElementDetails {
1386
+ details.definitionReferences.sort((left, right) =>
1387
+ `${left.role}:${definitionReferenceKey(left.reference)}`.localeCompare(
1388
+ `${right.role}:${definitionReferenceKey(right.reference)}`,
1389
+ ))
1390
+ details.valueContracts.sort((left, right) => left.role.localeCompare(right.role))
1391
+ return deepFreeze({
1392
+ definitionReferences: details.definitionReferences,
1393
+ source: details.source,
1394
+ sourceMap: details.sourceMap,
1395
+ sourcePath: details.sourcePath,
1396
+ valueContracts: details.valueContracts,
1397
+ })
1398
+ }
1399
+
1400
+ function readDefinitionReference(raw: unknown): ExecutionDefinitionReference | null {
1401
+ if (!isRecord(raw)) {
1402
+ return null
1403
+ }
1404
+ const definitionId = readString(raw.definitionId)
1405
+ const revisionId = readString(raw.revisionId)
1406
+ const fingerprint = readFingerprint(raw.fingerprint)
1407
+ return definitionId && revisionId && fingerprint
1408
+ ? { definitionId, fingerprint, revisionId }
1409
+ : null
1410
+ }
1411
+
1412
+ function readFingerprint(raw: unknown): ExecutionDefinitionFingerprint | null {
1413
+ if (!isRecord(raw)) {
1414
+ return null
1415
+ }
1416
+ const algorithm = readString(raw.algorithm)
1417
+ const canonicalization = readString(raw.canonicalization)
1418
+ const value = readString(raw.value)
1419
+ return algorithm && canonicalization && value
1420
+ ? { algorithm, canonicalization, value }
1421
+ : null
1422
+ }
1423
+
1424
+ function missingDefinitionReference(
1425
+ metadata: Readonly<Record<string, unknown>>,
1426
+ ): DeepReadonly<ExecutionDefinitionReference> {
1427
+ return deepFreeze({
1428
+ definitionId: readString(metadata.definitionId) ?? '<missing>',
1429
+ fingerprint: {
1430
+ algorithm: '<missing>',
1431
+ canonicalization: '<missing>',
1432
+ value: '<missing>',
1433
+ },
1434
+ revisionId: readString(metadata.revisionId) ?? '<missing>',
1435
+ })
1436
+ }
1437
+
1438
+ function definitionReferenceKey(reference: DeepReadonly<ExecutionDefinitionReference>): string {
1439
+ return [
1440
+ reference.definitionId,
1441
+ reference.revisionId,
1442
+ reference.fingerprint.algorithm,
1443
+ reference.fingerprint.canonicalization,
1444
+ reference.fingerprint.value,
1445
+ ].join('|')
1446
+ }
1447
+
1448
+ function addDiagnostic(
1449
+ diagnostics: ProcessPresentationDiagnostic[],
1450
+ code: ProcessPresentationDiagnosticCode,
1451
+ path: readonly string[],
1452
+ message: string,
1453
+ subject: string | null = null,
1454
+ severity: ProcessPresentationDiagnosticSeverity = 'error',
1455
+ ) {
1456
+ diagnostics.push(deepFreeze({
1457
+ code,
1458
+ id: `diagnostic/${encodeURIComponent(code)}/${encodeURIComponent(formatPath(path))}/${encodeURIComponent(subject ?? '')}`,
1459
+ message,
1460
+ path: [...path],
1461
+ severity,
1462
+ subject,
1463
+ }))
1464
+ }
1465
+
1466
+ function freezeProjectionResult(
1467
+ graph: ProcessPresentationGraph | null,
1468
+ diagnostics: ProcessPresentationDiagnostic[],
1469
+ ): ProcessPresentationProjectionResult {
1470
+ diagnostics.sort((left, right) => left.id.localeCompare(right.id))
1471
+ return deepFreeze({ diagnostics: freezeDiagnostics(diagnostics), graph })
1472
+ }
1473
+
1474
+ function freezeDiagnostics(
1475
+ diagnostics: ProcessPresentationDiagnostic[],
1476
+ ): readonly ProcessPresentationDiagnostic[] {
1477
+ return deepFreeze([...diagnostics])
1478
+ }
1479
+
1480
+ function derivedEdgeId(
1481
+ kind: ProcessPresentationEdgeKind,
1482
+ source: string,
1483
+ target: string,
1484
+ role: string,
1485
+ ) {
1486
+ return `derived/${encodeURIComponent(kind)}/${encodeURIComponent(source)}/${encodeURIComponent(role)}/${encodeURIComponent(target)}`
1487
+ }
1488
+
1489
+ function isCanonicalProcessNodeKind(value: string): value is CanonicalProcessNodeKind {
1490
+ return (canonicalProcessNodeKinds as readonly string[]).includes(value)
1491
+ }
1492
+
1493
+ function isCanonicalProcessAwaitClauseKind(value: string): value is CanonicalProcessAwaitClauseKind {
1494
+ return (canonicalProcessAwaitClauseKinds as readonly string[]).includes(value)
1495
+ }
1496
+
1497
+ function readString(value: unknown): string | null {
1498
+ return typeof value === 'string' && value.trim().length > 0 ? value : null
1499
+ }
1500
+
1501
+ function isStringArray(value: readonly unknown[]): value is readonly string[] {
1502
+ return value.every((item) => typeof item === 'string')
1503
+ }
1504
+
1505
+ function isPrefix(prefix: readonly string[], value: readonly string[]) {
1506
+ return prefix.length <= value.length && prefix.every((segment, index) => segment === value[index])
1507
+ }
1508
+
1509
+ function sourceMapKey(source: ExecutionSourceProvenance) {
1510
+ return `${source.semanticPath?.segments.join('/') ?? ''}|${source.reference}|${source.description ?? ''}`
1511
+ }
1512
+
1513
+ function formatPath(path: readonly string[]) {
1514
+ return path.length === 0
1515
+ ? '/'
1516
+ : `/${path.map((segment) => segment.replaceAll('~', '~0').replaceAll('/', '~1')).join('/')}`
1517
+ }