@saptools/service-flow 0.1.64 → 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.
- package/CHANGELOG.md +18 -0
- package/README.md +67 -11
- package/TECHNICAL-NOTE.md +41 -1
- package/dist/{chunk-TBH33OYC.js → chunk-TOVX4WYH.js} +3765 -643
- package/dist/chunk-TOVX4WYH.js.map +1 -0
- package/dist/cli.js +231 -292
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +196 -1
- package/dist/index.js +7 -3
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/002-doctor-lifecycle.ts +66 -0
- package/src/cli/doctor.ts +14 -27
- package/src/cli.ts +130 -102
- package/src/db/000-call-fact-repository.ts +537 -0
- package/src/db/001-fact-lifecycle.ts +111 -0
- package/src/db/migrations.ts +16 -1
- package/src/db/repositories.ts +1 -315
- package/src/db/schema.ts +4 -2
- package/src/index.ts +22 -0
- package/src/linker/004-event-subscription-handler-linker.ts +300 -0
- package/src/linker/cross-repo-linker.ts +23 -2
- package/src/output/001-compact-json-output.ts +6 -0
- package/src/parsers/imported-wrapper-parser.ts +4 -0
- package/src/parsers/outbound-call-parser.ts +11 -1
- package/src/parsers/symbol-parser.ts +110 -2
- package/src/trace/010-traversal-scope.ts +188 -0
- package/src/trace/011-event-subscriber-traversal.ts +366 -0
- package/src/trace/012-trace-graph-lookups.ts +74 -0
- package/src/trace/013-trace-root-scopes.ts +279 -0
- package/src/trace/014-compact-contract.ts +347 -0
- package/src/trace/015-trace-edge-recorder.ts +336 -0
- package/src/trace/016-compact-projector.ts +697 -0
- package/src/trace/017-trace-context.ts +378 -0
- package/src/trace/018-compact-trace.ts +87 -0
- package/src/trace/019-trace-edge-semantics.ts +328 -0
- package/src/trace/020-compact-field-projection.ts +387 -0
- package/src/trace/dynamic-branches.ts +35 -13
- package/src/trace/trace-engine.ts +271 -245
- package/src/types.ts +10 -0
- package/src/version.ts +1 -1
- package/dist/chunk-TBH33OYC.js.map +0 -1
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { Db } from '../db/connection.js';
|
|
2
2
|
import { reposByName } from '../db/repositories.js';
|
|
3
|
-
import { extractPlaceholders } from '../linker/dynamic-edge-resolver.js';
|
|
4
3
|
import type { ImplementationHint, TraceEdge, TraceOptions, TraceResult, TraceStart } from '../types.js';
|
|
5
4
|
import { baseTraceEvidence, edgeTarget, runtimeNoCandidateDiagnostics, runtimeResolution, runtimeVariableDiagnostic, type TraceGraphRow } from './evidence.js';
|
|
6
5
|
import { dynamicCandidateBranches } from './dynamic-branches.js';
|
|
@@ -23,7 +22,38 @@ import {
|
|
|
23
22
|
withSelectedHandlerProvenance,
|
|
24
23
|
type SelectedHandlerEvidence,
|
|
25
24
|
} from './009-selected-handler-provenance.js';
|
|
26
|
-
import {
|
|
25
|
+
import { schemaLifecycleDiagnostic } from '../db/001-fact-lifecycle.js';
|
|
26
|
+
import { TraversalScopeScheduler } from './010-traversal-scope.js';
|
|
27
|
+
import { planEventSubscriberTransitions } from './011-event-subscriber-traversal.js';
|
|
28
|
+
import {
|
|
29
|
+
graphForCalls,
|
|
30
|
+
operationNode,
|
|
31
|
+
symbolNode,
|
|
32
|
+
type TraceGraphEdgeRow as GraphRow,
|
|
33
|
+
} from './012-trace-graph-lookups.js';
|
|
34
|
+
import {
|
|
35
|
+
createTraceRootPlan,
|
|
36
|
+
enqueueCausalScope,
|
|
37
|
+
nextPendingRoot,
|
|
38
|
+
} from './013-trace-root-scopes.js';
|
|
39
|
+
import {
|
|
40
|
+
contextForSymbolCall,
|
|
41
|
+
knownBindingsForCalls,
|
|
42
|
+
knownBindingsForScope,
|
|
43
|
+
parseTraceEvidence as parseEvidence,
|
|
44
|
+
receiverFromTraceEvidence as receiverFromEvidence,
|
|
45
|
+
} from './017-trace-context.js';
|
|
46
|
+
import type { CompactTraceObserver } from './014-compact-contract.js';
|
|
47
|
+
import { TraceEdgeRecorder } from './015-trace-edge-recorder.js';
|
|
48
|
+
import {
|
|
49
|
+
recordCycleObservation,
|
|
50
|
+
recordDynamicBranchObservation,
|
|
51
|
+
recordEventBridgeObservation,
|
|
52
|
+
recordEventCycleObservation,
|
|
53
|
+
recordImplementationObservation,
|
|
54
|
+
recordLocalCallObservation,
|
|
55
|
+
recordOutboundObservation,
|
|
56
|
+
} from './019-trace-edge-semantics.js';
|
|
27
57
|
import {
|
|
28
58
|
ambiguousStartDiagnostic,
|
|
29
59
|
selectorNotFoundDiagnostic,
|
|
@@ -54,22 +84,21 @@ interface CallRow extends Record<string, unknown> {
|
|
|
54
84
|
call_type: string;
|
|
55
85
|
confidence: number;
|
|
56
86
|
source_symbol_id?: number;
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
id: number;
|
|
60
|
-
edge_type: string;
|
|
61
|
-
from_id: string;
|
|
62
|
-
to_kind: string;
|
|
63
|
-
to_id: string;
|
|
64
|
-
confidence: number;
|
|
65
|
-
evidence_json: string;
|
|
66
|
-
unresolved_reason?: string;
|
|
67
|
-
status?: string;
|
|
87
|
+
workspaceId: number;
|
|
88
|
+
graphGeneration: number;
|
|
68
89
|
}
|
|
69
90
|
interface ImplementationHintOptions {
|
|
70
91
|
implementationRepo?: string;
|
|
71
92
|
implementationHints?: ImplementationHint[];
|
|
72
93
|
}
|
|
94
|
+
const compactObserverKey = Symbol('service-flow.compact-trace-observer');
|
|
95
|
+
type ObservedTraceOptions = TraceOptions & {
|
|
96
|
+
[compactObserverKey]?: CompactTraceObserver;
|
|
97
|
+
};
|
|
98
|
+
function compactObserver(options: TraceOptions): CompactTraceObserver | undefined {
|
|
99
|
+
const observed: ObservedTraceOptions = options;
|
|
100
|
+
return observed[compactObserverKey];
|
|
101
|
+
}
|
|
73
102
|
function normalizeOperation(value: string | undefined): string | undefined {
|
|
74
103
|
if (!value) return undefined;
|
|
75
104
|
return value.startsWith('/') ? value.slice(1) : value;
|
|
@@ -212,14 +241,11 @@ function handlerScope(db: Db, methodId: string): { repoId?: number; files: Set<s
|
|
|
212
241
|
if (!row || typeof row.symbolId !== 'number') return undefined;
|
|
213
242
|
return { repoId: row.repoId, files: new Set(row.sourceFile ? [row.sourceFile] : []), symbolId: row.symbolId };
|
|
214
243
|
}
|
|
215
|
-
|
|
216
|
-
|
|
217
244
|
function traceEdgeType(call: CallRow, row: GraphRow): string {
|
|
218
245
|
if (row.to_kind === 'operation' && row.edge_type === 'REMOTE_CALL_RESOLVES_TO_OPERATION') return 'remote_action';
|
|
219
246
|
if (row.to_kind === 'operation' && row.edge_type === 'LOCAL_CALL_RESOLVES_TO_OPERATION') return 'local_service_call';
|
|
220
247
|
return String(call.call_type);
|
|
221
248
|
}
|
|
222
|
-
|
|
223
249
|
function includeCall(
|
|
224
250
|
type: string,
|
|
225
251
|
options: {
|
|
@@ -233,202 +259,37 @@ function includeCall(
|
|
|
233
259
|
if (!options.includeAsync && type.startsWith('async_')) return false;
|
|
234
260
|
return true;
|
|
235
261
|
}
|
|
236
|
-
function graphForCalls(db: Db, callIds: number[]): Map<number, GraphRow[]> {
|
|
237
|
-
const map = new Map<number, GraphRow[]>();
|
|
238
|
-
if (callIds.length === 0) return map;
|
|
239
|
-
const rows = db
|
|
240
|
-
.prepare(
|
|
241
|
-
`SELECT * FROM graph_edges WHERE from_kind='call' AND from_id IN (${callIds.map(() => '?').join(',')}) ORDER BY id`,
|
|
242
|
-
)
|
|
243
|
-
.all(...callIds.map((id) => String(id))) as GraphRow[];
|
|
244
|
-
for (const row of rows) {
|
|
245
|
-
const id = Number(row.from_id);
|
|
246
|
-
map.set(id, [...(map.get(id) ?? []), row]);
|
|
247
|
-
}
|
|
248
|
-
return map;
|
|
249
|
-
}
|
|
250
|
-
function symbolNode(db: Db, symbolId: number): Record<string, unknown> | undefined {
|
|
251
|
-
const row = db.prepare(`SELECT s.id symbolId,s.name symbolName,s.qualified_name qualifiedName,s.source_file sourceFile,s.start_line startLine,s.end_line endLine,r.name repoName,r.id repoId FROM symbols s JOIN repositories r ON r.id=s.repo_id WHERE s.id=?`).get(symbolId) as Record<string, unknown> | undefined;
|
|
252
|
-
if (!row) return undefined;
|
|
253
|
-
const fileName = String(row.sourceFile ?? '').split('/').at(-1) ?? String(row.sourceFile ?? '');
|
|
254
|
-
return { id: `symbol:${symbolId}`, kind: 'symbol', label: `${fileName}:${String(row.qualifiedName ?? row.symbolName)}`, ...row };
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
function operationNode(db: Db, operationId: string): Record<string, unknown> | undefined {
|
|
258
|
-
const row = db.prepare(`SELECT o.id operationId,o.operation_name operationName,o.operation_type operationType,o.operation_path operationPath,o.source_file sourceFile,o.source_line sourceLine,s.id serviceId,s.service_name serviceName,s.qualified_name qualifiedName,s.service_path servicePath,r.id repoId,r.name repoName FROM cds_operations o JOIN cds_services s ON s.id=o.service_id JOIN repositories r ON r.id=s.repo_id WHERE o.id=?`).get(operationId) as Record<string, unknown> | undefined;
|
|
259
|
-
if (!row) return undefined;
|
|
260
|
-
return { id: `operation:${operationId}`, kind: 'operation', label: `${String(row.repoName)}:${String(row.servicePath)}${String(row.operationPath)}`, ...row };
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
function workspaceIdForCall(db: Db, callId: string): number | undefined {
|
|
264
|
-
return (db.prepare('SELECT r.workspace_id workspaceId FROM outbound_calls c JOIN repositories r ON r.id=c.repo_id WHERE c.id=?').get(callId) as { workspaceId?: number } | undefined)?.workspaceId;
|
|
265
|
-
}
|
|
266
|
-
function parseEvidence(value: unknown): Record<string, unknown> {
|
|
267
|
-
try {
|
|
268
|
-
const parsed = JSON.parse(String(value || '{}')) as unknown;
|
|
269
|
-
return isEvidenceRecord(parsed) ? boundCandidateLikeEvidence(parsed) : {};
|
|
270
|
-
} catch {
|
|
271
|
-
return {};
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
function isEvidenceRecord(value: unknown): value is Record<string, unknown> {
|
|
276
|
-
return Boolean(value && typeof value === 'object' && !Array.isArray(value));
|
|
277
|
-
}
|
|
278
|
-
function receiverFromEvidence(value: unknown): string | undefined {
|
|
279
|
-
const evidence = parseEvidence(value);
|
|
280
|
-
return typeof evidence.receiver === 'string' ? evidence.receiver : undefined;
|
|
281
|
-
}
|
|
282
|
-
function hasDynamicPlaceholder(value: string | undefined): boolean {
|
|
283
|
-
return extractPlaceholders(value).length > 0;
|
|
284
|
-
}
|
|
285
|
-
function enrichBinding(row: ContextBinding): ContextBinding {
|
|
286
|
-
const effectiveServicePath = row.servicePathExpr && !hasDynamicPlaceholder(row.servicePathExpr) ? row.servicePathExpr : !row.servicePathExpr ? row.requireServicePath : undefined;
|
|
287
|
-
const effectiveDestination = row.destinationExpr && !hasDynamicPlaceholder(row.destinationExpr) ? row.destinationExpr : !row.destinationExpr ? row.requireDestination : undefined;
|
|
288
|
-
return { ...row, effectiveServicePath, effectiveDestination };
|
|
289
|
-
}
|
|
290
|
-
function knownBindingsForCalls(db: Db, calls: CallRow[]): Map<string, ContextBinding> {
|
|
291
|
-
const map = new Map<string, ContextBinding>();
|
|
292
|
-
for (const call of calls) {
|
|
293
|
-
const receiver = receiverFromEvidence(call.evidence_json);
|
|
294
|
-
const bindingId = Number(call.service_binding_id ?? 0);
|
|
295
|
-
if (!receiver || !bindingId) continue;
|
|
296
|
-
const row = db.prepare(`SELECT b.id,b.alias,b.alias_expr aliasExpr,b.destination_expr destinationExpr,b.service_path_expr servicePathExpr,b.source_file sourceFile,b.source_line sourceLine,req.service_path requireServicePath,req.destination requireDestination
|
|
297
|
-
FROM service_bindings b LEFT JOIN cds_requires req ON req.repo_id=b.repo_id AND req.alias=b.alias
|
|
298
|
-
WHERE b.id=?`).get(bindingId) as ContextBinding | undefined;
|
|
299
|
-
if (row) map.set(receiver, enrichBinding({ ...row, bindingId, source: 'local_service_binding', calleeReceiver: receiver }));
|
|
300
|
-
}
|
|
301
|
-
return map;
|
|
302
|
-
}
|
|
303
|
-
function knownBindingsForScope(db: Db, repoId: number | undefined, symbolIds: Set<number> | undefined, files: Set<string> | undefined): Map<string, ContextBinding> {
|
|
304
|
-
const map = new Map<string, ContextBinding>();
|
|
305
|
-
if (repoId === undefined) return map;
|
|
306
|
-
type BindingRow = Omit<ContextBinding, 'bindingId' | 'source' | 'calleeReceiver'> & { id?: number; symbolId?: number | null; variableName?: string };
|
|
307
|
-
const rows = db.prepare(`SELECT b.id,b.symbol_id symbolId,b.variable_name variableName,b.alias,b.alias_expr aliasExpr,b.destination_expr destinationExpr,b.service_path_expr servicePathExpr,b.source_file sourceFile,b.source_line sourceLine,req.service_path requireServicePath,req.destination requireDestination
|
|
308
|
-
FROM service_bindings b LEFT JOIN cds_requires req ON req.repo_id=b.repo_id AND req.alias=b.alias
|
|
309
|
-
WHERE b.repo_id=?`).all(repoId) as BindingRow[];
|
|
310
|
-
for (const row of rows) {
|
|
311
|
-
if (!row.variableName) continue;
|
|
312
|
-
if (files && !files.has(String(row.sourceFile))) continue;
|
|
313
|
-
if (symbolIds && symbolIds.size > 0 && row.symbolId != null
|
|
314
|
-
&& !symbolIds.has(Number(row.symbolId))) continue;
|
|
315
|
-
const candidate = enrichBinding({
|
|
316
|
-
...row,
|
|
317
|
-
bindingId: Number(row.id),
|
|
318
|
-
source: 'local_service_binding',
|
|
319
|
-
calleeReceiver: row.variableName,
|
|
320
|
-
resolutionStatus: 'selected',
|
|
321
|
-
});
|
|
322
|
-
const existing = map.get(row.variableName);
|
|
323
|
-
if (!existing) {
|
|
324
|
-
map.set(row.variableName, candidate);
|
|
325
|
-
continue;
|
|
326
|
-
}
|
|
327
|
-
const candidates = uniqueBindingCandidates([
|
|
328
|
-
...(existing.bindingCandidates ?? [bindingCandidateEvidence(existing)]),
|
|
329
|
-
bindingCandidateEvidence(candidate),
|
|
330
|
-
]);
|
|
331
|
-
map.set(row.variableName, {
|
|
332
|
-
...candidate,
|
|
333
|
-
bindingId: undefined,
|
|
334
|
-
source: 'ambiguous_local_service_bindings',
|
|
335
|
-
resolutionStatus: 'ambiguous',
|
|
336
|
-
bindingCandidates: candidates,
|
|
337
|
-
});
|
|
338
|
-
}
|
|
339
|
-
return map;
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
function bindingCandidateEvidence(binding: ContextBinding): Record<string, unknown> {
|
|
343
|
-
return {
|
|
344
|
-
bindingId: binding.bindingId,
|
|
345
|
-
sourceFile: binding.sourceFile,
|
|
346
|
-
sourceLine: binding.sourceLine,
|
|
347
|
-
alias: binding.alias,
|
|
348
|
-
aliasExpr: binding.aliasExpr,
|
|
349
|
-
destinationExpr: binding.destinationExpr,
|
|
350
|
-
servicePathExpr: binding.servicePathExpr,
|
|
351
|
-
};
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
function uniqueBindingCandidates(
|
|
355
|
-
candidates: Array<Record<string, unknown>>,
|
|
356
|
-
): Array<Record<string, unknown>> {
|
|
357
|
-
const seen = new Set<string>();
|
|
358
|
-
return candidates.filter((candidate) => {
|
|
359
|
-
const key = JSON.stringify(candidate);
|
|
360
|
-
if (seen.has(key)) return false;
|
|
361
|
-
seen.add(key);
|
|
362
|
-
return true;
|
|
363
|
-
});
|
|
364
|
-
}
|
|
365
|
-
function contextForSymbolCall(db: Db, symbolCall: Record<string, unknown>, callerBindings: Map<string, ContextBinding>): Map<string, ContextBinding> {
|
|
366
|
-
const next = new Map<string, ContextBinding>();
|
|
367
|
-
if (callerBindings.size === 0) return next;
|
|
368
|
-
const callEvidence = parseEvidence(symbolCall.evidence_json);
|
|
369
|
-
const callee = db.prepare('SELECT evidence_json evidenceJson,source_file sourceFile,start_line startLine FROM symbols WHERE id=?').get(symbolCall.callee_symbol_id) as { evidenceJson?: string; sourceFile?: string; startLine?: number } | undefined;
|
|
370
|
-
const calleeEvidence = parseEvidence(callee?.evidenceJson);
|
|
371
|
-
const params = Array.isArray(calleeEvidence.parameters) ? calleeEvidence.parameters.filter((item): item is string => typeof item === 'string') : [];
|
|
372
|
-
const parameterBindings = Array.isArray(calleeEvidence.parameterBindings) ? calleeEvidence.parameterBindings.filter((item): item is Record<string, unknown> => Boolean(item && typeof item === 'object' && !Array.isArray(item))) : [];
|
|
373
|
-
const parameterPropertyAliases = Array.isArray(calleeEvidence.parameterPropertyAliases) ? calleeEvidence.parameterPropertyAliases.filter((item): item is Record<string, unknown> => Boolean(item && typeof item === 'object' && !Array.isArray(item))) : [];
|
|
374
|
-
const args = Array.isArray(callEvidence.callArguments) ? callEvidence.callArguments as Array<Record<string, unknown>> : [];
|
|
375
|
-
const provenance = {
|
|
376
|
-
callerSite: { sourceFile: String(symbolCall.source_file ?? ''), sourceLine: Number(symbolCall.source_line ?? 0) },
|
|
377
|
-
calleeSite: { sourceFile: callee?.sourceFile, sourceLine: callee?.startLine },
|
|
378
|
-
};
|
|
379
|
-
args.forEach((arg, index) => {
|
|
380
|
-
const paramBinding = parameterBindings.find((binding) => binding.index === index);
|
|
381
|
-
const param = paramBinding?.kind === 'identifier' && typeof paramBinding.name === 'string' ? paramBinding.name : params[index];
|
|
382
|
-
if (arg.kind === 'identifier' && typeof arg.name === 'string') {
|
|
383
|
-
const binding = callerBindings.get(arg.name);
|
|
384
|
-
if (binding && param) next.set(param, { ...binding, ...provenance, source: 'local_symbol_argument', callerArgument: arg.name, calleeParameter: param, calleeReceiver: param });
|
|
385
|
-
}
|
|
386
|
-
if (arg.kind === 'object_literal' && Array.isArray(arg.properties)) {
|
|
387
|
-
for (const prop of arg.properties as Array<Record<string, unknown>>) {
|
|
388
|
-
if (typeof prop.property !== 'string' || typeof prop.argument !== 'string') continue;
|
|
389
|
-
const binding = callerBindings.get(prop.argument);
|
|
390
|
-
if (!binding) continue;
|
|
391
|
-
const destructured = paramBinding?.kind === 'object_pattern' && Array.isArray(paramBinding.properties)
|
|
392
|
-
? (paramBinding.properties as Array<Record<string, unknown>>).find((item) => item.property === prop.property && typeof item.local === 'string')
|
|
393
|
-
: undefined;
|
|
394
|
-
if (destructured && typeof destructured.local === 'string') next.set(destructured.local, { ...binding, ...provenance, source: 'local_symbol_destructured_object_argument', callerProperty: prop.property, callerArgument: prop.argument, calleeParameter: String(index), calleeReceiver: destructured.local });
|
|
395
|
-
else if (param) {
|
|
396
|
-
next.set(`${param}.${prop.property}`, { ...binding, ...provenance, source: 'local_symbol_object_argument', callerProperty: prop.property, callerArgument: prop.argument, calleeParameter: param, calleeReceiver: `${param}.${prop.property}` });
|
|
397
|
-
for (const alias of parameterPropertyAliases) {
|
|
398
|
-
if (alias.parameter === param && alias.property === prop.property && typeof alias.local === 'string') next.set(alias.local, { ...binding, ...provenance, source: 'local_symbol_object_parameter_destructure', callerProperty: prop.property, callerArgument: prop.argument, calleeParameter: param, calleeObjectProperty: `${param}.${prop.property}`, calleeReceiver: alias.local, calleeLocalDestructuredIdentifier: alias.local, parameterPropertyAliasKind: alias.kind, parameterPropertyAliasLine: alias.line });
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
if (arg.kind === 'array_literal' && Array.isArray(arg.elements) && paramBinding?.kind === 'array_pattern' && Array.isArray(paramBinding.elements)) {
|
|
404
|
-
for (const element of arg.elements as Array<Record<string, unknown>>) {
|
|
405
|
-
const target = (paramBinding.elements as Array<Record<string, unknown>>).find((item) => item.index === element.index);
|
|
406
|
-
if (element.kind !== 'identifier' || typeof element.name !== 'string' || typeof target?.local !== 'string') continue;
|
|
407
|
-
const binding = callerBindings.get(element.name);
|
|
408
|
-
if (binding) next.set(target.local, { ...binding, ...provenance, source: 'local_symbol_destructured_array_argument', callerArgument: element.name, calleeParameter: String(index), calleeReceiver: target.local });
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
});
|
|
412
|
-
return next;
|
|
413
|
-
}
|
|
414
262
|
export function trace(
|
|
415
263
|
db: Db,
|
|
416
264
|
start: TraceStart,
|
|
417
265
|
options: TraceOptions,
|
|
418
266
|
): TraceResult {
|
|
267
|
+
const observer = compactObserver(options);
|
|
268
|
+
const schemaLifecycle = schemaLifecycleDiagnostic(db);
|
|
269
|
+
if (schemaLifecycle)
|
|
270
|
+
return { start, nodes: [], edges: [], diagnostics: [schemaLifecycle] };
|
|
419
271
|
const hintOptions = { implementationRepo: options.implementationRepo, implementationHints: options.implementationHints };
|
|
420
272
|
const scope = startScope(db, start, hintOptions, options.workspaceId);
|
|
421
273
|
const hasSelector = Boolean(start.repo || start.handler || start.operation
|
|
422
274
|
|| start.operationPath || start.servicePath);
|
|
423
275
|
const diagnosticRepoId = scope.executionRepoId ?? scope.repo?.id;
|
|
276
|
+
const scheduler = new TraversalScopeScheduler();
|
|
277
|
+
const roots = createTraceRootPlan(db, scheduler, {
|
|
278
|
+
repoId: diagnosticRepoId, files: scope.sourceFiles,
|
|
279
|
+
symbolIds: scope.symbolIds, selectorMatched: scope.selectorMatched,
|
|
280
|
+
}, options.workspaceId, Boolean(options.includeAsync));
|
|
281
|
+
observer?.setWorkspaceId?.(roots.workspaceId);
|
|
282
|
+
if (roots.diagnostic)
|
|
283
|
+
return { start, nodes: [], edges: [], diagnostics: [roots.diagnostic] };
|
|
284
|
+
const { workspaceId, queue, pendingRoots } = roots;
|
|
424
285
|
const diagnostics = loadTraceDiagnostics(
|
|
425
286
|
db,
|
|
426
287
|
diagnosticRepoId,
|
|
427
288
|
!hasSelector,
|
|
428
|
-
|
|
289
|
+
workspaceId,
|
|
429
290
|
);
|
|
430
291
|
const stale = diagnosticRepoId !== undefined || !hasSelector
|
|
431
|
-
? db.prepare('SELECT name,graph_stale_reason reason FROM repositories WHERE graph_stale_reason IS NOT NULL AND (? IS NULL OR id=?) AND (? IS NULL OR workspace_id=?) ORDER BY name,id').all(diagnosticRepoId, diagnosticRepoId,
|
|
292
|
+
? db.prepare('SELECT name,graph_stale_reason reason FROM repositories WHERE graph_stale_reason IS NOT NULL AND (? IS NULL OR id=?) AND (? IS NULL OR workspace_id=?) ORDER BY name,id').all(diagnosticRepoId, diagnosticRepoId, workspaceId, workspaceId) as Array<{ name?: string; reason?: string }>
|
|
432
293
|
: [];
|
|
433
294
|
for (const row of stale)
|
|
434
295
|
prependTraceDiagnostic(diagnostics, { severity: 'warning', code: 'graph_stale', message: `Graph is stale for ${row.name ?? 'repository'}: ${row.reason ?? 'facts_changed'}. Run service-flow link.` });
|
|
@@ -438,12 +299,8 @@ export function trace(
|
|
|
438
299
|
prependTraceDiagnostic(diagnostics, selectorNotFoundDiagnostic(start));
|
|
439
300
|
const maxDepth = positiveDepth(options.depth);
|
|
440
301
|
const edges: TraceEdge[] = [];
|
|
302
|
+
const recorder = new TraceEdgeRecorder(edges, observer);
|
|
441
303
|
const nodes = new Map<string, Record<string, unknown>>();
|
|
442
|
-
const seenEdges = new Set<number>();
|
|
443
|
-
const queue: Array<{ repoId?: number; files?: Set<string>; symbolIds?: Set<number>; depth: number; context?: Map<string, ContextBinding> }> =
|
|
444
|
-
scope.selectorMatched
|
|
445
|
-
? [{ repoId: scope.executionRepoId, files: scope.sourceFiles, symbolIds: scope.symbolIds, depth: 1, context: new Map() }]
|
|
446
|
-
: [];
|
|
447
304
|
if (scope.startOperationId && scope.selectorMatched) {
|
|
448
305
|
const op = operationNode(db, scope.startOperationId);
|
|
449
306
|
const impl = implementationScope(db, scope.startOperationId);
|
|
@@ -472,44 +329,107 @@ export function trace(
|
|
|
472
329
|
: { evidence: implEvidence };
|
|
473
330
|
if (selected.diagnostic) prependTraceDiagnostic(diagnostics, selected.diagnostic);
|
|
474
331
|
if (selected.handler) nodes.set(String(selected.handler.id), selected.handler);
|
|
475
|
-
|
|
476
|
-
|
|
332
|
+
const unresolvedReason = selected.unresolvedReason
|
|
333
|
+
?? (impl.edge.status === 'resolved' || startSelection.methodId
|
|
334
|
+
? undefined : String(impl.edge.unresolved_reason ?? impl.edge.status));
|
|
335
|
+
const selectedScope = selectedMethodId
|
|
336
|
+
? handlerScope(db, selectedMethodId) : undefined;
|
|
337
|
+
const edge: TraceEdge = { step: 1, type: 'operation_implemented_by_handler', from: op?.label ? String(op.label) : `operation:${scope.startOperationId}`, to: selected.handler?.label ? String(selected.handler.label) : `${impl.edge.to_kind}:${impl.edge.to_id}`, evidence: selected.evidence, confidence: Number(impl.edge.confidence ?? 0), unresolvedReason };
|
|
338
|
+
recordImplementationObservation(recorder, edge, {
|
|
339
|
+
operationId: scope.startOperationId,
|
|
340
|
+
handlerMethodId: selectedMethodId,
|
|
341
|
+
handlerSymbolId: selectedScope?.symbolId,
|
|
342
|
+
graphEdgeId: impl.edge.id,
|
|
343
|
+
persistedStatus: impl.edge.status,
|
|
344
|
+
persistedTargetKind: impl.edge.to_kind,
|
|
345
|
+
persistedTargetId: impl.edge.to_id,
|
|
346
|
+
effectiveStatus: startSelection.methodId
|
|
347
|
+
? 'resolved' : String(impl.edge.status ?? 'unresolved'),
|
|
348
|
+
strategy: String(startSelection.evidence.strategy
|
|
349
|
+
?? 'indexed_operation_graph'),
|
|
350
|
+
guided: startSelection.evidence.guided === true,
|
|
351
|
+
unresolvedReason, evidence: selected.evidence, site: op ?? {},
|
|
352
|
+
});
|
|
477
353
|
}
|
|
478
354
|
}
|
|
479
|
-
|
|
480
|
-
|
|
355
|
+
while (queue.length > 0 || pendingRoots.length > 0) {
|
|
356
|
+
if ((queue[0]?.depth ?? Number.POSITIVE_INFINITY) > 1
|
|
357
|
+
&& workspaceId !== undefined) {
|
|
358
|
+
const root = nextPendingRoot(pendingRoots, scheduler, workspaceId);
|
|
359
|
+
if (root) queue.unshift(root);
|
|
360
|
+
}
|
|
481
361
|
const current = queue.shift();
|
|
482
362
|
if (!current || current.depth > maxDepth) continue;
|
|
483
|
-
|
|
484
|
-
const key = `${current.repoId ?? '*'}:${[...(current.symbolIds ?? new Set(['*']))].sort().join(',')}:${[...(current.files ?? new Set(['*']))].sort().join(',')}:${contextKey}`;
|
|
485
|
-
if (seenScopes.has(key)) continue;
|
|
486
|
-
seenScopes.add(key);
|
|
363
|
+
if (!scheduler.markExpanded(current.state)) continue;
|
|
487
364
|
const calls = db
|
|
488
365
|
.prepare(
|
|
489
|
-
`SELECT c.*,r.name repoName
|
|
366
|
+
`SELECT c.*,r.name repoName,r.workspace_id workspaceId,
|
|
367
|
+
r.graph_generation graphGeneration
|
|
368
|
+
FROM outbound_calls c JOIN repositories r ON r.id=c.repo_id
|
|
369
|
+
WHERE (? IS NULL OR c.repo_id=?) AND (? IS NULL OR r.workspace_id=?)
|
|
370
|
+
ORDER BY c.source_file COLLATE BINARY,c.call_site_start_offset,
|
|
371
|
+
c.call_site_end_offset,c.source_line,c.id`,
|
|
490
372
|
)
|
|
491
|
-
.all(current.repoId, current.repoId,
|
|
373
|
+
.all(current.repoId, current.repoId, workspaceId, workspaceId) as CallRow[];
|
|
492
374
|
const filtered = calls.filter(
|
|
493
375
|
(c) =>
|
|
494
|
-
(
|
|
376
|
+
(current.unownedOnly ? c.source_symbol_id == null
|
|
377
|
+
: !current.symbolIds || current.symbolIds.has(Number(c.source_symbol_id)))
|
|
378
|
+
&& (!current.files || current.files.has(String(c.source_file))) &&
|
|
495
379
|
includeCall(String(c.call_type), options),
|
|
496
380
|
);
|
|
497
|
-
const callerBindings = new Map<string, ContextBinding>([...
|
|
381
|
+
const callerBindings = new Map<string, ContextBinding>([...current.context, ...knownBindingsForScope(db, current.repoId, current.symbolIds, current.files), ...knownBindingsForCalls(db, filtered)]);
|
|
498
382
|
|
|
499
|
-
if (current.
|
|
500
|
-
|
|
383
|
+
if (!current.rootObservationOnly && current.symbolIds
|
|
384
|
+
&& current.symbolIds.size > 0 && current.depth < maxDepth) {
|
|
385
|
+
const symbolRows = db.prepare(`SELECT sc.*,s.repo_id calleeRepoId,
|
|
386
|
+
s.source_file calleeFile FROM symbol_calls sc
|
|
387
|
+
LEFT JOIN symbols s ON s.id=sc.callee_symbol_id
|
|
388
|
+
WHERE sc.call_role='ordinary_call'
|
|
389
|
+
AND sc.caller_symbol_id IN (${[...current.symbolIds].map(() => '?').join(',')})
|
|
390
|
+
ORDER BY sc.source_file COLLATE BINARY,sc.call_site_start_offset,
|
|
391
|
+
sc.call_site_end_offset,sc.source_line,sc.id`).all(
|
|
392
|
+
...current.symbolIds,
|
|
393
|
+
) as Array<Record<string, unknown>>;
|
|
501
394
|
for (const symbolCall of symbolRows) {
|
|
502
395
|
if (!symbolCall.callee_symbol_id) continue;
|
|
503
396
|
const nextSymbols = new Set([Number(symbolCall.callee_symbol_id)]);
|
|
504
397
|
const nextFiles = new Set([String(symbolCall.calleeFile)]);
|
|
505
398
|
const nextRepoId = Number(symbolCall.calleeRepoId);
|
|
506
|
-
const
|
|
399
|
+
const nextContext = contextForSymbolCall(db, symbolCall, callerBindings);
|
|
400
|
+
const scheduling = scheduler.schedule({
|
|
401
|
+
workspaceId,
|
|
402
|
+
repoId: nextRepoId,
|
|
403
|
+
files: nextFiles,
|
|
404
|
+
symbolIds: nextSymbols,
|
|
405
|
+
context: nextContext,
|
|
406
|
+
}, current.state);
|
|
507
407
|
const calleeNode = symbolNode(db, Number(symbolCall.callee_symbol_id));
|
|
508
408
|
if (calleeNode) nodes.set(String(calleeNode.id), calleeNode);
|
|
509
409
|
const evidence = { ...parseEvidence(symbolCall.evidence_json), sourceFile: symbolCall.source_file, sourceLine: symbolCall.source_line, calleeSymbolId: symbolCall.callee_symbol_id, calleeSymbolName: calleeNode?.symbolName, calleeSymbolFile: calleeNode?.sourceFile, resolutionStatus: symbolCall.status };
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
410
|
+
const unresolvedReason = String(symbolCall.status) === 'resolved'
|
|
411
|
+
? undefined : symbolCall.unresolved_reason
|
|
412
|
+
? String(symbolCall.unresolved_reason) : undefined;
|
|
413
|
+
const edge: TraceEdge = { step: current.depth, type: 'local_symbol_call', from: String(symbolCall.callee_expression), to: calleeNode?.label ? String(calleeNode.label) : `symbol:${String(symbolCall.callee_symbol_id)}`, evidence, confidence: Number(symbolCall.confidence ?? 0.8), unresolvedReason };
|
|
414
|
+
const target = recordLocalCallObservation(recorder, edge, {
|
|
415
|
+
symbolCall, evidence, unresolvedReason,
|
|
416
|
+
});
|
|
417
|
+
if (scheduling.kind === 'cycle') {
|
|
418
|
+
const cycleEvidence = { cycle: true,
|
|
419
|
+
cycleReason: 'structural_ancestry_cycle',
|
|
420
|
+
symbolCallId: symbolCall.id };
|
|
421
|
+
const cycleEdge: TraceEdge = { step: current.depth, type: 'cycle', from: String(symbolCall.callee_expression), to: scheduling.state.structuralKey, evidence: cycleEvidence, confidence: 1, unresolvedReason: 'Cycle detected in structural ancestry; downstream symbol was not expanded' };
|
|
422
|
+
recordCycleObservation(recorder, cycleEdge, target, {
|
|
423
|
+
workspaceId, repositoryId: nextRepoId, sourceFiles: nextFiles,
|
|
424
|
+
symbolIds: nextSymbols,
|
|
425
|
+
structuralKey: scheduling.state.structuralKey,
|
|
426
|
+
}, { symbolCallId: symbolCall.id,
|
|
427
|
+
symbolId: symbolCall.callee_symbol_id }, symbolCall);
|
|
428
|
+
}
|
|
429
|
+
if (scheduling.kind === 'scheduled') enqueueCausalScope(
|
|
430
|
+
queue, pendingRoots, { repoId: nextRepoId, files: nextFiles,
|
|
431
|
+
symbolIds: nextSymbols, depth: current.depth + 1,
|
|
432
|
+
context: nextContext, state: scheduling.state });
|
|
513
433
|
}
|
|
514
434
|
}
|
|
515
435
|
const graph = graphForCalls(
|
|
@@ -527,18 +447,16 @@ export function trace(
|
|
|
527
447
|
callType: call.call_type,
|
|
528
448
|
});
|
|
529
449
|
const persistedRowsForCall = graph.get(Number(call.id)) ?? [];
|
|
530
|
-
const contextual = contextualRuntimeResolution(db, call, callerBindings.get(receiverFromEvidence(call.evidence_json) ?? ''),
|
|
450
|
+
const contextual = contextualRuntimeResolution(db, call, callerBindings.get(receiverFromEvidence(call.evidence_json) ?? ''), call.workspaceId, persistedRowsForCall);
|
|
531
451
|
const graphRows = contextual.row ? [contextual.row] : persistedRowsForCall;
|
|
532
452
|
for (const row of graphRows) {
|
|
533
|
-
if (seenEdges.has(Number(row.id))) continue;
|
|
534
|
-
seenEdges.add(Number(row.id));
|
|
535
453
|
const persistedEvidence = parseEvidence(row.evidence_json);
|
|
536
454
|
const rawEvidence = baseTraceEvidence(row as TraceGraphRow, call, persistedEvidence, contextual.evidence);
|
|
537
455
|
const effective = runtimeResolution(db, row as TraceGraphRow, rawEvidence, {
|
|
538
456
|
vars: options.vars,
|
|
539
457
|
dynamicMode: options.dynamicMode ?? 'strict',
|
|
540
458
|
maxDynamicCandidates: options.maxDynamicCandidates,
|
|
541
|
-
},
|
|
459
|
+
}, call.workspaceId, contextual.state);
|
|
542
460
|
const evidence = effective.evidence;
|
|
543
461
|
const effectiveRow = effective.row;
|
|
544
462
|
const targetNode = `${effectiveRow.to_kind}:${effectiveRow.to_id}`;
|
|
@@ -549,7 +467,7 @@ export function trace(
|
|
|
549
467
|
label: effectiveRow.to_kind === 'db_entity' ? `Entity: ${effectiveRow.to_id || 'unknown'}` : effectiveRow.to_id,
|
|
550
468
|
});
|
|
551
469
|
const to = edgeTarget(effectiveRow, evidence);
|
|
552
|
-
|
|
470
|
+
const edge: TraceEdge = {
|
|
553
471
|
step: current.depth,
|
|
554
472
|
type: traceEdgeType(call, effectiveRow),
|
|
555
473
|
from: `${call.repoName}:${call.source_file}:${call.source_line}`,
|
|
@@ -557,10 +475,68 @@ export function trace(
|
|
|
557
475
|
evidence,
|
|
558
476
|
confidence: Number(effectiveRow.confidence ?? call.confidence),
|
|
559
477
|
unresolvedReason: effective.unresolvedReason,
|
|
478
|
+
};
|
|
479
|
+
const semanticWorkspaceId = workspaceId ?? call.workspaceId;
|
|
480
|
+
const semantic = recordOutboundObservation(recorder, edge, {
|
|
481
|
+
call, row: effectiveRow, evidence,
|
|
482
|
+
workspaceId: semanticWorkspaceId,
|
|
483
|
+
dynamicMode: options.dynamicMode,
|
|
484
|
+
unresolvedReason: effective.unresolvedReason,
|
|
560
485
|
});
|
|
486
|
+
if (options.includeAsync && call.call_type === 'async_emit'
|
|
487
|
+
&& effectiveRow.edge_type === 'HANDLER_EMITS_EVENT'
|
|
488
|
+
&& typeof call.event_name_expr === 'string') {
|
|
489
|
+
const plans = planEventSubscriberTransitions(db, {
|
|
490
|
+
workspaceId: workspaceId ?? call.workspaceId,
|
|
491
|
+
graphGeneration: call.graphGeneration,
|
|
492
|
+
eventName: call.event_name_expr,
|
|
493
|
+
}, scheduler, current.state, current.depth, maxDepth);
|
|
494
|
+
for (const plan of plans) {
|
|
495
|
+
const nodeId = String(plan.node.id);
|
|
496
|
+
const targetLabel = String(plan.node.label ?? nodeId);
|
|
497
|
+
nodes.set(nodeId, plan.node);
|
|
498
|
+
const bridgeEdge: TraceEdge = {
|
|
499
|
+
step: current.depth,
|
|
500
|
+
type: 'event_name_matches_subscription_handler',
|
|
501
|
+
from: plan.transition.eventName,
|
|
502
|
+
to: targetLabel,
|
|
503
|
+
evidence: plan.evidence,
|
|
504
|
+
confidence: plan.transition.confidence,
|
|
505
|
+
unresolvedReason: plan.transition.unresolvedReason,
|
|
506
|
+
};
|
|
507
|
+
const handler = plan.transition.handler;
|
|
508
|
+
const bridgeTarget = recordEventBridgeObservation(
|
|
509
|
+
recorder, bridgeEdge, plan, semanticWorkspaceId, plans.length,
|
|
510
|
+
);
|
|
511
|
+
if (plan.bodyExpansion === 'cycle_blocked' && plan.state) {
|
|
512
|
+
const cycleEvidence = { cycle: true,
|
|
513
|
+
cycleReason: 'structural_ancestry_cycle',
|
|
514
|
+
graphEdgeId: plan.transition.graphEdgeId };
|
|
515
|
+
const cycleEdge: TraceEdge = { step: current.depth, type: 'cycle', from: targetLabel, to: plan.state.structuralKey, evidence: cycleEvidence, confidence: 1, unresolvedReason: 'Cycle detected across an event subscriber boundary; downstream symbol was not expanded' };
|
|
516
|
+
recordEventCycleObservation(recorder, cycleEdge, plan,
|
|
517
|
+
bridgeTarget, semanticWorkspaceId);
|
|
518
|
+
}
|
|
519
|
+
if (plan.bodyExpansion === 'scheduled' && plan.state && handler) {
|
|
520
|
+
const files = new Set([handler.sourceFile]);
|
|
521
|
+
const symbolIds = new Set([handler.symbolId]);
|
|
522
|
+
enqueueCausalScope(queue, pendingRoots, {
|
|
523
|
+
repoId: handler.repoId, files, symbolIds,
|
|
524
|
+
depth: current.depth + 1, context: new Map(), state: plan.state,
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
561
529
|
if ((options.dynamicMode ?? 'strict') === 'candidates'
|
|
562
|
-
&& effectiveRow.status !== 'resolved')
|
|
563
|
-
|
|
530
|
+
&& effectiveRow.status !== 'resolved') {
|
|
531
|
+
for (const branch of dynamicCandidateBranches(
|
|
532
|
+
current.depth, call, evidence,
|
|
533
|
+
)) {
|
|
534
|
+
recordDynamicBranchObservation(
|
|
535
|
+
recorder, branch, call, semantic.source, evidence,
|
|
536
|
+
semanticWorkspaceId,
|
|
537
|
+
);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
564
540
|
if (effectiveRow.to_kind === 'operation') {
|
|
565
541
|
const implementation = implementationScope(db, effectiveRow.to_id);
|
|
566
542
|
const contextSelection = contextualImplementationSelection(
|
|
@@ -601,17 +577,40 @@ export function trace(
|
|
|
601
577
|
? String(selected.handler.label)
|
|
602
578
|
: `${implementation.edge.to_kind}:${implementation.edge.to_id}`;
|
|
603
579
|
if (selected.handler) nodes.set(String(selected.handler.id), selected.handler);
|
|
604
|
-
|
|
580
|
+
const unresolvedReason = selected.unresolvedReason
|
|
581
|
+
?? (implementation.edge.status === 'resolved' || contextMethodId
|
|
582
|
+
? undefined
|
|
583
|
+
: String(implementation.edge.unresolved_reason
|
|
584
|
+
?? implementation.edge.status));
|
|
585
|
+
const implementationTraceEdge: TraceEdge = {
|
|
605
586
|
step: current.depth,
|
|
606
587
|
type: 'operation_implemented_by_handler',
|
|
607
588
|
from: to,
|
|
608
589
|
to: implTo,
|
|
609
590
|
evidence: selected.evidence,
|
|
610
591
|
confidence: Number(implementation.edge.confidence ?? 0),
|
|
611
|
-
unresolvedReason
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
592
|
+
unresolvedReason,
|
|
593
|
+
};
|
|
594
|
+
const selectedScope = selectedMethodId
|
|
595
|
+
? handlerScope(db, selectedMethodId) : undefined;
|
|
596
|
+
recordImplementationObservation(recorder, implementationTraceEdge, {
|
|
597
|
+
operationId: effectiveRow.to_id,
|
|
598
|
+
handlerMethodId: selectedMethodId,
|
|
599
|
+
handlerSymbolId: selectedScope?.symbolId,
|
|
600
|
+
graphEdgeId: implementation.edge.id,
|
|
601
|
+
persistedStatus: implementation.edge.status,
|
|
602
|
+
persistedTargetKind: implementation.edge.to_kind,
|
|
603
|
+
persistedTargetId: implementation.edge.to_id,
|
|
604
|
+
effectiveStatus: contextMethodId
|
|
605
|
+
? 'resolved' : String(implementation.edge.status),
|
|
606
|
+
strategy: String(contextSelection.evidence.strategy
|
|
607
|
+
?? (contextMethodId ? 'contextual_implementation_selection'
|
|
608
|
+
: 'indexed_operation_graph')),
|
|
609
|
+
guided: contextSelection.evidence.guided === true,
|
|
610
|
+
contextual: Boolean(contextMethodId
|
|
611
|
+
&& contextSelection.evidence.strategy
|
|
612
|
+
!== 'implementation_repo_hint'),
|
|
613
|
+
unresolvedReason, evidence: selected.evidence, site: call,
|
|
615
614
|
});
|
|
616
615
|
}
|
|
617
616
|
if (current.depth >= maxDepth) continue;
|
|
@@ -626,25 +625,39 @@ export function trace(
|
|
|
626
625
|
'SELECT s.repo_id repoId FROM cds_operations o JOIN cds_services s ON s.id=o.service_id WHERE o.id=?',
|
|
627
626
|
)
|
|
628
627
|
.get(effectiveRow.to_id)?.repoId as number | undefined);
|
|
629
|
-
const
|
|
630
|
-
|
|
631
|
-
|
|
628
|
+
const nextContext = new Map<string, ContextBinding>();
|
|
629
|
+
const scheduling = scheduler.schedule({
|
|
630
|
+
workspaceId: workspaceId ?? call.workspaceId,
|
|
631
|
+
repoId: targetRepoId,
|
|
632
|
+
files,
|
|
633
|
+
symbolIds,
|
|
634
|
+
context: nextContext,
|
|
635
|
+
}, current.state);
|
|
636
|
+
if (scheduling.kind === 'cycle') {
|
|
637
|
+
const cycleEvidence = { ...evidence, cycle: true,
|
|
638
|
+
cycleReason: 'structural_ancestry_cycle' };
|
|
639
|
+
const cycleEdge: TraceEdge = {
|
|
632
640
|
step: current.depth,
|
|
633
641
|
type: 'cycle',
|
|
634
642
|
from: to,
|
|
635
|
-
to:
|
|
636
|
-
evidence:
|
|
643
|
+
to: scheduling.state.structuralKey,
|
|
644
|
+
evidence: cycleEvidence,
|
|
637
645
|
confidence: 1,
|
|
638
646
|
unresolvedReason:
|
|
639
|
-
'Cycle detected; downstream scope
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
647
|
+
'Cycle detected in structural ancestry; downstream scope was not expanded',
|
|
648
|
+
};
|
|
649
|
+
recordCycleObservation(recorder, cycleEdge, semantic.target, {
|
|
650
|
+
workspaceId: semanticWorkspaceId, repositoryId: targetRepoId,
|
|
651
|
+
sourceFiles: files, symbolIds,
|
|
652
|
+
structuralKey: scheduling.state.structuralKey,
|
|
653
|
+
}, { graphEdgeId: evidence.persistedGraphEdgeId,
|
|
654
|
+
outboundCallId: call.id,
|
|
655
|
+
operationId: effectiveRow.to_id }, call);
|
|
656
|
+
}
|
|
657
|
+
if (scheduling.kind === 'scheduled') enqueueCausalScope(
|
|
658
|
+
queue, pendingRoots, { repoId: targetRepoId, files, symbolIds,
|
|
659
|
+
depth: current.depth + 1, context: nextContext,
|
|
660
|
+
state: scheduling.state });
|
|
648
661
|
}
|
|
649
662
|
}
|
|
650
663
|
}
|
|
@@ -656,3 +669,16 @@ export function trace(
|
|
|
656
669
|
prependTraceDiagnostic(diagnostics, diagnostic);
|
|
657
670
|
return { start, nodes: [...nodes.values()], edges, diagnostics };
|
|
658
671
|
}
|
|
672
|
+
|
|
673
|
+
export function traceWithObserver(
|
|
674
|
+
db: Db,
|
|
675
|
+
start: TraceStart,
|
|
676
|
+
options: TraceOptions,
|
|
677
|
+
observer: CompactTraceObserver,
|
|
678
|
+
): TraceResult {
|
|
679
|
+
const observed: ObservedTraceOptions = {
|
|
680
|
+
...options,
|
|
681
|
+
[compactObserverKey]: observer,
|
|
682
|
+
};
|
|
683
|
+
return trace(db, start, observed);
|
|
684
|
+
}
|