@saptools/service-flow 0.1.66 → 0.1.68

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