@saptools/service-flow 0.1.44 → 0.1.46

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.
@@ -2,8 +2,9 @@ import type { Db } from '../db/connection.js';
2
2
  import { extractPlaceholders } from '../linker/dynamic-edge-resolver.js';
3
3
  import { normalizeODataOperationInvocationPath } from '../linker/odata-path-normalizer.js';
4
4
  import { resolveOperation } from '../linker/service-resolver.js';
5
- import type { TraceEdge, TraceResult, TraceStart } from '../types.js';
5
+ import type { ImplementationHint, TraceEdge, TraceResult, TraceStart } from '../types.js';
6
6
  import { baseTraceEvidence, edgeTarget, runtimeResolution, runtimeVariableDiagnostic, type TraceGraphRow } from './evidence.js';
7
+ import { implementationHintDiagnostic, selectImplementation, type ImplementationSelection } from './implementation-hints.js';
7
8
 
8
9
  interface RepoRef {
9
10
  id: number;
@@ -59,10 +60,12 @@ interface ContextBinding {
59
60
  parameterPropertyAliasKind?: unknown;
60
61
  parameterPropertyAliasLine?: unknown;
61
62
  calleeReceiver: string;
63
+ callerSite?: { sourceFile?: string; sourceLine?: number };
64
+ calleeSite?: { sourceFile?: string; sourceLine?: number };
62
65
  }
63
- interface ImplementationSelection {
64
- methodId?: string;
65
- evidence: Record<string, unknown>;
66
+ interface ImplementationHintOptions {
67
+ implementationRepo?: string;
68
+ implementationHints?: ImplementationHint[];
66
69
  }
67
70
  function normalizeOperation(value: string | undefined): string | undefined {
68
71
  if (!value) return undefined;
@@ -72,7 +75,7 @@ function positiveDepth(value: number): number {
72
75
  return Number.isFinite(value) && value > 0 ? Math.floor(value) : 25;
73
76
  }
74
77
 
75
- function operationStartScope(db: Db, repoId: number | undefined, start: TraceStart, implementationRepo?: string): { 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 {
76
79
  const requested = normalizeOperation(start.operationPath ?? start.operation);
77
80
  if (!requested) return undefined;
78
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
@@ -88,14 +91,16 @@ function operationStartScope(db: Db, repoId: number | undefined, start: TraceSta
88
91
  const operationId = String(rows[0]?.operationId);
89
92
  const impl = implementationScope(db, operationId);
90
93
  if (impl.edge?.status === 'resolved' && impl.files.size > 0) return { files: impl.files, symbols: impl.symbolId ? new Set([impl.symbolId]) : undefined, operationId, diagnostics: [] };
91
- const hinted = implementationMethodIdFromHint(impl.edge, implementationRepo);
94
+ const hinted = implementationMethodIdFromHint(impl.edge, hintOptions);
92
95
  if (hinted.methodId) {
93
96
  const hintedScope = handlerScope(db, hinted.methodId);
94
97
  if (hintedScope?.files.size) return { files: hintedScope.files, symbols: hintedScope.symbolId ? new Set([hintedScope.symbolId]) : undefined, operationId, diagnostics: [] };
95
98
  }
96
99
  if (impl.edge) {
97
100
  const evidence = parseEvidence(impl.edge.evidence_json);
98
- 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, implementationAmbiguityReasons: evidence.ambiguityReasons, candidates: evidence.candidates }] };
101
+ const hintDiagnostic = implementationHintDiagnostic(hinted, evidence.implementationHintSuggestions);
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, implementationHintSuggestions: evidence.implementationHintSuggestions, candidates: evidence.candidates }];
103
+ return { operationId, diagnostics: hintDiagnostic ? [hintDiagnostic, ...diagnostics] : diagnostics };
99
104
  }
100
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' }] };
101
106
  }
@@ -146,7 +151,7 @@ function sourceFilesForStart(
146
151
  }
147
152
  return undefined;
148
153
  }
149
- function startScope(db: Db, start: TraceStart, implementationRepo?: string): StartScope {
154
+ function startScope(db: Db, start: TraceStart, hintOptions: ImplementationHintOptions): StartScope {
150
155
  const repo = start.repo
151
156
  ? (db
152
157
  .prepare(
@@ -155,7 +160,7 @@ function startScope(db: Db, start: TraceStart, implementationRepo?: string): Sta
155
160
  .get(start.repo, start.repo) as RepoRef | undefined)
156
161
  : undefined;
157
162
  if (start.repo && !repo) return { repo, selectorMatched: false };
158
- const operationScope = operationStartScope(db, repo?.id, start, implementationRepo);
163
+ const operationScope = operationStartScope(db, repo?.id, start, hintOptions);
159
164
  const terminalOperationScope = operationScope && !operationScope.files && (operationScope.diagnostics ?? []).some((d) => d.resolutionStage === 'operation' || d.resolutionStage === 'implementation');
160
165
  const sourceScope = operationScope?.files || terminalOperationScope ? operationScope : sourceFilesForStart(db, repo?.id, start);
161
166
  const sourceFiles = sourceScope?.files;
@@ -211,32 +216,16 @@ function implementationScope(db: Db, operationId: string): { repoId?: number; fi
211
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;
212
217
  return { repoId: row?.repoId, files: new Set(row?.sourceFile ? [row.sourceFile] : []), symbolId: row?.symbolId, edge };
213
218
  }
214
- function implementationMethodIdFromHint(edge: GraphRow | undefined, implementationRepo: string | undefined): ImplementationSelection {
215
- if (!edge || edge.status !== 'ambiguous' || !implementationRepo) return { evidence: { status: 'not_applicable' } };
216
- const evidence = parseEvidence(edge.evidence_json) as { ambiguityReasons?: string[]; candidates?: Array<{ accepted?: boolean; methodId?: number; handlerPackage?: { name?: string; packageName?: string }; sourceFile?: string }> };
217
- const matches = (evidence.candidates ?? []).filter((item) => item.accepted && (
218
- item.handlerPackage?.name === implementationRepo ||
219
- item.handlerPackage?.packageName === implementationRepo ||
220
- item.sourceFile?.startsWith(implementationRepo)
221
- ));
222
- if (matches.length !== 1 || matches[0]?.methodId === undefined) return { evidence: { status: matches.length > 1 ? 'tied' : 'not_matched', strategy: 'implementation_repo_hint', selectedRepo: implementationRepo, candidateCount: matches.length } };
223
- return {
224
- methodId: String(matches[0].methodId),
225
- evidence: {
226
- status: 'selected',
227
- guided: true,
228
- strategy: 'implementation_repo_hint',
229
- selectedRepo: implementationRepo,
230
- selectedMethodId: matches[0].methodId,
231
- ambiguityReason: evidence.ambiguityReasons?.[0],
232
- },
233
- };
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);
234
222
  }
235
223
 
236
- function contextImplementationMethodId(edge: GraphRow | undefined, callerRepoId: number | undefined, remoteEvidence: Record<string, unknown> = {}, implementationRepo?: string): ImplementationSelection {
237
- const hinted = implementationMethodIdFromHint(edge, implementationRepo);
224
+ function contextImplementationMethodId(edge: GraphRow | undefined, callerRepoId: number | undefined, remoteEvidence: Record<string, unknown>, hintOptions: ImplementationHintOptions): ImplementationSelection {
225
+ const hinted = implementationMethodIdFromHint(edge, hintOptions);
238
226
  if (hinted.methodId) return hinted;
239
- if (!edge || edge.status !== 'ambiguous' || callerRepoId === undefined) return { evidence: { status: 'not_applicable' } };
227
+ if (hinted.blocksAutomatic) return hinted;
228
+ if (!edge || edge.status !== 'ambiguous' || callerRepoId === undefined) return hinted;
240
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 }> };
241
230
  const scores = (evidence.candidates ?? []).filter((item) => item.accepted).map((item) => {
242
231
  const reasons: string[] = [];
@@ -246,10 +235,10 @@ function contextImplementationMethodId(edge: GraphRow | undefined, callerRepoId:
246
235
  if (typeof remoteEvidence.effectiveServicePath === 'string' || typeof remoteEvidence.effectiveDestination === 'string' || typeof remoteEvidence.effectiveAlias === 'string') { score += 1; reasons.push('remote_call_context_available'); }
247
236
  return { methodId: item.methodId, score, reasons, handlerPackage: item.handlerPackage, applicationPackage: item.applicationPackage };
248
237
  }).sort((a, b) => b.score - a.score);
249
- if (scores.length === 0) return { evidence: { status: 'not_applicable', candidateScores: [] } };
238
+ if (scores.length === 0) return { blocksAutomatic: false, evidence: { status: 'not_applicable', candidateScores: [] } };
250
239
  const [first, second] = scores;
251
- 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 } };
252
- 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 } };
253
242
  }
254
243
  function handlerScope(db: Db, methodId: string): { repoId?: number; files: Set<string>; symbolId?: number } | undefined {
255
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;
@@ -361,18 +350,22 @@ function contextForSymbolCall(db: Db, symbolCall: Record<string, unknown>, calle
361
350
  const next = new Map<string, ContextBinding>();
362
351
  if (callerBindings.size === 0) return next;
363
352
  const callEvidence = parseEvidence(symbolCall.evidence_json);
364
- 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;
365
354
  const calleeEvidence = parseEvidence(callee?.evidenceJson);
366
355
  const params = Array.isArray(calleeEvidence.parameters) ? calleeEvidence.parameters.filter((item): item is string => typeof item === 'string') : [];
367
356
  const parameterBindings = Array.isArray(calleeEvidence.parameterBindings) ? calleeEvidence.parameterBindings.filter((item): item is Record<string, unknown> => Boolean(item && typeof item === 'object' && !Array.isArray(item))) : [];
368
357
  const parameterPropertyAliases = Array.isArray(calleeEvidence.parameterPropertyAliases) ? calleeEvidence.parameterPropertyAliases.filter((item): item is Record<string, unknown> => Boolean(item && typeof item === 'object' && !Array.isArray(item))) : [];
369
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
+ };
370
363
  args.forEach((arg, index) => {
371
364
  const paramBinding = parameterBindings.find((binding) => binding.index === index);
372
365
  const param = paramBinding?.kind === 'identifier' && typeof paramBinding.name === 'string' ? paramBinding.name : params[index];
373
366
  if (arg.kind === 'identifier' && typeof arg.name === 'string') {
374
367
  const binding = callerBindings.get(arg.name);
375
- 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 });
376
369
  }
377
370
  if (arg.kind === 'object_literal' && Array.isArray(arg.properties)) {
378
371
  for (const prop of arg.properties as Array<Record<string, unknown>>) {
@@ -382,15 +375,23 @@ function contextForSymbolCall(db: Db, symbolCall: Record<string, unknown>, calle
382
375
  const destructured = paramBinding?.kind === 'object_pattern' && Array.isArray(paramBinding.properties)
383
376
  ? (paramBinding.properties as Array<Record<string, unknown>>).find((item) => item.property === prop.property && typeof item.local === 'string')
384
377
  : undefined;
385
- 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 });
386
379
  else if (param) {
387
- 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}` });
388
381
  for (const alias of parameterPropertyAliases) {
389
- 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 });
390
383
  }
391
384
  }
392
385
  }
393
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
+ }
394
395
  });
395
396
  return next;
396
397
  }
@@ -401,7 +402,7 @@ function contextualRuntimeResolution(db: Db, call: CallRow, binding: ContextBind
401
402
  const servicePath = binding.effectiveServicePath ?? binding.servicePathExpr ?? binding.requireServicePath;
402
403
  const destination = binding.effectiveDestination ?? binding.destinationExpr ?? binding.requireDestination;
403
404
  const resolution = resolveOperation(db, { servicePath, operationPath: op, alias: binding.aliasExpr ?? binding.alias, destination, hasExplicitOverride: true, isDynamic: false }, workspaceId);
404
- 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 };
405
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' };
406
407
  const resolvedEvidence = { ...evidence, contextualServiceBindingSelected: true, targetRepo: resolution.target.repoName, targetServicePath: resolution.target.servicePath, targetOperationPath: resolution.target.operationPath, targetOperation: resolution.target.operationName };
407
408
  const persistedResolved = persistedRows.find((item) => item.status === 'resolved');
@@ -418,9 +419,11 @@ export function trace(
418
419
  includeDb?: boolean;
419
420
  includeAsync?: boolean;
420
421
  implementationRepo?: string;
422
+ implementationHints?: ImplementationHint[];
421
423
  },
422
424
  ): TraceResult {
423
- const scope = startScope(db, start, options.implementationRepo);
425
+ const hintOptions = { implementationRepo: options.implementationRepo, implementationHints: options.implementationHints };
426
+ const scope = startScope(db, start, hintOptions);
424
427
  const diagnostics = db
425
428
  .prepare(
426
429
  'SELECT severity,code,message,source_file sourceFile,source_line sourceLine FROM diagnostics WHERE (? IS NULL OR repo_id=?)',
@@ -448,7 +451,7 @@ export function trace(
448
451
  const op = operationNode(db, scope.startOperationId);
449
452
  const impl = implementationScope(db, scope.startOperationId);
450
453
  if (op) nodes.set(String(op.id), op);
451
- const startSelection = implementationMethodIdFromHint(impl.edge, options.implementationRepo);
454
+ const startSelection = implementationMethodIdFromHint(impl.edge, hintOptions);
452
455
  if (impl.edge && (impl.edge.status === 'resolved' || startSelection.methodId)) {
453
456
  const selectedMethodId = impl.edge.status === 'resolved' ? impl.edge.to_id : startSelection.methodId;
454
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 };
@@ -540,11 +543,13 @@ export function trace(
540
543
  });
541
544
  if (effectiveRow.to_kind === 'operation') {
542
545
  const implementation = implementationScope(db, effectiveRow.to_id);
543
- const contextSelection = contextImplementationMethodId(implementation.edge, current.repoId, evidence, options.implementationRepo);
546
+ const contextSelection = contextImplementationMethodId(implementation.edge, current.repoId, evidence, hintOptions);
544
547
  const contextMethodId = contextSelection.methodId;
545
548
  const contextNode = contextMethodId ? handlerMethodNode(db, contextMethodId) : undefined;
546
549
  if (implementation.edge) {
547
550
  const implEvidence = JSON.parse(String(implementation.edge.evidence_json || '{}')) as Record<string, unknown>;
551
+ const hintDiagnostic = implementationHintDiagnostic(contextSelection, implEvidence.implementationHintSuggestions);
552
+ if (hintDiagnostic) diagnostics.unshift(hintDiagnostic);
548
553
  const handlerNode = implementation.edge.status === 'resolved' ? handlerMethodNode(db, implementation.edge.to_id) : contextNode;
549
554
  const implTo = handlerNode?.label ? String(handlerNode.label) : `${implementation.edge.to_kind}:${implementation.edge.to_id}`;
550
555
  if (handlerNode) nodes.set(String(handlerNode.id), handlerNode);
@@ -553,7 +558,9 @@ export function trace(
553
558
  type: 'operation_implemented_by_handler',
554
559
  from: to,
555
560
  to: implTo,
556
- evidence: contextMethodId ? { ...implEvidence, contextualImplementationSelected: contextSelection.evidence.strategy !== 'implementation_repo_hint', contextualImplementation: contextSelection.evidence, implementationSelection: 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 },
557
564
  confidence: Number(implementation.edge.confidence ?? 0),
558
565
  unresolvedReason: implementation.edge.status === 'resolved' || contextMethodId ? undefined : String(implementation.edge.unresolved_reason ?? implementation.edge.status),
559
566
  });
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;