@saptools/service-flow 0.1.67 → 0.1.69

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 (101) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +27 -9
  3. package/TECHNICAL-NOTE.md +36 -0
  4. package/dist/chunk-3N3B5KHV.js +19596 -0
  5. package/dist/chunk-3N3B5KHV.js.map +1 -0
  6. package/dist/cli.js +2645 -521
  7. package/dist/cli.js.map +1 -1
  8. package/dist/index.d.ts +67 -2
  9. package/dist/index.js +1 -1
  10. package/package.json +1 -1
  11. package/src/cli/001-index-summary.ts +22 -0
  12. package/src/cli/003-doctor-package-resolution.ts +68 -0
  13. package/src/cli/doctor.ts +25 -14
  14. package/src/cli.ts +151 -87
  15. package/src/db/000-call-fact-repository.ts +499 -340
  16. package/src/db/001-fact-lifecycle.ts +60 -30
  17. package/src/db/002-fact-json-inventory.ts +169 -0
  18. package/src/db/003-current-fact-semantics.ts +699 -0
  19. package/src/db/004-package-target-invalidation.ts +183 -0
  20. package/src/db/005-schema-structure.ts +201 -0
  21. package/src/db/006-relative-symbol-resolution.ts +464 -0
  22. package/src/db/007-package-fact-semantics.ts +573 -0
  23. package/src/db/008-relative-fact-semantics.ts +210 -0
  24. package/src/db/009-binding-fact-semantics.ts +352 -0
  25. package/src/db/010-package-symbol-surface-semantics.ts +320 -0
  26. package/src/db/011-symbol-call-semantics.ts +144 -0
  27. package/src/db/012-binding-reference-proof.ts +268 -0
  28. package/src/db/013-index-publication-failure.ts +91 -0
  29. package/src/db/014-binding-helper-provenance.ts +17 -0
  30. package/src/db/migrations.ts +16 -3
  31. package/src/db/repositories.ts +130 -6
  32. package/src/db/schema.ts +4 -2
  33. package/src/index.ts +12 -0
  34. package/src/indexer/cds-extension-resolver.ts +27 -3
  35. package/src/indexer/repository-indexer.ts +135 -13
  36. package/src/indexer/workspace-indexer.ts +237 -34
  37. package/src/linker/003-package-import-symbol-resolver.ts +363 -131
  38. package/src/linker/004-event-subscription-handler-linker.ts +34 -11
  39. package/src/linker/005-odata-path-structure.ts +371 -0
  40. package/src/linker/006-event-template-link.ts +72 -0
  41. package/src/linker/007-call-edge-insertion.ts +568 -0
  42. package/src/linker/cross-repo-linker.ts +4 -166
  43. package/src/linker/odata-path-normalizer.ts +273 -180
  44. package/src/linker/service-resolver.ts +197 -77
  45. package/src/parsers/000-direct-query-execution.ts +11 -0
  46. package/src/parsers/002-symbol-import-bindings.ts +516 -0
  47. package/src/parsers/003-package-public-surface.ts +661 -0
  48. package/src/parsers/004-fact-identity.ts +108 -0
  49. package/src/parsers/005-event-subscription-facts.ts +281 -0
  50. package/src/parsers/006-binding-identity.ts +348 -0
  51. package/src/parsers/007-source-fact-reconciliation.ts +105 -0
  52. package/src/parsers/008-package-surface-publication.ts +82 -0
  53. package/src/parsers/009-symbol-call-facts.ts +528 -0
  54. package/src/parsers/010-package-public-surface-analysis.ts +352 -0
  55. package/src/parsers/011-binding-lexical-scope.ts +583 -0
  56. package/src/parsers/012-package-fact-contract.ts +306 -0
  57. package/src/parsers/013-executable-body-eligibility.ts +35 -0
  58. package/src/parsers/014-service-binding-helper-flow.ts +306 -0
  59. package/src/parsers/015-service-binding-collector.ts +693 -0
  60. package/src/parsers/016-local-symbol-reference.ts +261 -0
  61. package/src/parsers/017-symbol-derived-contexts.ts +268 -0
  62. package/src/parsers/018-package-commonjs-syntax.ts +142 -0
  63. package/src/parsers/019-binding-assignment-targets.ts +76 -0
  64. package/src/parsers/020-stable-local-value.ts +217 -0
  65. package/src/parsers/021-binding-visibility.ts +168 -0
  66. package/src/parsers/022-outbound-expression-analysis.ts +700 -0
  67. package/src/parsers/023-outbound-call-classifier.ts +692 -0
  68. package/src/parsers/operation-path-analysis.ts +6 -1
  69. package/src/parsers/outbound-call-parser.ts +162 -512
  70. package/src/parsers/package-json-parser.ts +45 -3
  71. package/src/parsers/service-binding-parser-helpers.ts +86 -15
  72. package/src/parsers/service-binding-parser.ts +147 -597
  73. package/src/parsers/symbol-parser.ts +513 -352
  74. package/src/trace/002-trace-diagnostics.ts +36 -1
  75. package/src/trace/007-implementation-start-diagnostic.ts +1 -0
  76. package/src/trace/011-event-subscriber-traversal.ts +100 -8
  77. package/src/trace/013-trace-root-scopes.ts +2 -3
  78. package/src/trace/014-compact-contract.ts +6 -0
  79. package/src/trace/015-trace-edge-recorder.ts +61 -4
  80. package/src/trace/016-compact-projector.ts +15 -17
  81. package/src/trace/019-trace-edge-semantics.ts +6 -10
  82. package/src/trace/020-compact-field-projection.ts +122 -38
  83. package/src/trace/021-compact-decision-normalization.ts +171 -0
  84. package/src/trace/022-trace-fact-preflight.ts +21 -0
  85. package/src/trace/023-nested-event-scopes.ts +23 -0
  86. package/src/trace/024-compact-observation-decision.ts +81 -0
  87. package/src/trace/025-trace-implementation-scope.ts +123 -0
  88. package/src/trace/026-trace-start-scope.ts +336 -0
  89. package/src/trace/027-trace-scope-execution.ts +566 -0
  90. package/src/trace/028-trace-operation-execution.ts +336 -0
  91. package/src/trace/029-trace-start-implementation.ts +172 -0
  92. package/src/trace/030-event-runtime-resolution.ts +151 -0
  93. package/src/trace/031-local-call-expansion.ts +37 -0
  94. package/src/trace/implementation-hints.ts +9 -6
  95. package/src/trace/selectors.ts +1 -0
  96. package/src/trace/trace-engine.ts +122 -624
  97. package/src/types.ts +57 -0
  98. package/src/utils/001-placeholders.ts +188 -10
  99. package/src/version.ts +1 -1
  100. package/dist/chunk-ZQABU7MR.js +0 -12151
  101. package/dist/chunk-ZQABU7MR.js.map +0 -1
@@ -0,0 +1,566 @@
1
+ import type { Db } from '../db/connection.js';
2
+ import type { TraceEdge, TraceOptions } from '../types.js';
3
+ import {
4
+ baseTraceEvidence,
5
+ edgeTarget,
6
+ runtimeResolution,
7
+ type TraceGraphRow,
8
+ } from './evidence.js';
9
+ import { dynamicCandidateBranches } from './dynamic-branches.js';
10
+ import {
11
+ contextualRuntimeResolution,
12
+ type ContextBinding,
13
+ type ContextualGraphRow,
14
+ type ContextualRuntimeResolution,
15
+ } from './008-contextual-runtime-state.js';
16
+ import {
17
+ TraversalScopeScheduler,
18
+ type TraversalScopeState,
19
+ } from './010-traversal-scope.js';
20
+ import type { PlannedEventSubscriberTransition } from './011-event-subscriber-traversal.js';
21
+ import {
22
+ graphForCalls,
23
+ operationNode,
24
+ symbolNode,
25
+ type TraceGraphEdgeRow,
26
+ } from './012-trace-graph-lookups.js';
27
+ import {
28
+ enqueueCausalScope,
29
+ nextPendingRoot,
30
+ type PendingTraceRootScope,
31
+ type TraceQueueScope,
32
+ } from './013-trace-root-scopes.js';
33
+ import type { CompactSemanticEndpoint } from './014-compact-contract.js';
34
+ import { TraceEdgeRecorder } from './015-trace-edge-recorder.js';
35
+ import {
36
+ knownBindingsForCalls,
37
+ knownBindingsForScope,
38
+ parseTraceEvidence,
39
+ receiverFromTraceEvidence,
40
+ } from './017-trace-context.js';
41
+ import {
42
+ recordCycleObservation,
43
+ recordDynamicBranchObservation,
44
+ recordEventBridgeObservation,
45
+ recordEventCycleObservation,
46
+ recordLocalCallObservation,
47
+ recordOutboundObservation,
48
+ } from './019-trace-edge-semantics.js';
49
+ import { outboundScopeSymbolIds } from './023-nested-event-scopes.js';
50
+ import type { ImplementationHintOptions } from './025-trace-implementation-scope.js';
51
+ import { processOperationTarget } from './028-trace-operation-execution.js';
52
+ import { runtimeEventResolution, runtimeEventSubscriberPlans } from './030-event-runtime-resolution.js';
53
+ import { planLocalCallExpansion } from './031-local-call-expansion.js';
54
+
55
+ export interface CallRow extends Record<string, unknown> {
56
+ id: number;
57
+ repo_id: number;
58
+ repoName: string;
59
+ source_file: string;
60
+ source_line: number;
61
+ call_type: string;
62
+ confidence: number;
63
+ source_symbol_id?: number;
64
+ workspaceId: number;
65
+ graphGeneration: number;
66
+ }
67
+
68
+ export interface EffectiveOutbound {
69
+ evidence: Record<string, unknown>;
70
+ row: TraceGraphRow;
71
+ to: string;
72
+ semanticWorkspaceId: number;
73
+ semantic: {
74
+ source: CompactSemanticEndpoint;
75
+ target: CompactSemanticEndpoint;
76
+ };
77
+ }
78
+
79
+ export interface TraceExecutionRuntime {
80
+ db: Db;
81
+ options: TraceOptions;
82
+ hintOptions: ImplementationHintOptions;
83
+ workspaceId?: number;
84
+ maxDepth: number;
85
+ scheduler: TraversalScopeScheduler;
86
+ queue: TraceQueueScope[];
87
+ pendingRoots: PendingTraceRootScope[];
88
+ diagnostics: Array<Record<string, unknown>>;
89
+ nodes: Map<string, Record<string, unknown>>;
90
+ recorder: TraceEdgeRecorder;
91
+ }
92
+
93
+ function traceEdgeType(call: CallRow, row: TraceGraphRow): string {
94
+ if (row.to_kind === 'operation'
95
+ && row.edge_type === 'REMOTE_CALL_RESOLVES_TO_OPERATION')
96
+ return 'remote_action';
97
+ if (row.to_kind === 'operation'
98
+ && row.edge_type === 'LOCAL_CALL_RESOLVES_TO_OPERATION')
99
+ return 'local_service_call';
100
+ return String(call.call_type);
101
+ }
102
+
103
+ function includeCall(type: string, options: TraceOptions): boolean {
104
+ if (!options.includeDb && type === 'local_db_query') return false;
105
+ if (!options.includeExternal && type === 'external_http') return false;
106
+ if (!options.includeAsync && type.startsWith('async_')) return false;
107
+ return true;
108
+ }
109
+
110
+ function nextTraceScope(
111
+ runtime: TraceExecutionRuntime,
112
+ ): TraceQueueScope | undefined {
113
+ const depth = runtime.queue[0]?.depth ?? Number.POSITIVE_INFINITY;
114
+ if (depth > 1 && runtime.workspaceId !== undefined) {
115
+ const root = nextPendingRoot(
116
+ runtime.pendingRoots, runtime.scheduler, runtime.workspaceId,
117
+ );
118
+ if (root) runtime.queue.unshift(root);
119
+ }
120
+ return runtime.queue.shift();
121
+ }
122
+
123
+ export function executeTraceScopes(runtime: TraceExecutionRuntime): void {
124
+ while (runtime.queue.length > 0 || runtime.pendingRoots.length > 0) {
125
+ const current = nextTraceScope(runtime);
126
+ if (!current || current.depth > runtime.maxDepth) continue;
127
+ if (!runtime.scheduler.markExpanded(current.state)) continue;
128
+ processTraceScope(runtime, current);
129
+ }
130
+ }
131
+
132
+ function processTraceScope(
133
+ runtime: TraceExecutionRuntime,
134
+ current: TraceQueueScope,
135
+ ): void {
136
+ const calls = callsForScope(runtime, current);
137
+ const bindings = callerBindings(runtime.db, current, calls);
138
+ processLocalCalls(runtime, current, bindings);
139
+ const graph = graphForCalls(
140
+ runtime.db, calls.map((call) => Number(call.id)),
141
+ );
142
+ for (const call of calls)
143
+ processOutboundCall(runtime, current, bindings, call,
144
+ graph.get(Number(call.id)) ?? []);
145
+ }
146
+
147
+ function callsForScope(
148
+ runtime: TraceExecutionRuntime,
149
+ current: TraceQueueScope,
150
+ ): CallRow[] {
151
+ const rows = runtime.db.prepare(`SELECT c.*,r.name repoName,
152
+ r.workspace_id workspaceId,r.graph_generation graphGeneration
153
+ FROM outbound_calls c JOIN repositories r ON r.id=c.repo_id
154
+ WHERE (? IS NULL OR c.repo_id=?) AND (? IS NULL OR r.workspace_id=?)
155
+ ORDER BY c.source_file COLLATE BINARY,c.call_site_start_offset,
156
+ c.call_site_end_offset,c.source_line,c.id`).all(
157
+ current.repoId, current.repoId,
158
+ runtime.workspaceId, runtime.workspaceId,
159
+ ) as CallRow[];
160
+ const symbols = outboundScopeSymbolIds(
161
+ runtime.db, current.symbolIds, Boolean(runtime.options.includeAsync),
162
+ );
163
+ return rows.filter((call) =>
164
+ callInScope(call, current, symbols)
165
+ && includeCall(String(call.call_type), runtime.options));
166
+ }
167
+
168
+ function callInScope(
169
+ call: CallRow,
170
+ current: TraceQueueScope,
171
+ symbols: ReadonlySet<number> | undefined,
172
+ ): boolean {
173
+ const ownerMatches = current.unownedOnly
174
+ ? call.source_symbol_id == null
175
+ : !symbols || symbols.has(Number(call.source_symbol_id));
176
+ return ownerMatches
177
+ && (!current.files || current.files.has(String(call.source_file)));
178
+ }
179
+
180
+ function callerBindings(
181
+ db: Db,
182
+ current: TraceQueueScope,
183
+ calls: CallRow[],
184
+ ): Map<string, ContextBinding> {
185
+ return new Map<string, ContextBinding>([
186
+ ...current.context,
187
+ ...knownBindingsForScope(
188
+ db, current.repoId, current.symbolIds, current.files,
189
+ ),
190
+ ...knownBindingsForCalls(db, calls),
191
+ ]);
192
+ }
193
+
194
+ function localCallRows(
195
+ db: Db,
196
+ symbolIds: ReadonlySet<number>,
197
+ ): Array<Record<string, unknown>> {
198
+ const ids = [...symbolIds];
199
+ return db.prepare(`SELECT sc.*,s.repo_id calleeRepoId,
200
+ s.source_file calleeFile FROM symbol_calls sc
201
+ LEFT JOIN symbols s ON s.id=sc.callee_symbol_id
202
+ WHERE sc.call_role='ordinary_call'
203
+ AND sc.caller_symbol_id IN (${ids.map(() => '?').join(',')})
204
+ ORDER BY sc.source_file COLLATE BINARY,sc.call_site_start_offset,
205
+ sc.call_site_end_offset,sc.source_line,sc.id`).all(
206
+ ...ids,
207
+ ) as Array<Record<string, unknown>>;
208
+ }
209
+
210
+ function processLocalCalls(
211
+ runtime: TraceExecutionRuntime,
212
+ current: TraceQueueScope,
213
+ bindings: Map<string, ContextBinding>,
214
+ ): void {
215
+ if (current.rootObservationOnly || !current.symbolIds
216
+ || current.symbolIds.size === 0 || current.depth >= runtime.maxDepth) return;
217
+ for (const row of localCallRows(runtime.db, current.symbolIds))
218
+ processLocalCall(runtime, current, bindings, row);
219
+ }
220
+
221
+ function processLocalCall(
222
+ runtime: TraceExecutionRuntime,
223
+ current: TraceQueueScope,
224
+ bindings: Map<string, ContextBinding>,
225
+ row: Record<string, unknown>,
226
+ ): void {
227
+ const symbolId = row.callee_symbol_id
228
+ ? Number(row.callee_symbol_id) : undefined;
229
+ const node = symbolId === undefined
230
+ ? undefined : symbolNode(runtime.db, symbolId);
231
+ if (node) runtime.nodes.set(String(node.id), node);
232
+ const evidence = localCallEvidence(row, node);
233
+ const unresolvedReason = localCallUnresolvedReason(row);
234
+ const edge = localTraceEdge(current.depth, row, node, evidence, unresolvedReason);
235
+ const target = recordLocalCallObservation(runtime.recorder, edge, {
236
+ symbolCall: row, evidence, unresolvedReason,
237
+ });
238
+ if (symbolId === undefined || row.status !== 'resolved' || !node) return;
239
+ const { repoId, files, symbols, context, scheduling } =
240
+ planLocalCallExpansion(
241
+ runtime.db, runtime.scheduler, runtime.workspaceId, current.state,
242
+ row, bindings, symbolId,
243
+ );
244
+ if (scheduling.kind === 'cycle')
245
+ recordLocalCycle(runtime, current, row, target, scheduling.state,
246
+ repoId, files, symbols);
247
+ if (scheduling.kind === 'scheduled')
248
+ enqueueCausalScope(runtime.queue, runtime.pendingRoots, {
249
+ repoId, files, symbolIds: symbols, depth: current.depth + 1,
250
+ context, state: scheduling.state,
251
+ });
252
+ }
253
+
254
+ function localCallEvidence(
255
+ row: Record<string, unknown>,
256
+ node: Record<string, unknown> | undefined,
257
+ ): Record<string, unknown> {
258
+ return {
259
+ ...parseTraceEvidence(row.evidence_json),
260
+ sourceFile: row.source_file,
261
+ sourceLine: row.source_line,
262
+ calleeSymbolId: row.callee_symbol_id,
263
+ calleeSymbolName: node?.symbolName,
264
+ calleeSymbolFile: node?.sourceFile,
265
+ resolutionStatus: row.status,
266
+ };
267
+ }
268
+
269
+ function localCallUnresolvedReason(
270
+ row: Record<string, unknown>,
271
+ ): string | undefined {
272
+ if (String(row.status) === 'resolved') return undefined;
273
+ return row.unresolved_reason ? String(row.unresolved_reason) : undefined;
274
+ }
275
+
276
+ function localTraceEdge(
277
+ depth: number,
278
+ row: Record<string, unknown>,
279
+ node: Record<string, unknown> | undefined,
280
+ evidence: Record<string, unknown>,
281
+ unresolvedReason: string | undefined,
282
+ ): TraceEdge {
283
+ return {
284
+ step: depth,
285
+ type: 'local_symbol_call',
286
+ from: String(row.callee_expression),
287
+ to: node?.label
288
+ ? String(node.label)
289
+ : `${String(row.status)}:${String(row.callee_expression)}`,
290
+ evidence,
291
+ confidence: Number(row.confidence ?? 0.8),
292
+ unresolvedReason,
293
+ };
294
+ }
295
+
296
+ function recordLocalCycle(
297
+ runtime: TraceExecutionRuntime,
298
+ current: TraceQueueScope,
299
+ row: Record<string, unknown>,
300
+ target: CompactSemanticEndpoint,
301
+ state: TraversalScopeState,
302
+ repoId: number,
303
+ files: Set<string>,
304
+ symbols: Set<number>,
305
+ ): void {
306
+ const evidence = {
307
+ cycle: true,
308
+ cycleReason: 'structural_ancestry_cycle',
309
+ symbolCallId: row.id,
310
+ };
311
+ const edge: TraceEdge = {
312
+ step: current.depth,
313
+ type: 'cycle',
314
+ from: String(row.callee_expression),
315
+ to: state.structuralKey,
316
+ evidence,
317
+ confidence: 1,
318
+ unresolvedReason:
319
+ 'Cycle detected in structural ancestry; downstream symbol was not expanded',
320
+ };
321
+ recordCycleObservation(runtime.recorder, edge, target, {
322
+ workspaceId: runtime.workspaceId,
323
+ repositoryId: repoId,
324
+ sourceFiles: files,
325
+ symbolIds: symbols,
326
+ structuralKey: state.structuralKey,
327
+ }, { symbolCallId: row.id, symbolId: row.callee_symbol_id }, row);
328
+ }
329
+
330
+ function processOutboundCall(
331
+ runtime: TraceExecutionRuntime,
332
+ current: TraceQueueScope,
333
+ bindings: Map<string, ContextBinding>,
334
+ call: CallRow,
335
+ persistedRows: TraceGraphEdgeRow[],
336
+ ): void {
337
+ recordCallNode(runtime.nodes, call);
338
+ const receiver = receiverFromTraceEvidence(call.evidence_json) ?? '';
339
+ const contextual = contextualRuntimeResolution(
340
+ runtime.db, call, bindings.get(receiver), call.workspaceId, persistedRows,
341
+ );
342
+ const rows = contextual.row ? [contextual.row] : persistedRows;
343
+ for (const row of rows)
344
+ processOutboundRow(runtime, current, call, { ...row }, contextual);
345
+ }
346
+
347
+ function recordCallNode(
348
+ nodes: Map<string, Record<string, unknown>>,
349
+ call: CallRow,
350
+ ): void {
351
+ const id = `call:${call.id}`;
352
+ nodes.set(id, {
353
+ id,
354
+ kind: 'outbound_call',
355
+ repo: call.repoName,
356
+ file: call.source_file,
357
+ line: call.source_line,
358
+ callType: call.call_type,
359
+ });
360
+ }
361
+
362
+ function processOutboundRow(
363
+ runtime: TraceExecutionRuntime,
364
+ current: TraceQueueScope,
365
+ call: CallRow,
366
+ row: TraceGraphRow | ContextualGraphRow,
367
+ contextual: ContextualRuntimeResolution,
368
+ ): void {
369
+ const graphRow: TraceGraphRow = { ...row };
370
+ const effective = recordEffectiveOutbound(
371
+ runtime, current, call, graphRow, contextual,
372
+ );
373
+ processEventTransitions(runtime, current, call, effective);
374
+ recordDynamicBranches(runtime, current, call, effective);
375
+ if (effective.row.to_kind === 'operation')
376
+ processOperationTarget(runtime, current, call, effective);
377
+ }
378
+
379
+ function recordEffectiveOutbound(
380
+ runtime: TraceExecutionRuntime,
381
+ current: TraceQueueScope,
382
+ call: CallRow,
383
+ row: TraceGraphRow,
384
+ contextual: ContextualRuntimeResolution,
385
+ ): EffectiveOutbound {
386
+ const persisted = parseTraceEvidence(row.evidence_json);
387
+ const raw = baseTraceEvidence(row, call, persisted, contextual.evidence);
388
+ const effective = runtimeEventResolution(row, raw, runtime.options.vars)
389
+ ?? runtimeResolution(runtime.db, row, raw, {
390
+ vars: runtime.options.vars,
391
+ dynamicMode: runtime.options.dynamicMode ?? 'strict',
392
+ maxDynamicCandidates: runtime.options.maxDynamicCandidates,
393
+ }, call.workspaceId, contextual.state);
394
+ const target = `${effective.row.to_kind}:${effective.row.to_id}`;
395
+ const operation = effective.row.to_kind === 'operation'
396
+ ? operationNode(runtime.db, effective.row.to_id) : undefined;
397
+ runtime.nodes.set(target, operation ?? targetNode(target, effective.row));
398
+ const to = edgeTarget(effective.row, effective.evidence);
399
+ const edge = outboundTraceEdge(
400
+ current.depth, call, effective.row, to,
401
+ effective.evidence, effective.unresolvedReason,
402
+ );
403
+ const semanticWorkspaceId = runtime.workspaceId ?? call.workspaceId;
404
+ const semantic = recordOutboundObservation(runtime.recorder, edge, {
405
+ call,
406
+ row: effective.row,
407
+ evidence: effective.evidence,
408
+ workspaceId: semanticWorkspaceId,
409
+ dynamicMode: runtime.options.dynamicMode,
410
+ unresolvedReason: effective.unresolvedReason,
411
+ });
412
+ return {
413
+ evidence: effective.evidence,
414
+ row: effective.row,
415
+ to,
416
+ semanticWorkspaceId,
417
+ semantic,
418
+ };
419
+ }
420
+
421
+ function targetNode(
422
+ id: string,
423
+ row: TraceGraphRow,
424
+ ): Record<string, unknown> {
425
+ return {
426
+ id,
427
+ kind: row.to_kind,
428
+ label: row.to_kind === 'db_entity'
429
+ ? `Entity: ${row.to_id || 'unknown'}` : row.to_id,
430
+ };
431
+ }
432
+
433
+ function outboundTraceEdge(
434
+ depth: number,
435
+ call: CallRow,
436
+ row: TraceGraphRow,
437
+ to: string,
438
+ evidence: Record<string, unknown>,
439
+ unresolvedReason: string | undefined,
440
+ ): TraceEdge {
441
+ return {
442
+ step: depth,
443
+ type: traceEdgeType(call, row),
444
+ from: `${call.repoName}:${call.source_file}:${call.source_line}`,
445
+ to,
446
+ evidence,
447
+ confidence: Number(row.confidence ?? call.confidence),
448
+ unresolvedReason,
449
+ };
450
+ }
451
+
452
+ function processEventTransitions(
453
+ runtime: TraceExecutionRuntime,
454
+ current: TraceQueueScope,
455
+ call: CallRow,
456
+ effective: EffectiveOutbound,
457
+ ): void {
458
+ if (!runtime.options.includeAsync || call.call_type !== 'async_emit'
459
+ || effective.row.edge_type !== 'HANDLER_EMITS_EVENT'
460
+ || effective.row.to_kind !== 'event') return;
461
+ const planned = runtimeEventSubscriberPlans(
462
+ runtime.db, {
463
+ workspaceId: runtime.workspaceId ?? call.workspaceId,
464
+ graphGeneration: call.graphGeneration,
465
+ eventName: effective.row.to_id,
466
+ vars: runtime.options.vars ?? {},
467
+ }, runtime.scheduler, current.state, current.depth, runtime.maxDepth,
468
+ );
469
+ if (planned.diagnostic) runtime.diagnostics.push(planned.diagnostic);
470
+ for (const plan of planned.plans)
471
+ recordEventTransition(runtime, current, plan,
472
+ effective.semanticWorkspaceId, planned.plans.length);
473
+ }
474
+
475
+ function recordEventTransition(
476
+ runtime: TraceExecutionRuntime,
477
+ current: TraceQueueScope,
478
+ plan: PlannedEventSubscriberTransition,
479
+ workspaceId: number,
480
+ subscriptionCount: number,
481
+ ): void {
482
+ const nodeId = String(plan.node.id);
483
+ const targetLabel = String(plan.node.label ?? nodeId);
484
+ runtime.nodes.set(nodeId, plan.node);
485
+ const bridge: TraceEdge = {
486
+ step: current.depth,
487
+ type: 'event_name_matches_subscription_handler',
488
+ from: plan.transition.eventName,
489
+ to: targetLabel,
490
+ evidence: plan.evidence,
491
+ confidence: plan.transition.confidence,
492
+ unresolvedReason: plan.transition.unresolvedReason,
493
+ };
494
+ const target = recordEventBridgeObservation(
495
+ runtime.recorder, bridge, plan, workspaceId, subscriptionCount,
496
+ );
497
+ if (plan.bodyExpansion === 'cycle_blocked' && plan.state)
498
+ recordEventCycle(runtime, current, plan, target, targetLabel, workspaceId);
499
+ if (plan.bodyExpansion === 'scheduled' && plan.state
500
+ && plan.transition.handler)
501
+ enqueueEventHandler(runtime, current, plan);
502
+ }
503
+
504
+ function recordEventCycle(
505
+ runtime: TraceExecutionRuntime,
506
+ current: TraceQueueScope,
507
+ plan: PlannedEventSubscriberTransition,
508
+ target: CompactSemanticEndpoint,
509
+ targetLabel: string,
510
+ workspaceId: number,
511
+ ): void {
512
+ if (!plan.state) return;
513
+ const evidence = {
514
+ cycle: true,
515
+ cycleReason: 'structural_ancestry_cycle',
516
+ graphEdgeId: plan.transition.graphEdgeId,
517
+ };
518
+ const edge: TraceEdge = {
519
+ step: current.depth,
520
+ type: 'cycle',
521
+ from: targetLabel,
522
+ to: plan.state.structuralKey,
523
+ evidence,
524
+ confidence: 1,
525
+ unresolvedReason:
526
+ 'Cycle detected across an event subscriber boundary; downstream symbol was not expanded',
527
+ };
528
+ recordEventCycleObservation(
529
+ runtime.recorder, edge, plan, target, workspaceId,
530
+ );
531
+ }
532
+
533
+ function enqueueEventHandler(
534
+ runtime: TraceExecutionRuntime,
535
+ current: TraceQueueScope,
536
+ plan: PlannedEventSubscriberTransition,
537
+ ): void {
538
+ const handler = plan.transition.handler;
539
+ if (!handler || !plan.state) return;
540
+ enqueueCausalScope(runtime.queue, runtime.pendingRoots, {
541
+ repoId: handler.repoId,
542
+ files: new Set([handler.sourceFile]),
543
+ symbolIds: new Set([handler.symbolId]),
544
+ depth: current.depth + 1,
545
+ context: new Map(),
546
+ state: plan.state,
547
+ });
548
+ }
549
+
550
+ function recordDynamicBranches(
551
+ runtime: TraceExecutionRuntime,
552
+ current: TraceQueueScope,
553
+ call: CallRow,
554
+ effective: EffectiveOutbound,
555
+ ): void {
556
+ if ((runtime.options.dynamicMode ?? 'strict') !== 'candidates'
557
+ || effective.row.status === 'resolved') return;
558
+ for (const branch of dynamicCandidateBranches(
559
+ current.depth, call, effective.evidence,
560
+ )) {
561
+ recordDynamicBranchObservation(
562
+ runtime.recorder, branch, call, effective.semantic.source,
563
+ effective.evidence, effective.semanticWorkspaceId,
564
+ );
565
+ }
566
+ }