@cohesivesystems/processes-presentation 0.1.0-alpha.63

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