@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.
@@ -1,8 +1,10 @@
1
1
  import type { Db } from '../db/connection.js';
2
- import { extractPlaceholders, substituteVariables, type RuntimeSubstitution } from '../linker/dynamic-edge-resolver.js';
2
+ import { extractPlaceholders } from '../linker/dynamic-edge-resolver.js';
3
3
  import { normalizeODataOperationInvocationPath } from '../linker/odata-path-normalizer.js';
4
- import { resolveOperation, type OperationTarget } from '../linker/service-resolver.js';
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 Candidate {
63
- servicePath?: string;
64
- operationPath?: string;
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
- if (impl.edge) return { operationId, 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, candidates: parseEvidence(impl.edge.evidence_json).candidates }] };
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 contextImplementationMethodId(edge: GraphRow | undefined, callerRepoId: number | undefined, remoteEvidence: Record<string, unknown> = {}): { methodId?: string; evidence: Record<string, unknown> } {
210
- if (!edge || edge.status !== 'ambiguous' || callerRepoId === undefined) return { evidence: { status: 'not_applicable' } };
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 scope = startScope(db, start);
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
- if (impl.edge && impl.edge.status === 'resolved') {
505
- const implEvidence = { ...parseEvidence(impl.edge.evidence_json), startResolution: { strategy: 'indexed_operation_graph', matchedOperationId: scope.startOperationId, implementationEdgeId: impl.edge.id, implementationStatus: impl.edge.status, selectedHandlerMethodId: impl.edge.status === 'resolved' ? impl.edge.to_id : undefined } };
506
- const handlerNode = impl.edge.status === 'resolved' ? handlerMethodNode(db, impl.edge.to_id) : undefined;
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 = { ...persistedEvidence, ...(contextual.evidence ?? {}), graphEdgeId: row.id, persistedGraphEdgeId: row.id > 0 ? row.id : undefined, outboundCallId: call.id, callSite: { sourceFile: call.source_file, sourceLine: call.source_line }, sourceFile: call.source_file, sourceLine: call.source_line, file: call.source_file, line: call.source_line, linker: { status: row.status, confidence: row.confidence, reason: row.unresolved_reason, edgeType: row.edge_type }, persistedTarget: { kind: row.to_kind, id: row.to_id }, contextualResolutionParticipated: Boolean(contextual.evidence?.contextualServiceBindingAttempted) };
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 ? { ...implEvidence, contextualImplementationSelected: true, contextualImplementation: contextSelection.evidence } : { ...implEvidence, contextualImplementation: contextSelection.evidence },
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;