@saptools/service-flow 0.1.21 → 0.1.23

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 CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.22
4
+
5
+ - Propagate conservative same-file identity aliases of connected service clients, including typed, `as`, `satisfies`, helper-returned, and transitive aliases, while preserving `.tx()` alias evidence.
6
+ - Link outbound `send(...)` calls through identity aliases to the closest same-file binding row so destination, service-path, placeholder, and helper-chain evidence remains available.
7
+ - Add narrow same-file wrapper literal-path propagation for wrappers that pass a client parameter directly to `send({ path })`; dynamic wrapper paths remain semantic dynamic targets with parser warnings.
8
+ - Improve trace-time contextual implementation selection evidence and suppress stale ambiguous unresolved reasons when a unique contextual handler is selected.
9
+ - Expand `doctor --strict` with alias-binding, no-binding remote-action, contextual implementation, and wrapper path propagation quality aggregates.
10
+
3
11
  ## 0.1.21
4
12
 
5
13
  - Propagate helper-returned connected clients from function declarations, arrow-function variables, function-expression variables, named export lists, and aliased exports into caller destructuring and transaction aliases.
package/README.md CHANGED
@@ -318,7 +318,7 @@ service-flow trace --workspace /path/to/workspace --repo facade-service --operat
318
318
 
319
319
  When a concrete target exists after variable substitution, the trace shows both the parameterized evidence and the resolved match. When it does not, `service-flow` keeps the edge as a dynamic candidate or unresolved edge so the missing link remains visible.
320
320
 
321
- Service-binding evidence keeps these fields distinct: service alias, alias expression, destination expression, service-path expression, operation-path expression, and runtime placeholders. Helpers that return concrete connected clients inside object properties are followed through destructuring and simple transaction aliases while preserving helper-chain evidence. This is important for common CAP helpers such as `cds.connect.to(`remote_${code}`, { credentials: { destination: `remote_${code}`, path: `/${entityType}ProcessService` } })`, where the alias is not the service path.
321
+ Service-binding evidence keeps these fields distinct: service alias, alias expression, destination expression, service-path expression, operation-path expression, and runtime placeholders. Helpers that return concrete connected clients inside object properties are followed through destructuring, simple identity aliases, transitive same-file aliases, and transaction aliases while preserving helper-chain evidence. This is important for common CAP helpers such as `cds.connect.to(`remote_${code}`, { credentials: { destination: `remote_${code}`, path: `/${entityType}ProcessService` } })`, where the alias is not the service path.
322
322
 
323
323
  By default, production traces should be built from production source files. Keep generated credentials and local state out of git, and use explicit fixture/test workspaces when validating test-only mocked service clients so they do not pollute production graph interpretation.
324
324
 
@@ -418,8 +418,8 @@ Run `service-flow index` after source, CDS, package metadata, or helper-package
418
418
  <details>
419
419
  <summary><b>Why is an expected call unresolved?</b></summary>
420
420
 
421
- Check `service-flow doctor`, then inspect the facts with `service-flow list services`, `service-flow list operations`, and `service-flow list calls`. Dynamic destinations may need `--var key=value`; the key is the full trimmed expression inside `${...}` and is matched literally without JavaScript evaluation. Operation-path-only ambiguous remote actions usually mean the call had no service binding id; inspect `list calls`, `inspect operation`, and `doctor --strict` to determine whether helper-return propagation or wrapper support is missing. Contextual implementation selection only continues into a handler when static evidence such as caller repository, resolved service path, destination/alias expression, dependency edges, registration package, and local service ownership makes exactly one candidate stronger; ties remain ambiguous with reasons.
422
- Default doctor output is intended to focus on actionable indexing or trace-impacting issues; use `--strict` when you need exhaustive model-shape diagnostics for entity-only or extension-heavy CDS models. Strict mode also reports normalized OData invocation ambiguity and remote-action target quality, including whether unresolved unknown/dynamic paths are semantic instead of numeric call ids.
421
+ Check `service-flow doctor`, then inspect the facts with `service-flow list services`, `service-flow list operations`, and `service-flow list calls`. Dynamic destinations may need `--var key=value`; the key is the full trimmed expression inside `${...}` and is matched literally without JavaScript evaluation. Operation-path-only ambiguous remote actions usually mean the call had no service binding id; same-file identity aliases are propagated, while property/call-expression aliases and ambiguous wrapper flows remain conservative; inspect `list calls`, `inspect operation`, and `doctor --strict` to determine whether helper-return propagation or wrapper support is missing. Contextual implementation selection only continues into a handler when static evidence such as caller repository, resolved service path, destination/alias expression, dependency edges, registration package, and local service ownership makes exactly one candidate stronger; ties remain ambiguous with reasons.
422
+ Default doctor output is intended to focus on actionable indexing or trace-impacting issues; use `--strict` when you need exhaustive model-shape diagnostics for entity-only or extension-heavy CDS models. Strict mode also reports normalized OData invocation ambiguity, remote-action target quality, likely missed identity aliases, no-binding remote actions, contextual implementation stops, and wrapper dynamic-path candidates, including whether unresolved unknown/dynamic paths are semantic instead of numeric call ids.
423
423
 
424
424
  </details>
425
425
 
package/TECHNICAL-NOTE.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Service Flow Resolution Notes
2
2
 
3
+ ## 0.1.22 alias, wrapper, contextual trace, and strict doctor notes
4
+
5
+ - Same-file service-client identity aliases now create their own service binding rows when the right-hand side is a known connected client variable. The parser accepts direct identifier aliases plus typed, `as`, and `satisfies` forms, supports source-order transitive aliases, and records `aliasKind: identity` helper-chain evidence. It still does not infer aliases from property reads, indexed access, function calls, object metadata, or cross-scope guesses.
6
+ - Outbound calls through identity aliases can attach to the closest alias binding row in the same source file, preserving alias, destination, service path, placeholder, and helper-chain evidence for link and trace.
7
+ - Wrapper path propagation is intentionally narrow: same-file wrappers can resolve literal caller paths only when a wrapper client parameter is passed directly to `.send(...)` and a wrapper path parameter is passed as `send({ path })`. Dynamic caller paths remain dynamic with parser-warning evidence.
8
+ - Contextual implementation selection now records selected/tied score evidence. When trace selects a unique contextual handler, the implementation hop sets `contextualImplementationSelected` and no longer renders the original ambiguous reason as a failure.
9
+ - Strict doctor adds compact aggregates for likely missed identity aliases, remote actions with operation paths but no binding id, contextual implementation stops, and wrapper dynamic-path candidates.
10
+
3
11
  ## 0.1.21 helper-return propagation and contextual trace notes
4
12
 
5
13
  - Helper-return binding analysis uses the same returned-object scanner for `function` declarations, `async function` declarations, arrow-function variables, async arrow-function variables, and function-expression variables. Named export lists and aliases are resolved, so `export { connectCatalog as createCatalogClient }` can be destructured by callers without losing evidence.
@@ -638,11 +638,47 @@ function isSupportedEventReceiver(receiver, rootReceiver, serviceVariables) {
638
638
  if (/^(srv|service|serviceClient|messaging|messageClient|eventClient)$/.test(candidate)) return true;
639
639
  return false;
640
640
  }
641
+ function collectWrapperSpecs(source) {
642
+ const specs = /* @__PURE__ */ new Map();
643
+ const scanFunction = (name, fn) => {
644
+ const params = fn.parameters.map((param) => ts4.isIdentifier(param.name) ? param.name.text : void 0);
645
+ let found;
646
+ const visit = (node) => {
647
+ if (found) return;
648
+ if (node !== fn && (ts4.isFunctionDeclaration(node) || ts4.isArrowFunction(node) || ts4.isFunctionExpression(node))) {
649
+ ts4.forEachChild(node, visit);
650
+ return;
651
+ }
652
+ if (ts4.isCallExpression(node) && ts4.isPropertyAccessExpression(node.expression) && node.expression.name.text === "send" && ts4.isIdentifier(node.expression.expression)) {
653
+ const objectArg = node.arguments[0];
654
+ if (objectArg && ts4.isObjectLiteralExpression(objectArg)) {
655
+ const pathProp = objectArg.properties.find((property) => ts4.isShorthandPropertyAssignment(property) && property.name.text === "path");
656
+ if (pathProp) found = { client: node.expression.expression.text, path: pathProp.name.text, method: objectPropertyText(objectArg, "method") };
657
+ }
658
+ }
659
+ ts4.forEachChild(node, visit);
660
+ };
661
+ visit(fn);
662
+ if (!found) return;
663
+ const clientIndex = params.indexOf(found.client);
664
+ const pathIndex = params.indexOf(found.path);
665
+ const methodIndex = found.method && params.includes(found.method) ? params.indexOf(found.method) : void 0;
666
+ if (clientIndex >= 0 && pathIndex >= 0) specs.set(name, { clientIndex, pathIndex, methodIndex });
667
+ };
668
+ for (const stmt of source.statements) {
669
+ if (ts4.isFunctionDeclaration(stmt) && stmt.name) scanFunction(stmt.name.text, stmt);
670
+ if (ts4.isVariableStatement(stmt)) {
671
+ for (const decl of stmt.declarationList.declarations) if (ts4.isIdentifier(decl.name) && decl.initializer && (ts4.isArrowFunction(decl.initializer) || ts4.isFunctionExpression(decl.initializer))) scanFunction(decl.name.text, decl.initializer);
672
+ }
673
+ }
674
+ return specs;
675
+ }
641
676
  function classifyOutboundCallsInSource(source, filePath) {
642
677
  const calls = [];
643
678
  const sourceFile = normalizePath(filePath);
644
679
  const initializers = variableInitializers(source);
645
680
  const serviceVariables = collectServiceVariables(source);
681
+ const wrapperSpecs = collectWrapperSpecs(source);
646
682
  const add = (node, fact, extra) => {
647
683
  calls.push({ node, fact: { ...fact, sourceFile, sourceLine: lineOf3(source.text, node.getStart(source)), confidence: fact.confidence ?? 0.8, evidence: parserEvidence(source, node, extra) } });
648
684
  };
@@ -664,6 +700,14 @@ function classifyOutboundCallsInSource(source, filePath) {
664
700
  const shorthandPath = objectPropertyIsShorthand(objectArg, "path");
665
701
  add(node, { callType: query ? "remote_query" : "remote_action", serviceVariableName: receiver, method: stripQuotes(objectPropertyText(objectArg, "method") ?? "POST"), operationPathExpr: op && !shorthandPath ? `/${stripQuotes(op).replace(/^\//, "")}` : void 0, queryEntity: query ? extractQueryEntity(query) : void 0, payloadSummary: summarizeExpression(objectArg.getText(source)), confidence: op || query ? 0.8 : 0.4, unresolvedReason: !query && shorthandPath ? "dynamic_operation_path_identifier" : void 0 }, { receiver, classifier: "service_client_send_object", operationPathExpression: shorthandPath ? op : void 0, parserWarning: shorthandPath ? "dynamic_operation_path_identifier" : void 0 });
666
702
  }
703
+ } else if (ts4.isCallExpression(expr) && ts4.isIdentifier(expr.expression) && wrapperSpecs.has(expr.expression.text)) {
704
+ const spec = wrapperSpecs.get(expr.expression.text);
705
+ const clientArg = spec ? expr.arguments[spec.clientIndex] : void 0;
706
+ const pathArg = spec ? expr.arguments[spec.pathIndex] : void 0;
707
+ const methodArg = spec?.methodIndex === void 0 ? void 0 : expr.arguments[spec.methodIndex];
708
+ if (spec && clientArg && ts4.isIdentifier(clientArg) && isStringLike(pathArg)) {
709
+ add(node, { callType: "remote_action", serviceVariableName: clientArg.text, method: stripQuotes(methodArg?.getText(source) ?? "POST"), operationPathExpr: `/${pathArg.text.replace(/^\//, "")}`, payloadSummary: summarizeExpression(node.getText(source)), confidence: 0.75 }, { receiver: clientArg.text, classifier: "local_wrapper_literal_path", wrapperFunction: expr.expression.text, literalCallerArgumentDetected: true });
710
+ }
667
711
  } else if (ts4.isPropertyAccessExpression(expr) && ["emit", "publish", "on"].includes(expr.name.text)) {
668
712
  const receiver = receiverName(expr.expression);
669
713
  const rootReceiver = rootReceiverName(expr.expression);
@@ -815,9 +859,19 @@ function connectFactFromCall(call) {
815
859
  }
816
860
  function unwrapCall(expr) {
817
861
  if (ts5.isAwaitExpression(expr)) return unwrapCall(expr.expression);
862
+ if (ts5.isParenthesizedExpression(expr)) return unwrapCall(expr.expression);
863
+ if (ts5.isAsExpression(expr) || ts5.isSatisfiesExpression(expr)) return unwrapCall(expr.expression);
864
+ if (ts5.isTypeAssertionExpression(expr)) return unwrapCall(expr.expression);
818
865
  if (ts5.isCallExpression(expr)) return expr;
819
866
  return void 0;
820
867
  }
868
+ function unwrapIdentityExpression(expr) {
869
+ if (ts5.isAwaitExpression(expr)) return unwrapIdentityExpression(expr.expression);
870
+ if (ts5.isParenthesizedExpression(expr)) return unwrapIdentityExpression(expr.expression);
871
+ if (ts5.isAsExpression(expr) || ts5.isSatisfiesExpression(expr)) return unwrapIdentityExpression(expr.expression);
872
+ if (ts5.isTypeAssertionExpression(expr)) return unwrapIdentityExpression(expr.expression);
873
+ return expr;
874
+ }
821
875
  function findConnectInExpression(expr) {
822
876
  const direct = unwrapCall(expr);
823
877
  if (direct) {
@@ -1075,23 +1129,51 @@ async function parseServiceBindings(repoPath, filePath) {
1075
1129
  async function importedHelper(localName) {
1076
1130
  return (await importedHelpers(localName)).find((row) => !row.helper.returnedProperty);
1077
1131
  }
1078
- async function recordVariable(decl) {
1132
+ function bindingForVariable(variableName) {
1133
+ const sourceFile = normalizePath(filePath);
1134
+ return [...out].reverse().find((row) => row.variableName === variableName && row.sourceFile === sourceFile);
1135
+ }
1136
+ function cloneAliasBinding(targetName, sourceName, aliasKind, node) {
1137
+ const existing = bindingForVariable(sourceName);
1138
+ if (!existing) return;
1139
+ out.push({
1140
+ ...existing,
1141
+ variableName: targetName,
1142
+ sourceLine: lineOf4(sourceFileAst, node),
1143
+ helperChain: [
1144
+ ...existing.helperChain ?? [],
1145
+ {
1146
+ callerVariable: targetName,
1147
+ aliasOf: sourceName,
1148
+ aliasKind,
1149
+ scopeRule: "same-file-source-order"
1150
+ }
1151
+ ]
1152
+ });
1153
+ }
1154
+ function recordIdentityAlias(decl) {
1079
1155
  if (!ts5.isIdentifier(decl.name) || !decl.initializer) return;
1080
- const call = unwrapCall(decl.initializer);
1156
+ const unwrapped = unwrapIdentityExpression(decl.initializer);
1157
+ if (!ts5.isIdentifier(unwrapped)) return;
1158
+ cloneAliasBinding(decl.name.text, unwrapped.text, "identity", decl);
1159
+ }
1160
+ async function recordBindingFromExpression(targetName, expr, node, aliasKind) {
1161
+ const call = unwrapCall(expr);
1081
1162
  if (!call) return;
1082
1163
  const direct = connectFactFromCall(call);
1083
1164
  if (direct)
1084
1165
  out.push({
1085
- variableName: decl.name.text,
1166
+ variableName: targetName,
1086
1167
  ...direct,
1087
1168
  sourceFile: normalizePath(filePath),
1088
- sourceLine: lineOf4(sourceFileAst, decl)
1169
+ sourceLine: lineOf4(sourceFileAst, node),
1170
+ helperChain: aliasKind === "assignment" ? [{ callerVariable: targetName, assignedFrom: call.expression.getText(sourceFileAst), aliasKind, scopeRule: "same-file-source-order" }] : void 0
1089
1171
  });
1090
1172
  else if (ts5.isIdentifier(call.expression)) {
1091
1173
  const resolved = await importedHelper(call.expression.text);
1092
1174
  if (resolved)
1093
1175
  out.push({
1094
- variableName: decl.name.text,
1176
+ variableName: targetName,
1095
1177
  alias: resolved.helper.alias,
1096
1178
  aliasExpr: resolved.helper.aliasExpr,
1097
1179
  destinationExpr: resolved.helper.destinationExpr,
@@ -1099,10 +1181,11 @@ async function parseServiceBindings(repoPath, filePath) {
1099
1181
  isDynamic: resolved.helper.isDynamic,
1100
1182
  placeholders: resolved.helper.placeholders,
1101
1183
  sourceFile: normalizePath(filePath),
1102
- sourceLine: lineOf4(sourceFileAst, decl),
1184
+ sourceLine: lineOf4(sourceFileAst, node),
1103
1185
  helperChain: [
1104
1186
  {
1105
- callerVariable: decl.name.text,
1187
+ callerVariable: targetName,
1188
+ ...aliasKind === "assignment" ? { assignedFrom: call.expression.text, aliasKind, scopeRule: "same-file-source-order" } : {},
1106
1189
  importedHelper: call.expression.text,
1107
1190
  importSource: resolved.imp.sourceFile,
1108
1191
  exportedSymbol: resolved.imp.exportedName,
@@ -1113,6 +1196,10 @@ async function parseServiceBindings(repoPath, filePath) {
1113
1196
  });
1114
1197
  }
1115
1198
  }
1199
+ async function recordVariable(decl) {
1200
+ if (!ts5.isIdentifier(decl.name) || !decl.initializer) return;
1201
+ await recordBindingFromExpression(decl.name.text, decl.initializer, decl, "declaration");
1202
+ }
1116
1203
  async function helpersForCall(call) {
1117
1204
  if (!ts5.isIdentifier(call.expression)) return [];
1118
1205
  const local = localObjectHelpers.get(call.expression.text) ?? [];
@@ -1145,6 +1232,39 @@ async function parseServiceBindings(repoPath, filePath) {
1145
1232
  });
1146
1233
  }
1147
1234
  }
1235
+ async function recordDestructuredAssignment(pattern, expr, node) {
1236
+ const call = unwrapCall(expr);
1237
+ if (!call) return;
1238
+ const helpers = await helpersForCall(call);
1239
+ if (helpers.length === 0) return;
1240
+ for (const prop of pattern.properties) {
1241
+ let propertyName;
1242
+ let targetName;
1243
+ if (ts5.isShorthandPropertyAssignment(prop)) {
1244
+ propertyName = prop.name.text;
1245
+ targetName = prop.name.text;
1246
+ } else if (ts5.isPropertyAssignment(prop)) {
1247
+ propertyName = ts5.isIdentifier(prop.name) || ts5.isStringLiteralLike(prop.name) ? prop.name.text : void 0;
1248
+ targetName = ts5.isIdentifier(prop.initializer) ? prop.initializer.text : void 0;
1249
+ }
1250
+ if (!propertyName || !targetName) continue;
1251
+ const matches = helpers.filter((row) => row.helper.returnedProperty === propertyName);
1252
+ if (matches.length !== 1) continue;
1253
+ const resolved = matches[0];
1254
+ out.push({
1255
+ variableName: targetName,
1256
+ alias: resolved.helper.alias,
1257
+ aliasExpr: resolved.helper.aliasExpr,
1258
+ destinationExpr: resolved.helper.destinationExpr,
1259
+ servicePathExpr: resolved.helper.servicePathExpr,
1260
+ isDynamic: resolved.helper.isDynamic,
1261
+ placeholders: resolved.helper.placeholders,
1262
+ sourceFile: normalizePath(filePath),
1263
+ sourceLine: lineOf4(sourceFileAst, node),
1264
+ helperChain: [{ callerVariable: targetName, assignedFrom: call.expression.getText(sourceFileAst), aliasKind: "assignment", scopeRule: "same-file-source-order", returnedProperty: propertyName, importSource: resolved.imp?.sourceFile, exportedSymbol: resolved.imp?.exportedName, helperSourceFile: resolved.helper.sourceFile, helperSourceLine: resolved.helper.sourceLine }]
1265
+ });
1266
+ }
1267
+ }
1148
1268
  function recordDestructuredClassHelper(decl) {
1149
1269
  if (!ts5.isObjectBindingPattern(decl.name) || !decl.initializer) return;
1150
1270
  const call = unwrapCall(decl.initializer);
@@ -1177,21 +1297,40 @@ async function parseServiceBindings(repoPath, filePath) {
1177
1297
  });
1178
1298
  }
1179
1299
  }
1180
- const declarations = [];
1181
- function collectDeclarations(node) {
1182
- if (ts5.isVariableDeclaration(node)) declarations.push(node);
1183
- ts5.forEachChild(node, collectDeclarations);
1184
- }
1185
- collectDeclarations(sourceFileAst);
1186
- for (const decl of declarations) {
1187
- await recordDestructuredHelper(decl);
1188
- recordDestructuredClassHelper(decl);
1189
- await recordVariable(decl);
1190
- if (ts5.isIdentifier(decl.name) && decl.initializer && ts5.isCallExpression(decl.initializer) && ts5.isPropertyAccessExpression(decl.initializer.expression) && decl.initializer.expression.name.text === "tx" && ts5.isIdentifier(decl.initializer.expression.expression)) {
1191
- const sourceName = decl.initializer.expression.expression.text;
1192
- const existing = out.find((row) => row.variableName === sourceName && row.sourceFile === normalizePath(filePath));
1193
- if (existing) out.push({ ...existing, variableName: decl.name.text, sourceLine: lineOf4(sourceFileAst, decl), helperChain: [...existing.helperChain ?? [], { callerVariable: decl.name.text, aliasOf: sourceName, aliasKind: "transaction" }] });
1300
+ const events = [];
1301
+ function collectEvents(node) {
1302
+ if (ts5.isVariableDeclaration(node)) events.push({ pos: node.getStart(sourceFileAst), node });
1303
+ if (ts5.isBinaryExpression(node) && node.operatorToken.kind === ts5.SyntaxKind.EqualsToken)
1304
+ events.push({ pos: node.getStart(sourceFileAst), node });
1305
+ ts5.forEachChild(node, collectEvents);
1306
+ }
1307
+ collectEvents(sourceFileAst);
1308
+ events.sort((a, b) => a.pos - b.pos);
1309
+ for (const event of events) {
1310
+ if (ts5.isVariableDeclaration(event.node)) {
1311
+ const decl = event.node;
1312
+ await recordDestructuredHelper(decl);
1313
+ recordDestructuredClassHelper(decl);
1314
+ await recordVariable(decl);
1315
+ recordIdentityAlias(decl);
1316
+ if (ts5.isIdentifier(decl.name) && decl.initializer && ts5.isCallExpression(decl.initializer) && ts5.isPropertyAccessExpression(decl.initializer.expression) && decl.initializer.expression.name.text === "tx" && ts5.isIdentifier(decl.initializer.expression.expression)) {
1317
+ cloneAliasBinding(decl.name.text, decl.initializer.expression.expression.text, "transaction", decl);
1318
+ }
1319
+ continue;
1194
1320
  }
1321
+ const assignment = event.node;
1322
+ if (ts5.isIdentifier(assignment.left)) {
1323
+ const rhs = unwrapIdentityExpression(assignment.right);
1324
+ if (ts5.isIdentifier(rhs)) {
1325
+ cloneAliasBinding(assignment.left.text, rhs.text, "identity-assignment", assignment);
1326
+ continue;
1327
+ }
1328
+ await recordBindingFromExpression(assignment.left.text, assignment.right, assignment, "assignment");
1329
+ continue;
1330
+ }
1331
+ const left = ts5.isParenthesizedExpression(assignment.left) ? assignment.left.expression : assignment.left;
1332
+ if (ts5.isObjectLiteralExpression(left))
1333
+ await recordDestructuredAssignment(left, assignment.right, assignment);
1195
1334
  }
1196
1335
  return out;
1197
1336
  }
@@ -1998,10 +2137,25 @@ function implementationScope(db, operationId) {
1998
2137
  return { repoId: row?.repoId, files: new Set(row?.sourceFile ? [row.sourceFile] : []), symbolId: row?.symbolId, edge };
1999
2138
  }
2000
2139
  function contextImplementationMethodId(edge, callerRepoId) {
2001
- if (!edge || edge.status !== "ambiguous" || callerRepoId === void 0) return void 0;
2140
+ if (!edge || edge.status !== "ambiguous" || callerRepoId === void 0) return { evidence: { status: "not_applicable" } };
2002
2141
  const evidence = JSON.parse(String(edge.evidence_json || "{}"));
2003
- const candidate = evidence.candidates?.find((item) => item.accepted && (Number(item.handlerPackage?.id) === callerRepoId || Number(item.applicationPackage?.id) === callerRepoId));
2004
- return candidate?.methodId === void 0 ? void 0 : String(candidate.methodId);
2142
+ const scores = (evidence.candidates ?? []).filter((item) => item.accepted).map((item) => {
2143
+ const reasons = [];
2144
+ let score = Number(item.score ?? 0);
2145
+ if (Number(item.handlerPackage?.id) === callerRepoId) {
2146
+ score += 10;
2147
+ reasons.push("handler_package_matches_caller_repository");
2148
+ }
2149
+ if (Number(item.applicationPackage?.id) === callerRepoId) {
2150
+ score += 10;
2151
+ reasons.push("registration_package_matches_caller_repository");
2152
+ }
2153
+ return { methodId: item.methodId, score, reasons, handlerPackage: item.handlerPackage, applicationPackage: item.applicationPackage };
2154
+ }).sort((a, b) => b.score - a.score);
2155
+ if (scores.length === 0) return { evidence: { status: "not_applicable", candidateScores: [] } };
2156
+ const [first, second] = scores;
2157
+ if (first && first.methodId !== void 0 && first.score > 0 && (!second || first.score > second.score)) return { methodId: String(first.methodId), evidence: { status: "selected", selectedMethodId: first.methodId, candidateScores: scores } };
2158
+ return { evidence: { status: "tied", tieReason: "no_unique_materially_stronger_candidate", candidateScores: scores } };
2005
2159
  }
2006
2160
  function handlerScope(db, methodId) {
2007
2161
  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);
@@ -2207,7 +2361,8 @@ function trace(db, start, options) {
2207
2361
  });
2208
2362
  if (effectiveRow.to_kind === "operation") {
2209
2363
  const implementation = implementationScope(db, effectiveRow.to_id);
2210
- const contextMethodId = contextImplementationMethodId(implementation.edge, current.repoId);
2364
+ const contextSelection = contextImplementationMethodId(implementation.edge, current.repoId);
2365
+ const contextMethodId = contextSelection.methodId;
2211
2366
  const contextNode = contextMethodId ? handlerMethodNode(db, contextMethodId) : void 0;
2212
2367
  if (implementation.edge) {
2213
2368
  const implEvidence = JSON.parse(String(implementation.edge.evidence_json || "{}"));
@@ -2219,9 +2374,9 @@ function trace(db, start, options) {
2219
2374
  type: "operation_implemented_by_handler",
2220
2375
  from: to,
2221
2376
  to: implTo,
2222
- evidence: implEvidence,
2377
+ evidence: contextMethodId ? { ...implEvidence, contextualImplementationSelected: true, contextualImplementation: contextSelection.evidence } : { ...implEvidence, contextualImplementation: contextSelection.evidence },
2223
2378
  confidence: Number(implementation.edge.confidence ?? 0),
2224
- unresolvedReason: implementation.edge.status === "resolved" ? void 0 : String(implementation.edge.unresolved_reason ?? implementation.edge.status)
2379
+ unresolvedReason: implementation.edge.status === "resolved" || contextMethodId ? void 0 : String(implementation.edge.unresolved_reason ?? implementation.edge.status)
2225
2380
  });
2226
2381
  }
2227
2382
  if (current.depth >= maxDepth) continue;
@@ -2278,4 +2433,4 @@ export {
2278
2433
  linkWorkspace,
2279
2434
  trace
2280
2435
  };
2281
- //# sourceMappingURL=chunk-TSMDIKML.js.map
2436
+ //# sourceMappingURL=chunk-LCXDOSWL.js.map