@saptools/service-flow 0.1.65 → 0.1.66

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +67 -11
  3. package/TECHNICAL-NOTE.md +41 -1
  4. package/dist/{chunk-OONNRIDL.js → chunk-TOVX4WYH.js} +3764 -643
  5. package/dist/chunk-TOVX4WYH.js.map +1 -0
  6. package/dist/cli.js +158 -295
  7. package/dist/cli.js.map +1 -1
  8. package/dist/index.d.ts +196 -1
  9. package/dist/index.js +7 -3
  10. package/dist/index.js.map +1 -1
  11. package/package.json +1 -1
  12. package/src/cli/002-doctor-lifecycle.ts +66 -0
  13. package/src/cli/doctor.ts +14 -27
  14. package/src/cli.ts +130 -102
  15. package/src/db/000-call-fact-repository.ts +537 -0
  16. package/src/db/001-fact-lifecycle.ts +111 -0
  17. package/src/db/migrations.ts +16 -1
  18. package/src/db/repositories.ts +1 -315
  19. package/src/db/schema.ts +4 -2
  20. package/src/index.ts +22 -0
  21. package/src/linker/004-event-subscription-handler-linker.ts +300 -0
  22. package/src/linker/cross-repo-linker.ts +23 -2
  23. package/src/output/001-compact-json-output.ts +6 -0
  24. package/src/parsers/imported-wrapper-parser.ts +4 -0
  25. package/src/parsers/outbound-call-parser.ts +11 -1
  26. package/src/parsers/symbol-parser.ts +7 -4
  27. package/src/trace/010-traversal-scope.ts +188 -0
  28. package/src/trace/011-event-subscriber-traversal.ts +366 -0
  29. package/src/trace/012-trace-graph-lookups.ts +74 -0
  30. package/src/trace/013-trace-root-scopes.ts +279 -0
  31. package/src/trace/014-compact-contract.ts +347 -0
  32. package/src/trace/015-trace-edge-recorder.ts +336 -0
  33. package/src/trace/016-compact-projector.ts +697 -0
  34. package/src/trace/017-trace-context.ts +378 -0
  35. package/src/trace/018-compact-trace.ts +87 -0
  36. package/src/trace/019-trace-edge-semantics.ts +328 -0
  37. package/src/trace/020-compact-field-projection.ts +387 -0
  38. package/src/trace/dynamic-branches.ts +35 -13
  39. package/src/trace/trace-engine.ts +271 -245
  40. package/src/types.ts +10 -0
  41. package/src/version.ts +1 -1
  42. package/dist/chunk-OONNRIDL.js.map +0 -1
@@ -0,0 +1,697 @@
1
+ import type { Db } from '../db/connection.js';
2
+ import { redactText } from '../utils/redaction.js';
3
+ import { compareBinary as binaryCompare } from './010-traversal-scope.js';
4
+ import {
5
+ type CompactDecisionTargetInput,
6
+ type CompactProjectedDiagnostic,
7
+ type CompactDecisionV1,
8
+ type CompactDiagnosticRowV1,
9
+ type CompactEdgeDetailsV1,
10
+ type CompactEdgeObservation,
11
+ type CompactEdgeRowV1,
12
+ type CompactGraphV1,
13
+ type CompactProjectionInput,
14
+ type CompactReferenceGroupV1,
15
+ type CompactReferenceInput,
16
+ type CompactReferencesV1,
17
+ type CompactSemanticEndpoint,
18
+ type CompactSourceSite,
19
+ type CompactStatus,
20
+ } from './014-compact-contract.js';
21
+ import {
22
+ compactCompleteness, compactSafeCode, compactStatusCounts, compactStatusTotal,
23
+ projectCompactDecision, projectCompactDecisionTarget, projectCompactDiagnostics,
24
+ projectCompactQuery, projectCompactStart,
25
+ removeEquivalentCompactPersistedDecision,
26
+ } from './020-compact-field-projection.js';
27
+
28
+ const REFERENCE_LIMIT = 5;
29
+
30
+ interface ResolvedNode {
31
+ key: string;
32
+ kind: string;
33
+ label: string;
34
+ repo?: string;
35
+ file?: string;
36
+ line?: number;
37
+ synthetic: boolean;
38
+ decisionTarget?: string;
39
+ }
40
+
41
+ interface ResolvedObservation {
42
+ input: CompactEdgeObservation;
43
+ source: ResolvedNode;
44
+ target: ResolvedNode;
45
+ decision: CompactDecisionV1;
46
+ }
47
+
48
+ interface EdgeAggregate {
49
+ source: ResolvedNode;
50
+ target: ResolvedNode;
51
+ step: number;
52
+ type: string;
53
+ status: CompactStatus;
54
+ confidence: number;
55
+ decision: CompactDecisionV1;
56
+ ordinals: number[];
57
+ refs: CompactReferenceInput[];
58
+ site?: CompactSourceSite;
59
+ }
60
+
61
+ export function projectCompactGraph(input: CompactProjectionInput): CompactGraphV1 {
62
+ validateObservationOrdinals(input.observations, input.trace.edges.length);
63
+ const resolved = input.observations.map((item) => resolveObservation(input.db, item));
64
+ const diagnostics = projectCompactDiagnostics(input.trace.diagnostics);
65
+ const aggregates = aggregateObservations(resolved);
66
+ const nodes = canonicalNodes(resolved);
67
+ const repos = sortedUnique(nodes.flatMap((node) => node.repo ? [node.repo] : []));
68
+ const files = sortedUnique([
69
+ ...nodes.flatMap((node) => node.file ? [node.file] : []),
70
+ ...diagnostics.flatMap((item) => item.file ? [item.file] : []),
71
+ ]);
72
+ const nodeRows = compactNodeRows(nodes, repos, files);
73
+ const edgeRows = compactEdgeRows(aggregates, nodes);
74
+ const diagnosticRows = compactDiagnosticRows(diagnostics, files);
75
+ const result = compactResult(input, nodes, nodeRows, edgeRows, diagnosticRows, repos, files);
76
+ validateCompactResult(result);
77
+ return result;
78
+ }
79
+
80
+ function resolveObservation(db: Db, input: CompactEdgeObservation): ResolvedObservation {
81
+ const target = resolveEndpoint(db, input.target, input.ordinal, 'target', input.site);
82
+ return {
83
+ input,
84
+ source: resolveEndpoint(db, input.source, input.ordinal, 'source', input.site),
85
+ target,
86
+ decision: observationDecision(db, input, target),
87
+ };
88
+ }
89
+
90
+ function observationDecision(
91
+ db: Db,
92
+ input: CompactEdgeObservation,
93
+ target: ResolvedNode,
94
+ ): CompactDecisionV1 {
95
+ const decision = projectCompactDecision(input.decision);
96
+ if (decision.effectiveResolutionStatus && target.decisionTarget)
97
+ decision.effectiveTarget = target.decisionTarget;
98
+ if (decision.persistedResolutionStatus && input.decision?.persistedTarget) {
99
+ const persisted = persistedDecisionTarget(db, input.decision.persistedTarget);
100
+ if (persisted) decision.persistedTarget = persisted;
101
+ }
102
+ removeEquivalentCompactPersistedDecision(decision);
103
+ return decision;
104
+ }
105
+
106
+ function persistedDecisionTarget(
107
+ db: Db,
108
+ target: CompactDecisionTargetInput,
109
+ ): string | undefined {
110
+ const numeric = numericId(target.id);
111
+ if (target.kind === 'operation' && numeric !== undefined)
112
+ return operationNode(db, numeric)?.decisionTarget;
113
+ if (target.kind === 'symbol' && numeric !== undefined)
114
+ return symbolNode(db, numeric)?.decisionTarget;
115
+ if (target.kind === 'handler_method' && numeric !== undefined)
116
+ return handlerNode(db, numeric)?.decisionTarget;
117
+ return projectCompactDecisionTarget(target.kind, target.id);
118
+ }
119
+
120
+ function resolveEndpoint(
121
+ db: Db,
122
+ endpoint: CompactSemanticEndpoint,
123
+ ordinal: number,
124
+ side: 'source' | 'target',
125
+ site: CompactSourceSite | undefined,
126
+ ): ResolvedNode {
127
+ if (endpoint.kind === 'operation')
128
+ return resolvedOrUnavailable(operationNode(db, endpoint.operationId), side,
129
+ 'operation', ordinal, site);
130
+ if (endpoint.kind === 'symbol')
131
+ return resolvedOrUnavailable(symbolNode(db, endpoint.symbolId), side,
132
+ 'symbol', ordinal, site);
133
+ if (endpoint.kind === 'handler_method')
134
+ return resolvedOrUnavailable(handlerNode(db, endpoint.handlerMethodId), side,
135
+ 'handler_method', ordinal, site);
136
+ if (endpoint.kind === 'event') return eventNode(endpoint.workspaceId, endpoint.eventName);
137
+ if (endpoint.kind === 'target') return targetNode(db, endpoint, ordinal, side, site);
138
+ if (endpoint.kind === 'call_site') return callSiteNode(db, endpoint);
139
+ if (endpoint.kind === 'scope') return scopeNode(db, endpoint);
140
+ return unavailableNode(endpoint.side, endpoint.endpointKind,
141
+ endpoint.detailedEdgeIndex, endpoint.site ?? site);
142
+ }
143
+
144
+ function resolvedOrUnavailable(
145
+ node: ResolvedNode | undefined,
146
+ side: 'source' | 'target',
147
+ kind: string,
148
+ ordinal: number,
149
+ site?: CompactSourceSite,
150
+ ): ResolvedNode {
151
+ return node ?? unavailableNode(side, kind, ordinal, site);
152
+ }
153
+
154
+ function operationNode(db: Db, operationId: number): ResolvedNode | undefined {
155
+ const row = db.prepare(`SELECT o.id,o.operation_name operationName,
156
+ o.operation_path operationPath,o.source_file sourceFile,o.source_line sourceLine,
157
+ s.service_path servicePath,r.workspace_id workspaceId,r.relative_path relativePath,
158
+ r.name repoName
159
+ FROM cds_operations o JOIN cds_services s ON s.id=o.service_id
160
+ JOIN repositories r ON r.id=s.repo_id WHERE o.id=?`).get(operationId);
161
+ if (!row) return undefined;
162
+ const operationPath = stringValue(row.operationPath);
163
+ const servicePath = stringValue(row.servicePath);
164
+ const repo = repositoryLabel(row);
165
+ const workspaceId = numberValue(row.workspaceId);
166
+ if (!operationPath) return undefined;
167
+ if (!servicePath) return undefined;
168
+ if (!repo) return undefined;
169
+ if (workspaceId === undefined) return undefined;
170
+ const operationName = stringValue(row.operationName);
171
+ return {
172
+ key: canonicalKey('operation', workspaceId, repo, servicePath, operationPath),
173
+ kind: 'operation',
174
+ label: `${servicePath}:${operationName || operationPath}`,
175
+ repo,
176
+ file: stringValue(row.sourceFile),
177
+ line: numberValue(row.sourceLine),
178
+ synthetic: false,
179
+ decisionTarget: projectCompactDecisionTarget('operation',
180
+ `${repo}:${servicePath}:${operationPath}`),
181
+ };
182
+ }
183
+
184
+ function symbolNode(db: Db, symbolId: number): ResolvedNode | undefined {
185
+ const row = db.prepare(`SELECT s.id,s.kind,s.qualified_name qualifiedName,
186
+ s.source_file sourceFile,s.start_line startLine,s.start_offset startOffset,
187
+ s.end_offset endOffset,r.workspace_id workspaceId,r.relative_path relativePath,
188
+ r.name repoName FROM symbols s JOIN repositories r ON r.id=s.repo_id
189
+ WHERE s.id=?`).get(symbolId);
190
+ return symbolNodeFromRow(row);
191
+ }
192
+
193
+ function symbolNodeFromRow(row: Record<string, unknown> | undefined): ResolvedNode | undefined {
194
+ if (!row) return undefined;
195
+ const name = stringValue(row.qualifiedName);
196
+ const repo = repositoryLabel(row);
197
+ const file = stringValue(row.sourceFile);
198
+ const workspaceId = numberValue(row.workspaceId);
199
+ if (!name) return undefined;
200
+ if (!repo) return undefined;
201
+ if (!file) return undefined;
202
+ if (workspaceId === undefined) return undefined;
203
+ const startOffset = numberValue(row.startOffset);
204
+ const endOffset = numberValue(row.endOffset);
205
+ return {
206
+ key: canonicalKey('symbol', workspaceId, repo, file,
207
+ startOffset, endOffset, name),
208
+ kind: 'symbol', label: name, repo, file,
209
+ line: numberValue(row.startLine), synthetic: false,
210
+ decisionTarget: projectCompactDecisionTarget('symbol',
211
+ `${repo}:${file}:${startOffset ?? ''}:${endOffset ?? ''}:${name}`),
212
+ };
213
+ }
214
+
215
+ function handlerNode(db: Db, handlerMethodId: number): ResolvedNode | undefined {
216
+ const symbol = db.prepare(`SELECT s.kind,s.qualified_name qualifiedName,
217
+ s.source_file sourceFile,s.start_line startLine,s.start_offset startOffset,
218
+ s.end_offset endOffset,r.workspace_id workspaceId,r.relative_path relativePath,
219
+ r.name repoName
220
+ FROM handler_methods hm JOIN handler_classes hc ON hc.id=hm.handler_class_id
221
+ JOIN repositories r ON r.id=hc.repo_id LEFT JOIN symbols s
222
+ ON s.repo_id=hc.repo_id AND s.source_file=hc.source_file
223
+ AND s.qualified_name=hc.class_name || '.' || hm.method_name
224
+ AND s.start_line=hm.source_line WHERE hm.id=?
225
+ ORDER BY s.id LIMIT 1`).get(handlerMethodId);
226
+ const resolved = symbolNodeFromRow(symbol);
227
+ return resolved ?? standaloneHandlerNode(db, handlerMethodId);
228
+ }
229
+
230
+ function standaloneHandlerNode(db: Db, handlerMethodId: number): ResolvedNode | undefined {
231
+ const row = db.prepare(`SELECT hm.method_name methodName,hm.source_file sourceFile,
232
+ hm.source_line sourceLine,hc.class_name className,r.workspace_id workspaceId,
233
+ r.relative_path relativePath,r.name repoName FROM handler_methods hm
234
+ JOIN handler_classes hc ON hc.id=hm.handler_class_id
235
+ JOIN repositories r ON r.id=hc.repo_id WHERE hm.id=?`).get(handlerMethodId);
236
+ if (!row) return undefined;
237
+ const methodName = stringValue(row.methodName);
238
+ const className = stringValue(row.className);
239
+ const repo = repositoryLabel(row);
240
+ const file = stringValue(row.sourceFile);
241
+ const workspaceId = numberValue(row.workspaceId);
242
+ if (!methodName) return undefined;
243
+ if (!className) return undefined;
244
+ if (!repo) return undefined;
245
+ if (!file) return undefined;
246
+ if (workspaceId === undefined) return undefined;
247
+ const sourceLine = numberValue(row.sourceLine);
248
+ return {
249
+ key: canonicalKey('handler_method', workspaceId, repo, file,
250
+ sourceLine, className, methodName),
251
+ kind: 'handler_method', label: `${className}.${methodName}`, repo, file,
252
+ line: sourceLine, synthetic: false,
253
+ decisionTarget: projectCompactDecisionTarget('handler_method',
254
+ `${repo}:${file}:${sourceLine ?? ''}:${className}.${methodName}`),
255
+ };
256
+ }
257
+
258
+ function eventNode(workspaceId: number, eventName: string): ResolvedNode {
259
+ return {
260
+ key: canonicalKey('event', workspaceId, eventName),
261
+ kind: 'event', label: eventName, synthetic: false,
262
+ decisionTarget: projectCompactDecisionTarget('event', eventName),
263
+ };
264
+ }
265
+
266
+ function targetNode(
267
+ db: Db,
268
+ endpoint: Extract<CompactSemanticEndpoint, { kind: 'target' }>,
269
+ ordinal: number,
270
+ side: 'source' | 'target',
271
+ site: CompactSourceSite | undefined,
272
+ ): ResolvedNode {
273
+ const linked = linkedTargetNode(db, endpoint.targetKind, endpoint.targetId);
274
+ if (linked !== undefined)
275
+ return linked ?? unavailableNode(side, endpoint.targetKind, ordinal, site);
276
+ const repo = endpoint.repositoryId === undefined
277
+ ? undefined : repositoryById(db, endpoint.repositoryId);
278
+ return {
279
+ key: canonicalKey('target', endpoint.workspaceId, repo,
280
+ endpoint.targetKind, endpoint.targetId),
281
+ kind: compactTargetKind(endpoint.targetKind),
282
+ label: endpoint.targetId || endpoint.targetKind,
283
+ repo,
284
+ synthetic: false,
285
+ decisionTarget: projectCompactDecisionTarget(endpoint.targetKind, endpoint.targetId),
286
+ };
287
+ }
288
+
289
+ function linkedTargetNode(
290
+ db: Db,
291
+ kind: string,
292
+ id: string,
293
+ ): ResolvedNode | null | undefined {
294
+ const numeric = numericId(id);
295
+ if (kind === 'operation') {
296
+ if (numeric === undefined) return null;
297
+ return operationNode(db, numeric) ?? null;
298
+ }
299
+ if (kind === 'symbol') {
300
+ if (numeric === undefined) return null;
301
+ return symbolNode(db, numeric) ?? null;
302
+ }
303
+ if (kind === 'handler_method') {
304
+ if (numeric === undefined) return null;
305
+ return handlerNode(db, numeric) ?? null;
306
+ }
307
+ return undefined;
308
+ }
309
+
310
+ function callSiteNode(
311
+ db: Db,
312
+ endpoint: Extract<CompactSemanticEndpoint, { kind: 'call_site' }>,
313
+ ): ResolvedNode {
314
+ const repo = repositoryById(db, endpoint.repositoryId) ?? endpoint.repositoryName;
315
+ const span = endpoint.startOffset === undefined || endpoint.endOffset === undefined
316
+ ? ['line', endpoint.sourceLine] : [endpoint.startOffset, endpoint.endOffset];
317
+ return {
318
+ key: canonicalKey('call_site', endpoint.workspaceId, repo,
319
+ endpoint.sourceFile, ...span, endpoint.callId),
320
+ kind: 'call_site',
321
+ label: `${repo}:${endpoint.sourceFile}:${endpoint.sourceLine}`,
322
+ repo, file: endpoint.sourceFile, line: endpoint.sourceLine, synthetic: false,
323
+ decisionTarget: projectCompactDecisionTarget('call_site',
324
+ `${repo}:${endpoint.sourceFile}:${span.join(':')}`),
325
+ };
326
+ }
327
+
328
+ function scopeNode(
329
+ db: Db,
330
+ endpoint: Extract<CompactSemanticEndpoint, { kind: 'scope' }>,
331
+ ): ResolvedNode {
332
+ const repo = endpoint.repositoryId === undefined
333
+ ? undefined : repositoryById(db, endpoint.repositoryId);
334
+ const files = sortedUnique(endpoint.sourceFiles);
335
+ const symbols = sortedUnique(endpoint.symbolIds.flatMap((id) => {
336
+ const key = symbolNode(db, id)?.key;
337
+ return key ? [key] : [];
338
+ }));
339
+ const identity = canonicalKey('scope', endpoint.workspaceId, repo, files, symbols);
340
+ return {
341
+ key: symbols.length > 0 || files.length > 0 ? identity
342
+ : canonicalKey('scope', endpoint.workspaceId, repo, endpoint.structuralKey),
343
+ kind: 'scope',
344
+ label: repo ? `scope:${repo}` : 'scope:workspace',
345
+ repo, file: files.length === 1 ? files[0] : undefined,
346
+ synthetic: false,
347
+ decisionTarget: projectCompactDecisionTarget('scope', identity),
348
+ };
349
+ }
350
+
351
+ function unavailableNode(
352
+ side: 'source' | 'target',
353
+ endpointKind: string,
354
+ ordinal: number,
355
+ site?: CompactSourceSite,
356
+ ): ResolvedNode {
357
+ return {
358
+ key: canonicalKey('unavailable', side, endpointKind,
359
+ site?.repository, site?.sourceFile, site?.startOffset,
360
+ site?.endOffset, site?.sourceLine, ordinal),
361
+ kind: 'synthetic',
362
+ label: `${side}:${compactSafeCode(endpointKind) ?? 'unavailable'}`,
363
+ repo: site?.repository,
364
+ file: site?.sourceFile,
365
+ line: site?.sourceLine,
366
+ synthetic: true,
367
+ };
368
+ }
369
+
370
+ function aggregateObservations(items: ResolvedObservation[]): EdgeAggregate[] {
371
+ const groups = new Map<string, EdgeAggregate>();
372
+ for (const item of items) {
373
+ const key = aggregationKey(item);
374
+ const current = groups.get(key);
375
+ if (current) appendAggregate(current, item);
376
+ else groups.set(key, createAggregate(item));
377
+ }
378
+ return [...groups.values()].map(finalizeAggregate);
379
+ }
380
+
381
+ function aggregationKey(item: ResolvedObservation): string {
382
+ return JSON.stringify([
383
+ item.input.step, item.input.type, item.source.key, item.target.key,
384
+ item.input.status, normalizedConfidence(item.input.confidence), item.decision,
385
+ ]);
386
+ }
387
+
388
+ function createAggregate(item: ResolvedObservation): EdgeAggregate {
389
+ return {
390
+ source: item.source, target: item.target, step: item.input.step,
391
+ type: item.input.type, status: item.input.status,
392
+ confidence: normalizedConfidence(item.input.confidence),
393
+ decision: item.decision, ordinals: [item.input.ordinal],
394
+ refs: item.input.refs ? [item.input.refs] : [], site: item.input.site,
395
+ };
396
+ }
397
+
398
+ function appendAggregate(group: EdgeAggregate, item: ResolvedObservation): void {
399
+ group.ordinals.push(item.input.ordinal);
400
+ if (item.input.refs) group.refs.push(item.input.refs);
401
+ if (compareSite(item.input.site, group.site) < 0) group.site = item.input.site;
402
+ }
403
+
404
+ function finalizeAggregate(group: EdgeAggregate): EdgeAggregate {
405
+ group.ordinals.sort((left, right) => left - right);
406
+ return group;
407
+ }
408
+
409
+ function canonicalNodes(items: ResolvedObservation[]): ResolvedNode[] {
410
+ const nodes = new Map<string, ResolvedNode>();
411
+ for (const node of items.flatMap((item) => [item.source, item.target])) {
412
+ const existing = nodes.get(node.key);
413
+ if (!existing || compareNodeBody(node, existing) < 0) nodes.set(node.key, node);
414
+ }
415
+ return [...nodes.values()].sort((left, right) => binaryCompare(left.key, right.key));
416
+ }
417
+
418
+ function compactNodeRows(
419
+ nodes: ResolvedNode[],
420
+ repos: string[],
421
+ files: string[],
422
+ ): CompactGraphV1['nodes'] {
423
+ const repoIndexes = indexMap(repos);
424
+ const fileIndexes = indexMap(files);
425
+ return nodes.map((node, index) => [
426
+ `n${index}`, node.kind, redactText(node.label),
427
+ node.repo === undefined ? null : repoIndexes.get(node.repo) ?? null,
428
+ node.file === undefined ? null : fileIndexes.get(node.file) ?? null,
429
+ node.line ?? null,
430
+ ]);
431
+ }
432
+
433
+ function compactEdgeRows(
434
+ groups: EdgeAggregate[],
435
+ nodes: ResolvedNode[],
436
+ ): CompactGraphV1['edges'] {
437
+ const nodeIndexes = new Map(nodes.map((node, index) => [node.key, index]));
438
+ const sorted = [...groups].sort((left, right) => compareAggregate(left, right, nodeIndexes));
439
+ return sorted.map((group, index) => edgeRow(group, index, nodeIndexes));
440
+ }
441
+
442
+ function edgeRow(
443
+ group: EdgeAggregate,
444
+ index: number,
445
+ nodeIndexes: Map<string, number>,
446
+ ): CompactEdgeRowV1 {
447
+ const source = nodeIndexes.get(group.source.key);
448
+ const target = nodeIndexes.get(group.target.key);
449
+ if (source === undefined || target === undefined) throw compactError('edge_node_missing');
450
+ return [
451
+ `e${index}`, group.ordinals, group.step, group.type, `n${source}`, `n${target}`,
452
+ group.status, group.confidence, group.ordinals.length, edgeDetails(group),
453
+ ];
454
+ }
455
+
456
+ function edgeDetails(group: EdgeAggregate): CompactEdgeDetailsV1 | null {
457
+ const refs = projectReferences(group.refs);
458
+ if (Object.keys(group.decision).length === 0 && Object.keys(refs).length === 0) return null;
459
+ return { decision: group.decision, refs };
460
+ }
461
+
462
+ function projectReferences(values: CompactReferenceInput[]): CompactReferencesV1 {
463
+ const out: CompactReferencesV1 = {};
464
+ setReference(out, 'graphEdgeIds', values.flatMap((item) => item.graphEdgeIds ?? []));
465
+ setReference(out, 'outboundCallIds', values.flatMap((item) => item.outboundCallIds ?? []));
466
+ setReference(out, 'subscribeCallIds', values.flatMap((item) => item.subscribeCallIds ?? []));
467
+ setReference(out, 'symbolCallIds', values.flatMap((item) => item.symbolCallIds ?? []));
468
+ setReference(out, 'operationIds', values.flatMap((item) => item.operationIds ?? []));
469
+ setReference(out, 'symbolIds', values.flatMap((item) => item.symbolIds ?? []));
470
+ setReference(out, 'handlerMethodIds', values.flatMap((item) => item.handlerMethodIds ?? []));
471
+ return out;
472
+ }
473
+
474
+ function setReference(
475
+ out: CompactReferencesV1,
476
+ key: keyof CompactReferencesV1,
477
+ values: Array<number | string>,
478
+ ): void {
479
+ const unique = uniqueReferences(values);
480
+ if (unique.length === 0) return;
481
+ const shown = unique.slice(0, REFERENCE_LIMIT);
482
+ out[key] = {
483
+ values: shown, total: unique.length, shown: shown.length,
484
+ omitted: unique.length - shown.length,
485
+ } satisfies CompactReferenceGroupV1;
486
+ }
487
+
488
+ function compactDiagnosticRows(
489
+ diagnostics: CompactProjectedDiagnostic[],
490
+ files: string[],
491
+ ): CompactDiagnosticRowV1[] {
492
+ const fileIndexes = indexMap(files);
493
+ return diagnostics.map((item) => [
494
+ item.index, item.severity, item.code, item.message,
495
+ item.file === undefined ? null : fileIndexes.get(item.file) ?? null,
496
+ item.line ?? null, item.details ?? null,
497
+ ]);
498
+ }
499
+
500
+ function compactResult(
501
+ input: CompactProjectionInput,
502
+ resolvedNodes: ResolvedNode[],
503
+ nodes: CompactGraphV1['nodes'],
504
+ edges: CompactGraphV1['edges'],
505
+ diagnostics: CompactGraphV1['diagnostics'],
506
+ repos: string[],
507
+ files: string[],
508
+ ): CompactGraphV1 {
509
+ const statusCounts = compactStatusCounts(input.observations);
510
+ return {
511
+ schema: 'service-flow/compact-graph@1',
512
+ start: projectCompactStart(input.start),
513
+ query: projectCompactQuery(input.options),
514
+ source: input.source,
515
+ summary: {
516
+ completeness: compactCompleteness(statusCounts, diagnostics),
517
+ fullTraceNodes: input.trace.nodes.length,
518
+ fullTraceEdges: input.trace.edges.length,
519
+ fullTraceDiagnostics: input.trace.diagnostics.length,
520
+ nodes: nodes.length, edges: edges.length,
521
+ collapsedEdges: input.trace.edges.length - edges.length,
522
+ statusCounts,
523
+ projection: {
524
+ evidence: 'summary-only',
525
+ syntheticEndpoints: resolvedNodes.filter((node) => node.synthetic).length,
526
+ omittedUnreferencedFullNodes: omittedDetailedNodeCount(input),
527
+ },
528
+ },
529
+ repos, files,
530
+ nodeColumns: ['id', 'kind', 'label', 'repo', 'file', 'line'],
531
+ nodes,
532
+ edgeColumns: ['id', 'traceOrdinals', 'step', 'type', 'from', 'to',
533
+ 'status', 'confidence', 'count', 'details'],
534
+ edges,
535
+ diagnosticColumns: ['fullDiagnosticIndex', 'severity', 'code', 'message',
536
+ 'file', 'line', 'details'],
537
+ diagnostics,
538
+ };
539
+ }
540
+
541
+ function omittedDetailedNodeCount(input: CompactProjectionInput): number {
542
+ const referenced = new Set(input.observations.flatMap((item) => [
543
+ ...detailedNodeIds(item.source), ...detailedNodeIds(item.target),
544
+ ]));
545
+ return input.trace.nodes.filter((node) => {
546
+ const id = typeof node.id === 'string' ? node.id : undefined;
547
+ return id === undefined || !referenced.has(id);
548
+ }).length;
549
+ }
550
+
551
+ function detailedNodeIds(endpoint: CompactSemanticEndpoint): string[] {
552
+ if (endpoint.kind === 'operation') return [`operation:${endpoint.operationId}`];
553
+ if (endpoint.kind === 'symbol') return [`symbol:${endpoint.symbolId}`];
554
+ if (endpoint.kind === 'handler_method')
555
+ return [`handler_method:${endpoint.handlerMethodId}`];
556
+ if (endpoint.kind === 'event') return [`event:${endpoint.eventName}`];
557
+ if (endpoint.kind === 'target') return [`${endpoint.targetKind}:${endpoint.targetId}`];
558
+ if (endpoint.kind === 'call_site') return [`call:${endpoint.callId}`];
559
+ if (endpoint.kind === 'scope')
560
+ return endpoint.symbolIds.map((symbolId) => `symbol:${symbolId}`);
561
+ return [];
562
+ }
563
+
564
+ function validateObservationOrdinals(
565
+ observations: CompactEdgeObservation[],
566
+ fullEdgeCount: number,
567
+ ): void {
568
+ const ordinals = observations.map((item) => item.ordinal).sort((left, right) => left - right);
569
+ if (ordinals.length !== fullEdgeCount) throw compactError('observation_count_mismatch');
570
+ if (ordinals.some((value, index) => value !== index))
571
+ throw compactError('trace_ordinal_partition_invalid');
572
+ }
573
+
574
+ function validateCompactResult(result: CompactGraphV1): void {
575
+ if (result.summary.nodes !== result.nodes.length) throw compactError('node_count_mismatch');
576
+ if (result.summary.edges !== result.edges.length) throw compactError('edge_count_mismatch');
577
+ const statusTotal = compactStatusTotal(result.summary.statusCounts);
578
+ if (statusTotal !== result.summary.fullTraceEdges) throw compactError('status_count_mismatch');
579
+ const edgeTotal = result.edges.reduce((sum, edge) => sum + edge[8], 0);
580
+ if (edgeTotal !== result.summary.fullTraceEdges) throw compactError('edge_member_count_mismatch');
581
+ if (result.edges.some((edge) => edge.length !== 10 || edge[8] !== edge[1].length))
582
+ throw compactError('edge_tuple_invalid');
583
+ if (result.nodes.some((node) => node.length !== 6)) throw compactError('node_tuple_invalid');
584
+ if (result.diagnostics.some((item) => item.length !== 7)) throw compactError('diagnostic_tuple_invalid');
585
+ validateResultOrdinals(result);
586
+ }
587
+
588
+ function validateResultOrdinals(result: CompactGraphV1): void {
589
+ const ordinals = result.edges.flatMap((edge) => edge[1]).sort((left, right) => left - right);
590
+ if (ordinals.some((value, index) => value !== index)
591
+ || ordinals.length !== result.summary.fullTraceEdges)
592
+ throw compactError('output_trace_ordinal_partition_invalid');
593
+ if (result.summary.collapsedEdges !== result.summary.fullTraceEdges - result.summary.edges)
594
+ throw compactError('collapsed_edge_count_mismatch');
595
+ }
596
+
597
+ function compareAggregate(
598
+ left: EdgeAggregate,
599
+ right: EdgeAggregate,
600
+ nodeIndexes: Map<string, number>,
601
+ ): number {
602
+ return left.step - right.step
603
+ || indexFor(nodeIndexes, left.source.key) - indexFor(nodeIndexes, right.source.key)
604
+ || indexFor(nodeIndexes, left.target.key) - indexFor(nodeIndexes, right.target.key)
605
+ || binaryCompare(left.type, right.type)
606
+ || binaryCompare(left.status, right.status)
607
+ || compareSite(left.site, right.site)
608
+ || (left.ordinals[0] ?? 0) - (right.ordinals[0] ?? 0);
609
+ }
610
+
611
+ function compareSite(left: CompactSourceSite | undefined, right: CompactSourceSite | undefined): number {
612
+ return binaryCompare(siteSortKey(left), siteSortKey(right));
613
+ }
614
+
615
+ function siteSortKey(site: CompactSourceSite | undefined): string {
616
+ return JSON.stringify([
617
+ site?.repository ?? '', site?.sourceFile ?? '',
618
+ sortableNumber(site?.startOffset), sortableNumber(site?.endOffset),
619
+ sortableNumber(site?.sourceLine),
620
+ ]);
621
+ }
622
+
623
+ function sortableNumber(value: number | undefined): string {
624
+ return value === undefined ? 'z' : `n${String(value).padStart(16, '0')}`;
625
+ }
626
+
627
+ function compareNodeBody(left: ResolvedNode, right: ResolvedNode): number {
628
+ return binaryCompare(JSON.stringify([
629
+ left.kind, left.label, left.repo, left.file, left.line,
630
+ ]), JSON.stringify([
631
+ right.kind, right.label, right.repo, right.file, right.line,
632
+ ]));
633
+ }
634
+
635
+ function compactTargetKind(kind: string): string {
636
+ if (kind === 'db_entity') return 'database_entity';
637
+ if (kind === 'operation_candidate') return 'dynamic_target';
638
+ if (kind === 'symbol_reference' || kind === 'subscription_handler')
639
+ return 'unresolved_target';
640
+ return compactSafeCode(kind) ?? 'target';
641
+ }
642
+
643
+ function repositoryById(db: Db, repositoryId: number): string | undefined {
644
+ const row = db.prepare('SELECT relative_path relativePath,name repoName FROM repositories WHERE id=?')
645
+ .get(repositoryId);
646
+ return repositoryLabel(row);
647
+ }
648
+
649
+ function repositoryLabel(row: Record<string, unknown> | undefined): string | undefined {
650
+ return stringValue(row?.relativePath) ?? stringValue(row?.repoName);
651
+ }
652
+
653
+ function normalizedConfidence(value: number): number {
654
+ return Number.isFinite(value) ? Math.max(0, Math.min(1, value)) : 0;
655
+ }
656
+
657
+ function numericId(value: string): number | undefined {
658
+ return /^\d+$/.test(value) ? Number(value) : undefined;
659
+ }
660
+
661
+ function numberValue(value: unknown): number | undefined {
662
+ return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
663
+ }
664
+
665
+ function stringValue(value: unknown): string | undefined {
666
+ return typeof value === 'string' ? value : undefined;
667
+ }
668
+
669
+ function uniqueReferences(values: Array<number | string>): Array<number | string> {
670
+ const unique = new Map(values.map((value) => [`${typeof value}:${String(value)}`, value]));
671
+ return [...unique.values()].sort((left, right) => {
672
+ if (typeof left === 'number' && typeof right === 'number') return left - right;
673
+ return binaryCompare(`${typeof left}:${String(left)}`, `${typeof right}:${String(right)}`);
674
+ });
675
+ }
676
+
677
+ function sortedUnique(values: string[]): string[] {
678
+ return [...new Set(values)].sort(binaryCompare);
679
+ }
680
+
681
+ function canonicalKey(...parts: unknown[]): string {
682
+ return JSON.stringify(parts);
683
+ }
684
+
685
+ function indexMap(values: string[]): Map<string, number> {
686
+ return new Map(values.map((value, index) => [value, index]));
687
+ }
688
+
689
+ function indexFor(values: Map<string, number>, key: string): number {
690
+ const value = values.get(key);
691
+ if (value === undefined) throw compactError('canonical_node_index_missing');
692
+ return value;
693
+ }
694
+
695
+ function compactError(code: string): Error {
696
+ return new Error(`compact_graph_invariant:${code}`);
697
+ }