@saptools/service-flow 0.1.43 → 0.1.45
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 +14 -1
- package/README.md +9 -1
- package/dist/{chunk-KHQK7CFH.js → chunk-BXSCE5CR.js} +1978 -1510
- package/dist/chunk-BXSCE5CR.js.map +1 -0
- package/dist/cli.js +471 -476
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +13 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/doctor.ts +435 -0
- package/src/cli.ts +18 -453
- package/src/index.ts +2 -0
- package/src/indexer/repository-indexer.ts +52 -26
- package/src/indexer/workspace-indexer.ts +17 -5
- package/src/linker/cross-repo-linker.ts +45 -3
- package/src/linker/odata-path-normalizer.ts +4 -1
- package/src/parsers/imported-wrapper-parser.ts +262 -0
- package/src/parsers/outbound-call-parser.ts +17 -5
- package/src/parsers/service-binding-parser-helpers.ts +160 -0
- package/src/parsers/service-binding-parser.ts +57 -242
- package/src/parsers/symbol-parser.ts +25 -10
- package/src/trace/evidence.ts +210 -0
- package/src/trace/implementation-hints.ts +151 -0
- package/src/trace/trace-engine.ts +75 -117
- package/src/types.ts +8 -0
- package/dist/chunk-KHQK7CFH.js.map +0 -1
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type { Db } from '../db/connection.js';
|
|
2
|
-
import { extractPlaceholders
|
|
2
|
+
import { extractPlaceholders } from '../linker/dynamic-edge-resolver.js';
|
|
3
3
|
import { normalizeODataOperationInvocationPath } from '../linker/odata-path-normalizer.js';
|
|
4
|
-
import { resolveOperation
|
|
5
|
-
import type { TraceEdge, TraceResult, TraceStart } from '../types.js';
|
|
4
|
+
import { resolveOperation } from '../linker/service-resolver.js';
|
|
5
|
+
import type { ImplementationHint, TraceEdge, TraceResult, TraceStart } from '../types.js';
|
|
6
|
+
import { baseTraceEvidence, edgeTarget, runtimeResolution, runtimeVariableDiagnostic, type TraceGraphRow } from './evidence.js';
|
|
7
|
+
import { implementationHintDiagnostic, selectImplementation, type ImplementationSelection } from './implementation-hints.js';
|
|
6
8
|
|
|
7
9
|
interface RepoRef {
|
|
8
10
|
id: number;
|
|
@@ -58,15 +60,13 @@ interface ContextBinding {
|
|
|
58
60
|
parameterPropertyAliasKind?: unknown;
|
|
59
61
|
parameterPropertyAliasLine?: unknown;
|
|
60
62
|
calleeReceiver: string;
|
|
63
|
+
callerSite?: { sourceFile?: string; sourceLine?: number };
|
|
64
|
+
calleeSite?: { sourceFile?: string; sourceLine?: number };
|
|
61
65
|
}
|
|
62
|
-
interface
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
repoName?: string;
|
|
66
|
-
operationName?: string;
|
|
67
|
-
score?: number;
|
|
66
|
+
interface ImplementationHintOptions {
|
|
67
|
+
implementationRepo?: string;
|
|
68
|
+
implementationHints?: ImplementationHint[];
|
|
68
69
|
}
|
|
69
|
-
|
|
70
70
|
function normalizeOperation(value: string | undefined): string | undefined {
|
|
71
71
|
if (!value) return undefined;
|
|
72
72
|
return value.startsWith('/') ? value.slice(1) : value;
|
|
@@ -75,7 +75,7 @@ function positiveDepth(value: number): number {
|
|
|
75
75
|
return Number.isFinite(value) && value > 0 ? Math.floor(value) : 25;
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
function operationStartScope(db: Db, repoId: number | undefined, start: TraceStart): { files?: Set<string>; symbols?: Set<number>; operationId?: string; diagnostics?: Array<Record<string, unknown>> } | undefined {
|
|
78
|
+
function operationStartScope(db: Db, repoId: number | undefined, start: TraceStart, hintOptions: ImplementationHintOptions): { files?: Set<string>; symbols?: Set<number>; operationId?: string; diagnostics?: Array<Record<string, unknown>> } | undefined {
|
|
79
79
|
const requested = normalizeOperation(start.operationPath ?? start.operation);
|
|
80
80
|
if (!requested) return undefined;
|
|
81
81
|
const rows = db.prepare(`SELECT o.id operationId,o.operation_name operationName,o.operation_path operationPath,s.service_path servicePath,r.id repoId,r.name repoName
|
|
@@ -91,7 +91,17 @@ function operationStartScope(db: Db, repoId: number | undefined, start: TraceSta
|
|
|
91
91
|
const operationId = String(rows[0]?.operationId);
|
|
92
92
|
const impl = implementationScope(db, operationId);
|
|
93
93
|
if (impl.edge?.status === 'resolved' && impl.files.size > 0) return { files: impl.files, symbols: impl.symbolId ? new Set([impl.symbolId]) : undefined, operationId, diagnostics: [] };
|
|
94
|
-
|
|
94
|
+
const hinted = implementationMethodIdFromHint(impl.edge, hintOptions);
|
|
95
|
+
if (hinted.methodId) {
|
|
96
|
+
const hintedScope = handlerScope(db, hinted.methodId);
|
|
97
|
+
if (hintedScope?.files.size) return { files: hintedScope.files, symbols: hintedScope.symbolId ? new Set([hintedScope.symbolId]) : undefined, operationId, diagnostics: [] };
|
|
98
|
+
}
|
|
99
|
+
if (impl.edge) {
|
|
100
|
+
const evidence = parseEvidence(impl.edge.evidence_json);
|
|
101
|
+
const hintDiagnostic = implementationHintDiagnostic(hinted);
|
|
102
|
+
const diagnostics = [{ severity: 'warning', code: impl.edge.status === 'ambiguous' ? 'trace_start_ambiguous' : 'trace_start_implementation_unresolved', message: `Indexed operation matched but implementation edge is ${String(impl.edge.status ?? 'unresolved')}`, resolutionStage: 'implementation', resolutionStatus: impl.edge.status === 'ambiguous' ? 'ambiguous_implementation' : 'rejected_implementation', implementationEdgeId: impl.edge.id, implementationStatus: impl.edge.status, implementationAmbiguityReasons: evidence.ambiguityReasons, candidates: evidence.candidates }];
|
|
103
|
+
return { operationId, diagnostics: hintDiagnostic ? [hintDiagnostic, ...diagnostics] : diagnostics };
|
|
104
|
+
}
|
|
95
105
|
return { operationId, diagnostics: [{ severity: 'warning', code: 'trace_start_implementation_unresolved', message: 'Indexed operation matched but no implementation candidate exists', resolutionStage: 'implementation', resolutionStatus: 'operation_without_implementation' }] };
|
|
96
106
|
}
|
|
97
107
|
|
|
@@ -141,7 +151,7 @@ function sourceFilesForStart(
|
|
|
141
151
|
}
|
|
142
152
|
return undefined;
|
|
143
153
|
}
|
|
144
|
-
function startScope(db: Db, start: TraceStart): StartScope {
|
|
154
|
+
function startScope(db: Db, start: TraceStart, hintOptions: ImplementationHintOptions): StartScope {
|
|
145
155
|
const repo = start.repo
|
|
146
156
|
? (db
|
|
147
157
|
.prepare(
|
|
@@ -150,7 +160,7 @@ function startScope(db: Db, start: TraceStart): StartScope {
|
|
|
150
160
|
.get(start.repo, start.repo) as RepoRef | undefined)
|
|
151
161
|
: undefined;
|
|
152
162
|
if (start.repo && !repo) return { repo, selectorMatched: false };
|
|
153
|
-
const operationScope = operationStartScope(db, repo?.id, start);
|
|
163
|
+
const operationScope = operationStartScope(db, repo?.id, start, hintOptions);
|
|
154
164
|
const terminalOperationScope = operationScope && !operationScope.files && (operationScope.diagnostics ?? []).some((d) => d.resolutionStage === 'operation' || d.resolutionStage === 'implementation');
|
|
155
165
|
const sourceScope = operationScope?.files || terminalOperationScope ? operationScope : sourceFilesForStart(db, repo?.id, start);
|
|
156
166
|
const sourceFiles = sourceScope?.files;
|
|
@@ -206,8 +216,16 @@ function implementationScope(db: Db, operationId: string): { repoId?: number; fi
|
|
|
206
216
|
const row = db.prepare('SELECT hc.repo_id repoId,hc.source_file sourceFile,s.id symbolId FROM handler_methods hm JOIN handler_classes hc ON hc.id=hm.handler_class_id LEFT JOIN symbols s ON s.repo_id=hc.repo_id AND s.source_file=hc.source_file AND s.name=hm.method_name WHERE hm.id=?').get(edge.to_id) as { repoId?: number; sourceFile?: string; symbolId?: number } | undefined;
|
|
207
217
|
return { repoId: row?.repoId, files: new Set(row?.sourceFile ? [row.sourceFile] : []), symbolId: row?.symbolId, edge };
|
|
208
218
|
}
|
|
209
|
-
function
|
|
210
|
-
if (!edge || edge.status !== 'ambiguous'
|
|
219
|
+
function implementationMethodIdFromHint(edge: GraphRow | undefined, options: ImplementationHintOptions): ImplementationSelection {
|
|
220
|
+
if (!edge || edge.status !== 'ambiguous') return { blocksAutomatic: false, evidence: { status: 'not_applicable' } };
|
|
221
|
+
return selectImplementation(parseEvidence(edge.evidence_json), options.implementationHints, options.implementationRepo);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function contextImplementationMethodId(edge: GraphRow | undefined, callerRepoId: number | undefined, remoteEvidence: Record<string, unknown>, hintOptions: ImplementationHintOptions): ImplementationSelection {
|
|
225
|
+
const hinted = implementationMethodIdFromHint(edge, hintOptions);
|
|
226
|
+
if (hinted.methodId) return hinted;
|
|
227
|
+
if (hinted.blocksAutomatic) return hinted;
|
|
228
|
+
if (!edge || edge.status !== 'ambiguous' || callerRepoId === undefined) return hinted;
|
|
211
229
|
const evidence = JSON.parse(String(edge.evidence_json || '{}')) as { candidates?: Array<{ accepted?: boolean; methodId?: number; handlerPackage?: { id?: number; name?: string }; applicationPackage?: { id?: number; name?: string }; reasons?: string[]; score?: number }> };
|
|
212
230
|
const scores = (evidence.candidates ?? []).filter((item) => item.accepted).map((item) => {
|
|
213
231
|
const reasons: string[] = [];
|
|
@@ -217,10 +235,10 @@ function contextImplementationMethodId(edge: GraphRow | undefined, callerRepoId:
|
|
|
217
235
|
if (typeof remoteEvidence.effectiveServicePath === 'string' || typeof remoteEvidence.effectiveDestination === 'string' || typeof remoteEvidence.effectiveAlias === 'string') { score += 1; reasons.push('remote_call_context_available'); }
|
|
218
236
|
return { methodId: item.methodId, score, reasons, handlerPackage: item.handlerPackage, applicationPackage: item.applicationPackage };
|
|
219
237
|
}).sort((a, b) => b.score - a.score);
|
|
220
|
-
if (scores.length === 0) return { evidence: { status: 'not_applicable', candidateScores: [] } };
|
|
238
|
+
if (scores.length === 0) return { blocksAutomatic: false, evidence: { status: 'not_applicable', candidateScores: [] } };
|
|
221
239
|
const [first, second] = scores;
|
|
222
|
-
if (first && first.methodId !== undefined && first.score > 0 && (!second || first.score > second.score)) return { methodId: String(first.methodId), evidence: { status: 'selected', selectedMethodId: first.methodId, candidateScores: scores } };
|
|
223
|
-
return { evidence: { status: 'tied', tieReason: scores.length > 1 ? 'duplicate_helper_implementation_candidates' : 'no_unique_materially_stronger_candidate', candidateScores: scores } };
|
|
240
|
+
if (first && first.methodId !== undefined && first.score > 0 && (!second || first.score > second.score)) return { methodId: String(first.methodId), blocksAutomatic: false, evidence: { status: 'selected', selectedMethodId: first.methodId, candidateScores: scores } };
|
|
241
|
+
return { blocksAutomatic: false, evidence: hinted.evidence.reason === 'no_scoped_hint_matched_edge' ? hinted.evidence : { status: 'tied', tieReason: scores.length > 1 ? 'duplicate_helper_implementation_candidates' : 'no_unique_materially_stronger_candidate', candidateScores: scores } };
|
|
224
242
|
}
|
|
225
243
|
function handlerScope(db: Db, methodId: string): { repoId?: number; files: Set<string>; symbolId?: number } | undefined {
|
|
226
244
|
const row = db.prepare('SELECT hc.repo_id repoId,hc.source_file sourceFile,s.id symbolId FROM handler_methods hm JOIN handler_classes hc ON hc.id=hm.handler_class_id LEFT JOIN symbols s ON s.repo_id=hc.repo_id AND s.source_file=hc.source_file AND s.name=hm.method_name WHERE hm.id=?').get(methodId) as { repoId?: number; sourceFile?: string; symbolId?: number } | undefined;
|
|
@@ -262,38 +280,6 @@ function graphForCalls(db: Db, callIds: number[]): Map<number, GraphRow[]> {
|
|
|
262
280
|
}
|
|
263
281
|
return map;
|
|
264
282
|
}
|
|
265
|
-
function hasRuntimeVariable(value: unknown, vars: Record<string, string>): boolean {
|
|
266
|
-
return typeof value === 'string' && extractPlaceholders(value).some((key) => Object.hasOwn(vars, key));
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
function isRemoteRuntimeCandidate(row: GraphRow, evidence: Record<string, unknown>, vars: Record<string, string> | undefined): boolean {
|
|
270
|
-
if (!vars || Object.keys(vars).length === 0) return false;
|
|
271
|
-
if (!['dynamic', 'ambiguous', 'unresolved'].includes(String(row.status ?? ''))) return false;
|
|
272
|
-
if (!['DYNAMIC_EDGE_CANDIDATE', 'UNRESOLVED_EDGE', 'REMOTE_CALL_RESOLVES_TO_OPERATION'].includes(row.edge_type)) return false;
|
|
273
|
-
if (row.status === 'resolved') return false;
|
|
274
|
-
return ['servicePath', 'operationPath', 'serviceAliasExpr', 'serviceAlias', 'destination'].some((key) => hasRuntimeVariable(evidence[key], vars));
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
function evidenceWithRuntimeVariables(
|
|
278
|
-
evidence: Record<string, unknown>,
|
|
279
|
-
vars: Record<string, string> | undefined,
|
|
280
|
-
): Record<string, unknown> {
|
|
281
|
-
if (!vars || Object.keys(vars).length === 0) return evidence;
|
|
282
|
-
const substitutions: Record<string, RuntimeSubstitution> = {};
|
|
283
|
-
for (const key of ['servicePath', 'operationPath', 'serviceAliasExpr', 'serviceAlias', 'destination']) {
|
|
284
|
-
const substitution = substituteVariables(typeof evidence[key] === 'string' ? String(evidence[key]) : undefined, vars);
|
|
285
|
-
if (substitution.placeholders.length > 0) substitutions[key] = substitution;
|
|
286
|
-
}
|
|
287
|
-
const next: Record<string, unknown> = { ...evidence, runtimeVariablesApplied: true, runtimeSubstitutions: substitutions };
|
|
288
|
-
for (const [key, value] of Object.entries(substitutions)) {
|
|
289
|
-
if (value.effective) next[key] = value.effective;
|
|
290
|
-
}
|
|
291
|
-
const missing = Object.values(substitutions).flatMap((value) => value.missing);
|
|
292
|
-
if (missing.length > 0) next.missingRuntimeVariables = [...new Set(missing)];
|
|
293
|
-
return next;
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
|
|
297
283
|
function symbolNode(db: Db, symbolId: number): Record<string, unknown> | undefined {
|
|
298
284
|
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;
|
|
299
285
|
if (!row) return undefined;
|
|
@@ -309,24 +295,6 @@ function operationNode(db: Db, operationId: string): Record<string, unknown> | u
|
|
|
309
295
|
function workspaceIdForCall(db: Db, callId: string): number | undefined {
|
|
310
296
|
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;
|
|
311
297
|
}
|
|
312
|
-
function runtimeResolution(db: Db, row: GraphRow, evidence: Record<string, unknown>, vars: Record<string, string> | undefined): { row: GraphRow; evidence: Record<string, unknown>; target?: OperationTarget; unresolvedReason?: string } {
|
|
313
|
-
if (!isRemoteRuntimeCandidate(row, evidence, vars))
|
|
314
|
-
return { row, evidence, unresolvedReason: row.unresolved_reason };
|
|
315
|
-
const nextEvidence = evidenceWithRuntimeVariables(evidence, vars);
|
|
316
|
-
const servicePath = typeof nextEvidence.servicePath === 'string' ? nextEvidence.servicePath : undefined;
|
|
317
|
-
const operationPath = typeof nextEvidence.normalizedOperationPath === 'string' ? nextEvidence.normalizedOperationPath : typeof nextEvidence.operationPath === 'string' ? nextEvidence.operationPath : undefined;
|
|
318
|
-
const alias = typeof nextEvidence.serviceAliasExpr === 'string' ? nextEvidence.serviceAliasExpr : typeof nextEvidence.serviceAlias === 'string' ? nextEvidence.serviceAlias : undefined;
|
|
319
|
-
const destination = typeof nextEvidence.destination === 'string' ? nextEvidence.destination : undefined;
|
|
320
|
-
const resolution = resolveOperation(db, { servicePath, operationPath, alias, destination, hasExplicitOverride: true, isDynamic: true }, workspaceIdForCall(db, row.from_id));
|
|
321
|
-
nextEvidence.runtimeResolutionStatus = resolution.status;
|
|
322
|
-
nextEvidence.runtimeResolutionReasons = resolution.reasons;
|
|
323
|
-
if (resolution.target) {
|
|
324
|
-
nextEvidence.runtimeResolvedCandidate = resolution.target;
|
|
325
|
-
return { row: { ...row, to_kind: 'operation', to_id: String(resolution.target.operationId), unresolved_reason: undefined, confidence: Math.max(0, Math.min(1, resolution.target.score)) }, evidence: nextEvidence, target: resolution.target };
|
|
326
|
-
}
|
|
327
|
-
const unresolvedReason = resolution.status === 'dynamic' ? `Dynamic target is missing runtime variables: ${resolution.reasons.join(', ')}` : resolution.status === 'ambiguous' ? 'Ambiguous runtime operation candidates' : 'No runtime operation candidate matched substituted service and operation path';
|
|
328
|
-
return { row, evidence: nextEvidence, unresolvedReason };
|
|
329
|
-
}
|
|
330
298
|
function parseEvidence(value: unknown): Record<string, unknown> {
|
|
331
299
|
try {
|
|
332
300
|
const parsed = JSON.parse(String(value || '{}')) as unknown;
|
|
@@ -382,18 +350,22 @@ function contextForSymbolCall(db: Db, symbolCall: Record<string, unknown>, calle
|
|
|
382
350
|
const next = new Map<string, ContextBinding>();
|
|
383
351
|
if (callerBindings.size === 0) return next;
|
|
384
352
|
const callEvidence = parseEvidence(symbolCall.evidence_json);
|
|
385
|
-
const callee = db.prepare('SELECT evidence_json evidenceJson FROM symbols WHERE id=?').get(symbolCall.callee_symbol_id) as { evidenceJson?: string } | undefined;
|
|
353
|
+
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;
|
|
386
354
|
const calleeEvidence = parseEvidence(callee?.evidenceJson);
|
|
387
355
|
const params = Array.isArray(calleeEvidence.parameters) ? calleeEvidence.parameters.filter((item): item is string => typeof item === 'string') : [];
|
|
388
356
|
const parameterBindings = Array.isArray(calleeEvidence.parameterBindings) ? calleeEvidence.parameterBindings.filter((item): item is Record<string, unknown> => Boolean(item && typeof item === 'object' && !Array.isArray(item))) : [];
|
|
389
357
|
const parameterPropertyAliases = Array.isArray(calleeEvidence.parameterPropertyAliases) ? calleeEvidence.parameterPropertyAliases.filter((item): item is Record<string, unknown> => Boolean(item && typeof item === 'object' && !Array.isArray(item))) : [];
|
|
390
358
|
const args = Array.isArray(callEvidence.callArguments) ? callEvidence.callArguments as Array<Record<string, unknown>> : [];
|
|
359
|
+
const provenance = {
|
|
360
|
+
callerSite: { sourceFile: String(symbolCall.source_file ?? ''), sourceLine: Number(symbolCall.source_line ?? 0) },
|
|
361
|
+
calleeSite: { sourceFile: callee?.sourceFile, sourceLine: callee?.startLine },
|
|
362
|
+
};
|
|
391
363
|
args.forEach((arg, index) => {
|
|
392
364
|
const paramBinding = parameterBindings.find((binding) => binding.index === index);
|
|
393
365
|
const param = paramBinding?.kind === 'identifier' && typeof paramBinding.name === 'string' ? paramBinding.name : params[index];
|
|
394
366
|
if (arg.kind === 'identifier' && typeof arg.name === 'string') {
|
|
395
367
|
const binding = callerBindings.get(arg.name);
|
|
396
|
-
if (binding && param) next.set(param, { ...binding, source: 'local_symbol_argument', callerArgument: arg.name, calleeParameter: param, calleeReceiver: param });
|
|
368
|
+
if (binding && param) next.set(param, { ...binding, ...provenance, source: 'local_symbol_argument', callerArgument: arg.name, calleeParameter: param, calleeReceiver: param });
|
|
397
369
|
}
|
|
398
370
|
if (arg.kind === 'object_literal' && Array.isArray(arg.properties)) {
|
|
399
371
|
for (const prop of arg.properties as Array<Record<string, unknown>>) {
|
|
@@ -403,15 +375,23 @@ function contextForSymbolCall(db: Db, symbolCall: Record<string, unknown>, calle
|
|
|
403
375
|
const destructured = paramBinding?.kind === 'object_pattern' && Array.isArray(paramBinding.properties)
|
|
404
376
|
? (paramBinding.properties as Array<Record<string, unknown>>).find((item) => item.property === prop.property && typeof item.local === 'string')
|
|
405
377
|
: undefined;
|
|
406
|
-
if (destructured && typeof destructured.local === 'string') next.set(destructured.local, { ...binding, source: 'local_symbol_destructured_object_argument', callerProperty: prop.property, callerArgument: prop.argument, calleeParameter: String(index), calleeReceiver: destructured.local });
|
|
378
|
+
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 });
|
|
407
379
|
else if (param) {
|
|
408
|
-
next.set(`${param}.${prop.property}`, { ...binding, source: 'local_symbol_object_argument', callerProperty: prop.property, callerArgument: prop.argument, calleeParameter: param, calleeReceiver: `${param}.${prop.property}` });
|
|
380
|
+
next.set(`${param}.${prop.property}`, { ...binding, ...provenance, source: 'local_symbol_object_argument', callerProperty: prop.property, callerArgument: prop.argument, calleeParameter: param, calleeReceiver: `${param}.${prop.property}` });
|
|
409
381
|
for (const alias of parameterPropertyAliases) {
|
|
410
|
-
if (alias.parameter === param && alias.property === prop.property && typeof alias.local === 'string') next.set(alias.local, { ...binding, 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 });
|
|
382
|
+
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 });
|
|
411
383
|
}
|
|
412
384
|
}
|
|
413
385
|
}
|
|
414
386
|
}
|
|
387
|
+
if (arg.kind === 'array_literal' && Array.isArray(arg.elements) && paramBinding?.kind === 'array_pattern' && Array.isArray(paramBinding.elements)) {
|
|
388
|
+
for (const element of arg.elements as Array<Record<string, unknown>>) {
|
|
389
|
+
const target = (paramBinding.elements as Array<Record<string, unknown>>).find((item) => item.index === element.index);
|
|
390
|
+
if (element.kind !== 'identifier' || typeof element.name !== 'string' || typeof target?.local !== 'string') continue;
|
|
391
|
+
const binding = callerBindings.get(element.name);
|
|
392
|
+
if (binding) next.set(target.local, { ...binding, ...provenance, source: 'local_symbol_destructured_array_argument', callerArgument: element.name, calleeParameter: String(index), calleeReceiver: target.local });
|
|
393
|
+
}
|
|
394
|
+
}
|
|
415
395
|
});
|
|
416
396
|
return next;
|
|
417
397
|
}
|
|
@@ -422,46 +402,13 @@ function contextualRuntimeResolution(db: Db, call: CallRow, binding: ContextBind
|
|
|
422
402
|
const servicePath = binding.effectiveServicePath ?? binding.servicePathExpr ?? binding.requireServicePath;
|
|
423
403
|
const destination = binding.effectiveDestination ?? binding.destinationExpr ?? binding.requireDestination;
|
|
424
404
|
const resolution = resolveOperation(db, { servicePath, operationPath: op, alias: binding.aliasExpr ?? binding.alias, destination, hasExplicitOverride: true, isDynamic: false }, workspaceId);
|
|
425
|
-
const evidence: Record<string, unknown> = { contextualServiceBindingAttempted: true, contextualBinding: { source: binding.source, callerArgument: binding.callerArgument, callerProperty: binding.callerProperty, calleeParameter: binding.calleeParameter, calleeReceiver: binding.calleeReceiver, bindingSourceFile: binding.sourceFile, bindingSourceLine: binding.sourceLine, alias: binding.alias, aliasExpr: binding.aliasExpr, requireServicePath: binding.requireServicePath, requireDestination: binding.requireDestination, effectiveServicePath: binding.effectiveServicePath, effectiveDestination: binding.effectiveDestination }, operationPath: op, rawOperationPath: normalized?.rawOperationPath, normalizedOperationPath: normalized?.wasInvocation ? normalized.normalizedOperationPath : undefined, invocationArgumentPlaceholderKeys: normalized?.invocationArgumentPlaceholderKeys.length ? normalized.invocationArgumentPlaceholderKeys : undefined, servicePath, serviceAlias: binding.alias, serviceAliasExpr: binding.aliasExpr, destination, requireServicePath: binding.requireServicePath, requireDestination: binding.requireDestination, effectiveServicePath: binding.effectiveServicePath, effectiveDestination: binding.effectiveDestination, contextualResolutionStatus: resolution.status, contextualCandidateCount: resolution.candidates.length, candidates: resolution.candidates, contextualResolutionReasons: resolution.reasons, resolutionReasons: resolution.reasons };
|
|
405
|
+
const evidence: Record<string, unknown> = { contextualServiceBindingAttempted: true, contextualBinding: { source: binding.source, callerArgument: binding.callerArgument, callerProperty: binding.callerProperty, calleeParameter: binding.calleeParameter, calleeReceiver: binding.calleeReceiver, callerSite: binding.callerSite, calleeSite: binding.calleeSite, bindingSourceFile: binding.sourceFile, bindingSourceLine: binding.sourceLine, alias: binding.alias, aliasExpr: binding.aliasExpr, requireServicePath: binding.requireServicePath, requireDestination: binding.requireDestination, effectiveServicePath: binding.effectiveServicePath, effectiveDestination: binding.effectiveDestination }, operationPath: op, rawOperationPath: normalized?.rawOperationPath, normalizedOperationPath: normalized?.wasInvocation ? normalized.normalizedOperationPath : undefined, invocationArgumentPlaceholderKeys: normalized?.invocationArgumentPlaceholderKeys.length ? normalized.invocationArgumentPlaceholderKeys : undefined, servicePath, serviceAlias: binding.alias, serviceAliasExpr: binding.aliasExpr, destination, requireServicePath: binding.requireServicePath, requireDestination: binding.requireDestination, effectiveServicePath: binding.effectiveServicePath, effectiveDestination: binding.effectiveDestination, contextualResolutionStatus: resolution.status, contextualCandidateCount: resolution.candidates.length, candidates: resolution.candidates, contextualResolutionReasons: resolution.reasons, resolutionReasons: resolution.reasons };
|
|
426
406
|
if (!resolution.target) return { evidence, unresolvedReason: resolution.status === 'ambiguous' ? 'Ambiguous contextual operation candidates' : resolution.status === 'dynamic' ? `Dynamic contextual target is missing runtime variables: ${resolution.reasons.join(', ')}` : 'No contextual operation candidate matched' };
|
|
427
407
|
const resolvedEvidence = { ...evidence, contextualServiceBindingSelected: true, targetRepo: resolution.target.repoName, targetServicePath: resolution.target.servicePath, targetOperationPath: resolution.target.operationPath, targetOperation: resolution.target.operationName };
|
|
428
408
|
const persistedResolved = persistedRows.find((item) => item.status === 'resolved');
|
|
429
409
|
if (persistedResolved) return { row: undefined, evidence: { ...resolvedEvidence, contextualPreservedPersistedResolvedEdge: true }, unresolvedReason: undefined };
|
|
430
410
|
return { row: { id: -Number(call.id), edge_type: 'REMOTE_CALL_RESOLVES_TO_OPERATION', from_id: String(call.id), to_kind: 'operation', to_id: String(resolution.target.operationId), confidence: resolution.target.score, evidence_json: JSON.stringify(resolvedEvidence), status: 'resolved' }, evidence: resolvedEvidence, unresolvedReason: undefined };
|
|
431
411
|
}
|
|
432
|
-
function edgeTarget(row: GraphRow, evidence: Record<string, unknown>): string {
|
|
433
|
-
const runtimeCandidate = evidence.runtimeResolvedCandidate as
|
|
434
|
-
| Candidate
|
|
435
|
-
| undefined;
|
|
436
|
-
if (runtimeCandidate?.servicePath && runtimeCandidate.operationPath)
|
|
437
|
-
return `${runtimeCandidate.servicePath}${runtimeCandidate.operationPath}`;
|
|
438
|
-
const targetServicePath = typeof evidence.targetServicePath === 'string' ? evidence.targetServicePath : undefined;
|
|
439
|
-
const targetOperationPath = typeof evidence.targetOperationPath === 'string' ? evidence.targetOperationPath : undefined;
|
|
440
|
-
if (targetServicePath && targetOperationPath) return `${targetServicePath}${targetOperationPath}`;
|
|
441
|
-
const servicePath =
|
|
442
|
-
typeof evidence.servicePath === 'string' ? evidence.servicePath : undefined;
|
|
443
|
-
const operationPath =
|
|
444
|
-
typeof evidence.operationPath === 'string'
|
|
445
|
-
? evidence.operationPath
|
|
446
|
-
: undefined;
|
|
447
|
-
const targetOperation =
|
|
448
|
-
typeof evidence.targetOperation === 'string'
|
|
449
|
-
? evidence.targetOperation
|
|
450
|
-
: undefined;
|
|
451
|
-
const targetRepo =
|
|
452
|
-
typeof evidence.targetRepo === 'string' ? evidence.targetRepo : '';
|
|
453
|
-
if (row.edge_type === 'HANDLER_RUNS_DB_QUERY') return `Entity: ${row.to_id || 'unknown'}`;
|
|
454
|
-
if (row.edge_type === 'HANDLER_RUNS_REMOTE_QUERY') return typeof evidence.remoteQueryTarget === 'string' ? evidence.remoteQueryTarget : `Remote query: ${row.to_id || 'unknown'}`;
|
|
455
|
-
if (row.edge_type === 'HANDLER_CALLS_EXTERNAL_HTTP') {
|
|
456
|
-
const target = evidence.externalTarget as Record<string, unknown> | undefined;
|
|
457
|
-
return typeof target?.label === 'string' ? target.label : `External endpoint: ${row.to_id || 'unknown'}`;
|
|
458
|
-
}
|
|
459
|
-
return servicePath && operationPath
|
|
460
|
-
? `${servicePath}${operationPath}`
|
|
461
|
-
: targetOperation
|
|
462
|
-
? `${targetRepo}:${targetOperation}`
|
|
463
|
-
: row.to_id;
|
|
464
|
-
}
|
|
465
412
|
export function trace(
|
|
466
413
|
db: Db,
|
|
467
414
|
start: TraceStart,
|
|
@@ -471,9 +418,12 @@ export function trace(
|
|
|
471
418
|
includeExternal?: boolean;
|
|
472
419
|
includeDb?: boolean;
|
|
473
420
|
includeAsync?: boolean;
|
|
421
|
+
implementationRepo?: string;
|
|
422
|
+
implementationHints?: ImplementationHint[];
|
|
474
423
|
},
|
|
475
424
|
): TraceResult {
|
|
476
|
-
const
|
|
425
|
+
const hintOptions = { implementationRepo: options.implementationRepo, implementationHints: options.implementationHints };
|
|
426
|
+
const scope = startScope(db, start, hintOptions);
|
|
477
427
|
const diagnostics = db
|
|
478
428
|
.prepare(
|
|
479
429
|
'SELECT severity,code,message,source_file sourceFile,source_line sourceLine FROM diagnostics WHERE (? IS NULL OR repo_id=?)',
|
|
@@ -501,12 +451,14 @@ export function trace(
|
|
|
501
451
|
const op = operationNode(db, scope.startOperationId);
|
|
502
452
|
const impl = implementationScope(db, scope.startOperationId);
|
|
503
453
|
if (op) nodes.set(String(op.id), op);
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
const
|
|
454
|
+
const startSelection = implementationMethodIdFromHint(impl.edge, hintOptions);
|
|
455
|
+
if (impl.edge && (impl.edge.status === 'resolved' || startSelection.methodId)) {
|
|
456
|
+
const selectedMethodId = impl.edge.status === 'resolved' ? impl.edge.to_id : startSelection.methodId;
|
|
457
|
+
const implEvidence = { ...parseEvidence(impl.edge.evidence_json), startResolution: { strategy: 'indexed_operation_graph', matchedOperationId: scope.startOperationId, implementationEdgeId: impl.edge.id, implementationStatus: impl.edge.status, selectedHandlerMethodId: selectedMethodId }, implementationSelection: startSelection.methodId ? startSelection.evidence : undefined };
|
|
458
|
+
const handlerNode = selectedMethodId ? handlerMethodNode(db, selectedMethodId) : undefined;
|
|
507
459
|
if (handlerNode) nodes.set(String(handlerNode.id), handlerNode);
|
|
508
460
|
seenEdges.add(Number(impl.edge.id));
|
|
509
|
-
edges.push({ step: 1, type: 'operation_implemented_by_handler', from: op?.label ? String(op.label) : `operation:${scope.startOperationId}`, to: handlerNode?.label ? String(handlerNode.label) : `${impl.edge.to_kind}:${impl.edge.to_id}`, evidence: implEvidence, confidence: Number(impl.edge.confidence ?? 0), unresolvedReason: impl.edge.status === 'resolved' ? undefined : String(impl.edge.unresolved_reason ?? impl.edge.status) });
|
|
461
|
+
edges.push({ step: 1, type: 'operation_implemented_by_handler', from: op?.label ? String(op.label) : `operation:${scope.startOperationId}`, to: handlerNode?.label ? String(handlerNode.label) : `${impl.edge.to_kind}:${impl.edge.to_id}`, evidence: implEvidence, confidence: Number(impl.edge.confidence ?? 0), unresolvedReason: impl.edge.status === 'resolved' || startSelection.methodId ? undefined : String(impl.edge.unresolved_reason ?? impl.edge.status) });
|
|
510
462
|
}
|
|
511
463
|
}
|
|
512
464
|
const seenScopes = new Set<string>();
|
|
@@ -568,8 +520,8 @@ export function trace(
|
|
|
568
520
|
const persistedEvidence = JSON.parse(
|
|
569
521
|
String(row.evidence_json || '{}'),
|
|
570
522
|
) as Record<string, unknown>;
|
|
571
|
-
const rawEvidence =
|
|
572
|
-
const effective = runtimeResolution(db, row, rawEvidence, options.vars);
|
|
523
|
+
const rawEvidence = baseTraceEvidence(row as TraceGraphRow, call, persistedEvidence, contextual.evidence);
|
|
524
|
+
const effective = runtimeResolution(db, row as TraceGraphRow, rawEvidence, options.vars, workspaceIdForCall(db, String(call.id)));
|
|
573
525
|
const evidence = effective.evidence;
|
|
574
526
|
const effectiveRow = effective.row;
|
|
575
527
|
const targetNode = `${effectiveRow.to_kind}:${effectiveRow.to_id}`;
|
|
@@ -591,10 +543,12 @@ export function trace(
|
|
|
591
543
|
});
|
|
592
544
|
if (effectiveRow.to_kind === 'operation') {
|
|
593
545
|
const implementation = implementationScope(db, effectiveRow.to_id);
|
|
594
|
-
const contextSelection = contextImplementationMethodId(implementation.edge, current.repoId, evidence);
|
|
546
|
+
const contextSelection = contextImplementationMethodId(implementation.edge, current.repoId, evidence, hintOptions);
|
|
595
547
|
const contextMethodId = contextSelection.methodId;
|
|
596
548
|
const contextNode = contextMethodId ? handlerMethodNode(db, contextMethodId) : undefined;
|
|
597
549
|
if (implementation.edge) {
|
|
550
|
+
const hintDiagnostic = implementationHintDiagnostic(contextSelection);
|
|
551
|
+
if (hintDiagnostic) diagnostics.unshift(hintDiagnostic);
|
|
598
552
|
const implEvidence = JSON.parse(String(implementation.edge.evidence_json || '{}')) as Record<string, unknown>;
|
|
599
553
|
const handlerNode = implementation.edge.status === 'resolved' ? handlerMethodNode(db, implementation.edge.to_id) : contextNode;
|
|
600
554
|
const implTo = handlerNode?.label ? String(handlerNode.label) : `${implementation.edge.to_kind}:${implementation.edge.to_id}`;
|
|
@@ -604,7 +558,9 @@ export function trace(
|
|
|
604
558
|
type: 'operation_implemented_by_handler',
|
|
605
559
|
from: to,
|
|
606
560
|
to: implTo,
|
|
607
|
-
evidence: contextMethodId
|
|
561
|
+
evidence: contextMethodId
|
|
562
|
+
? { ...implEvidence, contextualImplementationSelected: contextSelection.evidence.strategy !== 'implementation_repo_hint', contextualImplementation: contextSelection.evidence, implementationSelection: contextSelection.evidence }
|
|
563
|
+
: { ...implEvidence, contextualImplementation: contextSelection.evidence, implementationSelection: contextSelection.evidence },
|
|
608
564
|
confidence: Number(implementation.edge.confidence ?? 0),
|
|
609
565
|
unresolvedReason: implementation.edge.status === 'resolved' || contextMethodId ? undefined : String(implementation.edge.unresolved_reason ?? implementation.edge.status),
|
|
610
566
|
});
|
|
@@ -643,5 +599,7 @@ export function trace(
|
|
|
643
599
|
}
|
|
644
600
|
}
|
|
645
601
|
}
|
|
602
|
+
const runtimeDiagnostic = runtimeVariableDiagnostic(edges);
|
|
603
|
+
if (runtimeDiagnostic) diagnostics.unshift(runtimeDiagnostic);
|
|
646
604
|
return { start, nodes: [...nodes.values()], edges, diagnostics };
|
|
647
605
|
}
|
package/src/types.ts
CHANGED
|
@@ -179,6 +179,14 @@ export interface TraceStart {
|
|
|
179
179
|
operationPath?: string;
|
|
180
180
|
handler?: string;
|
|
181
181
|
}
|
|
182
|
+
export interface ImplementationHint {
|
|
183
|
+
servicePath?: string;
|
|
184
|
+
operationPath?: string;
|
|
185
|
+
packageName?: string;
|
|
186
|
+
repositoryName?: string;
|
|
187
|
+
candidateFamily?: string;
|
|
188
|
+
implementationRepo: string;
|
|
189
|
+
}
|
|
182
190
|
export interface TraceEdge {
|
|
183
191
|
step: number;
|
|
184
192
|
type: string;
|