@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,366 @@
1
+ import type { Db } from '../db/connection.js';
2
+ import {
3
+ type TraversalScopeScheduler,
4
+ type TraversalScopeState,
5
+ } from './010-traversal-scope.js';
6
+
7
+ export type EventSubscriberTransitionStatus =
8
+ | 'resolved'
9
+ | 'ambiguous'
10
+ | 'unresolved';
11
+
12
+ export interface EventSubscriberSymbolTarget {
13
+ symbolId: number;
14
+ kind: string;
15
+ qualifiedName: string;
16
+ repoId: number;
17
+ repoName: string;
18
+ packageName?: string;
19
+ sourceFile: string;
20
+ sourceLine: number;
21
+ endLine: number;
22
+ startOffset?: number;
23
+ endOffset?: number;
24
+ }
25
+
26
+ export interface EventSubscriberTransition {
27
+ graphEdgeId: number;
28
+ graphGeneration: number;
29
+ eventName: string;
30
+ status: EventSubscriberTransitionStatus;
31
+ targetKind: 'symbol' | 'symbol_reference' | 'subscription_handler';
32
+ targetId: string;
33
+ confidence: number;
34
+ unresolvedReason?: string;
35
+ reasonCode?: string;
36
+ subscribeCallId?: number;
37
+ symbolCallId?: number;
38
+ roleSiteMatchCount: number;
39
+ callRole?: string;
40
+ factOrigin?: string;
41
+ associationBasis?: string;
42
+ dispatchScope?: string;
43
+ subscriptionRepoId?: number;
44
+ subscriptionRepoName?: string;
45
+ sourceFile?: string;
46
+ sourceLine?: number;
47
+ callSiteStartOffset?: number;
48
+ callSiteEndOffset?: number;
49
+ wrapperFunction?: string;
50
+ resolutionStrategy?: string;
51
+ associationStatus?: string;
52
+ symbolCallResolutionStatus?: string;
53
+ candidateCount: number;
54
+ symbolCallUnresolvedReason?: string;
55
+ omittedSymbolCallUnresolvedReasonCharacterCount?: number;
56
+ handler?: EventSubscriberSymbolTarget;
57
+ }
58
+
59
+ export interface EventSubscriberTransitionQuery {
60
+ workspaceId: number;
61
+ graphGeneration: number;
62
+ eventName: string;
63
+ }
64
+
65
+ export type EventBodyExpansion =
66
+ | 'scheduled'
67
+ | 'already_scheduled'
68
+ | 'already_expanded'
69
+ | 'cycle_blocked'
70
+ | 'depth_limited'
71
+ | 'not_resolved';
72
+
73
+ export interface PlannedEventSubscriberTransition {
74
+ transition: EventSubscriberTransition;
75
+ node: Record<string, unknown>;
76
+ evidence: Record<string, unknown>;
77
+ bodyExpansion: EventBodyExpansion;
78
+ state?: TraversalScopeState;
79
+ }
80
+
81
+ export function planEventSubscriberTransitions(
82
+ db: Db,
83
+ query: EventSubscriberTransitionQuery,
84
+ scheduler: TraversalScopeScheduler,
85
+ parent: TraversalScopeState,
86
+ depth: number,
87
+ maxDepth: number,
88
+ ): PlannedEventSubscriberTransition[] {
89
+ return loadEventSubscriberTransitions(db, query).map((transition) => {
90
+ const handler = transition.handler;
91
+ if (!handler) return plannedTransition(transition, 'not_resolved');
92
+ if (depth >= maxDepth)
93
+ return plannedTransition(transition, 'depth_limited');
94
+ const state = scheduler.schedule({
95
+ workspaceId: query.workspaceId,
96
+ repoId: handler.repoId,
97
+ files: new Set([handler.sourceFile]),
98
+ symbolIds: new Set([handler.symbolId]),
99
+ context: new Map(),
100
+ }, parent);
101
+ const bodyExpansion: EventBodyExpansion = state.kind === 'scheduled'
102
+ ? 'scheduled'
103
+ : state.kind === 'cycle' ? 'cycle_blocked'
104
+ : state.alreadyExpanded ? 'already_expanded' : 'already_scheduled';
105
+ return plannedTransition(transition, bodyExpansion, state.state);
106
+ });
107
+ }
108
+
109
+ function plannedTransition(
110
+ transition: EventSubscriberTransition,
111
+ bodyExpansion: EventBodyExpansion,
112
+ state?: TraversalScopeState,
113
+ ): PlannedEventSubscriberTransition {
114
+ return {
115
+ transition,
116
+ node: eventSubscriberNode(transition),
117
+ evidence: eventTransitionEvidence(transition, bodyExpansion),
118
+ bodyExpansion,
119
+ state,
120
+ };
121
+ }
122
+
123
+ export function eventSubscriberNode(
124
+ transition: EventSubscriberTransition,
125
+ ): Record<string, unknown> {
126
+ const handler = transition.handler;
127
+ if (!handler) return {
128
+ id: `event_subscription:${transition.graphEdgeId}`,
129
+ kind: transition.targetKind,
130
+ label: `${transition.targetKind}:${transition.targetId}`,
131
+ graphEdgeId: transition.graphEdgeId,
132
+ };
133
+ const fileName = handler.sourceFile.split('/').at(-1) ?? handler.sourceFile;
134
+ return {
135
+ id: `symbol:${handler.symbolId}`,
136
+ kind: 'symbol',
137
+ label: `${fileName}:${handler.qualifiedName}`,
138
+ symbolId: handler.symbolId,
139
+ symbolName: handler.qualifiedName,
140
+ qualifiedName: handler.qualifiedName,
141
+ sourceFile: handler.sourceFile,
142
+ startLine: handler.sourceLine,
143
+ endLine: handler.endLine,
144
+ repoName: handler.repoName,
145
+ repoId: handler.repoId,
146
+ };
147
+ }
148
+
149
+ export function eventTransitionEvidence(
150
+ transition: EventSubscriberTransition,
151
+ bodyExpansion: EventBodyExpansion,
152
+ ): Record<string, unknown> {
153
+ return {
154
+ graphEdgeId: transition.graphEdgeId,
155
+ graphGeneration: transition.graphGeneration,
156
+ subscribeCallId: transition.subscribeCallId,
157
+ symbolCallId: transition.symbolCallId,
158
+ eventName: transition.eventName,
159
+ matchStrategy: 'workspace_exact_event_name',
160
+ dispatchCertainty: 'static_name_only',
161
+ associationBasis: transition.associationBasis,
162
+ dispatchScope: transition.dispatchScope,
163
+ roleSiteMatchCount: transition.roleSiteMatchCount,
164
+ callRole: transition.callRole,
165
+ factOrigin: transition.factOrigin,
166
+ repositoryId: transition.subscriptionRepoId,
167
+ repositoryName: transition.subscriptionRepoName,
168
+ sourceFile: transition.sourceFile,
169
+ sourceLine: transition.sourceLine,
170
+ callSiteStartOffset: transition.callSiteStartOffset,
171
+ callSiteEndOffset: transition.callSiteEndOffset,
172
+ wrapperFunction: transition.wrapperFunction,
173
+ handlerSymbolId: transition.handler?.symbolId,
174
+ handlerSourceFile: transition.handler?.sourceFile,
175
+ handlerSourceLine: transition.handler?.sourceLine,
176
+ associationStatus: transition.associationStatus ?? transition.status,
177
+ symbolCallResolutionStatus: transition.symbolCallResolutionStatus,
178
+ resolutionStatus: transition.status,
179
+ resolutionStrategy: transition.resolutionStrategy,
180
+ candidateCount: transition.candidateCount,
181
+ symbolCallUnresolvedReason: transition.symbolCallUnresolvedReason,
182
+ omittedSymbolCallUnresolvedReasonCharacterCount:
183
+ transition.omittedSymbolCallUnresolvedReasonCharacterCount,
184
+ reasonCode: transition.reasonCode,
185
+ bodyExpansion,
186
+ cycle: bodyExpansion === 'cycle_blocked' || undefined,
187
+ cycleReason: bodyExpansion === 'cycle_blocked'
188
+ ? 'structural_ancestry_cycle' : undefined,
189
+ };
190
+ }
191
+
192
+ export function loadEventSubscriberTransitions(
193
+ db: Db,
194
+ query: EventSubscriberTransitionQuery,
195
+ ): EventSubscriberTransition[] {
196
+ const rows = db.prepare(`SELECT ge.id graphEdgeId,ge.generation graphGeneration,
197
+ ge.from_id eventName,ge.status,ge.to_kind targetKind,ge.to_id targetId,
198
+ ge.confidence,ge.unresolved_reason unresolvedReason,ge.evidence_json evidenceJson,
199
+ subscribe.id subscribeCallId,subscribe.repo_id subscriptionRepoId,
200
+ subscribe.source_file sourceFile,subscribe.source_line sourceLine,
201
+ subscribe.call_site_start_offset callSiteStartOffset,
202
+ subscribe.call_site_end_offset callSiteEndOffset,
203
+ subscription_repo.name subscriptionRepoName,
204
+ handler.id handlerSymbolId,handler.kind handlerKind,
205
+ handler.qualified_name handlerQualifiedName,handler.source_file handlerSourceFile,
206
+ handler.start_line handlerSourceLine,handler.end_line handlerEndLine,
207
+ handler.start_offset handlerStartOffset,handler.end_offset handlerEndOffset,
208
+ handler_repo.id handlerRepoId,handler_repo.name handlerRepoName,
209
+ handler_repo.package_name handlerPackageName
210
+ FROM graph_edges ge
211
+ LEFT JOIN outbound_calls subscribe
212
+ ON subscribe.id=CAST(json_extract(ge.evidence_json,'$.subscribeCallId') AS INTEGER)
213
+ LEFT JOIN repositories subscription_repo
214
+ ON subscription_repo.id=subscribe.repo_id
215
+ AND subscription_repo.workspace_id=ge.workspace_id
216
+ LEFT JOIN symbols handler
217
+ ON ge.to_kind='symbol' AND handler.id=CAST(ge.to_id AS INTEGER)
218
+ LEFT JOIN repositories handler_repo
219
+ ON handler_repo.id=handler.repo_id AND handler_repo.workspace_id=ge.workspace_id
220
+ WHERE ge.workspace_id=? AND ge.generation=?
221
+ AND ge.edge_type='EVENT_SUBSCRIPTION_HANDLED_BY' AND ge.from_kind='event'
222
+ AND ge.from_id COLLATE BINARY=? COLLATE BINARY
223
+ ORDER BY COALESCE(subscription_repo.name,'') COLLATE BINARY,
224
+ COALESCE(subscription_repo.id,0),COALESCE(subscribe.source_file,'') COLLATE BINARY,
225
+ subscribe.call_site_start_offset,subscribe.call_site_end_offset,ge.id`).all(
226
+ query.workspaceId, query.graphGeneration, query.eventName,
227
+ );
228
+ return rows.flatMap((row) => {
229
+ const transition = transitionFromRow(row);
230
+ return transition ? [transition] : [];
231
+ });
232
+ }
233
+
234
+ function transitionFromRow(
235
+ row: Record<string, unknown>,
236
+ ): EventSubscriberTransition | undefined {
237
+ const graphEdgeId = numberValue(row.graphEdgeId);
238
+ const graphGeneration = numberValue(row.graphGeneration);
239
+ const eventName = stringValue(row.eventName);
240
+ const targetId = stringValue(row.targetId);
241
+ if (graphEdgeId === undefined || graphGeneration === undefined
242
+ || eventName === undefined || targetId === undefined) return undefined;
243
+ const evidence = parseEvidence(row.evidenceJson);
244
+ const handler = symbolTarget(row);
245
+ const status = transitionStatus(stringValue(row.status), handler);
246
+ const reasonCode = status === 'resolved'
247
+ ? undefined
248
+ : stringValue(evidence.reasonCode) ?? missingTargetReason(row, handler);
249
+ return {
250
+ graphEdgeId, graphGeneration, eventName, status,
251
+ targetKind: targetKind(row.targetKind), targetId,
252
+ confidence: numberValue(row.confidence) ?? 0,
253
+ unresolvedReason: status === 'resolved'
254
+ ? undefined : stringValue(row.unresolvedReason) ?? reasonCode,
255
+ reasonCode,
256
+ ...associationEvidence(row, evidence),
257
+ handler,
258
+ };
259
+ }
260
+
261
+ function associationEvidence(
262
+ row: Record<string, unknown>,
263
+ evidence: Record<string, unknown>,
264
+ ): Omit<EventSubscriberTransition,
265
+ 'graphEdgeId' | 'graphGeneration' | 'eventName' | 'status' | 'targetKind'
266
+ | 'targetId' | 'confidence' | 'unresolvedReason' | 'reasonCode' | 'handler'> {
267
+ const symbolCallUnresolvedReason = stringValue(
268
+ evidence.symbolCallUnresolvedReason,
269
+ );
270
+ return {
271
+ subscribeCallId: numberValue(row.subscribeCallId),
272
+ symbolCallId: numberValue(evidence.symbolCallId),
273
+ roleSiteMatchCount: nonNegativeCount(evidence.roleSiteMatchCount),
274
+ callRole: stringValue(evidence.callRole),
275
+ factOrigin: stringValue(evidence.factOrigin),
276
+ associationBasis: stringValue(evidence.associationBasis),
277
+ dispatchScope: stringValue(evidence.dispatchScope),
278
+ subscriptionRepoId: numberValue(row.subscriptionRepoId),
279
+ subscriptionRepoName: stringValue(row.subscriptionRepoName),
280
+ sourceFile: stringValue(row.sourceFile),
281
+ sourceLine: numberValue(row.sourceLine),
282
+ callSiteStartOffset: numberValue(row.callSiteStartOffset),
283
+ callSiteEndOffset: numberValue(row.callSiteEndOffset),
284
+ wrapperFunction: stringValue(evidence.wrapperFunction),
285
+ resolutionStrategy: stringValue(evidence.resolutionStrategy),
286
+ associationStatus: stringValue(evidence.associationStatus),
287
+ symbolCallResolutionStatus: stringValue(evidence.symbolCallResolutionStatus),
288
+ candidateCount: nonNegativeCount(evidence.candidateCount),
289
+ symbolCallUnresolvedReason,
290
+ omittedSymbolCallUnresolvedReasonCharacterCount: symbolCallUnresolvedReason
291
+ ? nonNegativeCount(evidence.omittedSymbolCallUnresolvedReasonCharacterCount)
292
+ : undefined,
293
+ };
294
+ }
295
+
296
+ function symbolTarget(
297
+ row: Record<string, unknown>,
298
+ ): EventSubscriberSymbolTarget | undefined {
299
+ const symbolId = numberValue(row.handlerSymbolId);
300
+ const repoId = numberValue(row.handlerRepoId);
301
+ const repoName = stringValue(row.handlerRepoName);
302
+ const sourceFile = stringValue(row.handlerSourceFile);
303
+ const sourceLine = numberValue(row.handlerSourceLine);
304
+ const endLine = numberValue(row.handlerEndLine);
305
+ const kind = stringValue(row.handlerKind);
306
+ const qualifiedName = stringValue(row.handlerQualifiedName);
307
+ if (symbolId === undefined || repoId === undefined || !repoName || !sourceFile
308
+ || sourceLine === undefined || endLine === undefined || !kind || !qualifiedName)
309
+ return undefined;
310
+ return {
311
+ symbolId, repoId, repoName, sourceFile, sourceLine, endLine, kind, qualifiedName,
312
+ packageName: stringValue(row.handlerPackageName),
313
+ startOffset: numberValue(row.handlerStartOffset),
314
+ endOffset: numberValue(row.handlerEndOffset),
315
+ };
316
+ }
317
+
318
+ function transitionStatus(
319
+ value: string | undefined,
320
+ handler: EventSubscriberSymbolTarget | undefined,
321
+ ): EventSubscriberTransitionStatus {
322
+ if (value === 'resolved' && handler) return 'resolved';
323
+ return value === 'ambiguous' ? 'ambiguous' : 'unresolved';
324
+ }
325
+
326
+ function missingTargetReason(
327
+ row: Record<string, unknown>,
328
+ handler: EventSubscriberSymbolTarget | undefined,
329
+ ): string | undefined {
330
+ return stringValue(row.status) === 'resolved' && !handler
331
+ ? 'subscription_handler_target_missing'
332
+ : undefined;
333
+ }
334
+
335
+ function targetKind(
336
+ value: unknown,
337
+ ): EventSubscriberTransition['targetKind'] {
338
+ return value === 'symbol' || value === 'symbol_reference'
339
+ || value === 'subscription_handler' ? value : 'subscription_handler';
340
+ }
341
+
342
+ function parseEvidence(value: unknown): Record<string, unknown> {
343
+ try {
344
+ const parsed: unknown = JSON.parse(String(value ?? '{}'));
345
+ return isRecord(parsed) ? parsed : {};
346
+ } catch {
347
+ return {};
348
+ }
349
+ }
350
+
351
+ function nonNegativeCount(value: unknown): number {
352
+ const count = numberValue(value);
353
+ return count === undefined ? 0 : Math.max(0, Math.floor(count));
354
+ }
355
+
356
+ function numberValue(value: unknown): number | undefined {
357
+ return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
358
+ }
359
+
360
+ function stringValue(value: unknown): string | undefined {
361
+ return typeof value === 'string' ? value : undefined;
362
+ }
363
+
364
+ function isRecord(value: unknown): value is Record<string, unknown> {
365
+ return Boolean(value && typeof value === 'object' && !Array.isArray(value));
366
+ }
@@ -0,0 +1,74 @@
1
+ import type { Db } from '../db/connection.js';
2
+
3
+ export interface TraceGraphEdgeRow extends Record<string, unknown> {
4
+ id: number;
5
+ edge_type: string;
6
+ from_id: string;
7
+ to_kind: string;
8
+ to_id: string;
9
+ confidence: number;
10
+ evidence_json: string;
11
+ unresolved_reason?: string;
12
+ status?: string;
13
+ }
14
+
15
+ export function graphForCalls(
16
+ db: Db,
17
+ callIds: number[],
18
+ ): Map<number, TraceGraphEdgeRow[]> {
19
+ const map = new Map<number, TraceGraphEdgeRow[]>();
20
+ if (callIds.length === 0) return map;
21
+ const rows = db.prepare(`SELECT * FROM graph_edges
22
+ WHERE from_kind='call'
23
+ AND from_id IN (${callIds.map(() => '?').join(',')})
24
+ ORDER BY id`).all(
25
+ ...callIds.map(String),
26
+ ) as TraceGraphEdgeRow[];
27
+ for (const row of rows) {
28
+ const id = Number(row.from_id);
29
+ map.set(id, [...(map.get(id) ?? []), row]);
30
+ }
31
+ return map;
32
+ }
33
+
34
+ export function symbolNode(
35
+ db: Db,
36
+ symbolId: number,
37
+ ): Record<string, unknown> | undefined {
38
+ const row = db.prepare(`SELECT s.id symbolId,s.name symbolName,
39
+ s.qualified_name qualifiedName,s.source_file sourceFile,
40
+ s.start_line startLine,s.end_line endLine,r.name repoName,r.id repoId
41
+ FROM symbols s JOIN repositories r ON r.id=s.repo_id
42
+ WHERE s.id=?`).get(symbolId) as Record<string, unknown> | undefined;
43
+ if (!row) return undefined;
44
+ const sourceFile = String(row.sourceFile ?? '');
45
+ const fileName = sourceFile.split('/').at(-1) ?? sourceFile;
46
+ return {
47
+ id: `symbol:${symbolId}`,
48
+ kind: 'symbol',
49
+ label: `${fileName}:${String(row.qualifiedName ?? row.symbolName)}`,
50
+ ...row,
51
+ };
52
+ }
53
+
54
+ export function operationNode(
55
+ db: Db,
56
+ operationId: string,
57
+ ): Record<string, unknown> | undefined {
58
+ const row = db.prepare(`SELECT o.id operationId,o.operation_name operationName,
59
+ o.operation_type operationType,o.operation_path operationPath,
60
+ o.source_file sourceFile,o.source_line sourceLine,s.id serviceId,
61
+ s.service_name serviceName,s.qualified_name qualifiedName,
62
+ s.service_path servicePath,r.id repoId,r.name repoName
63
+ FROM cds_operations o JOIN cds_services s ON s.id=o.service_id
64
+ JOIN repositories r ON r.id=s.repo_id WHERE o.id=?`).get(
65
+ operationId,
66
+ ) as Record<string, unknown> | undefined;
67
+ if (!row) return undefined;
68
+ return {
69
+ id: `operation:${operationId}`,
70
+ kind: 'operation',
71
+ label: `${String(row.repoName)}:${String(row.servicePath)}${String(row.operationPath)}`,
72
+ ...row,
73
+ };
74
+ }