@saptools/service-flow 0.1.67 → 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.
- package/CHANGELOG.md +10 -0
- package/README.md +20 -8
- package/TECHNICAL-NOTE.md +27 -0
- package/dist/{chunk-ZQABU7MR.js → chunk-AEM4JY22.js} +12676 -6714
- package/dist/chunk-AEM4JY22.js.map +1 -0
- package/dist/cli.js +2325 -413
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +61 -2
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/src/cli/003-doctor-package-resolution.ts +68 -0
- package/src/cli/doctor.ts +25 -14
- package/src/db/000-call-fact-repository.ts +475 -342
- package/src/db/001-fact-lifecycle.ts +60 -30
- package/src/db/002-fact-json-inventory.ts +169 -0
- package/src/db/003-current-fact-semantics.ts +698 -0
- package/src/db/004-package-target-invalidation.ts +173 -0
- package/src/db/005-schema-structure.ts +201 -0
- package/src/db/006-relative-symbol-resolution.ts +443 -0
- package/src/db/007-package-fact-semantics.ts +573 -0
- package/src/db/008-relative-fact-semantics.ts +207 -0
- package/src/db/009-binding-fact-semantics.ts +347 -0
- package/src/db/010-package-symbol-surface-semantics.ts +320 -0
- package/src/db/011-symbol-call-semantics.ts +144 -0
- package/src/db/012-binding-reference-proof.ts +264 -0
- package/src/db/migrations.ts +16 -3
- package/src/db/repositories.ts +113 -6
- package/src/db/schema.ts +4 -2
- package/src/index.ts +12 -0
- package/src/indexer/repository-indexer.ts +72 -11
- package/src/indexer/workspace-indexer.ts +123 -32
- package/src/linker/003-package-import-symbol-resolver.ts +363 -131
- package/src/linker/004-event-subscription-handler-linker.ts +2 -0
- package/src/linker/005-odata-path-structure.ts +371 -0
- package/src/linker/cross-repo-linker.ts +2 -1
- package/src/linker/odata-path-normalizer.ts +273 -180
- package/src/linker/service-resolver.ts +197 -77
- package/src/parsers/002-symbol-import-bindings.ts +516 -0
- package/src/parsers/003-package-public-surface.ts +661 -0
- package/src/parsers/004-fact-identity.ts +108 -0
- package/src/parsers/005-event-subscription-facts.ts +281 -0
- package/src/parsers/006-binding-identity.ts +343 -0
- package/src/parsers/007-source-fact-reconciliation.ts +105 -0
- package/src/parsers/008-package-surface-publication.ts +82 -0
- package/src/parsers/009-symbol-call-facts.ts +528 -0
- package/src/parsers/010-package-public-surface-analysis.ts +352 -0
- package/src/parsers/011-binding-lexical-scope.ts +583 -0
- package/src/parsers/012-package-fact-contract.ts +306 -0
- package/src/parsers/013-executable-body-eligibility.ts +35 -0
- package/src/parsers/014-service-binding-helper-flow.ts +240 -0
- package/src/parsers/015-service-binding-collector.ts +693 -0
- package/src/parsers/016-local-symbol-reference.ts +261 -0
- package/src/parsers/017-symbol-derived-contexts.ts +268 -0
- package/src/parsers/018-package-commonjs-syntax.ts +142 -0
- package/src/parsers/019-binding-assignment-targets.ts +76 -0
- package/src/parsers/020-stable-local-value.ts +217 -0
- package/src/parsers/021-binding-visibility.ts +152 -0
- package/src/parsers/operation-path-analysis.ts +6 -1
- package/src/parsers/outbound-call-parser.ts +19 -6
- package/src/parsers/package-json-parser.ts +45 -3
- package/src/parsers/service-binding-parser-helpers.ts +86 -15
- package/src/parsers/service-binding-parser.ts +147 -597
- package/src/parsers/symbol-parser.ts +482 -353
- package/src/trace/002-trace-diagnostics.ts +36 -1
- package/src/trace/016-compact-projector.ts +16 -17
- package/src/trace/020-compact-field-projection.ts +48 -37
- package/src/trace/021-compact-decision-normalization.ts +105 -0
- package/src/trace/022-trace-fact-preflight.ts +21 -0
- package/src/trace/023-nested-event-scopes.ts +23 -0
- package/src/trace/024-compact-observation-decision.ts +76 -0
- package/src/trace/025-trace-implementation-scope.ts +123 -0
- package/src/trace/026-trace-start-scope.ts +335 -0
- package/src/trace/027-trace-scope-execution.ts +566 -0
- package/src/trace/028-trace-operation-execution.ts +336 -0
- package/src/trace/029-trace-start-implementation.ts +172 -0
- package/src/trace/trace-engine.ts +122 -624
- package/src/types.ts +56 -0
- package/src/utils/001-placeholders.ts +188 -10
- package/src/version.ts +1 -1
- 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 {
|
|
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
|
+
}
|