@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.
- package/LICENSE +55 -0
- package/README.md +29 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/process-presentation-graph.d.ts +83 -0
- package/dist/process-presentation-graph.d.ts.map +1 -0
- package/dist/process-presentation-graph.js +883 -0
- package/dist/process-presentation-graph.js.map +1 -0
- package/package.json +50 -0
- package/src/index.ts +1 -0
- package/src/process-presentation-graph.ts +1558 -0
|
@@ -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
|
+
}
|